Skip to content

Commit fb1953e

Browse files
perf: share McpServer instance across HTTP sessions
Each HTTP session was creating a new McpServer and re-registering all 180 tools, resources, and prompts. With 100 max sessions, that meant 100 copies of tool registrations in memory. Now a singleton template McpServer is created once at startup with all registrations. Per-session servers share the template's internal registration dictionaries and request handlers by reference, avoiding the overhead of 180+ registerTool() calls per connection. - Add createSessionServer() factory in server.ts - Singleton template created lazily on first HTTP session - tcp-server.ts uses createSessionServer() instead of createArcaneServer() - stdio entry point unchanged (still uses createArcaneServer()) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e49ba93 commit fb1953e

2 files changed

Lines changed: 98 additions & 4 deletions

File tree

src/server.ts

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
/**
22
* MCP Server factory for Arcane
3+
*
4+
* Provides two factory functions:
5+
* - createArcaneServer(): Full server for single-connection transports (stdio)
6+
* - createSessionServer(): Lightweight server for HTTP sessions that shares
7+
* tool/resource/prompt registrations from a singleton template to avoid
8+
* re-registering 180+ tools per session (PERF-01)
39
*/
410

511
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -14,7 +20,8 @@ const require = createRequire(import.meta.url);
1420
const { version: VERSION } = require("../package.json") as { version: string };
1521

1622
/**
17-
* Create and configure the Arcane MCP Server
23+
* Create and configure the Arcane MCP Server (full registration).
24+
* Used by stdio transport where there is only one connection.
1825
*/
1926
export function createArcaneServer(): McpServer {
2027
// Load configuration
@@ -41,4 +48,91 @@ export function createArcaneServer(): McpServer {
4148
return server;
4249
}
4350

44-
export default { createArcaneServer };
51+
// ---------------------------------------------------------------------------
52+
// Shared-template optimisation for HTTP sessions (PERF-01)
53+
// ---------------------------------------------------------------------------
54+
55+
/**
56+
* Singleton template McpServer. Created once on first call to
57+
* createSessionServer(). All tools, resources, and prompts are registered
58+
* on this instance; per-session servers share the registrations by
59+
* copying internal references rather than re-registering 180+ tools.
60+
*/
61+
let _template: McpServer | null = null;
62+
63+
/**
64+
* Initialise (or return) the singleton template.
65+
*/
66+
function getTemplate(): McpServer {
67+
if (_template) return _template;
68+
69+
loadConfig();
70+
logger.info(`Creating shared McpServer template v${VERSION}`);
71+
72+
_template = new McpServer({ name: "arcane", version: VERSION });
73+
74+
registerAllTools(_template);
75+
registerResources(_template);
76+
registerPrompts(_template);
77+
78+
logger.info("Shared McpServer template ready");
79+
return _template;
80+
}
81+
82+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
83+
type AnyRecord = Record<string, any>;
84+
85+
/**
86+
* Create a lightweight McpServer for an HTTP session.
87+
*
88+
* Instead of calling registerAllTools / registerResources / registerPrompts
89+
* (which would create 180+ tool registrations per session), this copies
90+
* the internal registration dictionaries and request-handler map from a
91+
* shared template so that every session shares the same tool definitions
92+
* and handler closures.
93+
*
94+
* The MCP SDK stores registrations in plain objects
95+
* (_registeredTools, _registeredResources, etc.) and request handlers in
96+
* a Map on the underlying Protocol/Server. By assigning these from the
97+
* template before connecting, the new McpServer is fully configured
98+
* without the overhead of individual registerTool() calls.
99+
*/
100+
export function createSessionServer(): McpServer {
101+
const template = getTemplate();
102+
const tpl = template as unknown as AnyRecord;
103+
104+
// Create a bare McpServer (no tool registrations)
105+
const session = new McpServer({ name: "arcane", version: VERSION });
106+
const ses = session as unknown as AnyRecord;
107+
108+
// --- Share registration dictionaries (read-only references) ---
109+
ses._registeredTools = tpl._registeredTools;
110+
ses._registeredResources = tpl._registeredResources;
111+
ses._registeredResourceTemplates = tpl._registeredResourceTemplates;
112+
ses._registeredPrompts = tpl._registeredPrompts;
113+
114+
// --- Copy initialisation flags so McpServer skips re-setup guards ---
115+
ses._toolHandlersInitialized = true;
116+
ses._resourceHandlersInitialized = true;
117+
ses._promptHandlersInitialized = true;
118+
ses._completionHandlerInitialized = tpl._completionHandlerInitialized;
119+
120+
// --- Copy request handlers & capabilities from the template's Server ---
121+
const tplServer = tpl.server as AnyRecord;
122+
const sesServer = ses.server as AnyRecord;
123+
124+
// _requestHandlers is a Map<string, handler>. We copy all entries so
125+
// that tools/list, tools/call, resources/list, etc. are already wired.
126+
const tplHandlers: Map<string, unknown> = tplServer._requestHandlers;
127+
const sesHandlers: Map<string, unknown> = sesServer._requestHandlers;
128+
for (const [method, handler] of tplHandlers) {
129+
sesHandlers.set(method, handler);
130+
}
131+
132+
// Capabilities must be set before connect() (SDK throws otherwise)
133+
sesServer._capabilities = { ...tplServer._capabilities };
134+
135+
return session;
136+
}
137+
138+
export default { createArcaneServer, createSessionServer };

src/tcp-server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import express, { type Request, type Response, type NextFunction } from "express";
1212
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
13-
import { createArcaneServer } from "./server.js";
13+
import { createSessionServer } from "./server.js";
1414
import { getConfig, loadConfig } from "./config.js";
1515
import { logger } from "./utils/logger.js";
1616
import {
@@ -280,7 +280,7 @@ export async function startTcpServer(): Promise<void> {
280280
},
281281
});
282282

283-
const server = createArcaneServer();
283+
const server = createSessionServer();
284284
await server.connect(transport);
285285

286286
// Set up cleanup handler

0 commit comments

Comments
 (0)