diff --git a/packages/browseros-agent/apps/server/src/api/routes/mcp.ts b/packages/browseros-agent/apps/server/src/api/routes/mcp.ts index 053e353c2..e3de614b7 100644 --- a/packages/browseros-agent/apps/server/src/api/routes/mcp.ts +++ b/packages/browseros-agent/apps/server/src/api/routes/mcp.ts @@ -123,25 +123,25 @@ export function createMcpRoutes(deps: McpRouteDeps) { logger.debug('MCP request received', logContext) - // Per-request server + transport: no shared state, no race conditions, - // no ID collisions. Required by MCP SDK 1.26.0+ security fix (GHSA-345p-7cg4-v4c7). - const mcpServer = makeMcpServer({ - version: deps.version, - browserSession: deps.browserSession, - klavis: deps.klavis, - connectorScope: { selectedServerNames }, - defaultWindowId, - defaultTabGroupId, - executionDir: deps.executionDir, - remoteAgentHarness: harness, - activity: deps.activity, - }) - const transport = makeMcpTransport({ - sessionIdGenerator: undefined, - enableJsonResponse: true, - }) - try { + // Per-request server + transport: no shared state, no race conditions, + // no ID collisions. Required by MCP SDK 1.26.0+ security fix (GHSA-345p-7cg4-v4c7). + const mcpServer = makeMcpServer({ + version: deps.version, + browserSession: deps.browserSession, + klavis: deps.klavis, + connectorScope: { selectedServerNames }, + defaultWindowId, + defaultTabGroupId, + executionDir: deps.executionDir, + remoteAgentHarness: harness, + activity: deps.activity, + }) + const transport = makeMcpTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, + }) + await mcpServer.connect(transport) logger.debug('MCP request transport connected', logContext) const response = await transport.handleRequest(c) @@ -156,15 +156,21 @@ export function createMcpRoutes(deps: McpRouteDeps) { scope.setTag('scopeId', scopeId) Sentry.captureException(error) }) + const errorMessage = + error instanceof Error ? error.message : String(error) logger.error('Error handling MCP request', { ...logContext, - error: error instanceof Error ? error.message : String(error), + error: errorMessage, }) return c.json( { jsonrpc: '2.0', - error: { code: -32603, message: 'Internal server error' }, + error: { + code: -32603, + message: 'Internal server error', + data: errorMessage, + }, id: null, }, 500, diff --git a/packages/browseros-agent/apps/server/tests/api/routes/mcp.test.ts b/packages/browseros-agent/apps/server/tests/api/routes/mcp.test.ts index 3f3992c6e..9b1a8d827 100644 --- a/packages/browseros-agent/apps/server/tests/api/routes/mcp.test.ts +++ b/packages/browseros-agent/apps/server/tests/api/routes/mcp.test.ts @@ -91,6 +91,24 @@ async function postMcp( }) } +async function expectJsonRpcInternalError( + response: Response, + errorMessage: string, +) { + expect(response.status).toBe(500) + const body = await response.json() + expect(body).toEqual({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + data: errorMessage, + }, + id: null, + }) + expect(body.error).not.toHaveProperty('name') +} + describe('parseManagedMcpServersHeader', () => { it('returns an empty scope for missing or empty headers', () => { expect(parseManagedMcpServersHeader(undefined)).toEqual([]) @@ -111,6 +129,18 @@ describe('parseManagedMcpServersHeader', () => { }) describe('createMcpRoutes', () => { + it('returns the MCP server status for GET requests', async () => { + const app = createTestMcpRoutes() + + const response = await app.request('/') + + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ + status: 'ok', + message: 'MCP server is running. Use POST to interact.', + }) + }) + it('passes latest Klavis status and selected connector scope per request', async () => { let status: KlavisProxyStatus = { state: 'connecting' } const klavis = { @@ -185,4 +215,58 @@ describe('createMcpRoutes', () => { serverCreations[1].remoteAgentHarness, ) }) + + it('returns a JSON-RPC error when MCP server construction throws', async () => { + const app = createTestMcpRoutes({ + createMcpServer: (() => { + throw new Error('tool registration failed') + }) as never, + }) + + const response = await postMcp(app) + + await expectJsonRpcInternalError(response, 'tool registration failed') + expect(createMcpTransportSpy).not.toHaveBeenCalled() + }) + + it('returns a JSON-RPC error when MCP transport construction throws', async () => { + const app = createTestMcpRoutes({ + createMcpTransport: (() => { + throw new Error('transport setup failed') + }) as never, + }) + + const response = await postMcp(app) + + await expectJsonRpcInternalError(response, 'transport setup failed') + }) + + it('returns a JSON-RPC error when connecting the MCP server fails', async () => { + const app = createTestMcpRoutes({ + createMcpServer: (() => ({ + connect: async () => { + throw new Error('server connection failed') + }, + })) as never, + }) + + const response = await postMcp(app) + + await expectJsonRpcInternalError(response, 'server connection failed') + }) + + it('returns a JSON-RPC error when handling the MCP request fails', async () => { + const app = createTestMcpRoutes({ + createMcpTransport: ((options: unknown) => ({ + options, + handleRequest: async () => { + throw new Error('request handling failed') + }, + })) as never, + }) + + const response = await postMcp(app) + + await expectJsonRpcInternalError(response, 'request handling failed') + }) })