openclaw - 💡(How to fix) Fix bundle-mcp Streamable HTTP client: opens optional GET SSE stream before POST initialize; fails 405 on POST-only MCP servers [2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#72757Fetched 2026-04-28 06:32:28
View on GitHub
Comments
2
Participants
2
Timeline
3
Reactions
0
Timeline (top)
commented ×2closed ×1

Error Message

[bundle-mcp] failed to start server "silo" (https://silo.hsprecisao.com/mcp?token=): Error: SSE error: Non-200 status code (405) [bundle-mcp] failed to start server "hs-data" (http://100.106.203.114:18796/mcp?token=): Error: SSE error: Non-200 status code (405)

Root Cause

For self-hosted MCP servers (custom company memory bridges, internal data APIs, etc.) the bundle-mcp path is the documented integration point in OpenClaw's openclaw.json + openclaw mcp set flow. Users following the documented setup will find that self-hosted MCP servers work everywhere except OpenClaw, with no clear indication that the issue is on the OpenClaw side.

Fix Action

Workaround

I tried several server-side workarounds, none satisfactory:

  • Implement a dummy GET handler that returns 200 with an empty SSE streambundle-mcp then errors with Error: SSE error: undefined (Flask sync workers can't hold persistent streams; bundle-mcp seems to expect a long-lived connection).
  • Implement a keep-alive SSE handler (Express, with setInterval comments) → bundle-mcp receives 200 but then waits 30 seconds for the server to push tool definitions over SSE (per its expectation), times out with Error: MCP server connection timed out after 30000ms, and fails.
  • URL-token auth fallback → confirmed our auth wasn't the blocker; bundle-mcp passes auth but still uses GET-only.

Currently the only working pattern is a Jarvis-side workaround using OpenClaw's exec tool with curl and the MCP token in an env var. Functional but bypasses the entire bundled-MCP integration.

Code Example

[bundle-mcp] failed to start server "silo" (https://silo.hsprecisao.com/mcp?token=***): Error: SSE error: Non-200 status code (405)
[bundle-mcp] failed to start server "hs-data" (http://100.106.203.114:18796/mcp?token=***): Error: SSE error: Non-200 status code (405)

---

172.18.0.2 - - [27/Apr/2026:10:13:14 +0000] "GET /mcp HTTP/1.1" 405 49 "-" "undici"

---

"POST /mcp HTTP/1.1" 202 5
"POST /mcp HTTP/1.1" 200 182  (initialize response)
"POST /mcp HTTP/1.1" 202 5
"POST /mcp HTTP/1.1" 200 3896  (tools/list response)
"GET /mcp HTTP/1.1" 405 49     (optional SSE stream — 405 ignored, client degrades gracefully)

---

curl -X POST https://example/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer ..." \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# → HTTP 200 with tools list
RAW_BUFFERClick to expand / collapse

Bug: bundle-mcp opens SSE GET /mcp without first POSTing initialize, fails on spec-compliant MCP servers that only implement POST

Title (suggested): bundle-mcp Streamable HTTP client: opens optional GET SSE stream before doing POST initialize handshake; fails 405 on POST-only MCP servers

Affects: OpenClaw 2026.4.21 and 2026.4.24 confirmed. Likely earlier versions too.

Severity: High for users with self-hosted MCP servers — bundle-mcp cannot proxy them to agents, even though the same servers work fine with desktop Claude Code, Claude Desktop, and other MCP clients.

Relationship to existing issues

This is filed per the invitation in #70753 ("If AgentMail or another hosted SSE MCP still reproduces on v2026.4.23 or newer, track it as a fresh transport-specific bug with updated logs"). Reproduces on v2026.4.24, but with a different failure surface than the closed #70753:

  • #70753 (closed, completed) — fixed the 4.22 undici stream-timeout regression for hosted SSE servers. Different surface (HTTP/2: stream timeout after 15000). Fix landed in 4.23+, confirmed not the issue here — we're on 4.24 and the timeout-floor is in place.
  • #70901 (closed)${ENV_VAR} expansion in MCP headers was broken on 4.22, fixed since. We confirmed this works on 4.24 (the literal string of Bearer ${SILO_MCP_TOKEN} does get expanded to the actual env-var value before sending).

The remaining failure I'm reporting here is distinct from both: bundle-mcp opens the optional SSE GET first and never falls through to POST initialize when GET returns a non-2xx, even though the failure is on a code path the spec marks optional.


Environment

  • OpenClaw 2026.4.21 → 2026.4.24, Linux container, Node 24.14.0
  • Configured via openclaw mcp set <name> '{"type":"http","url":"https://...","headers":{"Authorization":"Bearer ..."}}'
  • Using @modelcontextprotocol/sdk's StreamableHTTPClientTransport for HTTP transport (per pi-bundle-mcp-runtime-*.js in dist)

Symptom

Configured MCP servers fail to start with:

[bundle-mcp] failed to start server "silo" (https://silo.hsprecisao.com/mcp?token=***): Error: SSE error: Non-200 status code (405)
[bundle-mcp] failed to start server "hs-data" (http://100.106.203.114:18796/mcp?token=***): Error: SSE error: Non-200 status code (405)

bundle-mcp never delivers the configured server's tools to the agent. The agent has no MCP-bundled tools available, even though the underlying servers are healthy and reachable.

What bundle-mcp actually sends

Captured from nginx access log (silo server) and direct curl probes:

172.18.0.2 - - [27/Apr/2026:10:13:14 +0000] "GET /mcp HTTP/1.1" 405 49 "-" "undici"

i.e. bundle-mcp makes only a single GET /mcp request and never sends the spec-required POST to do the initialize handshake. There is no preceding POST /mcp HTTP/1.1 from the same client — verified across multiple agent runs and gateway restarts.

Compare to working clients (e.g. Claude Code on desktop) hitting the same server:

"POST /mcp HTTP/1.1" 202 5
"POST /mcp HTTP/1.1" 200 182  (initialize response)
"POST /mcp HTTP/1.1" 202 5
"POST /mcp HTTP/1.1" 200 3896  (tools/list response)
"GET /mcp HTTP/1.1" 405 49     (optional SSE stream — 405 ignored, client degrades gracefully)

The desktop client does POST first; the GET is optional and a 405 there does not block functionality.

MCP Streamable HTTP spec reference

Per the MCP Streamable HTTP transport spec:

  • POST to the configured URL is required for client-to-server requests.
  • GET is optional — it opens an SSE stream for unsolicited server-to-client messages. Servers MAY reject GET (the spec calls this out). Clients SHOULD continue to function on POST alone if GET is unavailable.

bundle-mcp appears to invert this: it requires GET to succeed, never tries POST initialize, and treats a 405 on the optional GET endpoint as a fatal "non-200 status code" error.

Reproduction

  1. Stand up any MCP server that implements POST /mcp per spec but does not implement an SSE GET handler (returns 405 for GET /mcp).
  2. Configure it in OpenClaw: openclaw mcp set foo '{"type":"http","url":"https://example/mcp","headers":{"Authorization":"Bearer ..."}}'.
  3. Restart gateway, run openclaw agent --agent main --message "ping".
  4. Observe [bundle-mcp] failed to start server "foo" ...: Error: SSE error: Non-200 status code (405).
  5. Observe in the server's access log only a single GET /mcp 405. There is no POST from this client, ever.

The same server can be verified working via curl:

curl -X POST https://example/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer ..." \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# → HTTP 200 with tools list

Expected behavior

bundle-mcp (or the underlying StreamableHTTPClientTransport invocation) should:

  1. POST initialize to the configured URL first.
  2. POST tools/list to discover tools.
  3. Optionally open a GET SSE stream for unsolicited messages — but a 405 on this should not prevent the client from being functional.

This matches the behavior of other MCP clients (Claude Code, Claude Desktop, etc.) and the spec.

Actual behavior

bundle-mcp opens the GET SSE stream as the first (and only) request. If the server returns a non-200 (including 405 for "method not allowed"), bundle-mcp marks the server as failed and never falls back to POST.

Workaround

I tried several server-side workarounds, none satisfactory:

  • Implement a dummy GET handler that returns 200 with an empty SSE streambundle-mcp then errors with Error: SSE error: undefined (Flask sync workers can't hold persistent streams; bundle-mcp seems to expect a long-lived connection).
  • Implement a keep-alive SSE handler (Express, with setInterval comments) → bundle-mcp receives 200 but then waits 30 seconds for the server to push tool definitions over SSE (per its expectation), times out with Error: MCP server connection timed out after 30000ms, and fails.
  • URL-token auth fallback → confirmed our auth wasn't the blocker; bundle-mcp passes auth but still uses GET-only.

Currently the only working pattern is a Jarvis-side workaround using OpenClaw's exec tool with curl and the MCP token in an env var. Functional but bypasses the entire bundled-MCP integration.

Why this matters

For self-hosted MCP servers (custom company memory bridges, internal data APIs, etc.) the bundle-mcp path is the documented integration point in OpenClaw's openclaw.json + openclaw mcp set flow. Users following the documented setup will find that self-hosted MCP servers work everywhere except OpenClaw, with no clear indication that the issue is on the OpenClaw side.

Suggested fix direction

In pi-bundle-mcp-runtime-*.js (where StreamableHTTPClientTransport is constructed):

  1. Verify the SDK's documented connection sequence is being used. The SDK's StreamableHTTPClientTransport should do POST initialize first; if bundle-mcp is calling some lower-level SSE-only path, switch to the spec-compliant path.
  2. If GET fails with 4xx (especially 405), do not treat that as fatal — log a warning and continue with POST-based request/response.
  3. Consider an explicit config knob like transport: "post-only" that disables the optional GET stream entirely for users whose servers don't implement it.

Happy to test patches on this host. Self-hosted MCP servers in question are simple Express/Flask implementations matching the canonical Streamable HTTP spec.

extent analysis

TL;DR

The most likely fix is to modify the StreamableHTTPClientTransport in pi-bundle-mcp-runtime-*.js to prioritize the POST initialize handshake over the optional GET SSE stream.

Guidance

  • Verify that the StreamableHTTPClientTransport is being used correctly and that it is not calling a lower-level SSE-only path.
  • Modify the transport to treat a 405 error on the GET SSE stream as a non-fatal error and continue with the POST-based request/response.
  • Consider adding a config option to disable the optional GET stream entirely for users whose servers don't implement it.
  • Test the changes with a self-hosted MCP server that implements POST /mcp but does not implement an SSE GET handler.

Example

No code example is provided as the issue does not contain sufficient information about the implementation details of pi-bundle-mcp-runtime-*.js.

Notes

The fix direction suggested by the user is a good starting point, but the actual implementation details may vary depending on the specifics of the StreamableHTTPClientTransport and the pi-bundle-mcp-runtime-*.js code.

Recommendation

Apply a workaround by modifying the StreamableHTTPClientTransport to prioritize the POST initialize handshake and treat a 405 error on the GET SSE stream as a non-fatal error. This should allow bundle-mcp to work with self-hosted MCP servers that implement POST /mcp but do not implement an SSE GET handler.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

bundle-mcp (or the underlying StreamableHTTPClientTransport invocation) should:

  1. POST initialize to the configured URL first.
  2. POST tools/list to discover tools.
  3. Optionally open a GET SSE stream for unsolicited messages — but a 405 on this should not prevent the client from being functional.

This matches the behavior of other MCP clients (Claude Code, Claude Desktop, etc.) and the spec.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix bundle-mcp Streamable HTTP client: opens optional GET SSE stream before POST initialize; fails 405 on POST-only MCP servers [2 comments, 2 participants]