MCP Custom Tool: SSE fallback always fails with 'Invalid response body, expected a web ReadableStream' (node-fetch v2 body is not a web ReadableStream)
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
- Run any MCP SSE server (e.g. an apigen/
@modelcontextprotocol/sdkSSEServerTransportathttp://127.0.0.1:3100/sse). - Add a Custom MCP tool with config
{"url": "http://127.0.0.1:3100/sse"}. - 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/[email protected],[email protected],[email protected]
Source: FlowiseAI/Flowise