@cogitator-ai/mcp 17.0.10 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,7 +28,7 @@ import type { MCPServerConfig, MCPResourceConfig, MCPPromptConfig } from '../typ
28
28
  * ```
29
29
  */
30
30
  export declare class MCPServer {
31
- private server;
31
+ private server?;
32
32
  private config;
33
33
  private tools;
34
34
  private resources;
@@ -88,18 +88,40 @@ export declare class MCPServer {
88
88
  * Get list of registered prompt names
89
89
  */
90
90
  getRegisteredPrompts(): string[];
91
+ /**
92
+ * Build a fresh SDK McpServer instance and register every tool, resource
93
+ * and prompt from the local maps onto it.
94
+ *
95
+ * The maps are the single source of truth: registration with the SDK is
96
+ * deferred until start() (stdio) or until each incoming request (HTTP), so
97
+ * unregister* calls made before start() are fully honoured and concurrent
98
+ * HTTP requests never share a single server/transport binding.
99
+ */
100
+ private buildServer;
91
101
  /**
92
102
  * Register a tool with the MCP server
93
103
  */
94
104
  private registerMCPTool;
95
105
  /**
96
- * Build the input schema for MCP tool registration
106
+ * Resolve the Zod schema the SDK uses to advertise and validate tool input.
107
+ *
108
+ * The full tool schema is handed to the SDK (not just its shape) so object
109
+ * modifiers, refinements and transforms are honoured, and the SDK passes
110
+ * already-parsed arguments to the handler. Tools without a schema accept
111
+ * any object.
97
112
  */
98
113
  private buildInputSchema;
99
114
  /**
100
- * Execute a tool and return MCP-formatted result
115
+ * Execute a tool with SDK-validated arguments and return an MCP-formatted result
101
116
  */
102
117
  private executeTool;
118
+ /**
119
+ * Write a diagnostic line to stderr when logging is enabled.
120
+ *
121
+ * stdout is reserved for JSON-RPC frames on the stdio transport, so every
122
+ * log line goes to stderr regardless of level.
123
+ */
124
+ private log;
103
125
  /**
104
126
  * Register a resource with the MCP server
105
127
  */
@@ -116,6 +138,11 @@ export declare class MCPServer {
116
138
  * Start HTTP server for MCP
117
139
  */
118
140
  private startHttpServer;
141
+ /**
142
+ * Port the HTTP transport is listening on (useful with `port: 0`).
143
+ * Returns undefined for stdio or before the server has started.
144
+ */
145
+ getPort(): number | undefined;
119
146
  /**
120
147
  * Stop the server
121
148
  */
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../../src/server/mcp-server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAe,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EAEjB,eAAe,EAChB,MAAM,UAAU,CAAC;AAUlB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,SAAS,CAAwC;IACzD,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAC,CAA6B;gBAEpC,MAAM,EAAE,eAAe;IAQnC;;OAEG;IACH,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAS9B;;OAEG;IACH,aAAa,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI;IAMlC;;;;OAIG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAOrC;;OAEG;IACH,kBAAkB,IAAI,MAAM,EAAE;IAI9B;;OAEG;IACH,gBAAgB,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI;IASjD;;OAEG;IACH,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,IAAI;IAMrD;;;OAGG;IACH,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAOxC;;OAEG;IACH,sBAAsB,IAAI,MAAM,EAAE;IAIlC;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI;IAS7C;;OAEG;IACH,eAAe,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,IAAI;IAMjD;;;OAGG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAOvC;;OAEG;IACH,oBAAoB,IAAI,MAAM,EAAE;IAIhC;;OAEG;IACH,OAAO,CAAC,eAAe;IAavB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAUxB;;OAEG;YACW,WAAW;IAmDzB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAoE3B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAoDzB;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA0C5B;;OAEG;YACW,eAAe;IAiD7B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB3B;;OAEG;IACH,SAAS,IAAI,OAAO;CAGrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CAK9F"}
1
+ {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../../src/server/mcp-server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,IAAI,EAAe,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EAEjB,eAAe,EAGhB,MAAM,UAAU,CAAC;AAwDlB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAC,CAAY;IAC3B,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,SAAS,CAAwC;IACzD,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAC,CAAa;gBAEpB,MAAM,EAAE,eAAe;IAInC;;OAEG;IACH,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAQ9B;;OAEG;IACH,aAAa,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI;IAMlC;;;;OAIG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAOrC;;OAEG;IACH,kBAAkB,IAAI,MAAM,EAAE;IAI9B;;OAEG;IACH,gBAAgB,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI;IAQjD;;OAEG;IACH,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,IAAI;IAMrD;;;OAGG;IACH,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAOxC;;OAEG;IACH,sBAAsB,IAAI,MAAM,EAAE;IAIlC;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI;IAQ7C;;OAEG;IACH,eAAe,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,IAAI;IAMjD;;;OAGG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAOvC;;OAEG;IACH,oBAAoB,IAAI,MAAM,EAAE;IAIhC;;;;;;;;OAQG;IACH,OAAO,CAAC,WAAW;IAmBnB;;OAEG;IACH,OAAO,CAAC,eAAe;IAavB;;;;;;;OAOG;IACH,OAAO,CAAC,gBAAgB;IAKxB;;OAEG;YACW,WAAW;IAyBzB;;;;;OAKG;IACH,OAAO,CAAC,GAAG;IAQX;;OAEG;IACH,OAAO,CAAC,mBAAmB;IA2D3B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA8CzB;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmC5B;;OAEG;YACW,eAAe;IA8G7B;;;OAGG;IACH,OAAO,IAAI,MAAM,GAAG,SAAS;IAK7B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B3B;;OAEG;IACH,SAAS,IAAI,OAAO;CAGrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CAK9F"}
@@ -4,10 +4,45 @@
4
4
  * Exposes Cogitator tools as an MCP server that can be used by
5
5
  * other MCP clients (e.g., Claude Desktop, other AI assistants).
6
6
  */
7
+ import { randomUUID } from 'node:crypto';
7
8
  import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
8
9
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
- import { resultToMCPContent, zodToJsonSchema } from '../adapter/tool-adapter.js';
10
+ import { resultToMCPContent } from '../adapter/tool-adapter.js';
10
11
  import { z } from 'zod';
12
+ function toServerContent(item) {
13
+ switch (item.type) {
14
+ case 'text':
15
+ return { type: 'text', text: item.text ?? '' };
16
+ case 'image':
17
+ case 'audio':
18
+ if (typeof item.data === 'string' && typeof item.mimeType === 'string') {
19
+ return { type: item.type, data: item.data, mimeType: item.mimeType };
20
+ }
21
+ break;
22
+ case 'resource':
23
+ case 'resource_link': {
24
+ const resource = item.resource;
25
+ if (resource && typeof resource.blob === 'string') {
26
+ return {
27
+ type: 'resource',
28
+ resource: { uri: resource.uri, mimeType: resource.mimeType, blob: resource.blob },
29
+ };
30
+ }
31
+ if (resource && typeof resource.text === 'string') {
32
+ return {
33
+ type: 'resource',
34
+ resource: { uri: resource.uri, mimeType: resource.mimeType, text: resource.text },
35
+ };
36
+ }
37
+ break;
38
+ }
39
+ }
40
+ return { type: 'text', text: JSON.stringify(item) };
41
+ }
42
+ function errorMessageOf(error) {
43
+ return error instanceof Error ? error.message : String(error);
44
+ }
45
+ const PERMISSIVE_INPUT_SCHEMA = z.looseObject({});
11
46
  /**
12
47
  * MCP Server for exposing Cogitator tools
13
48
  *
@@ -39,10 +74,6 @@ export class MCPServer {
39
74
  httpServer;
40
75
  constructor(config) {
41
76
  this.config = config;
42
- this.server = new McpServer({
43
- name: config.name,
44
- version: config.version,
45
- });
46
77
  }
47
78
  /**
48
79
  * Register a single Cogitator tool
@@ -52,7 +83,6 @@ export class MCPServer {
52
83
  throw new Error('Cannot register tools after server has started');
53
84
  }
54
85
  this.tools.set(tool.name, tool);
55
- this.registerMCPTool(tool);
56
86
  }
57
87
  /**
58
88
  * Register multiple Cogitator tools
@@ -87,7 +117,6 @@ export class MCPServer {
87
117
  throw new Error('Cannot register resources after server has started');
88
118
  }
89
119
  this.resources.set(config.uri, config);
90
- this.registerMCPResource(config);
91
120
  }
92
121
  /**
93
122
  * Register multiple resources
@@ -121,7 +150,6 @@ export class MCPServer {
121
150
  throw new Error('Cannot register prompts after server has started');
122
151
  }
123
152
  this.prompts.set(config.name, config);
124
- this.registerMCPPrompt(config);
125
153
  }
126
154
  /**
127
155
  * Register multiple prompts
@@ -147,77 +175,93 @@ export class MCPServer {
147
175
  getRegisteredPrompts() {
148
176
  return Array.from(this.prompts.keys());
149
177
  }
178
+ /**
179
+ * Build a fresh SDK McpServer instance and register every tool, resource
180
+ * and prompt from the local maps onto it.
181
+ *
182
+ * The maps are the single source of truth: registration with the SDK is
183
+ * deferred until start() (stdio) or until each incoming request (HTTP), so
184
+ * unregister* calls made before start() are fully honoured and concurrent
185
+ * HTTP requests never share a single server/transport binding.
186
+ */
187
+ buildServer() {
188
+ const server = new McpServer({
189
+ name: this.config.name,
190
+ version: this.config.version,
191
+ });
192
+ for (const tool of this.tools.values()) {
193
+ this.registerMCPTool(server, tool);
194
+ }
195
+ for (const resource of this.resources.values()) {
196
+ this.registerMCPResource(server, resource);
197
+ }
198
+ for (const prompt of this.prompts.values()) {
199
+ this.registerMCPPrompt(server, prompt);
200
+ }
201
+ return server;
202
+ }
150
203
  /**
151
204
  * Register a tool with the MCP server
152
205
  */
153
- registerMCPTool(tool) {
154
- const inputSchema = this.buildInputSchema(tool);
155
- this.server.tool(tool.name, tool.description, inputSchema, async (args) => {
156
- return this.executeTool(tool, args);
206
+ registerMCPTool(server, tool) {
207
+ server.registerTool(tool.name, {
208
+ description: tool.description,
209
+ inputSchema: this.buildInputSchema(tool),
210
+ }, async (args, extra) => {
211
+ return this.executeTool(tool, args, extra.signal);
157
212
  });
158
213
  }
159
214
  /**
160
- * Build the input schema for MCP tool registration
215
+ * Resolve the Zod schema the SDK uses to advertise and validate tool input.
216
+ *
217
+ * The full tool schema is handed to the SDK (not just its shape) so object
218
+ * modifiers, refinements and transforms are honoured, and the SDK passes
219
+ * already-parsed arguments to the handler. Tools without a schema accept
220
+ * any object.
161
221
  */
162
222
  buildInputSchema(tool) {
163
- if (tool.parameters) {
164
- const jsonSchema = zodToJsonSchema(tool.parameters);
165
- return jsonSchema.properties ?? {};
166
- }
167
- const schema = tool.toJSON();
168
- return schema.parameters.properties;
223
+ const params = tool.parameters;
224
+ return params instanceof z.ZodType ? params : PERMISSIVE_INPUT_SCHEMA;
169
225
  }
170
226
  /**
171
- * Execute a tool and return MCP-formatted result
227
+ * Execute a tool with SDK-validated arguments and return an MCP-formatted result
172
228
  */
173
- async executeTool(tool, args) {
229
+ async executeTool(tool, args, signal) {
174
230
  const context = {
175
231
  agentId: 'mcp-server',
176
- runId: `mcp_${Date.now()}`,
177
- signal: new AbortController().signal,
232
+ runId: `mcp_${randomUUID()}`,
233
+ signal,
178
234
  };
179
235
  try {
180
- let validatedArgs = args;
181
- if (tool.parameters) {
182
- const parseResult = tool.parameters.safeParse(args);
183
- if (!parseResult.success) {
184
- return {
185
- content: [
186
- {
187
- type: 'text',
188
- text: `Validation error: ${parseResult.error.message}`,
189
- },
190
- ],
191
- isError: true,
192
- };
193
- }
194
- validatedArgs = parseResult.data;
195
- }
196
- const result = await tool.execute(validatedArgs, context);
197
- const rawContent = resultToMCPContent(result);
198
- const content = rawContent.map((item) => {
199
- if (item.type === 'text') {
200
- return { type: 'text', text: item.text };
201
- }
202
- return { type: 'text', text: JSON.stringify(item) };
203
- });
204
- return { content };
236
+ const result = await tool.execute(args ?? {}, context);
237
+ return { content: resultToMCPContent(result).map(toServerContent) };
205
238
  }
206
239
  catch (error) {
207
- const errorMessage = error instanceof Error ? error.message : String(error);
208
- if (this.config.logging) {
209
- console.error(`[MCPServer] Tool ${tool.name} error:`, errorMessage);
210
- }
240
+ const errorMessage = errorMessageOf(error);
241
+ this.log('error', `Tool ${tool.name} error: ${errorMessage}`);
211
242
  return {
212
243
  content: [{ type: 'text', text: `Error: ${errorMessage}` }],
213
244
  isError: true,
214
245
  };
215
246
  }
216
247
  }
248
+ /**
249
+ * Write a diagnostic line to stderr when logging is enabled.
250
+ *
251
+ * stdout is reserved for JSON-RPC frames on the stdio transport, so every
252
+ * log line goes to stderr regardless of level.
253
+ */
254
+ log(level, message) {
255
+ if (!this.config.logging) {
256
+ return;
257
+ }
258
+ const prefix = level === 'error' ? '[MCPServer] ERROR' : '[MCPServer]';
259
+ console.error(`${prefix} ${message}`);
260
+ }
217
261
  /**
218
262
  * Register a resource with the MCP server
219
263
  */
220
- registerMCPResource(config) {
264
+ registerMCPResource(server, config) {
221
265
  const isTemplate = config.uri.includes('{');
222
266
  const formatContents = (result, uriHref) => {
223
267
  const contents = Array.isArray(result) ? result : [result];
@@ -228,100 +272,84 @@ export class MCPServer {
228
272
  if (c.mimeType || config.mimeType) {
229
273
  base.mimeType = c.mimeType || config.mimeType;
230
274
  }
231
- if (c.blob) {
275
+ if (typeof c.blob === 'string') {
232
276
  return { ...base, blob: c.blob };
233
277
  }
234
- return { ...base, text: c.text || '' };
278
+ return { ...base, text: c.text ?? '' };
235
279
  });
236
280
  };
281
+ const readResource = async (params) => {
282
+ try {
283
+ return await config.read(params);
284
+ }
285
+ catch (error) {
286
+ this.log('error', `Resource ${config.name} error: ${errorMessageOf(error)}`);
287
+ throw error;
288
+ }
289
+ };
237
290
  if (isTemplate) {
238
- this.server.registerResource(config.name, new ResourceTemplate(config.uri, { list: undefined }), {
291
+ server.registerResource(config.name, new ResourceTemplate(config.uri, { list: undefined }), {
239
292
  description: config.description,
240
293
  mimeType: config.mimeType,
241
294
  }, async (uri, variables) => {
242
- try {
243
- const params = {};
244
- for (const [key, value] of Object.entries(variables)) {
245
- params[key] = Array.isArray(value) ? value[0] || '' : value;
246
- }
247
- const result = await config.read(params);
248
- return { contents: formatContents(result, uri.href) };
249
- }
250
- catch (error) {
251
- const errorMessage = error instanceof Error ? error.message : String(error);
252
- if (this.config.logging) {
253
- console.error(`[MCPServer] Resource ${config.name} error:`, errorMessage);
254
- }
255
- return { contents: [{ uri: uri.href, text: `Error: ${errorMessage}` }] };
295
+ const params = {};
296
+ for (const [key, value] of Object.entries(variables)) {
297
+ params[key] = Array.isArray(value) ? value.join(',') : value;
256
298
  }
299
+ return { contents: formatContents(await readResource(params), uri.href) };
257
300
  });
258
301
  }
259
302
  else {
260
- this.server.registerResource(config.name, config.uri, {
303
+ server.registerResource(config.name, config.uri, {
261
304
  description: config.description,
262
305
  mimeType: config.mimeType,
263
306
  }, async (uri) => {
264
- try {
265
- const result = await config.read({});
266
- return { contents: formatContents(result, uri.href) };
267
- }
268
- catch (error) {
269
- const errorMessage = error instanceof Error ? error.message : String(error);
270
- if (this.config.logging) {
271
- console.error(`[MCPServer] Resource ${config.name} error:`, errorMessage);
272
- }
273
- return { contents: [{ uri: uri.href, text: `Error: ${errorMessage}` }] };
274
- }
307
+ return { contents: formatContents(await readResource({}), uri.href) };
275
308
  });
276
309
  }
277
310
  }
278
311
  /**
279
312
  * Register a prompt with the MCP server
280
313
  */
281
- registerMCPPrompt(config) {
282
- const argsSchema = {};
283
- for (const arg of config.arguments || []) {
284
- let schema = z.string();
285
- if (arg.description) {
286
- schema = schema.describe(arg.description);
287
- }
288
- if (!arg.required) {
289
- schema = schema.optional();
290
- }
291
- argsSchema[arg.name] = schema;
292
- }
293
- this.server.registerPrompt(config.name, {
294
- title: config.title || config.name,
295
- description: config.description,
296
- argsSchema,
297
- }, async (args) => {
314
+ registerMCPPrompt(server, config) {
315
+ const render = async (args) => {
316
+ let result;
298
317
  try {
299
- const result = await config.get(args);
300
- return {
301
- description: result.description,
302
- messages: result.messages.map((m) => ({
303
- role: m.role,
304
- content: {
305
- type: 'text',
306
- text: typeof m.content === 'string' ? m.content : m.content.text || '',
307
- },
308
- })),
309
- };
318
+ result = await config.get(args);
310
319
  }
311
320
  catch (error) {
312
- const errorMessage = error instanceof Error ? error.message : String(error);
313
- if (this.config.logging) {
314
- console.error(`[MCPServer] Prompt ${config.name} error:`, errorMessage);
321
+ this.log('error', `Prompt ${config.name} error: ${errorMessageOf(error)}`);
322
+ throw error;
323
+ }
324
+ return {
325
+ description: result.description,
326
+ messages: result.messages.map((m) => ({
327
+ role: m.role,
328
+ content: typeof m.content === 'string'
329
+ ? { type: 'text', text: m.content }
330
+ : toServerContent(m.content),
331
+ })),
332
+ };
333
+ };
334
+ const metadata = { title: config.title || config.name, description: config.description };
335
+ const promptArgs = config.arguments ?? [];
336
+ if (promptArgs.length === 0) {
337
+ server.registerPrompt(config.name, metadata, async () => render({}));
338
+ return;
339
+ }
340
+ const argsSchema = {};
341
+ for (const arg of promptArgs) {
342
+ const base = arg.description ? z.string().describe(arg.description) : z.string();
343
+ argsSchema[arg.name] = arg.required ? base : base.optional();
344
+ }
345
+ server.registerPrompt(config.name, { ...metadata, argsSchema }, async (args) => {
346
+ const stringArgs = {};
347
+ for (const [key, value] of Object.entries(args ?? {})) {
348
+ if (typeof value === 'string') {
349
+ stringArgs[key] = value;
315
350
  }
316
- return {
317
- messages: [
318
- {
319
- role: 'assistant',
320
- content: { type: 'text', text: `Error: ${errorMessage}` },
321
- },
322
- ],
323
- };
324
351
  }
352
+ return render(stringArgs);
325
353
  });
326
354
  }
327
355
  /**
@@ -331,14 +359,13 @@ export class MCPServer {
331
359
  if (this.started) {
332
360
  throw new Error('Server already started');
333
361
  }
334
- if (this.config.logging) {
335
- console.log(`[MCPServer] Starting ${this.config.name} v${this.config.version}`);
336
- console.log(`[MCPServer] Registered tools: ${this.getRegisteredTools().join(', ') || '(none)'}`);
337
- console.log(`[MCPServer] Registered resources: ${this.getRegisteredResources().join(', ') || '(none)'}`);
338
- console.log(`[MCPServer] Registered prompts: ${this.getRegisteredPrompts().join(', ') || '(none)'}`);
339
- }
362
+ this.log('info', `Starting ${this.config.name} v${this.config.version}`);
363
+ this.log('info', `Registered tools: ${this.getRegisteredTools().join(', ') || '(none)'}`);
364
+ this.log('info', `Registered resources: ${this.getRegisteredResources().join(', ') || '(none)'}`);
365
+ this.log('info', `Registered prompts: ${this.getRegisteredPrompts().join(', ') || '(none)'}`);
340
366
  switch (this.config.transport) {
341
367
  case 'stdio': {
368
+ this.server = this.buildServer();
342
369
  const transport = new StdioServerTransport();
343
370
  await this.server.connect(transport);
344
371
  break;
@@ -352,9 +379,7 @@ export class MCPServer {
352
379
  throw new Error(`Unknown transport: ${this.config.transport}`);
353
380
  }
354
381
  this.started = true;
355
- if (this.config.logging) {
356
- console.log(`[MCPServer] Server started on ${this.config.transport} transport`);
357
- }
382
+ this.log('info', `Server started on ${this.config.transport} transport`);
358
383
  }
359
384
  /**
360
385
  * Start HTTP server for MCP
@@ -364,39 +389,107 @@ export class MCPServer {
364
389
  const { StreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/streamableHttp.js');
365
390
  const port = this.config.port ?? 3000;
366
391
  const host = this.config.host ?? 'localhost';
392
+ const maxBodySize = this.config.maxBodySize ?? 10 * 1024 * 1024;
393
+ const corsOrigin = this.config.corsOrigin ?? '*';
367
394
  this.httpServer = createServer(async (req, res) => {
368
- res.setHeader('Access-Control-Allow-Origin', '*');
369
- res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
370
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
395
+ res.setHeader('Access-Control-Allow-Origin', corsOrigin);
396
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
397
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');
398
+ res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
371
399
  if (req.method === 'OPTIONS') {
372
400
  res.writeHead(204);
373
401
  res.end();
374
402
  return;
375
403
  }
376
- if (req.method !== 'POST' || req.url !== '/mcp') {
404
+ const pathname = new URL(req.url ?? '/', 'http://localhost').pathname;
405
+ if (pathname !== '/mcp') {
377
406
  res.writeHead(404);
378
407
  res.end('Not Found');
379
408
  return;
380
409
  }
381
- const chunks = [];
382
- for await (const chunk of req) {
383
- chunks.push(chunk);
384
- }
385
- const body = JSON.parse(Buffer.concat(chunks).toString());
410
+ const server = this.buildServer();
386
411
  const transport = new StreamableHTTPServerTransport({
387
412
  sessionIdGenerator: undefined,
388
413
  });
389
- await this.server.connect(transport);
390
- await transport.handleRequest(req, res, body);
391
- });
392
- await new Promise((resolve) => {
393
- this.httpServer.listen(port, host, () => {
394
- if (this.config.logging) {
395
- console.log(`[MCPServer] HTTP server listening on http://${host}:${port}/mcp`);
396
- }
397
- resolve();
414
+ res.on('close', () => {
415
+ transport.close().catch(() => { });
416
+ server.close().catch(() => { });
398
417
  });
418
+ try {
419
+ await server.connect(transport);
420
+ if (req.method === 'POST') {
421
+ const chunks = [];
422
+ let totalSize = 0;
423
+ let tooLarge = false;
424
+ for await (const chunk of req) {
425
+ const buffer = chunk;
426
+ totalSize += buffer.length;
427
+ if (totalSize > maxBodySize) {
428
+ tooLarge = true;
429
+ break;
430
+ }
431
+ chunks.push(buffer);
432
+ }
433
+ if (tooLarge) {
434
+ res.writeHead(413, { 'Content-Type': 'text/plain' });
435
+ res.end('Payload Too Large');
436
+ req.destroy();
437
+ return;
438
+ }
439
+ let body;
440
+ try {
441
+ body = JSON.parse(Buffer.concat(chunks).toString());
442
+ }
443
+ catch {
444
+ res.writeHead(400, { 'Content-Type': 'application/json' });
445
+ res.end(JSON.stringify({
446
+ jsonrpc: '2.0',
447
+ error: { code: -32700, message: 'Parse error: invalid JSON' },
448
+ id: null,
449
+ }));
450
+ return;
451
+ }
452
+ await transport.handleRequest(req, res, body);
453
+ }
454
+ else if (req.method === 'GET' || req.method === 'DELETE') {
455
+ await transport.handleRequest(req, res);
456
+ }
457
+ else {
458
+ res.writeHead(405, { 'Content-Type': 'text/plain' });
459
+ res.end('Method Not Allowed');
460
+ }
461
+ }
462
+ catch (error) {
463
+ this.log('error', `HTTP request error: ${errorMessageOf(error)}`);
464
+ if (!res.headersSent) {
465
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
466
+ res.end('Internal Server Error');
467
+ }
468
+ }
399
469
  });
470
+ const httpServer = this.httpServer;
471
+ try {
472
+ await new Promise((resolve, reject) => {
473
+ httpServer.once('error', reject);
474
+ httpServer.listen(port, host, () => {
475
+ httpServer.off('error', reject);
476
+ resolve();
477
+ });
478
+ });
479
+ }
480
+ catch (error) {
481
+ this.httpServer = undefined;
482
+ throw error;
483
+ }
484
+ this.log('info', `HTTP server listening on http://${host}:${this.getPort() ?? port}/mcp`);
485
+ }
486
+ /**
487
+ * Port the HTTP transport is listening on (useful with `port: 0`).
488
+ * Returns undefined for stdio or before the server has started.
489
+ */
490
+ getPort() {
491
+ const address = this.httpServer?.address();
492
+ return address && typeof address === 'object' ? address.port : undefined;
400
493
  }
401
494
  /**
402
495
  * Stop the server
@@ -405,17 +498,25 @@ export class MCPServer {
405
498
  if (!this.started) {
406
499
  return;
407
500
  }
408
- if (this.httpServer) {
409
- await new Promise((resolve, reject) => {
410
- this.httpServer.close((err) => (err ? reject(err) : resolve()));
411
- });
412
- this.httpServer = undefined;
501
+ try {
502
+ const httpServer = this.httpServer;
503
+ if (httpServer) {
504
+ this.httpServer = undefined;
505
+ await new Promise((resolve, reject) => {
506
+ httpServer.close((err) => (err ? reject(err) : resolve()));
507
+ httpServer.closeAllConnections();
508
+ });
509
+ }
413
510
  }
414
- await this.server.close();
415
- this.started = false;
416
- if (this.config.logging) {
417
- console.log('[MCPServer] Server stopped');
511
+ finally {
512
+ const server = this.server;
513
+ this.server = undefined;
514
+ this.started = false;
515
+ if (server) {
516
+ await server.close();
517
+ }
418
518
  }
519
+ this.log('info', 'Server stopped');
419
520
  }
420
521
  /**
421
522
  * Check if server is running