import { meterMethod } from "./audit"; import { LEGACY_VERSIONS, META, RPC, RpcError, SERVER_INFO, SERVER_INSTRUCTIONS, SUPPORTED_VERSIONS } from "./protocol"; import type { JsonObject, McpCaller } from "./protocol"; import { getPrompt, PROMPTS } from "./prompts"; import { listResources, readResource, RESOURCE_TEMPLATES, subscribeResource } from "./resources"; import { callTool, listTools } from "./tools"; import type { ToolContext } from "./tools/base"; const LIST_TTL_MS = 60 * 60 * 1000; const READ_TTL_MS = 30 * 1000; function capabilities(era: "modern" | "legacy"): JsonObject { return { tools: { listChanged: false }, resources: { subscribe: true, listChanged: false }, prompts: { listChanged: false }, ...(era === "legacy" ? { logging: {} } : {}), }; } export function negotiateLegacyVersion(requested: unknown): string { return typeof requested === "string" && (LEGACY_VERSIONS as readonly string[]).includes(requested) ? requested : LEGACY_VERSIONS[0]; } export function initializeResult(protocolVersion: string): JsonObject { return { protocolVersion, capabilities: capabilities("legacy"), serverInfo: SERVER_INFO, instructions: SERVER_INSTRUCTIONS, }; } function cacheable(caller: McpCaller, ttlMs: number, scope: "public" | "private"): JsonObject { return caller.era === "modern" ? { ttlMs, cacheScope: scope } : {}; } function stringParam(params: JsonObject, key: string): string { const value = params[key]; if (typeof value !== "string" || !value) throw new RpcError(RPC.invalidParams, `${key} is required`); return value; } /** Routes one JSON-RPC request; transport concerns (sessions, streams, headers) live in transport.ts. */ export async function dispatch( caller: McpCaller, method: string, params: JsonObject, hooks: Pick, ): Promise { switch (method) { case "server/discover": return { supportedVersions: SUPPORTED_VERSIONS, capabilities: capabilities("modern"), instructions: SERVER_INSTRUCTIONS, ttlMs: LIST_TTL_MS, cacheScope: "public", }; case "ping": case "logging/setLevel": return {}; case "tools/list": return { tools: listTools(), ...cacheable(caller, LIST_TTL_MS, "public") }; case "tools/call": return callTool(caller, params, hooks); case "resources/list": await meterMethod(caller, method); return { ...(await listResources(caller)), ...cacheable(caller, READ_TTL_MS, "private") }; case "resources/templates/list": return { resourceTemplates: RESOURCE_TEMPLATES, ...cacheable(caller, LIST_TTL_MS, "public") }; case "resources/read": await meterMethod(caller, method, true); return { ...(await readResource(caller, stringParam(params, "uri"))), ...cacheable(caller, READ_TTL_MS, "private"), }; case "resources/subscribe": case "resources/unsubscribe": if (caller.era === "modern") throw new RpcError(RPC.methodNotFound, "Use subscriptions/listen", undefined, 404); await meterMethod(caller, method); await subscribeResource(caller, stringParam(params, "uri"), method === "resources/subscribe"); return {}; case "prompts/list": return { prompts: PROMPTS, ...cacheable(caller, LIST_TTL_MS, "public") }; case "prompts/get": { await meterMethod(caller, method, true); const args = typeof params.arguments === "object" && params.arguments !== null ? (params.arguments as JsonObject) : {}; return getPrompt(caller, stringParam(params, "name"), args); } case "completion/complete": return { completion: { values: [], hasMore: false } }; default: throw new RpcError( RPC.methodNotFound, `Method not found: ${method}`, undefined, caller.era === "modern" ? 404 : 200, ); } } /** 2026-era results carry `resultType` and identify the server in `_meta`. */ export function finishResult(caller: McpCaller, result: JsonObject): JsonObject { if (caller.era !== "modern") return result; const meta = typeof result._meta === "object" && result._meta !== null ? result._meta : {}; return { resultType: "complete", ...result, _meta: { ...meta, [META.serverInfo]: SERVER_INFO } }; }