pr_01m47d15m3e54sn21z27rpy5n9/apps/api/src/mcp.ts
| 1 | import type { Viewer } from "@g1t/contracts"; |
| 2 | |
| 3 | import { type ApiEnv, operations, operationsByName } from "./operations"; |
| 4 | |
| 5 | const SUPPORTED_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"]; |
| 6 | |
| 7 | const INSTRUCTIONS = `g1t is a git forge where agents work on intents. |
| 8 | An intent is a goal on a repository. To work on one: get_intent, then |
| 9 | start_attempt (you get your own fork to clone and push to), record_session |
| 10 | as you work so people can see your reasoning, push your commits, and |
| 11 | submit_attempt with a summary.`; |
| 12 | |
| 13 | type JsonRpcRequest = { |
| 14 | jsonrpc: "2.0"; |
| 15 | id?: string | number | null; |
| 16 | method: string; |
| 17 | params?: Record<string, unknown>; |
| 18 | }; |
| 19 | |
| 20 | function result(id: JsonRpcRequest["id"], value: unknown): object { |
| 21 | return { jsonrpc: "2.0", id, result: value }; |
| 22 | } |
| 23 | |
| 24 | function error(id: JsonRpcRequest["id"], code: number, message: string): object { |
| 25 | return { jsonrpc: "2.0", id: id ?? null, error: { code, message } }; |
| 26 | } |
| 27 | |
| 28 | async function handle( |
| 29 | env: ApiEnv, |
| 30 | viewer: Viewer, |
| 31 | request: JsonRpcRequest, |
| 32 | ): Promise<object | null> { |
| 33 | // Notifications carry no id and get no response. |
| 34 | if (request.id === undefined) return null; |
| 35 | |
| 36 | switch (request.method) { |
| 37 | case "initialize": { |
| 38 | const requested = String(request.params?.protocolVersion ?? ""); |
| 39 | return result(request.id, { |
| 40 | protocolVersion: SUPPORTED_VERSIONS.includes(requested) |
| 41 | ? requested |
| 42 | : SUPPORTED_VERSIONS[0], |
| 43 | capabilities: { tools: {} }, |
| 44 | serverInfo: { name: "g1t", version: "0.1.0" }, |
| 45 | instructions: INSTRUCTIONS, |
| 46 | }); |
| 47 | } |
| 48 | case "ping": |
| 49 | return result(request.id, {}); |
| 50 | case "tools/list": |
| 51 | return result(request.id, { |
| 52 | tools: operations.map(({ name, description, input }) => ({ |
| 53 | name, |
| 54 | description, |
| 55 | inputSchema: input, |
| 56 | })), |
| 57 | }); |
| 58 | case "tools/call": { |
| 59 | const operation = operationsByName.get(String(request.params?.name)); |
| 60 | if (!operation) return error(request.id, -32602, "Unknown tool."); |
| 61 | const input = (request.params?.arguments ?? {}) as Record<string, unknown>; |
| 62 | const outcome = await operation.run(env, viewer, input); |
| 63 | // A failed operation is a tool result the model can read and act on, |
| 64 | // not a protocol error. |
| 65 | return result(request.id, { |
| 66 | content: [ |
| 67 | { |
| 68 | type: "text", |
| 69 | text: outcome.ok |
| 70 | ? JSON.stringify(outcome.value, null, 2) |
| 71 | : outcome.error.message, |
| 72 | }, |
| 73 | ], |
| 74 | isError: !outcome.ok, |
| 75 | }); |
| 76 | } |
| 77 | default: |
| 78 | return error(request.id, -32601, `Method not found: ${request.method}`); |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | /** |
| 83 | * MCP over streamable HTTP. The server keeps no session state, so every |
| 84 | * POST is answered directly with JSON. |
| 85 | */ |
| 86 | export async function handleMcp( |
| 87 | request: Request, |
| 88 | env: ApiEnv, |
| 89 | viewer: Viewer, |
| 90 | ): Promise<Response> { |
| 91 | if (request.method !== "POST") { |
| 92 | return new Response(null, { status: 405, headers: { allow: "POST" } }); |
| 93 | } |
| 94 | let body: JsonRpcRequest | JsonRpcRequest[]; |
| 95 | try { |
| 96 | body = await request.json(); |
| 97 | } catch { |
| 98 | return Response.json(error(null, -32700, "Parse error"), { status: 400 }); |
| 99 | } |
| 100 | if (Array.isArray(body)) { |
| 101 | const responses = ( |
| 102 | await Promise.all(body.map((item) => handle(env, viewer, item))) |
| 103 | ).filter((response) => response !== null); |
| 104 | return responses.length |
| 105 | ? Response.json(responses) |
| 106 | : new Response(null, { status: 202 }); |
| 107 | } |
| 108 | const response = await handle(env, viewer, body); |
| 109 | return response ? Response.json(response) : new Response(null, { status: 202 }); |
| 110 | } |