Skip to content

Commit f1cc067

Browse files
Ripwordsclaude
andcommitted
feat(mcp): per-request McpServer factory with both tools
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b9e6a8d commit f1cc067

1 file changed

Lines changed: 62 additions & 0 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
2+
import { listProjectsTool } from "./tools/projects"
3+
import { getTicketTool } from "./tools/tickets"
4+
import type { McpRequestContext } from "./context"
5+
import { McpToolError } from "./errors"
6+
7+
/**
8+
* Per-request MCP server factory. We instantiate fresh per request rather
9+
* than caching a singleton because each tool handler closes over the
10+
* request's userId/clientId — a singleton would force us to thread context
11+
* through every call site, which is messier than recreating the server.
12+
* Construction is cheap (just registerTool calls).
13+
*/
14+
export function buildMcpServer(ctx: McpRequestContext): McpServer {
15+
const server = new McpServer({ name: "repro", version: "0.5.0" })
16+
17+
server.registerTool(listProjectsTool.name, listProjectsTool.config, async (input) => {
18+
try {
19+
return await listProjectsTool.handler(input as Record<string, never>, ctx)
20+
} catch (err) {
21+
return toolErrorResult(err)
22+
}
23+
})
24+
25+
server.registerTool(getTicketTool.name, getTicketTool.config, async (input) => {
26+
try {
27+
return await getTicketTool.handler(input as { ticketId: string }, ctx)
28+
} catch (err) {
29+
return toolErrorResult(err)
30+
}
31+
})
32+
33+
return server
34+
}
35+
36+
function toolErrorResult(err: unknown): {
37+
content: Array<{ type: "text"; text: string }>
38+
isError: true
39+
} {
40+
if (err instanceof McpToolError) {
41+
return {
42+
content: [{ type: "text", text: `${err.code}: ${err.message}` }],
43+
isError: true,
44+
}
45+
}
46+
// h3 createError thrown from requireProjectRoleByUser
47+
if (
48+
err &&
49+
typeof err === "object" &&
50+
"statusCode" in err &&
51+
typeof (err as { statusCode: unknown }).statusCode === "number"
52+
) {
53+
const e = err as { statusCode: number; statusMessage?: string }
54+
const code = e.statusCode === 403 ? "FORBIDDEN" : e.statusCode === 404 ? "NOT_FOUND" : "ERROR"
55+
return {
56+
content: [{ type: "text", text: `${code}: ${e.statusMessage ?? "request failed"}` }],
57+
isError: true,
58+
}
59+
}
60+
const msg = err instanceof Error ? err.message : String(err)
61+
return { content: [{ type: "text", text: `ERROR: ${msg}` }], isError: true }
62+
}

0 commit comments

Comments
 (0)