Custom MCP HTTP connector handshake stalls after notifications/initialized and GET /mcp — "Couldn't reach the MCP server"
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
A custom MCP server configured via .mcp.json (Streamable HTTP transport, static Bearer token auth) cannot complete handshake with the Cowork CUSTOM connector, even though:
- The server is publicly reachable on Railway with HTTPS
- Authentication succeeds (server logs ✅ Auth ok for every Cowork request)
- The MCP
initializehandshake completes (200 + valid application/json response) notifications/initializedis received (202 + text/plain)- GET /mcp SSE stream is opened by Cowork (200 + text/event-stream)
- The same server works PERFECTLY with
npx @modelcontextprotocol/inspectorand raw curl
Cowork shows: "Couldn't reach the MCP server" and never proceeds to tools/list.
Reference IDs from failed attempts: ofid_94b3c4f598e2d3ad, ofid_4f8f790b18f9d8f3, ofid_e6f7f557d5a31ad0, ofid_8f183333aba70478, ofid_4bf76d52e87fea00, ofid_c74b241ec28f25a1, ofid_32f6ccf6e3fff10a
What Should Happen?
Cowork should complete the MCP handshake (initialize → notifications/initialized → tools/list) and show the connector as Connected, exposing the server's tools to Claude.
Error Messages/Logs
Cowork sends initialize, receives valid JSON response, sends notifications/initialized (202), opens GET /mcp SSE stream (200), then shows "Couldn't reach the MCP server" without ever sending tools/list. Server logs show no further requests after the GET /mcp opens.
Cowork retries the entire handshake 5+ times before giving up.
Detailed server logs and trace available in attached technical report.
Steps to Reproduce
- Build a minimal MCP HTTP server using @modelcontextprotocol/sdk 1.29.0 with:
- StreamableHTTPServerTransport (sessionIdGenerator: undefined, enableJsonResponse: true)
- Static Bearer token auth via Authorization header
- Permissive CORS
- Deploy publicly (e.g., Railway). Server URL: https://alva-server-production.up.railway.app/mcp
- Create a Cowork plugin with .mcp.json:
{
"mcpServers": {
"alva": {
"type": "http",
"url": "https://alva-server-production.up.railway.app/mcp",
"headers": { "Authorization": "Bearer <token>" }
}
}
}
- Install plugin in Cowork (drag .plugin into chat window, accept)
- Customize → Personal plugins → <plugin> → Connectors → <name> → Connect
Result: "Couldn't reach the MCP server" error appears.
The same server URL + token works flawlessly via:
- npx @modelcontextprotocol/inspector
- curl with Streamable HTTP semantics
Claude Model
Opus
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
Claude 1.4758.0 (fb266c) 2026-04-24T20:22:30.000Z
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
- CRITICAL: Same server tested with
npx @modelcontextprotocol/inspectorworks perfectly (connects, lists tools, executes tool calls). This isolates the issue to Cowork's CUSTOM connector implementation.
- We tried 10+ workarounds, none resolved the issue:
- Multiple plugin install/uninstall cycles
- Full Cowork app quits and restarts
- Connector toggle OFF/ON
- Hardcoded vs ${ENV_VAR} Bearer token (env var substitution does NOT work — server receives literal "${ALVA_BEARER_TOKEN}")
- enableJsonResponse: true (vs SSE)
- Permissive CORS with all MCP headers exposed
- Manual Accept header normalization (forcing application/json + text/event-stream)
- Custom GET /mcp handler vs SDK-delegated
- OAuth discovery endpoints implemented (/.well-known/oauth-protected-resource etc.)
- Multiple server version bumps (0.3.0 → 0.3.7)
- Secondary issue worth investigating: ${ENV_VAR} substitution in .mcp.json headers does not happen. With launchctl setenv ALVA_BEARER_TOKEN tk_xxx and "Authorization": "Bearer ${ALVA_BEARER_TOKEN}" in .mcp.json, Cowork sends the literal string "${ALVA_BEARER_TOKEN}" to the server. This blocks the documented multi-user distribution pattern.
- Hypothesis: Cowork (claude-code SDK) opens the GET /mcp stream and waits for an event the server doesn't emit in stateless mode (e.g., SDK might be expecting an "endpoint" event from the older HTTP+SSE transport, falling back to that mode after Streamable HTTP handshake).
- Full technical report with annotated server logs available on request.
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗