Skip to content

[Bug] MCP Streamable HTTP: "fetch failed" with no network call — SDK connects, ZCode doesn't (same @modelcontextprotocol/sdk) #150

Description

@lgldsilva

Summary

A self-hosted MCP server (Streamable HTTP) connects fine through the standalone @modelcontextprotocol/sdk (the same version ZCode bundles, ^1.26.0), but ZCode reports fetch failed for the same server with the same token. The decisive finding: when ZCode is pointed at a capture proxy, no request reaches the proxy at all — ZCode fails before making any network call, inside the process, while the standalone SDK against the same proxy sends all requests and connects.

Environment

Item Value
ZCode 3.3.6 (3.3.6.3198)
Runtime Electron 41.0.3, macOS 15 (darwin 25.5.0 arm64)
MCP SDK (bundled) @modelcontextprotocol/sdk@^1.26.0
MCP server self-hosted, Streamable HTTP (single endpoint /mcp), returns mcp-session-id + text/event-stream
Transport type: "http", headers.Authorization: Bearer …

Reproduce

1. The server is healthy (verified independently)

POST /mcp  (initialize)        → 200, mcp-session-id: <uuid>, content-type: text/event-stream
POST /mcp  (notifications/init)→ 202
GET  /mcp  (with session-id)   → 200, text/event-stream (server→client stream)
POST /mcp  (tools/list)        → 200

2. The standalone SDK connects (same SDK ZCode bundles)

Running @modelcontextprotocol/sdk@^1.26.0 in plain Node against the server:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(new URL("https://<host>/mcp"), {
  requestInit: { headers: { Authorization: "Bearer <token>" } },
});
const client = new Client({ name: "diag", version: "0" }, { capabilities: {} });
await client.connect(transport);
console.log(await client.listTools()); // → 16 tools
✅ connected — 16 tools   (stable across 3/3 runs)

3. ZCode fails with no network call

I pointed the ZCode ai-memory server at a capture proxy (http://127.0.0.1:3129/mcp) that forwards to the real server and logs every request, then restarted ZCode.

  • The standalone SDK through the proxy → all 4 requests captured (initialize → 200, notifications → 202, GET stream, tools/list → 200), connects, 16 tools.
  • ZCode through the proxy → 0 requests captured, fetch failed.

So ZCode's MCP client throws fetch failed without ever opening a connection to the server/proxy. This is not a network/TLS/CA/token issue (the standalone SDK rules all of that out) and not an SDK-version issue (same ^1.26.0). It's specific to how ZCode invokes the SDK inside the Electron process.

ZCode log

{
  "event": "mcp.server.failed",
  "level": "warn",
  "message": "MCP server connection failed",
  "context": {
    "error": "fetch failed",
    "mcpServerName": "<name>",
    "transport": "http"
  },
  "durationMs": 90
}

Failures are consistently 44–185 ms — far below any network/TLS timeout and below the 30 s tool timeout — consistent with a synchronous, pre-network failure rather than a network timeout.

Two problems

  1. Pre-network failure: ZCode throws fetch failed before sending any request, while the same SDK in plain Node works against the same server. Something in how ZCode builds or dispatches the Streamable HTTP request fails (or is aborted) before reaching the network stack.
  2. Opaque error: The UI and log show only fetch failed — the error.cause / code from the SDK (which undici nests) is never serialized. Whether the root cause is a header issue, a URL parsing issue, an abort, or something else, there is no way to tell from ZCode's output. At minimum, surfacing error.cause would make cases like this self-diagnosing.

Expected

  • ZCode should reach the same result as the standalone SDK against the same server (connect, list tools).
  • When the MCP client fails, log/show the real cause (error.cause, error code), not just fetch failed.

Notes

  • TLS/CA is not the issue: NODE_EXTRA_CA_CERTS is set for the GUI process (verified via a LaunchAgent + ps eww on the ZCode PID), the file is readable, and the standalone SDK — which also uses that var — connects successfully.
  • Happy to provide a tiny capture-proxy repro so a maintainer can reproduce the "0 requests from ZCode" side on any Streamable HTTP server.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions