diff --git a/actions/setup/js/mount_mcp_as_cli.cjs b/actions/setup/js/mount_mcp_as_cli.cjs index 33d5c626f6c..494ca48dcef 100644 --- a/actions/setup/js/mount_mcp_as_cli.cjs +++ b/actions/setup/js/mount_mcp_as_cli.cjs @@ -146,6 +146,53 @@ function httpPostJSON(urlStr, headers, body, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS }); } +/** + * Parse an MCP response body that may be JSON or Server-Sent Events (SSE). + * + * Some MCP gateway responses are streamed as SSE and contain lines like: + * data: {"jsonrpc":"2.0","id":3,"result":{...}} + * + * @param {unknown} body - Parsed response body from httpPostJSON + * @returns {unknown} + */ +function parseMCPResponseBody(body) { + if (body && typeof body === "object" && !Array.isArray(body)) { + return body; + } + if (typeof body !== "string") { + return null; + } + + const trimmed = body.trim(); + if (!trimmed) { + return null; + } + + try { + return JSON.parse(trimmed); + } catch { + // Fall through to SSE parsing. + } + + /** @type {unknown} */ + let lastDataMessage = null; + for (const line of trimmed.split(/\r?\n/)) { + if (!line.startsWith("data:")) { + continue; + } + const payload = line.slice(5).trim(); + if (!payload || payload === "[DONE]") { + continue; + } + try { + lastDataMessage = JSON.parse(payload); + } catch { + // Ignore non-JSON SSE data lines. + } + } + return lastDataMessage; +} + /** * Query the tools list from an MCP server via JSON-RPC. * Follows the standard MCP handshake: initialize → notifications/initialized → tools/list. @@ -196,7 +243,7 @@ async function fetchMCPTools(serverUrl, apiKey, core) { // Step 3: tools/list – get the available tool definitions try { const listResp = await httpPostJSON(serverUrl, { ...authHeaders, ...sessionHeader }, { jsonrpc: "2.0", id: 2, method: "tools/list" }, DEFAULT_HTTP_TIMEOUT_MS); - const respBody = listResp.body; + const respBody = parseMCPResponseBody(listResp.body); if (respBody && typeof respBody === "object" && "result" in respBody && respBody.result && typeof respBody.result === "object") { const result = respBody.result; if ("tools" in result && Array.isArray(result.tools)) { @@ -396,4 +443,4 @@ async function main() { core.setOutput("mounted-servers", mountedServers.join(",")); } -module.exports = { main, fetchMCPTools, generateCLIWrapperScript, isValidServerName, shellEscapeDoubleQuoted }; +module.exports = { main, fetchMCPTools, generateCLIWrapperScript, isValidServerName, shellEscapeDoubleQuoted, parseMCPResponseBody }; diff --git a/actions/setup/js/mount_mcp_as_cli.test.cjs b/actions/setup/js/mount_mcp_as_cli.test.cjs new file mode 100644 index 00000000000..cd86c1f53be --- /dev/null +++ b/actions/setup/js/mount_mcp_as_cli.test.cjs @@ -0,0 +1,41 @@ +// @ts-check +import { describe, expect, it } from "vitest"; + +import { parseMCPResponseBody } from "./mount_mcp_as_cli.cjs"; + +describe("mount_mcp_as_cli.cjs", () => { + it("parses JSON object responses unchanged", () => { + const body = { jsonrpc: "2.0", result: { tools: [{ name: "logs" }] } }; + expect(parseMCPResponseBody(body)).toEqual(body); + }); + + it("parses raw JSON string responses", () => { + const body = '{"jsonrpc":"2.0","result":{"tools":[{"name":"logs"}]}}'; + expect(parseMCPResponseBody(body)).toEqual({ + jsonrpc: "2.0", + result: { tools: [{ name: "logs" }] }, + }); + }); + + it("parses SSE data lines and returns the JSON payload", () => { + const sseToolListPayload = 'data: {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"logs","inputSchema":{"properties":{"count":{"type":"integer"}}}}]}}'; + const body = ["event: message", sseToolListPayload, ""].join("\n"); + + expect(parseMCPResponseBody(body)).toEqual({ + jsonrpc: "2.0", + id: 2, + result: { + tools: [ + { + name: "logs", + inputSchema: { + properties: { + count: { type: "integer" }, + }, + }, + }, + ], + }, + }); + }); +});