Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

mcp.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

mcp.tsBlame209 lines · 1 contributor
2c2163eClaude1/**
2 * Model Context Protocol (MCP) server — minimal JSON-RPC 2.0 router.
3 *
4 * MCP: https://spec.modelcontextprotocol.io/
5 *
6 * v1 scope (this file):
7 * - Streamable HTTP transport at POST /mcp
8 * - initialize / initialized handshake
9 * - tools/list (static manifest)
10 * - tools/call dispatching to a small set of read-only tools
11 *
12 * Out of scope for v1 (future moves):
13 * - resources/list + resources/read (the bible files, repo READMEs)
14 * - prompts/* (saved-replies as MCP prompts)
15 * - server-sent notifications (logs streaming)
16 * - oauth flow + per-call auth (we accept PAT + OAuth bearer for now)
17 *
18 * Pure JSON-RPC routing here — the per-tool handlers live in
19 * `mcp-tools.ts`. This split lets the router be tested without DB.
20 */
21
22import type { McpToolHandler, McpTool } from "./mcp-tools";
23
24export const MCP_PROTOCOL_VERSION = "2025-06-18";
25export const MCP_SERVER_NAME = "gluecron";
26export const MCP_SERVER_VERSION = "1.0";
27
28export type JsonRpcRequest = {
29 jsonrpc: "2.0";
30 id?: string | number | null;
31 method: string;
32 params?: unknown;
33};
34
35export type JsonRpcResponse =
36 | {
37 jsonrpc: "2.0";
38 id: string | number | null;
39 result: unknown;
40 }
41 | {
42 jsonrpc: "2.0";
43 id: string | number | null;
44 error: { code: number; message: string; data?: unknown };
45 };
46
47// JSON-RPC standard error codes
48export const ERR_PARSE = -32700;
49export const ERR_INVALID_REQUEST = -32600;
50export const ERR_METHOD_NOT_FOUND = -32601;
51export const ERR_INVALID_PARAMS = -32602;
52export const ERR_INTERNAL = -32603;
53
54export type McpContext = {
55 /** Authenticated user id; null for anonymous (only allowed for some calls). */
56 userId: string | null;
57};
58
59export type McpRouterArgs = {
60 ctx: McpContext;
61 tools: Record<string, McpToolHandler>;
62};
63
64function isJsonRpcRequest(v: unknown): v is JsonRpcRequest {
65 return (
66 !!v &&
67 typeof v === "object" &&
68 (v as JsonRpcRequest).jsonrpc === "2.0" &&
69 typeof (v as JsonRpcRequest).method === "string"
70 );
71}
72
73/**
74 * Route a single JSON-RPC request. Returns the response shape (or null
75 * for notifications, which have no `id`). Never throws — internal
76 * exceptions are caught and re-shaped as `-32603 internal error`.
77 */
78export async function routeMcpRequest(
79 req: unknown,
80 args: McpRouterArgs
81): Promise<JsonRpcResponse | null> {
82 if (!isJsonRpcRequest(req)) {
83 return {
84 jsonrpc: "2.0",
85 id: null,
86 error: { code: ERR_INVALID_REQUEST, message: "Invalid JSON-RPC request" },
87 };
88 }
89
90 const id = req.id ?? null;
91 const isNotification = req.id === undefined;
92
93 try {
94 const result = await dispatch(req.method, req.params, args);
95 if (isNotification) return null;
96 return { jsonrpc: "2.0", id, result };
97 } catch (err) {
98 if (isNotification) return null;
99 if (err instanceof McpError) {
100 return {
101 jsonrpc: "2.0",
102 id,
103 error: { code: err.code, message: err.message, data: err.data },
104 };
105 }
106 return {
107 jsonrpc: "2.0",
108 id,
109 error: {
110 code: ERR_INTERNAL,
111 message: err instanceof Error ? err.message : "internal error",
112 },
113 };
114 }
115}
116
117export class McpError extends Error {
118 code: number;
119 data?: unknown;
120 constructor(code: number, message: string, data?: unknown) {
121 super(message);
122 this.code = code;
123 this.data = data;
124 }
125}
126
127async function dispatch(
128 method: string,
129 params: unknown,
130 args: McpRouterArgs
131): Promise<unknown> {
132 switch (method) {
133 case "initialize":
134 return handleInitialize();
135 case "notifications/initialized":
136 // Client → server notification, no response.
137 return null;
138 case "tools/list":
139 return handleToolsList(args.tools);
140 case "tools/call":
141 return handleToolsCall(params, args);
142 case "ping":
143 return {};
144 default:
145 throw new McpError(
146 ERR_METHOD_NOT_FOUND,
147 `Method not supported: ${method}`
148 );
149 }
150}
151
152function handleInitialize() {
153 return {
154 protocolVersion: MCP_PROTOCOL_VERSION,
155 capabilities: {
156 tools: { listChanged: false },
157 },
158 serverInfo: {
159 name: MCP_SERVER_NAME,
160 version: MCP_SERVER_VERSION,
161 },
162 };
163}
164
165function handleToolsList(
166 tools: Record<string, McpToolHandler>
167): { tools: McpTool[] } {
168 return {
169 tools: Object.values(tools).map((t) => t.tool),
170 };
171}
172
173async function handleToolsCall(
174 params: unknown,
175 args: McpRouterArgs
176): Promise<unknown> {
177 if (!params || typeof params !== "object") {
178 throw new McpError(ERR_INVALID_PARAMS, "tools/call requires {name, arguments}");
179 }
180 const p = params as { name?: unknown; arguments?: unknown };
181 const name = typeof p.name === "string" ? p.name : "";
182 if (!name) {
183 throw new McpError(ERR_INVALID_PARAMS, "tools/call requires `name`");
184 }
185 const handler = args.tools[name];
186 if (!handler) {
187 throw new McpError(ERR_METHOD_NOT_FOUND, `Unknown tool: ${name}`);
188 }
189 const toolArgs =
190 p.arguments && typeof p.arguments === "object"
191 ? (p.arguments as Record<string, unknown>)
192 : {};
193 // The MCP tools/call result shape is `{ content: [{type, text}], isError? }`.
194 // Tool handlers may either return that shape directly or just a value
195 // we wrap. Errors from handlers re-throw as McpError so the wrapper
196 // can re-shape them.
197 const out = await handler.run(toolArgs, args.ctx);
198 if (out && typeof out === "object" && Array.isArray((out as any).content)) {
199 return out;
200 }
201 return {
202 content: [
203 {
204 type: "text",
205 text: typeof out === "string" ? out : JSON.stringify(out, null, 2),
206 },
207 ],
208 };
209}