Skip to content
Merged
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
26 changes: 25 additions & 1 deletion backend/src/mcp/orgClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { config } from '../config.js';

type OrgToolResult = { content: { type: string; text: string }[]; isError?: boolean };
type Envelope = { result?: OrgToolResult; error?: { message: string } };

let _requestId = 1;

Expand Down Expand Up @@ -38,7 +39,19 @@ export async function callOrgTool(
};
}

const envelope = await res.json() as { result?: OrgToolResult; error?: { message: string } };
const contentType = res.headers.get('content-type') ?? '';
const rawText = await res.text();
let envelope: Envelope;
try {
envelope = contentType.includes('text/event-stream')
? parseSseEnvelope(rawText)
: JSON.parse(rawText);
} catch (err) {
return {
content: [{ type: 'text', text: `Failed to parse mcp-org response (${contentType}): ${String(err)} β€” body: ${rawText.slice(0, 200)}` }],
isError: true,
};
}
if (envelope.error) {
return { content: [{ type: 'text', text: `mcp-org error: ${envelope.error.message}` }], isError: true };
}
Expand All @@ -47,3 +60,14 @@ export async function callOrgTool(
return { content: [{ type: 'text', text: `Failed to reach mcp-org: ${String(err)}` }], isError: true };
}
}

function parseSseEnvelope(text: string): Envelope {
for (const frame of text.split(/\r?\n\r?\n/)) {
const dataLines: string[] = [];
for (const line of frame.split(/\r?\n/)) {
if (line.startsWith('data:')) dataLines.push(line.slice(5).trimStart());
}
if (dataLines.length > 0) return JSON.parse(dataLines.join('\n')) as Envelope;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Continue scanning SSE frames until JSON-RPC result appears

parseSseEnvelope returns after parsing the first SSE frame that has any data: lines, but Streamable HTTP responses are allowed to include JSON-RPC notifications/requests before the actual response. In that case this code will either throw on non-JSON data or parse a non-response message and cause callOrgTool to return empty response, even though a valid result frame appears later in the same stream. The parser should keep iterating frames until it finds a JSON-RPC message with a matching response shape (or exhausts the stream).

Useful? React with πŸ‘Β / πŸ‘Ž.

}
throw new Error('no data lines in SSE response');
}
Loading