feat: add StreamableHTTP transport for shared daemon mode#4
feat: add StreamableHTTP transport for shared daemon mode#4Luc45 wants to merge 1 commit intokpanuragh:mainfrom
Conversation
When multiple AI agent sessions (e.g., several Claude Code windows) each spawn their own xdebug-mcp process, only the first binds port 9003 — the rest crash with EADDRINUSE. This adds an HTTP transport mode (MCP_TRANSPORT=http) where a single xdebug-mcp daemon serves multiple MCP clients over HTTP, while the existing stdio transport remains the default. New environment variables: - MCP_TRANSPORT: 'stdio' (default) or 'http' - MCP_HTTP_PORT: HTTP endpoint port (default 3100) - MCP_HTTP_HOST: HTTP bind address (default 127.0.0.1) Includes systemd user service example, vitest test suite (12 tests), and documentation updates.
kpanuragh
left a comment
There was a problem hiding this comment.
Great feature — the EADDRINUSE problem with multiple Claude sessions is a real pain point, and using StreamableHTTPServerTransport from the MCP SDK is the right approach. The architecture is clean: one daemon owns the DBGp port, multiple MCP clients connect over HTTP, and the existing SessionManager/ToolsContext already handle concurrent sessions.
A few things worth looking at:
HIGH: No request body size limit in collectBody
The collectBody function reads the entire request body into memory with no upper bound. A malicious or buggy client could send a huge payload and exhaust server memory.
// src/index.ts — collectBody()
const chunks: Buffer[] = [];
req.on('data', (chunk: Buffer) => chunks.push(chunk));Add a size limit — something like 1MB should be plenty for MCP JSON-RPC messages:
const MAX_BODY = 1_048_576; // 1 MB
let size = 0;
req.on('data', (chunk: Buffer) => {
size += chunk.length;
if (size > MAX_BODY) { req.destroy(); reject(new Error('Body too large')); return; }
chunks.push(chunk);
});HIGH: No authentication on HTTP endpoint
When MCP_HTTP_HOST is set to 0.0.0.0, anyone on the network can connect and control debugging sessions. Even on 127.0.0.1, any local process can connect.
Consider at minimum:
- A shared secret via env var (
MCP_HTTP_TOKEN) checked in anAuthorizationheader - Or document clearly that the HTTP endpoint should only be exposed on localhost/trusted networks
This is especially important since the MCP tools can inspect variables, set breakpoints, and control PHP execution.
MEDIUM: Hardcoded version: '1.0.0' in createMcpServer
const server = new McpServer({
name: 'xdebug-mcp',
version: '1.0.0', // package.json is at 1.3.0
});This was already hardcoded before this PR, but since it's now extracted into a factory, good time to fix it. Could read from package.json or use a constant.
MEDIUM: HTTP transport test duplicates server logic
The http-transport.test.ts re-implements the entire HTTP server handler in the test file instead of testing the actual code from index.ts. If someone changes the routing logic in index.ts, the tests would still pass because they're testing their own copy.
Ideally, extract the HTTP server creation from main() into a testable function (like createHttpServer(toolsContext)) and test that directly.
LOW: No request timeout for HTTP connections
Long-running or abandoned HTTP connections could accumulate. Consider adding server.setTimeout() or req.setTimeout() to clean up stale connections.
LOW: systemd service uses ExecStart=/usr/bin/env xdebug-mcp
This relies on PATH being set correctly in the systemd user context, which often isn't the case (systemd doesn't source .bashrc/.zshrc). Might want to document that users should use the full path, or add Environment=PATH=... to the service file.
NICE: Things done well
createMcpServer()extraction is clean — good factoring for reuse in HTTP vs stdio paths- stdin close handlers correctly only attached in stdio mode
- Shutdown handler properly iterates and closes all HTTP sessions before stopping the DBGp server
- The
httpSessionsMap withonclosecleanup prevents session leaks - Documentation is thorough — README, connection-modes, configuration, example configs, systemd unit
- Tests cover: 404 routing, session creation, session routing, unknown session rejection, multi-session isolation
- Comparison table in connection-modes docs is helpful
Summary
| Category | Count |
|---|---|
| HIGH | 2 (body size limit, auth) |
| MEDIUM | 2 (hardcoded version, test duplication) |
| LOW | 2 (timeout, systemd PATH) |
Solid implementation that solves a real problem cleanly. The HIGH items (body size limit and auth) are worth addressing before merge since this opens a network-facing endpoint. Everything else is polish.
Summary
MCP_TRANSPORT=http) so a single xdebug-mcp daemon can serve multiple MCP clients simultaneouslyEADDRINUSEwhen multiple AI agent sessions each try to bind port 9003Problem
When xdebug-mcp is configured as a stdio MCP server, each AI session spawns its own process. Each process tries to bind the DBGp listener on port 9003. Only the first succeeds; the rest crash.
Solution
Uses
StreamableHTTPServerTransportfrom@modelcontextprotocol/sdk. Each HTTP client gets its own MCP session, but all share the sameSessionManagerandToolsContext— which already support multiple concurrent debug sessions viasession_id.New environment variables
MCP_TRANSPORTstdiostdioorhttpMCP_HTTP_PORT3100MCP_HTTP_HOST127.0.0.1Client configuration
{ "mcpServers": { "xdebug": { "type": "http", "url": "http://localhost:3100/mcp" } } }What changed
src/config.ts— Three new Zod-validated config fieldssrc/index.ts— ExtractedcreateMcpServer()factory, added HTTP branch with per-client session management, updated shutdown handlerexamples/xdebug-mcp.service— Systemd user service for auto-starting the daemonmcp-config.http.example.json— Client config example.env.example— Documented new variablesREADME.md,docs/_reference/configuration.md,docs/_reference/connection-modes.md— Documentationpackage.json— Added vitest, updated test scriptTest plan
npx tsc— clean buildvitest run— 12/12 tests pass (7 config + 5 HTTP transport)