fix: handle SSE responses in MCP bridge and add Commander-level tests

The bridge now parses SSE text/event-stream responses (extracting data:
lines) in addition to plain JSON. Also sends correct Accept header
per MCP streamable HTTP spec. Added tests for SSE handling and
command option parsing (-p/--project).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-02-24 10:17:45 +00:00
parent eb49ede732
commit fdafe87a77
2 changed files with 60 additions and 5 deletions

View File

@@ -21,7 +21,7 @@ function postJsonRpc(
const parsed = new URL(url);
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Accept': 'application/json, text/event-stream',
};
if (sessionId) {
headers['mcp-session-id'] = sessionId;
@@ -95,6 +95,25 @@ function sendDelete(
});
}
/**
* Extract JSON-RPC messages from an HTTP response body.
* Handles both plain JSON and SSE (text/event-stream) formats.
*/
function extractJsonRpcMessages(contentType: string | undefined, body: string): string[] {
if (contentType?.includes('text/event-stream')) {
// Parse SSE: extract data: lines
const messages: string[] = [];
for (const line of body.split('\n')) {
if (line.startsWith('data: ')) {
messages.push(line.slice(6));
}
}
return messages;
}
// Plain JSON response
return [body];
}
/**
* STDIO-to-Streamable-HTTP MCP bridge.
*
@@ -126,10 +145,16 @@ export async function runMcpBridge(opts: McpBridgeOptions): Promise<void> {
if (result.status >= 400) {
stderr.write(`MCP bridge error: HTTP ${result.status}: ${result.body}\n`);
// Still forward the response body — it may contain a JSON-RPC error
}
stdout.write(result.body + '\n');
// Handle both plain JSON and SSE responses
const messages = extractJsonRpcMessages(result.headers['content-type'], result.body);
for (const msg of messages) {
const trimmedMsg = msg.trim();
if (trimmedMsg) {
stdout.write(trimmedMsg + '\n');
}
}
} catch (err) {
stderr.write(`MCP bridge error: ${err instanceof Error ? err.message : String(err)}\n`);
}

View File

@@ -94,8 +94,14 @@ beforeAll(async () => {
responseBody = JSON.stringify({ jsonrpc: '2.0', id: rpc.id, error: { code: -32601, message: 'Method not found' } });
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(responseBody);
// Respond in SSE format for /projects/sse-project/mcp
if (req.url?.includes('sse-project')) {
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
res.end(`event: message\ndata: ${responseBody}\n\n`);
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(responseBody);
}
} catch {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid JSON' }));
@@ -389,6 +395,30 @@ describe('MCP STDIO Bridge', () => {
expect(lines).toHaveLength(1);
});
it('handles SSE (text/event-stream) responses', async () => {
recorded.length = 0;
const stdin = new Readable({ read() {} });
const { stdout, stdoutChunks } = createMockStreams();
const initMsg = JSON.stringify({
jsonrpc: '2.0', id: 1, method: 'initialize',
params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test', version: '1.0' } },
});
pushAndEnd(stdin, [initMsg]);
await runMcpBridge({
projectName: 'sse-project', // triggers SSE response from mock server
mcplocalUrl: `http://localhost:${mockPort}`,
stdin, stdout, stderr: new Writable({ write(_, __, cb) { cb(); } }),
});
// Should extract JSON from SSE data: lines
const output = stdoutChunks.join('').trim();
const parsed = JSON.parse(output);
expect(parsed.result.serverInfo.name).toBe('test-server');
});
it('URL-encodes project name', async () => {
recorded.length = 0;
const stdin = new Readable({ read() {} });