Skip to content

feat(mcp): serve MCP over Streamable HTTP with of mcp --http - #55

Merged
stephendolan merged 1 commit into
mainfrom
feat/mcp-http
Oct 7, 2026
Merged

stephendolan merged 1 commit into
mainfrom
feat/mcp-http

Conversation

@stephendolan

Copy link
Copy Markdown
Owner

of mcp only spoke stdio, so agents on other machines couldn't reach it. of mcp --http [port] serves stateless Streamable HTTP at http://127.0.0.1:<port>/mcp (default 3939), building a fresh McpServer per request so concurrent clients stay isolated. Tool registration moves into createMcpServer() for that; the rest of server.ts is indentation.

It binds to loopback only. The Host allowlist (loopback names plus --allow-host) blocks DNS rebinding from local browsers while letting a reverse proxy through, so publishing it on a tailnet is:

of mcp --http --allow-host <machine>.<tailnet>.ts.net
tailscale serve --bg --https=8443 http://127.0.0.1:3939

An adversarial Codex review found a request target like POST //[ crashed the process through a synchronous new URL(); paths are now matched without URL parsing. It also flagged unbounded concurrent tool runs (now capped at 16 with 503 + Retry-After) and loose Host parsing (now strict host[:port], normalized on both sides). Tool calls still run to completion if a client disconnects.

Bun's HTTP server never reports a client that disconnects mid-request, so the cap test runs the built CLI under Node, the shipped runtime; it fails if the slot isn't released. The official MCP client completes initialize, tools/list, and tool calls through tailscale serve.

🤖 Generated with Claude Code

`of mcp` only spoke stdio, so agents on other machines couldn't reach
it. `--http [port]` serves stateless Streamable HTTP at
http://127.0.0.1:<port>/mcp (default 3939), building a fresh McpServer
per request so concurrent clients stay isolated.

It binds to loopback only and accepts loopback Host names plus any
`--allow-host` names, compared as host[:port] authorities, so a local
browser can't reach it through DNS rebinding while a proxy such as
`tailscale serve` can. Paths are matched without URL parsing so a
malformed request target can't throw, and at most 16 requests run at
once, since tool calls finish even after a client disconnects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@stephendolan
stephendolan merged commit fc8c05e into main Oct 7, 2026
1 check passed
@stephendolan
stephendolan deleted the feat/mcp-http branch October 7, 2026 17:57
stephendolan added a commit that referenced this pull request Oct 7, 2026
Minor release: `of mcp --http` serves the MCP server over Streamable
HTTP on loopback, with a Host allowlist for publishing it through a
proxy such as `tailscale serve` (#55).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant