| "use server"; |
|
|
| import { NextResponse } from "next/server"; |
|
|
| const TIMEOUT_MS = 8000; |
|
|
| |
| |
| async function probeMcp(url) { |
| const headers = { |
| "Content-Type": "application/json", |
| "Accept": "application/json, text/event-stream", |
| "MCP-Protocol-Version": "2025-06-18", |
| }; |
| const ac = new AbortController(); |
| const timer = setTimeout(() => ac.abort(), TIMEOUT_MS); |
| try { |
| |
| const initRes = await fetch(url, { |
| method: "POST", |
| headers, |
| body: JSON.stringify({ |
| jsonrpc: "2.0", id: 1, method: "initialize", |
| params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "9router", version: "1" } }, |
| }), |
| signal: ac.signal, |
| }); |
| if (initRes.status === 401 || initRes.status === 403) { |
| return { requiresAuth: true, tools: [] }; |
| } |
| if (!initRes.ok) { |
| return { error: `init ${initRes.status}`, tools: [] }; |
| } |
| const sessionId = initRes.headers.get("mcp-session-id") || ""; |
| await initRes.text().catch(() => {}); |
|
|
| const listHeaders = { ...headers }; |
| if (sessionId) listHeaders["mcp-session-id"] = sessionId; |
|
|
| |
| await fetch(url, { |
| method: "POST", |
| headers: listHeaders, |
| body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", params: {} }), |
| signal: ac.signal, |
| }).catch(() => {}); |
|
|
| |
| const listRes = await fetch(url, { |
| method: "POST", |
| headers: listHeaders, |
| body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" }), |
| signal: ac.signal, |
| }); |
| if (listRes.status === 401 || listRes.status === 403) { |
| return { requiresAuth: true, tools: [] }; |
| } |
| const ct = listRes.headers.get("content-type") || ""; |
| let parsed; |
| if (ct.includes("text/event-stream")) { |
| |
| const text = await listRes.text(); |
| const dataLines = text.split("\n").filter((l) => l.startsWith("data:")); |
| for (const line of dataLines) { |
| try { |
| const obj = JSON.parse(line.replace(/^data:\s*/, "")); |
| if (obj?.id === 2 && obj.result) { parsed = obj; break; } |
| } catch { } |
| } |
| } else { |
| parsed = await listRes.json().catch(() => null); |
| } |
| const tools = parsed?.result?.tools || []; |
| return { |
| tools: tools.map((t) => ({ name: t.name, description: t.description || "" })), |
| }; |
| } catch (e) { |
| return { error: e.name === "AbortError" ? "timeout" : e.message, tools: [] }; |
| } finally { |
| clearTimeout(timer); |
| } |
| } |
|
|
| export async function POST(request) { |
| try { |
| const { url } = await request.json(); |
| if (!url || typeof url !== "string") { |
| return NextResponse.json({ error: "url required" }, { status: 400 }); |
| } |
| const result = await probeMcp(url); |
| return NextResponse.json(result); |
| } catch (e) { |
| return NextResponse.json({ error: e.message, tools: [] }, { status: 500 }); |
| } |
| } |
|
|