From 0d51b78380d6c979a82bbd8208f1dae4f4f93fd8 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Sat, 24 Jan 2026 00:06:38 +0800 Subject: [PATCH] fix(sample-servers): use factory function for MCP server in stateless mode --- .../sample-servers/src/todo-manager/index.ts | 178 ++++++++++-------- packages/sample-servers/src/whoami/index.js | 55 +++--- packages/sample-servers/src/whoami/index.ts | 55 +++--- 3 files changed, 163 insertions(+), 125 deletions(-) diff --git a/packages/sample-servers/src/todo-manager/index.ts b/packages/sample-servers/src/todo-manager/index.ts index 54eb899..96883a2 100644 --- a/packages/sample-servers/src/todo-manager/index.ts +++ b/packages/sample-servers/src/todo-manager/index.ts @@ -22,6 +22,7 @@ if (!MCP_RESOURCE_IDENTIFIER) { throw new Error('MCP_RESOURCE_IDENTIFIER environment variable is required'); } +// TodoService is a singleton since we need to share state across requests const todoService = new TodoService(); const assertUserId = (authInfo?: AuthInfo) => { @@ -33,108 +34,113 @@ const hasRequiredScopes = (userScopes: string[], requiredScopes: string[]): bool return requiredScopes.every((scope) => userScopes.includes(scope)); }; -// Create an MCP server -const server = new McpServer({ - name: 'Todo Manager', - version: '0.0.0', -}); +// Factory function to create an MCP server instance +// In stateless mode, each request needs its own server instance +const createMcpServer = () => { + const mcpServer = new McpServer({ + name: 'Todo Manager', + version: '0.0.0', + }); -server.registerTool( - 'create-todo', - { - description: 'Create a new todo', - inputSchema: { content: z.string() }, - }, - ({ content }, { authInfo }) => { - const userId = assertUserId(authInfo); - - /** - * Only users with 'create:todos' scope can create todos - */ - if (!hasRequiredScopes(authInfo?.scopes ?? [], ['create:todos'])) { - throw new MCPAuthBearerAuthError('missing_required_scopes'); - } + mcpServer.registerTool( + 'create-todo', + { + description: 'Create a new todo', + inputSchema: { content: z.string() }, + }, + ({ content }, { authInfo }) => { + const userId = assertUserId(authInfo); - const createdTodo = todoService.createTodo({ content, ownerId: userId }); + /** + * Only users with 'create:todos' scope can create todos + */ + if (!hasRequiredScopes(authInfo?.scopes ?? [], ['create:todos'])) { + throw new MCPAuthBearerAuthError('missing_required_scopes'); + } - return { - content: [{ type: 'text', text: JSON.stringify(createdTodo) }], - }; - } -); + const createdTodo = todoService.createTodo({ content, ownerId: userId }); -server.registerTool( - 'get-todos', - { - description: 'List all todos', - inputSchema: {}, - }, - (_params, { authInfo }) => { - const userId = assertUserId(authInfo); - - /** - * If user has 'read:todos' scope, they can access all todos (todoOwnerId = undefined) - * If user doesn't have 'read:todos' scope, they can only access their own todos (todoOwnerId = userId) - */ - const todoOwnerId = hasRequiredScopes(authInfo?.scopes ?? [], ['read:todos']) - ? undefined - : userId; - - const todos = todoService.getAllTodos(todoOwnerId); - - return { - content: [{ type: 'text', text: JSON.stringify(todos) }], - }; - } -); + return { + content: [{ type: 'text', text: JSON.stringify(createdTodo) }], + }; + } + ); -server.registerTool( - 'delete-todo', - { - description: 'Delete a todo by id', - inputSchema: { id: z.string() }, - }, - ({ id }, { authInfo }) => { - const userId = assertUserId(authInfo); + mcpServer.registerTool( + 'get-todos', + { + description: 'List all todos', + inputSchema: {}, + }, + (_params, { authInfo }) => { + const userId = assertUserId(authInfo); - const todo = todoService.getTodoById(id); + /** + * If user has 'read:todos' scope, they can access all todos (todoOwnerId = undefined) + * If user doesn't have 'read:todos' scope, they can only access their own todos (todoOwnerId = userId) + */ + const todoOwnerId = hasRequiredScopes(authInfo?.scopes ?? [], ['read:todos']) + ? undefined + : userId; + + const todos = todoService.getAllTodos(todoOwnerId); - if (!todo) { return { - content: [{ type: 'text', text: JSON.stringify({ error: 'Failed to delete todo' }) }], + content: [{ type: 'text', text: JSON.stringify(todos) }], }; } + ); + + mcpServer.registerTool( + 'delete-todo', + { + description: 'Delete a todo by id', + inputSchema: { id: z.string() }, + }, + ({ id }, { authInfo }) => { + const userId = assertUserId(authInfo); + + const todo = todoService.getTodoById(id); + + if (!todo) { + return { + content: [{ type: 'text', text: JSON.stringify({ error: 'Failed to delete todo' }) }], + }; + } + + /** + * Users can only delete their own todos + * Users with 'delete:todos' scope can delete any todo + */ + if (todo.ownerId !== userId && !hasRequiredScopes(authInfo?.scopes ?? [], ['delete:todos'])) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ error: 'Failed to delete todo' }), + }, + ], + }; + } + + const deletedTodo = todoService.deleteTodo(id); - /** - * Users can only delete their own todos - * Users with 'delete:todos' scope can delete any todo - */ - if (todo.ownerId !== userId && !hasRequiredScopes(authInfo?.scopes ?? [], ['delete:todos'])) { return { content: [ { type: 'text', - text: JSON.stringify({ error: 'Failed to delete todo' }), + text: JSON.stringify({ + message: `Todo ${id} deleted`, + details: deletedTodo, + }), }, ], }; } + ); - const deletedTodo = todoService.deleteTodo(id); - - return { - content: [ - { - type: 'text', - text: JSON.stringify({ - message: `Todo ${id} deleted`, - details: deletedTodo, - }), - }, - ], - }; - } -); + return mcpServer; +}; const mcpAuth = new MCPAuth({ protectedResources: { @@ -158,11 +164,17 @@ app.use( ); app.post('/', async (request, response) => { + // In stateless mode, create a new instance of transport and server for each request + // to ensure complete isolation. A single instance would cause request ID collisions + // when multiple clients connect concurrently. + const mcpServer = createMcpServer(); + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); - await server.connect(transport); + await mcpServer.connect(transport); await transport.handleRequest(request, response, request.body); response.on('close', () => { void transport.close(); + void mcpServer.close(); }); }); diff --git a/packages/sample-servers/src/whoami/index.js b/packages/sample-servers/src/whoami/index.js index ab17b12..01fb9fb 100644 --- a/packages/sample-servers/src/whoami/index.js +++ b/packages/sample-servers/src/whoami/index.js @@ -12,27 +12,35 @@ import { fetchServerConfig, MCPAuth, MCPAuthTokenVerificationError } from 'mcp-a configDotenv(); -// Create an MCP server -const server = new McpServer({ - name: 'WhoAmI', - version: '0.0.0', -}); +// Factory function to create an MCP server instance +// In stateless mode, each request needs its own server instance +const createMcpServer = () => { + const mcpServer = new McpServer({ + name: 'WhoAmI', + version: '0.0.0', + }); -// Add a tool to the server that returns the current user's information -server.registerTool( - 'whoami', - { - description: 'Get the current user information', - inputSchema: {}, - }, - (_params, { authInfo }) => { - return { - content: [ - { type: 'text', text: JSON.stringify(authInfo?.claims ?? { error: 'Not authenticated' }) }, - ], - }; - } -); + // Add a tool to the server that returns the current user's information + mcpServer.registerTool( + 'whoami', + { + description: 'Get the current user information', + inputSchema: {}, + }, + (_params, { authInfo }) => { + return { + content: [ + { + type: 'text', + text: JSON.stringify(authInfo?.claims ?? { error: 'Not authenticated' }), + }, + ], + }; + } + ); + + return mcpServer; +}; const { MCP_AUTH_ISSUER } = process.env; @@ -91,11 +99,16 @@ app.use(mcpAuth.delegatedRouter()); app.use(mcpAuth.bearerAuth(verifyToken)); app.post('/', async (request, response) => { + // In stateless mode, create a new instance of transport and server for each request + // to ensure complete isolation. A single instance would cause request ID collisions + // when multiple clients connect concurrently. + const mcpServer = createMcpServer(); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); - await server.connect(transport); + await mcpServer.connect(transport); await transport.handleRequest(request, response, request.body); response.on('close', () => { transport.close(); + mcpServer.close(); }); }); diff --git a/packages/sample-servers/src/whoami/index.ts b/packages/sample-servers/src/whoami/index.ts index c328473..f24e0cd 100644 --- a/packages/sample-servers/src/whoami/index.ts +++ b/packages/sample-servers/src/whoami/index.ts @@ -18,27 +18,35 @@ import { configDotenv(); -// Create an MCP server -const server = new McpServer({ - name: 'WhoAmI', - version: '0.0.0', -}); +// Factory function to create an MCP server instance +// In stateless mode, each request needs its own server instance +const createMcpServer = () => { + const mcpServer = new McpServer({ + name: 'WhoAmI', + version: '0.0.0', + }); -// Add a tool to the server that returns the current user's information -server.registerTool( - 'whoami', - { - description: 'Get the current user information', - inputSchema: {}, - }, - (_params, { authInfo }) => { - return { - content: [ - { type: 'text', text: JSON.stringify(authInfo?.claims ?? { error: 'Not authenticated' }) }, - ], - }; - } -); + // Add a tool to the server that returns the current user's information + mcpServer.registerTool( + 'whoami', + { + description: 'Get the current user information', + inputSchema: {}, + }, + (_params, { authInfo }) => { + return { + content: [ + { + type: 'text', + text: JSON.stringify(authInfo?.claims ?? { error: 'Not authenticated' }), + }, + ], + }; + } + ); + + return mcpServer; +}; const { MCP_AUTH_ISSUER } = process.env; @@ -94,11 +102,16 @@ app.use(mcpAuth.delegatedRouter()); app.use(mcpAuth.bearerAuth(verifyToken)); app.post('/', async (request, response) => { + // In stateless mode, create a new instance of transport and server for each request + // to ensure complete isolation. A single instance would cause request ID collisions + // when multiple clients connect concurrently. + const mcpServer = createMcpServer(); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); - await server.connect(transport); + await mcpServer.connect(transport); await transport.handleRequest(request, response, request.body); response.on('close', () => { void transport.close(); + void mcpServer.close(); }); });