Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 26 additions & 20 deletions packages/browseros-agent/apps/server/src/api/routes/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand Down
84 changes: 84 additions & 0 deletions packages/browseros-agent/apps/server/tests/api/routes/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([])
Expand All @@ -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 = {
Expand Down Expand Up @@ -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')
})
})
Loading