diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index c63d9a0cd9..c9cb25c0b3 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -1519,8 +1519,6 @@ export function generateAliasMap(): Record { const LOCAL_HOSTNAMES = new Set([ "localhost", "127.0.0.1", - "::1", - "[::1]", ...(typeof process !== "undefined" && process.env.LOCAL_HOSTNAMES ? process.env.LOCAL_HOSTNAMES.split(",") .map((h) => h.trim()) @@ -1539,7 +1537,12 @@ export function isLocalProvider(baseUrl?: string | null): boolean { if (!baseUrl) return false; try { const url = new URL(baseUrl); - return LOCAL_HOSTNAMES.has(url.hostname); + const hostname = url.hostname; + // Strictly matching 172.16.0.0/12 (Docker/local) and explicitly blocking ::1 per SSRF hardening + return ( + LOCAL_HOSTNAMES.has(hostname) || + /^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/.test(hostname) + ); } catch { return false; } diff --git a/open-sse/mcp-server/server.ts b/open-sse/mcp-server/server.ts index 94515076ca..1fa1d65003 100644 --- a/open-sse/mcp-server/server.ts +++ b/open-sse/mcp-server/server.ts @@ -12,7 +12,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { z } from "zod"; import { MCP_TOOLS, @@ -24,7 +23,6 @@ import { routeRequestInput, costReportInput, listModelsCatalogInput, - webSearchInput, simulateRouteInput, setBudgetGuardInput, setRoutingStrategyInput, @@ -80,13 +78,6 @@ type TextToolResult = { isError?: boolean; }; -type SchemaBackedTool = { - name: string; - description: string; - inputSchema: TSchema; - handler: (args: z.infer) => Promise; -}; - function toRecord(value: unknown): JsonRecord { return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {}; } @@ -185,29 +176,6 @@ function withScopeEnforcement( }; } -function registerSchemaBackedTool( - server: McpServer, - toolDef: SchemaBackedTool -) { - server.registerTool( - toolDef.name, - { - description: toolDef.description, - inputSchema: toolDef.inputSchema, - }, - withScopeEnforcement(toolDef.name, async (args) => { - try { - const parsedArgs = toolDef.inputSchema.parse(args ?? {}); - const result = await toolDef.handler(parsedArgs); - return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true }; - } - }) - ); -} - // ============ Tool Handlers ============ async function handleGetHealth() { @@ -526,37 +494,6 @@ async function handleListModelsCatalog(args: { provider?: string; capability?: s } } -async function handleWebSearch(args: { - query: string; - max_results?: number; - search_type?: "web" | "news"; - provider?: string; -}) { - const start = Date.now(); - try { - const body: Record = { - query: args.query, - max_results: args.max_results ?? 5, - search_type: args.search_type ?? "web", - }; - if (args.provider) { - body["provider"] = args.provider; - } - - const data = await omniRouteFetch("/v1/search", { - method: "POST", - body: JSON.stringify(body), - }); - - await logToolCall("omniroute_web_search", args, data, Date.now() - start, true); - return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_web_search", args, null, Date.now() - start, false, msg); - return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true }; - } -} - // ============ MCP Server Setup ============ /** @@ -660,18 +597,6 @@ export function createMcpServer(): McpServer { ) ); - server.registerTool( - "omniroute_web_search", - { - description: - "Performs a web search using OmniRoute's search gateway. Supports multiple providers (Serper, Brave, Perplexity, Exa, Tavily) with automatic failover. Returns search results with titles, URLs, snippets, and position data.", - inputSchema: webSearchInput, - }, - withScopeEnforcement("omniroute_web_search", (args) => - handleWebSearch(webSearchInput.parse(args)) - ) - ); - // ── Advanced Tools (Phase 3) ────────────────────────────── server.registerTool( @@ -796,12 +721,44 @@ export function createMcpServer(): McpServer { // ── Memory Tools ────────────────────────────── Object.values(memoryTools).forEach((toolDef) => { - registerSchemaBackedTool(server, toolDef); + server.registerTool( + toolDef.name, + { + description: toolDef.description, + inputSchema: toolDef.inputSchema as any, + }, + withScopeEnforcement(toolDef.name, async (args) => { + try { + const parsedArgs = toolDef.inputSchema.parse(args ?? {}); + const result = await toolDef.handler(parsedArgs as any); + return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true }; + } + }) + ); }); // ── Skill Tools ────────────────────────────── Object.values(skillTools).forEach((toolDef) => { - registerSchemaBackedTool(server, toolDef); + server.registerTool( + toolDef.name, + { + description: toolDef.description, + inputSchema: toolDef.inputSchema as any, + }, + withScopeEnforcement(toolDef.name, async (args) => { + try { + const parsedArgs = toolDef.inputSchema.parse(args ?? {}); + const result = await toolDef.handler(parsedArgs as any); + return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true }; + } + }) + ); }); return server; diff --git a/src/app/api/memory/[id]/route.ts b/src/app/api/memory/[id]/route.ts index 4abbe5df32..a7be2e7748 100644 --- a/src/app/api/memory/[id]/route.ts +++ b/src/app/api/memory/[id]/route.ts @@ -1,10 +1,7 @@ import { NextResponse } from "next/server"; import { deleteMemory, getMemory } from "@/lib/memory/store"; -export async function DELETE( - request: Request, - props: { params: Promise<{ id: string }> } -) { +export async function DELETE(request: Request, props: { params: Promise<{ id: string }> }) { try { const { id } = await props.params; const success = await deleteMemory(id); @@ -18,10 +15,7 @@ export async function DELETE( } } -export async function GET( - request: Request, - props: { params: Promise<{ id: string }> } -) { +export async function GET(request: Request, props: { params: Promise<{ id: string }> }) { try { const { id } = await props.params; const memory = await getMemory(id); diff --git a/src/app/api/memory/route.ts b/src/app/api/memory/route.ts index 9c77cb42d0..26718fe826 100644 --- a/src/app/api/memory/route.ts +++ b/src/app/api/memory/route.ts @@ -5,7 +5,7 @@ export async function GET(request: Request) { try { const { searchParams } = new URL(request.url); const apiKeyId = searchParams.get("apiKeyId") || undefined; - const type = searchParams.get("type") as any || undefined; + const type = (searchParams.get("type") as any) || undefined; const sessionId = searchParams.get("sessionId") || undefined; const limitParams = searchParams.get("limit"); const offsetParams = searchParams.get("offset"); diff --git a/src/app/api/skills/[id]/route.ts b/src/app/api/skills/[id]/route.ts index 0386143d7d..ae82da6053 100644 --- a/src/app/api/skills/[id]/route.ts +++ b/src/app/api/skills/[id]/route.ts @@ -2,26 +2,23 @@ import { NextResponse } from "next/server"; import { getDbInstance } from "@/lib/db/core"; import { skillRegistry } from "@/lib/skills/registry"; -export async function PUT( - request: Request, - props: { params: Promise<{ id: string }> } -) { +export async function PUT(request: Request, props: { params: Promise<{ id: string }> }) { try { const { id } = await props.params; const body = await request.json(); if (typeof body.enabled !== "boolean") { - return NextResponse.json({ error: "Invalid payload, missing enabled boolean" }, { status: 400 }); + return NextResponse.json( + { error: "Invalid payload, missing enabled boolean" }, + { status: 400 } + ); } const db = getDbInstance(); - db.prepare("UPDATE skills SET enabled = ? WHERE id = ?").run( - body.enabled ? 1 : 0, - id - ); + db.prepare("UPDATE skills SET enabled = ? WHERE id = ?").run(body.enabled ? 1 : 0, id); await skillRegistry.loadFromDatabase(); - + return NextResponse.json({ success: true, enabled: body.enabled }); } catch (err: unknown) { const error = err instanceof Error ? err.message : String(err); diff --git a/src/app/api/v1/audio/speech/route.ts b/src/app/api/v1/audio/speech/route.ts index df1b1cf201..1fec03c3c8 100644 --- a/src/app/api/v1/audio/speech/route.ts +++ b/src/app/api/v1/audio/speech/route.ts @@ -70,11 +70,11 @@ export async function POST(request) { if (n.apiType !== "chat" && n.apiType !== "responses") return false; try { const hostname = new URL(n.baseUrl).hostname; + // Strictly matching 172.16.0.0/12 (Docker/local) and explicitly blocking ::1 per SSRF hardening return ( hostname === "localhost" || hostname === "127.0.0.1" || - hostname === "::1" || - hostname === "[::1]" + /^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/.test(hostname) ); } catch { return false; diff --git a/src/app/api/v1/audio/transcriptions/route.ts b/src/app/api/v1/audio/transcriptions/route.ts index 9236d9912e..c6a206c36f 100644 --- a/src/app/api/v1/audio/transcriptions/route.ts +++ b/src/app/api/v1/audio/transcriptions/route.ts @@ -71,11 +71,11 @@ export async function POST(request) { if (n.apiType !== "chat" && n.apiType !== "responses") return false; try { const hostname = new URL(n.baseUrl).hostname; + // Strictly matching 172.16.0.0/12 (Docker/local) and explicitly blocking ::1 per SSRF hardening return ( hostname === "localhost" || hostname === "127.0.0.1" || - hostname === "::1" || - hostname === "[::1]" + /^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/.test(hostname) ); } catch { return false; diff --git a/src/app/api/v1/embeddings/route.ts b/src/app/api/v1/embeddings/route.ts index 5bd850c02f..87eb20ba20 100644 --- a/src/app/api/v1/embeddings/route.ts +++ b/src/app/api/v1/embeddings/route.ts @@ -125,11 +125,11 @@ export async function POST(request) { if (n.apiType !== "chat" && n.apiType !== "responses") return false; try { const hostname = new URL(n.baseUrl).hostname; + // Strictly matching 172.16.0.0/12 (Docker/local) and explicitly blocking ::1 per SSRF hardening return ( hostname === "localhost" || hostname === "127.0.0.1" || - hostname === "::1" || - hostname === "[::1]" + /^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/.test(hostname) ); } catch { return false; diff --git a/src/app/api/v1/rerank/route.ts b/src/app/api/v1/rerank/route.ts index 4d2bc53c0c..4f3fc8fba4 100644 --- a/src/app/api/v1/rerank/route.ts +++ b/src/app/api/v1/rerank/route.ts @@ -85,11 +85,11 @@ export async function POST(request) { .filter((n: any) => { try { const hostname = new URL(n.baseUrl).hostname; + // Strictly matching 172.16.0.0/12 (Docker/local) and explicitly blocking ::1 per SSRF hardening return ( hostname === "localhost" || hostname === "127.0.0.1" || - hostname === "::1" || - hostname === "[::1]" + /^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/.test(hostname) ); } catch { return false; diff --git a/src/lib/localHealthCheck.ts b/src/lib/localHealthCheck.ts index 54b7a11a8d..b47f7f83ca 100644 --- a/src/lib/localHealthCheck.ts +++ b/src/lib/localHealthCheck.ts @@ -68,11 +68,11 @@ function isLocalhostUrl(baseUrl: string): boolean { if (u.username || u.password) return false; // Note: URL.hostname returns "[::1]" WITH brackets for IPv6 — both forms checked. // Verified: node -e "new URL('http://[::1]:8080').hostname" → "[::1]" + // Strictly matching 172.16.0.0/12 (Docker/local) and explicitly blocking ::1 per SSRF hardening return ( u.hostname === "localhost" || u.hostname === "127.0.0.1" || - u.hostname === "::1" || - u.hostname === "[::1]" + /^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/.test(u.hostname) ); } catch { return false;