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
94 changes: 94 additions & 0 deletions src/lib/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,100 @@ describe('Feature: MCP Proxy', () => {
}),
)
})

it('Scenario: Failed forward of a request surfaces a JSON-RPC error to the client', async () => {
// Given a server transport whose send() rejects, e.g. because the
// server expired the session and answers HTTP 404 (issue #106)
const mockTransportToClient = {
send: vi.fn().mockResolvedValue(undefined),
close: vi.fn().mockResolvedValue(undefined),
start: vi.fn().mockResolvedValue(undefined),
onmessage: vi.fn(),
onclose: vi.fn(),
onerror: vi.fn(),
} as unknown as Transport

const mockTransportToServer = {
send: vi.fn().mockRejectedValue(new Error('Error POSTing to endpoint (HTTP 404): Session not found')),
close: vi.fn().mockResolvedValue(undefined),
start: vi.fn().mockResolvedValue(undefined),
onmessage: vi.fn(),
onclose: vi.fn(),
onerror: vi.fn(),
} as unknown as Transport

mcpProxy({
transportToClient: mockTransportToClient,
transportToServer: mockTransportToServer,
ignoredTools: [],
})

// When the client sends a request (a message with an id)
const clientRequest = {
jsonrpc: '2.0' as const,
method: 'tools/call',
id: 42,
params: { name: 'SomeTool', arguments: {} },
}
if (mockTransportToClient.onmessage) {
mockTransportToClient.onmessage(clientRequest)
}

// Then the client receives a JSON-RPC error response for that id
// instead of waiting forever for a reply that never arrives
await vi.waitFor(() => {
expect(mockTransportToClient.send).toHaveBeenCalledWith(
expect.objectContaining({
jsonrpc: '2.0',
id: 42,
error: expect.objectContaining({
code: -32603,
message: expect.stringContaining('HTTP 404'),
}),
}),
)
})
})

it('Scenario: Failed forward of a notification does not produce a response', async () => {
// Given a server transport whose send() rejects
const mockTransportToClient = {
send: vi.fn().mockResolvedValue(undefined),
close: vi.fn().mockResolvedValue(undefined),
start: vi.fn().mockResolvedValue(undefined),
onmessage: vi.fn(),
onclose: vi.fn(),
onerror: vi.fn(),
} as unknown as Transport

const mockTransportToServer = {
send: vi.fn().mockRejectedValue(new Error('Error POSTing to endpoint (HTTP 404): Session not found')),
close: vi.fn().mockResolvedValue(undefined),
start: vi.fn().mockResolvedValue(undefined),
onmessage: vi.fn(),
onclose: vi.fn(),
onerror: vi.fn(),
} as unknown as Transport

mcpProxy({
transportToClient: mockTransportToClient,
transportToServer: mockTransportToServer,
ignoredTools: [],
})

// When the client sends a notification (no id)
const clientNotification = {
jsonrpc: '2.0' as const,
method: 'notifications/initialized',
}
if (mockTransportToClient.onmessage) {
mockTransportToClient.onmessage(clientNotification)
}

// Then no response is sent back — JSON-RPC forbids replies to notifications
await new Promise((resolve) => setTimeout(resolve, 10))
expect(mockTransportToClient.send).not.toHaveBeenCalled()
})
})

describe('setupOAuthCallbackServerWithLongPoll', () => {
Expand Down
21 changes: 20 additions & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,26 @@ export function mcpProxy({
debugLog('Initialize message with modified client info', { clientInfo })
}

transportToServer.send(message).catch(onServerError)
transportToServer.send(message).catch((error) => {
onServerError(error)

// If forwarding a request fails, the local client would wait forever
// for a response that never arrives (e.g. the server expired the
// session and answers 404, see #106). Surface the failure as a
// JSON-RPC error response instead. Notifications carry no id and
// must not be answered.
if (message.id !== undefined && message.id !== null) {
const errorResponse = {
jsonrpc: '2.0' as const,
id: message.id,
error: {
code: -32603,
message: `Failed to forward message to remote server: ${error instanceof Error ? error.message : String(error)}`,
},
}
transportToClient.send(errorResponse).catch(onClientError)
}
})
}

transportToServer.onmessage = (_message) => {
Expand Down