Skip to content

MCP Custom Tool: SSE fallback always fails with 'Invalid response body, expected a web ReadableStream' (node-fetch v2 body is not a web ReadableStream) #6715

Description

@PseudoSky

Summary

Connecting a Custom MCP tool to any remote SSE server (URL with sse transport) always fails with:

Failed to connect to Custom MCP server: SSE error: Invalid response body, expected a web ReadableStream

Root cause (traced in flowise-components 3.1.4)

In flowise-components/dist/nodes/tools/MCP/core.js, MCPToolkit.createClient() first tries StreamableHTTPClientTransport, and on failure falls back to SSEClientTransport with a custom fetch:

transport = new sse_js_1.SSEClientTransport(baseUrl, {
  eventSourceInit: {
    fetch: async (url, init) => {
      return (0, httpSecurity_1.secureFetch)(url.toString(), init);
    }
  }
});

secureFetch (flowise-components/dist/src/httpSecurity.js, ~line 234) performs the SSRF/TLS checks and then calls node-fetch v2:

const response = await (0, node_fetch_1.default)(currentUrl, { ...currentInit, agent: () => agent });

node-fetch v2 represents a streaming response body as a Node.js PassThrough stream. The MCP SDK's SSEClientTransport hands the response to the eventsource package, which hard-requires a web ReadableStream and rejects any other body shape (its check: if (typeof body !== 'object' || !body || !('getReader' in body)) failConnection(...)). A PassThrough has no getReader, so every SSE connection fails — regardless of which server is behind the URL. This reproduces against any spec-compliant MCP SSE server (tested against the MCP SDK's own SSEServerTransport).

How to reproduce

  1. Run any MCP SSE server (e.g. an apigen/@modelcontextprotocol/sdk SSEServerTransport at http://127.0.0.1:3100/sse).
  2. Add a Custom MCP tool with config {"url": "http://127.0.0.1:3100/sse"}.
  3. Load Available Actions / run the flow → the error above.

Suggested fix

Convert secureFetch's node-fetch body to a web ReadableStream before handing it to the SSE client (Node's static stream.Readable.toWeb(stream) — there is no instance .toWeb()). Minimal patch, verified working against a live SSE server:

const stream_1 = require("stream");

async function secureFetchForSse(url, init, headers) {
  const response = await (0, httpSecurity_1.secureFetch)(url, { ...init, ...(headers ? { headers } : {}) });
  const body = response.body;
  if (body && typeof body.pipe === 'function' && typeof stream_1.Readable.toWeb === 'function') {
    const headersObj = {};
    if (response.headers && typeof response.headers.forEach === 'function') {
      response.headers.forEach((value, key) => { headersObj[key] = value; });
    }
    return new Response(stream_1.Readable.toWeb(body), {
      status: response.status,
      statusText: response.statusText,
      headers: headersObj
    });
  }
  return response;
}

...and use secureFetchForSse(url, init[, headers]) in both SSE-fallback branches of createClient.

Environment

  • flowise 3.1.4 (global install via npm)
  • Node v24.11.1
  • @modelcontextprotocol/sdk@1.29.0, eventsource@3.0.7, node-fetch@2.7.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions