From 2ffee220bf611a8cf366705b0e088c54141b06a7 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 8 Aug 2026 23:12:00 -0300 Subject: [PATCH] refactor(mcp): modularize Radar catalog tool --- open-sse/mcp-server/radarCatalog.ts | 53 +++++++++++++++++ open-sse/mcp-server/schemas/index.ts | 5 +- open-sse/mcp-server/schemas/radarCatalog.ts | 65 ++++++++++++++++++++ open-sse/mcp-server/schemas/tools.ts | 66 +-------------------- open-sse/mcp-server/server.ts | 45 +------------- open-sse/mcp-server/toolResult.ts | 4 ++ 6 files changed, 128 insertions(+), 110 deletions(-) create mode 100644 open-sse/mcp-server/schemas/radarCatalog.ts create mode 100644 open-sse/mcp-server/toolResult.ts diff --git a/open-sse/mcp-server/radarCatalog.ts b/open-sse/mcp-server/radarCatalog.ts index 0feea50899..e17ce724bb 100644 --- a/open-sse/mcp-server/radarCatalog.ts +++ b/open-sse/mcp-server/radarCatalog.ts @@ -1,5 +1,19 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +import { logToolCall } from "./audit.ts"; +import { radarCatalogInput, radarCatalogOutput } from "./schemas/radarCatalog.ts"; +import type { McpToolExtraLike } from "./scopeEnforcement.ts"; +import type { TextToolResult } from "./toolResult.ts"; +import { sanitizeErrorMessage } from "../utils/error.ts"; + type JsonRecord = Record; +type ScopeEnforcer = ( + toolName: string, + handler: (args: unknown, extra?: McpToolExtraLike) => Promise, + toolScopes?: readonly string[] +) => (args: unknown, extra?: McpToolExtraLike) => Promise; + export interface McpRadarCatalogArgs { provider?: string; familyId?: string; @@ -115,3 +129,42 @@ export async function getMcpRadarCatalog( return { meta: normalizeMeta(raw.meta), models }; } + +async function handleRadarCatalog(args: { + provider?: string; + familyId?: string; + enabledOnly: boolean; +}): Promise { + const start = Date.now(); + try { + const result = radarCatalogOutput.parse(await getMcpRadarCatalog(args)); + await logToolCall( + "omniroute_radar_catalog", + args, + { modelCount: result.models.length }, + Date.now() - start, + true + ); + return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; + } catch (error) { + const message = sanitizeErrorMessage(error) || "Failed to read Radar catalog"; + await logToolCall("omniroute_radar_catalog", args, null, Date.now() - start, false, message); + return { content: [{ type: "text", text: `Error: ${message}` }], isError: true }; + } +} + +export function registerRadarCatalogTool( + server: McpServer, + withScopeEnforcement: ScopeEnforcer +): void { + server.registerTool( + "omniroute_radar_catalog", + { + description: "Reads the local signed Radar catalog with optional provider and family filters", + inputSchema: radarCatalogInput, + }, + withScopeEnforcement("omniroute_radar_catalog", (args) => + handleRadarCatalog(radarCatalogInput.parse(args)) + ) + ); +} diff --git a/open-sse/mcp-server/schemas/index.ts b/open-sse/mcp-server/schemas/index.ts index 1bc0f83b07..1c3cbdc029 100644 --- a/open-sse/mcp-server/schemas/index.ts +++ b/open-sse/mcp-server/schemas/index.ts @@ -35,9 +35,6 @@ export { listModelsCatalogInput, listModelsCatalogOutput, listModelsCatalogTool, - radarCatalogInput, - radarCatalogOutput, - radarCatalogTool, // Phase 2: Advanced tool schemas simulateRouteInput, simulateRouteOutput, @@ -94,6 +91,8 @@ export { ccrStatsTool, } from "./tools.ts"; +export { radarCatalogInput, radarCatalogOutput, radarCatalogTool } from "./radarCatalog.ts"; + // A2A schemas export { AgentCardSchema, diff --git a/open-sse/mcp-server/schemas/radarCatalog.ts b/open-sse/mcp-server/schemas/radarCatalog.ts new file mode 100644 index 0000000000..1bfb4040e2 --- /dev/null +++ b/open-sse/mcp-server/schemas/radarCatalog.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; + +import type { McpToolDefinition } from "./toolDefinition.ts"; + +export const radarCatalogInput = z.object({ + provider: z.string().trim().min(1).max(100).optional().describe("Filter by provider id"), + familyId: z.string().trim().min(1).max(120).optional().describe("Filter by curated family id"), + enabledOnly: z.boolean().default(true).describe("Exclude models disabled by the Radar feed"), +}); + +const radarLimitOutput = z.object({ + rpm: z.number().nullable(), + rpd: z.number().nullable(), + tpm: z.number().nullable(), + tpd: z.number().nullable(), +}); + +export const radarCatalogOutput = z.object({ + meta: z + .object({ + version: z.string(), + tier: z.string(), + fetchedAt: z.string(), + }) + .nullable(), + models: z.array( + z.object({ + provider: z.string(), + modelId: z.string(), + displayName: z.string(), + familyId: z.string().nullable(), + quota: z.object({ + monthlyTokens: z.number(), + creditTokens: z.number(), + freeType: z.string(), + limits: radarLimitOutput.nullable(), + }), + capabilities: z + .object({ + tools: z.boolean(), + vision: z.boolean(), + thinking: z.boolean(), + }) + .nullable(), + enabled: z.boolean(), + origin: z.enum(["baseline", "radar", "local"]), + disabledBy: z.literal("radar").nullable(), + }) + ), +}); + +export const radarCatalogTool: McpToolDefinition< + typeof radarCatalogInput, + typeof radarCatalogOutput +> = { + name: "omniroute_radar_catalog", + description: + "Reads the local signed Radar catalog with optional provider and curated-family filters. Never syncs or writes data.", + inputSchema: radarCatalogInput, + outputSchema: radarCatalogOutput, + scopes: ["read:radar"], + auditLevel: "none", + phase: 1, + sourceEndpoints: ["/api/radar/catalog"], +}; diff --git a/open-sse/mcp-server/schemas/tools.ts b/open-sse/mcp-server/schemas/tools.ts index dd39de481e..437a49f56c 100644 --- a/open-sse/mcp-server/schemas/tools.ts +++ b/open-sse/mcp-server/schemas/tools.ts @@ -13,11 +13,11 @@ import { z } from "zod"; import { toolSearchTool } from "./toolSearch.ts"; import { pickFastestModelTool } from "./pickFastestModel.ts"; import { CCR_MCP_TOOLS } from "./ccrTools.ts"; +import { radarCatalogTool } from "./radarCatalog.ts"; import { AUTO_ROUTING_STRATEGY_VALUES, ROUTING_STRATEGY_VALUES, } from "../../../src/shared/constants/routingStrategies.ts"; - // ============ Shared Types ============ // AuditLevel + McpToolDefinition live in the leaf ./toolDefinition.ts so that // toolSearch.ts can import the type without forming a tools.ts ↔ toolSearch.ts cycle. @@ -26,7 +26,6 @@ export type { AuditLevel, McpToolDefinition } from "./toolDefinition.ts"; import type { McpToolDefinition } from "./toolDefinition.ts"; export { pickFastestModelInput, pickFastestModelOutput } from "./pickFastestModel.ts"; export * from "./ccrTools.ts"; - // ============ Phase 1: Essential Tools ============ // --- Tool 1: omniroute_get_health --- @@ -432,69 +431,6 @@ export const listModelsCatalogTool: McpToolDefinition< sourceEndpoints: ["/api/models/catalog", "/v1/models"], }; -// --- Tool 9: omniroute_radar_catalog --- -export const radarCatalogInput = z.object({ - provider: z.string().trim().min(1).max(100).optional().describe("Filter by provider id"), - familyId: z.string().trim().min(1).max(120).optional().describe("Filter by curated family id"), - enabledOnly: z.boolean().default(true).describe("Exclude models disabled by the Radar feed"), -}); - -const radarLimitOutput = z.object({ - rpm: z.number().nullable(), - rpd: z.number().nullable(), - tpm: z.number().nullable(), - tpd: z.number().nullable(), -}); - -export const radarCatalogOutput = z.object({ - meta: z - .object({ - version: z.string(), - tier: z.string(), - fetchedAt: z.string(), - }) - .nullable(), - models: z.array( - z.object({ - provider: z.string(), - modelId: z.string(), - displayName: z.string(), - familyId: z.string().nullable(), - quota: z.object({ - monthlyTokens: z.number(), - creditTokens: z.number(), - freeType: z.string(), - limits: radarLimitOutput.nullable(), - }), - capabilities: z - .object({ - tools: z.boolean(), - vision: z.boolean(), - thinking: z.boolean(), - }) - .nullable(), - enabled: z.boolean(), - origin: z.enum(["baseline", "radar", "local"]), - disabledBy: z.literal("radar").nullable(), - }) - ), -}); - -export const radarCatalogTool: McpToolDefinition< - typeof radarCatalogInput, - typeof radarCatalogOutput -> = { - name: "omniroute_radar_catalog", - description: - "Reads the local signed Radar catalog with optional provider and curated-family filters. Never syncs or writes data.", - inputSchema: radarCatalogInput, - outputSchema: radarCatalogOutput, - scopes: ["read:radar"], - auditLevel: "none", - phase: 1, - sourceEndpoints: ["/api/radar/catalog"], -}; - // --- Tool 10: omniroute_web_search --- export const webSearchInput = z.object({ query: z diff --git a/open-sse/mcp-server/server.ts b/open-sse/mcp-server/server.ts index 00e8a2f8fd..811d87dce0 100644 --- a/open-sse/mcp-server/server.ts +++ b/open-sse/mcp-server/server.ts @@ -17,8 +17,6 @@ import { routeRequestInput, costReportInput, listModelsCatalogInput, - radarCatalogInput, - radarCatalogOutput, webSearchInput, webFetchInput, simulateRouteInput, @@ -95,9 +93,9 @@ import { normalizeQuotaResponse } from "../../src/shared/contracts/quota.ts"; import { resolveOmniRouteBaseUrl } from "../../src/shared/utils/resolveOmniRouteBaseUrl.ts"; import { sanitizeErrorMessage } from "../utils/error.ts"; import { getMcpModelsCatalog } from "./catalog.ts"; -import { getMcpRadarCatalog } from "./radarCatalog.ts"; +import { registerRadarCatalogTool } from "./radarCatalog.ts"; +import type { TextToolResult } from "./toolResult.ts"; export { getMcpModelsCatalog } from "./catalog.ts"; -export { getMcpRadarCatalog } from "./radarCatalog.ts"; const OMNIROUTE_BASE_URL = resolveOmniRouteBaseUrl(); const MCP_ENFORCE_SCOPES = process.env.OMNIROUTE_MCP_ENFORCE_SCOPES === "true"; @@ -150,11 +148,6 @@ function readMcpAccessibilityConfig(): McpAccessibilityConfig { } } -type TextToolResult = { - content: Array<{ type: "text"; text: string }>; - isError?: boolean; -}; - function toRecord(value: unknown): JsonRecord { return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {}; } @@ -574,29 +567,6 @@ async function handleListModelsCatalog(args: { provider?: string; capability?: s } } -async function handleRadarCatalog(args: { - provider?: string; - familyId?: string; - enabledOnly: boolean; -}) { - const start = Date.now(); - try { - const result = radarCatalogOutput.parse(await getMcpRadarCatalog(args)); - await logToolCall( - "omniroute_radar_catalog", - args, - { modelCount: result.models.length }, - Date.now() - start, - true - ); - return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; - } catch (error) { - const message = sanitizeErrorMessage(error) || "Failed to read Radar catalog"; - await logToolCall("omniroute_radar_catalog", args, null, Date.now() - start, false, message); - return { content: [{ type: "text" as const, text: `Error: ${message}` }], isError: true }; - } -} - async function handleWebSearch(args: { query: string; max_results?: number; @@ -842,16 +812,7 @@ export function createMcpServer(): McpServer { ) ); - server.registerTool( - "omniroute_radar_catalog", - { - description: "Reads the local signed Radar catalog with optional provider and family filters", - inputSchema: radarCatalogInput, - }, - withScopeEnforcement("omniroute_radar_catalog", (args) => - handleRadarCatalog(radarCatalogInput.parse(args)) - ) - ); + registerRadarCatalogTool(server, withScopeEnforcement); server.registerTool( "omniroute_simulate_route", diff --git a/open-sse/mcp-server/toolResult.ts b/open-sse/mcp-server/toolResult.ts new file mode 100644 index 0000000000..ea8535f545 --- /dev/null +++ b/open-sse/mcp-server/toolResult.ts @@ -0,0 +1,4 @@ +export type TextToolResult = { + content: Array<{ type: "text"; text: string }>; + isError?: boolean; +};