Bug Summary
The mcpgateway.wrapper stdio bridge sends every JSON-RPC message as an independent HTTP POST to the gateway without tracking or forwarding the mcp-session-id header. After the MCP Streamable HTTP transport hardening (#3344), the gateway now strictly requires mcp-session-id for post-initialize requests. This means any wrapper session with more than one request fails — tools/list, tools/call, resources/read, etc. all return HTTP 400.
Affected Component
Root Cause
mcpgateway/wrapper.py forward_once() (line 413-418) builds headers fresh for every request:
headers = {
"Content-Type": "application/json; charset=utf-8",
"Accept": "application/json, application/x-ndjson, text/event-stream",
}
if settings.auth_header:
headers["Authorization"] = settings.auth_header
# No mcp-session-id. No mcp-protocol-version. No session tracking.
The wrapper's main_async() (line 640-662) creates independent async tasks for each stdin message — no session state is shared between them.
Grep for session_id, mcp-session, or any session tracking in the wrapper returns zero matches.
Steps to Reproduce
export MCP_SERVER_URL='http://localhost:8080'
export MCP_AUTH='Bearer <valid-token>'
python -m mcpgateway.wrapper
Then send via stdin:
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": {"name": "test", "version": "1.0.0"}}}
{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}
Result:
- id=1: Initialize succeeds, returns capabilities
- id=2: Never arrives — instead get
{"jsonrpc": "2.0", "id": "bridge", "error": {"code": 400, "message": "HTTP 400"}}
Expected Behavior
The wrapper should:
- Extract
mcp-session-id from the initialize response headers
- Include it in all subsequent HTTP POST requests
- Optionally send
mcp-protocol-version header per the Streamable HTTP spec
Impact
- All
tests/e2e/test_mcp_rbac_transport.py tests fail (14+ test cases) — they use wrapper-based helpers
- Any production usage of the wrapper against a Streamable HTTP endpoint with multiple requests is broken
- The
test_mcp_protocol_e2e.py tests are unaffected (they use fastmcp.client.Client which handles sessions properly)
Code References
| Location |
Description |
mcpgateway/wrapper.py:392-458 |
forward_once() — stateless HTTP POST, no session headers |
mcpgateway/wrapper.py:640-662 |
main_async() — independent tasks per message, no shared state |
mcpgateway/wrapper.py:156-183 |
convert_url() — normalizes to /mcp/ (Streamable HTTP) |
mcpgateway/wrapper.py:231-242 |
make_error() — generates id: 'bridge' sentinel for transport errors |
Mitigation
The test_mcp_rbac_transport.py e2e tests are being migrated to use fastmcp.client.Client (same pattern as test_mcp_protocol_e2e.py) to unblock CI. The wrapper itself still needs a fix for production use.
Bug Summary
The
mcpgateway.wrapperstdio bridge sends every JSON-RPC message as an independent HTTP POST to the gateway without tracking or forwarding themcp-session-idheader. After the MCP Streamable HTTP transport hardening (#3344), the gateway now strictly requiresmcp-session-idfor post-initialize requests. This means any wrapper session with more than one request fails —tools/list,tools/call,resources/read, etc. all return HTTP 400.Affected Component
mcpgateway- APIRoot Cause
mcpgateway/wrapper.pyforward_once()(line 413-418) builds headers fresh for every request:The wrapper's
main_async()(line 640-662) creates independent async tasks for each stdin message — no session state is shared between them.Grep for
session_id,mcp-session, or any session tracking in the wrapper returns zero matches.Steps to Reproduce
Then send via stdin:
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": {"name": "test", "version": "1.0.0"}}} {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}Result:
{"jsonrpc": "2.0", "id": "bridge", "error": {"code": 400, "message": "HTTP 400"}}Expected Behavior
The wrapper should:
mcp-session-idfrom the initialize response headersmcp-protocol-versionheader per the Streamable HTTP specImpact
tests/e2e/test_mcp_rbac_transport.pytests fail (14+ test cases) — they use wrapper-based helperstest_mcp_protocol_e2e.pytests are unaffected (they usefastmcp.client.Clientwhich handles sessions properly)Code References
mcpgateway/wrapper.py:392-458forward_once()— stateless HTTP POST, no session headersmcpgateway/wrapper.py:640-662main_async()— independent tasks per message, no shared statemcpgateway/wrapper.py:156-183convert_url()— normalizes to/mcp/(Streamable HTTP)mcpgateway/wrapper.py:231-242make_error()— generatesid: 'bridge'sentinel for transport errorsMitigation
The
test_mcp_rbac_transport.pye2e tests are being migrated to usefastmcp.client.Client(same pattern astest_mcp_protocol_e2e.py) to unblock CI. The wrapper itself still needs a fix for production use.