1// MCP over HTTP, the JSON-RPC half: one message per POST, answered as JSON 2// (Streamable HTTP without the optional SSE stream). Shared by the dev hub's 3// `/mcp` (mcp.ts) and the hosted, OAuth-protected `/jev/mcp` 4// (hosted-mcp.ts); each supplies its tools and what calling one does. 5// 6// Both eras of the protocol are served, statelessly (there are no sessions 7// to keep in either): a legacy client opens with `initialize` (2025-11-25 8// and before, what today's SDKs send) and gets the version it asked for if 9// we speak it; a modern one (2026-07-28) carries its version in every 10// request's `_meta` and may ask `server/discover` first. An unsupported 11// version gets UnsupportedProtocolVersionError, which names the supported. 12// Every result says `resultType: "complete"` (modern clients need it, legacy 13// ones ignore it) and names the server in `_meta`. 14 15export const MODERN_VERSION = '2026-07-28'; 16export const SUPPORTED_VERSIONS = [MODERN_VERSION, '2025-11-25', '2025-06-18', '2025-03-26']; 17const VERSION_META = 'io.modelcontextprotocol/protocolVersion'; 18const SERVER_INFO_META = 'io.modelcontextprotocol/serverInfo'; 19export const UNSUPPORTED_VERSION = -32022; 20 21export type Tool = { name: string; description: string; inputSchema: Record<string, unknown> }; 22export type ToolResult = { text: string; isError?: boolean }; 23export type Server = { 24 name: string; 25 version: string; 26 instructions?: string; 27 tools: Tool[]; 28 // A tool's answer. Throwing an Error answers it as a tool error (isError), 29 // which the model reads; a `Response` thrown is sent as is (a scope challenge). 30 call(name: string, args: Record<string, unknown>): Promise<ToolResult | string>; 31}; 32 33type Message = { jsonrpc?: unknown; id?: unknown; method?: unknown; params?: unknown }; 34 35const isObject = (x: unknown): x is Record<string, unknown> => typeof x === 'object' && x !== null && !Array.isArray(x); 36const rpcError = (id: unknown, code: number, message: string, data?: unknown, status = 200) => 37 Response.json( 38 { jsonrpc: '2.0', id: id ?? null, error: { code, message, ...(data === undefined ? {} : { data }) } }, 39 { status, headers: { 'Cache-Control': 'no-store' } }, 40 ); 41 42// The version a request is in: its `_meta`'s, else the header's; undefined 43// when neither says (a legacy request after `initialize`). 44function requestedVersion(request: Request, params: Record<string, unknown>): string | undefined { 45 const meta = isObject(params._meta) ? params._meta : {}; 46 if (typeof meta[VERSION_META] === 'string') return meta[VERSION_META] as string; 47 return request.headers.get('MCP-Protocol-Version') ?? undefined; 48} 49 50// The body, at most `maxBytes`, parsed; or the JSON-RPC error to send. 51async function readMessage(request: Request, maxBytes: number): Promise<Message | Response> { 52 const declared = Number(request.headers.get('Content-Length')); 53 if (Number.isFinite(declared) && declared > maxBytes) return rpcError(null, -32600, 'request too large', undefined, 413); 54 const reader = request.body?.getReader(); 55 const chunks: Uint8Array[] = []; 56 let length = 0; 57 if (reader) { 58 for (;;) { 59 const { done, value } = await reader.read(); 60 if (done) break; 61 length += value.byteLength; 62 if (length > maxBytes) { 63 await reader.cancel(); 64 return rpcError(null, -32600, 'request too large', undefined, 413); 65 } 66 chunks.push(value); 67 } 68 } 69 const bytes = new Uint8Array(length); 70 let at = 0; 71 for (const c of chunks) { 72 bytes.set(c, at); 73 at += c.byteLength; 74 } 75 try { 76 const message = JSON.parse(new TextDecoder().decode(bytes)) as unknown; 77 if (!isObject(message)) return rpcError(null, -32600, 'one JSON-RPC message per request'); 78 return message; 79 } catch { 80 return rpcError(null, -32700, 'parse error'); 81 } 82} 83 84export async function serveRpc(request: Request, server: Server, { maxBytes }: { maxBytes: number }): Promise<Response> { 85 if (request.method !== 'POST') { 86 return new Response('POST only', { status: 405, headers: { Allow: 'POST', 'Cache-Control': 'no-store' } }); 87 } 88 const read = await readMessage(request, maxBytes); 89 if (read instanceof Response) return read; 90 const message = read; 91 // Notifications (no id) and responses are acknowledged without a body. 92 if (message.id === undefined || typeof message.method !== 'string') return new Response(null, { status: 202 }); 93 const id = message.id; 94 if (typeof id !== 'string' && typeof id !== 'number') return rpcError(null, -32600, 'id is a string or a number'); 95 const params = isObject(message.params) ? message.params : {}; 96 const meta = { [SERVER_INFO_META]: { name: server.name, version: server.version } }; 97 const ok = (result: Record<string, unknown>) => 98 Response.json( 99 { jsonrpc: '2.0', id, result: { resultType: 'complete', ...result, _meta: meta } }, 100 { headers: { 'Cache-Control': 'no-store' } }, 101 ); 102 103 if (message.method === 'initialize') { 104 // legacy: the client's version if we speak it, else our newest legacy one 105 const asked = typeof params.protocolVersion === 'string' ? params.protocolVersion : ''; 106 const legacy = SUPPORTED_VERSIONS.filter((v) => v !== MODERN_VERSION); 107 return ok({ 108 protocolVersion: legacy.includes(asked) ? asked : legacy[0], 109 capabilities: { tools: {} }, 110 serverInfo: { name: server.name, version: server.version }, 111 ...(server.instructions ? { instructions: server.instructions } : {}), 112 }); 113 } 114 const version = requestedVersion(request, params); 115 if (version !== undefined && !SUPPORTED_VERSIONS.includes(version)) { 116 return rpcError(id, UNSUPPORTED_VERSION, 'Unsupported protocol version', { 117 supported: SUPPORTED_VERSIONS, 118 requested: version, 119 }, 400); 120 } 121 122 switch (message.method) { 123 case 'server/discover': 124 return ok({ 125 supportedVersions: SUPPORTED_VERSIONS, 126 capabilities: { tools: {} }, 127 ...(server.instructions ? { instructions: server.instructions } : {}), 128 ttlMs: 3_600_000, 129 cacheScope: 'public', 130 }); 131 case 'ping': 132 return ok({}); 133 case 'tools/list': 134 return ok({ tools: server.tools, ttlMs: 300_000, cacheScope: 'public' }); 135 case 'tools/call': { 136 const name = params.name; 137 const args = isObject(params.arguments) ? params.arguments : {}; 138 if (typeof name !== 'string' || !server.tools.some((t) => t.name === name)) { 139 return rpcError(id, -32602, `unknown tool ${String(name)}`); 140 } 141 try { 142 const result = await server.call(name, args); 143 const { text, isError = false } = typeof result === 'string' ? { text: result } : result; 144 return ok({ content: [{ type: 'text', text }], isError }); 145 } catch (e) { 146 if (e instanceof Response) return e; 147 return ok({ content: [{ type: 'text', text: (e as Error).message }], isError: true }); 148 } 149 } 150 default: 151 return rpcError(id, -32601, `method not found: ${message.method}`); 152 } 153}