Skip to content

Commit c27524b

Browse files
authored
Handle SSE tools/list responses when mounting MCP servers as CLIs (#27207)
1 parent 042b7ec commit c27524b

File tree

2 files changed

+90
-2
lines changed

2 files changed

+90
-2
lines changed

actions/setup/js/mount_mcp_as_cli.cjs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,53 @@ function httpPostJSON(urlStr, headers, body, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS
146146
});
147147
}
148148

149+
/**
150+
* Parse an MCP response body that may be JSON or Server-Sent Events (SSE).
151+
*
152+
* Some MCP gateway responses are streamed as SSE and contain lines like:
153+
* data: {"jsonrpc":"2.0","id":3,"result":{...}}
154+
*
155+
* @param {unknown} body - Parsed response body from httpPostJSON
156+
* @returns {unknown}
157+
*/
158+
function parseMCPResponseBody(body) {
159+
if (body && typeof body === "object" && !Array.isArray(body)) {
160+
return body;
161+
}
162+
if (typeof body !== "string") {
163+
return null;
164+
}
165+
166+
const trimmed = body.trim();
167+
if (!trimmed) {
168+
return null;
169+
}
170+
171+
try {
172+
return JSON.parse(trimmed);
173+
} catch {
174+
// Fall through to SSE parsing.
175+
}
176+
177+
/** @type {unknown} */
178+
let lastDataMessage = null;
179+
for (const line of trimmed.split(/\r?\n/)) {
180+
if (!line.startsWith("data:")) {
181+
continue;
182+
}
183+
const payload = line.slice(5).trim();
184+
if (!payload || payload === "[DONE]") {
185+
continue;
186+
}
187+
try {
188+
lastDataMessage = JSON.parse(payload);
189+
} catch {
190+
// Ignore non-JSON SSE data lines.
191+
}
192+
}
193+
return lastDataMessage;
194+
}
195+
149196
/**
150197
* Query the tools list from an MCP server via JSON-RPC.
151198
* Follows the standard MCP handshake: initialize → notifications/initialized → tools/list.
@@ -196,7 +243,7 @@ async function fetchMCPTools(serverUrl, apiKey, core) {
196243
// Step 3: tools/list – get the available tool definitions
197244
try {
198245
const listResp = await httpPostJSON(serverUrl, { ...authHeaders, ...sessionHeader }, { jsonrpc: "2.0", id: 2, method: "tools/list" }, DEFAULT_HTTP_TIMEOUT_MS);
199-
const respBody = listResp.body;
246+
const respBody = parseMCPResponseBody(listResp.body);
200247
if (respBody && typeof respBody === "object" && "result" in respBody && respBody.result && typeof respBody.result === "object") {
201248
const result = respBody.result;
202249
if ("tools" in result && Array.isArray(result.tools)) {
@@ -396,4 +443,4 @@ async function main() {
396443
core.setOutput("mounted-servers", mountedServers.join(","));
397444
}
398445

399-
module.exports = { main, fetchMCPTools, generateCLIWrapperScript, isValidServerName, shellEscapeDoubleQuoted };
446+
module.exports = { main, fetchMCPTools, generateCLIWrapperScript, isValidServerName, shellEscapeDoubleQuoted, parseMCPResponseBody };
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// @ts-check
2+
import { describe, expect, it } from "vitest";
3+
4+
import { parseMCPResponseBody } from "./mount_mcp_as_cli.cjs";
5+
6+
describe("mount_mcp_as_cli.cjs", () => {
7+
it("parses JSON object responses unchanged", () => {
8+
const body = { jsonrpc: "2.0", result: { tools: [{ name: "logs" }] } };
9+
expect(parseMCPResponseBody(body)).toEqual(body);
10+
});
11+
12+
it("parses raw JSON string responses", () => {
13+
const body = '{"jsonrpc":"2.0","result":{"tools":[{"name":"logs"}]}}';
14+
expect(parseMCPResponseBody(body)).toEqual({
15+
jsonrpc: "2.0",
16+
result: { tools: [{ name: "logs" }] },
17+
});
18+
});
19+
20+
it("parses SSE data lines and returns the JSON payload", () => {
21+
const sseToolListPayload = 'data: {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"logs","inputSchema":{"properties":{"count":{"type":"integer"}}}}]}}';
22+
const body = ["event: message", sseToolListPayload, ""].join("\n");
23+
24+
expect(parseMCPResponseBody(body)).toEqual({
25+
jsonrpc: "2.0",
26+
id: 2,
27+
result: {
28+
tools: [
29+
{
30+
name: "logs",
31+
inputSchema: {
32+
properties: {
33+
count: { type: "integer" },
34+
},
35+
},
36+
},
37+
],
38+
},
39+
});
40+
});
41+
});

0 commit comments

Comments
 (0)