Skip to content

feat: add StreamableHTTP transport for shared daemon mode#4

Open
Luc45 wants to merge 1 commit intokpanuragh:mainfrom
Luc45:feat/streamable-http-transport
Open

feat: add StreamableHTTP transport for shared daemon mode#4
Luc45 wants to merge 1 commit intokpanuragh:mainfrom
Luc45:feat/streamable-http-transport

Conversation

@Luc45
Copy link
Copy Markdown

@Luc45 Luc45 commented Apr 3, 2026

Summary

  • Adds HTTP transport mode (MCP_TRANSPORT=http) so a single xdebug-mcp daemon can serve multiple MCP clients simultaneously
  • Solves EADDRINUSE when multiple AI agent sessions each try to bind port 9003
  • Stdio transport remains the default — fully backward compatible

Problem

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

PHP/Xdebug --TCP:9003--> xdebug-mcp daemon --HTTP:3100--> Claude Session 1
                          (one process)      (MCP)     --> Claude Session 2
                                                       --> Claude Session 3

Uses StreamableHTTPServerTransport from @modelcontextprotocol/sdk. Each HTTP client gets its own MCP session, but all share the same SessionManager and ToolsContext — which already support multiple concurrent debug sessions via session_id.

New environment variables

Variable Default Description
MCP_TRANSPORT stdio Transport mode: stdio or http
MCP_HTTP_PORT 3100 HTTP endpoint port
MCP_HTTP_HOST 127.0.0.1 HTTP bind address

Client configuration

{
  "mcpServers": {
    "xdebug": {
      "type": "http",
      "url": "http://localhost:3100/mcp"
    }
  }
}

What changed

  • src/config.ts — Three new Zod-validated config fields
  • src/index.ts — Extracted createMcpServer() factory, added HTTP branch with per-client session management, updated shutdown handler
  • examples/xdebug-mcp.service — Systemd user service for auto-starting the daemon
  • mcp-config.http.example.json — Client config example
  • .env.example — Documented new variables
  • README.md, docs/_reference/configuration.md, docs/_reference/connection-modes.md — Documentation
  • package.json — Added vitest, updated test script

Test plan

  • npx tsc — clean build
  • vitest run — 12/12 tests pass (7 config + 5 HTTP transport)
  • Manual: stdio mode works as before
  • Manual: HTTP mode — set breakpoint, run PHP script, step through, inspect variables
  • Manual: Multiple Claude Code sessions connect to same HTTP daemon without port conflicts

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.
Copy link
Copy Markdown
Owner

@kpanuragh kpanuragh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 an Authorization header
  • 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 httpSessions Map with onclose cleanup 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants