Repository navigation
everything: bind the HTTP transports to loopback by default, with HOST to widen - #5452
Conversation
…th HOST to widen The Streamable HTTP and HTTP+SSE transports called app.listen(port) with no host, so they bound every interface and a non-browser client on the local network (which sends no Origin) could reach the server's tools. listenOrExit now takes the host to bind; both transports resolve it from HOST, defaulting to 127.0.0.1, and the listening line and the port-in-use message name the bound address. The container image sets HOST=0.0.0.0, since loopback inside a container is unreachable from a published port. The boot smoke pins HOST to the loopback address it connects to. The README, the Dockerfile and the local-dev and client-smoke skills document HOST and the new listening line. Based on the prototype in #3303 by TheodorNEngoy. Closes #5371 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
🦋 Changeset detectedLatest commit: 41a758c The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
| Tools | Prompts | Resources | Resource templates | |
|---|---|---|---|---|
| Base | 22 | 4 | 8 | 5 |
| Head | 22 | 4 | 8 | 5 |
Updated by this run for 41a758c288398b7aa2b38f6ddaad43dd60e6498d.
There was a problem hiding this comment.
🟡 Changes recommended
The default binding change breaks remote access for existing non-Docker deployments without HOST configured, so the minor changeset must be changed to major under the release policy.
1 open issue
What changed in this PR
This PR addresses #5371 by making the everything server’s HTTP transports loopback-only by default, reducing unintended network exposure while allowing explicit access through HOST.
Changes:
- Defaults both HTTP transports to
127.0.0.1and includes the host in startup messages. - Preserves Docker port access and pins boot-smoke connections to loopback.
- Adds bind-address tests and updates documentation and release metadata.
| File | Description |
|---|---|
src/everything/transports/streamableHttp.ts |
Applies the configured host to Streamable HTTP. |
src/everything/transports/sse.ts |
Applies the configured host to HTTP+SSE. |
src/everything/transports/listen.ts |
Centralizes host selection, binding, and address formatting. |
src/everything/README.md |
Documents HOST, loopback defaults, and Docker behavior. |
src/everything/Dockerfile |
Sets HOST=0.0.0.0 for published-port access. |
src/everything/__tests__/streamable-http.test.ts |
Checks default and overridden launcher hosts. |
src/everything/__tests__/sse.test.ts |
Checks default and overridden SSE hosts. |
src/everything/__tests__/listen.test.ts |
Tests real socket binds, failures, and host helpers. |
scripts/smoke-servers.mjs |
Pins HTTP smoke-test servers to loopback. |
.claude/skills/local-dev/SKILL.md |
Updates bind configuration and logging guidance. |
.claude/skills/client-smoke/SKILL.md |
Updates the expected listening-line example. |
.changeset/everything-loopback-bind-5371.md |
Records the release impact and migration option. |
Review effort: Balanced
| @@ -0,0 +1,5 @@ | |||
| --- | |||
| "@modelcontextprotocol/server-everything": minor | |||
There was a problem hiding this comment.
Declined, keeping minor. This is a release-policy call the maintainers already made for this issue: a new HOST option is a minor change. It also matches how this repo has shipped comparable security-default tightening. #5241 made both HTTP transports refuse any non-localhost Origin with a 403, which likewise stopped some remote callers, and it went out as a patch ("Harden request handling in the HTTP transports", 2.0.1). AGENTS.md's examples of a major change are interface breaks (a tool removed or renamed, a schema change that breaks clients, a protocol or Node floor bump). This change touches none of those: no tool, schema or protocol changes, and every local client and the Docker image keep working unchanged.
The changeset and the PR's Breaking Changes section both say what the default change means for anyone who reached the server from another host, and how to restore it (HOST=0.0.0.0). If a maintainer wants this released as major instead, that is a one-word change to the changeset.
|
Copilot round 1: one finding, declined.
Nothing was pushed, and every finding in the round was declined, so the loop stops here (pr-flow exit: out of scope only). |

Closes #5371
Description
The
everythingserver's HTTP transports (Streamable HTTP and the deprecated HTTP+SSE) now bind the loopback interface,127.0.0.1, by default instead of every interface. A newHOSTenvironment variable widens the bind deliberately (HOST=0.0.0.0, or::, or any address to bind).transports/listen.ts:listenOrExit(app, port, host, message)now takes the host and passes it tolisten. It also exportsDEFAULT_HOST(127.0.0.1),bindHost()(HOSTwhen set and not empty, else loopback) andhostAndPort()(formatshost:port, bracketing an IPv6 literal). The port-in-use message now names the host too (Port 3001 is already in use on 127.0.0.1), and still matches the boot smoke'salready in useretry check.streamableHttp.tsandsse.ts: both resolve the host withbindHost(). Their listening lines now name the bound address:MCP Streamable HTTP Server listening on 127.0.0.1:3001andServer is running on 127.0.0.1:3001(previously… on port 3001).Dockerfile: setsENV HOST=0.0.0.0. Inside a container, loopback belongs to the container itself, so a published port could not reach a server bound there.docker run -p 127.0.0.1:3001:3001still keeps it off the host's network.scripts/smoke-servers.mjs: the boot smoke pinsHOST=127.0.0.1for the servers it launches, since that is the address its client connects to. Without the pin, aHOSTexported in the developer's environment could move the bind. ThefreePortcomment is updated: probing the wildcard address is still the strictest check.everythingREADME documentsHOSTand the loopback default (with aPORT/HOSTtable) and says why the Docker image setsHOST. Thelocal-devandclient-smokeskills no longer refer to the oldlistening on portwording.Credit: this follows the reference prototype in #3303 by @TheodorNEngoy, which bound both HTTP transports to
127.0.0.1with aHOSToverride. Its CORS half already shipped in #5241.Server Details
everythingHOSTenvironment variable, the Dockerfile, and the README. stdio is unchanged.Motivation and Context
With no host,
app.listen(port)binds every interface. A developer who startseverythingover HTTP on a laptop then exposes its tools to every host on the local network. The localhost-onlyOrigincheck (#5241) stops a foreign browser page, but a non-browser client on the LAN sends noOriginand gets through. See #5371.How Has This Been Tested?
Automated (vitest, real sockets):
listen.test.tschecks that the default bind is127.0.0.1(read back fromserver.address()), that a given host is bound (0.0.0.0), that a port held on loopback produces the host-naming port-in-use message and exit 1, and that an address on no interface (192.0.2.1, RFC 5737) fails withEADDRNOTAVAILand exit 1 without printing the listening line. It also coversbindHost(unset, empty, set, and read fromprocess.env) andhostAndPort(IPv4, name, bracketed IPv6).sse.test.tsandstreamable-http.test.tscheck that each launcher passes127.0.0.1by default andHOSTwhen set, with the matching listening line. Per-file coverage foreverythingstays at or above 90 on all four dimensions.npm run local:gateexits 0, including the boot smoke ofeverythingover stdio, SSE and Streamable HTTP.Inspector CLI (
@modelcontextprotocol/inspector@2.14.0 --cli <url> --protocol-era <era> --method tools/call --tool-name echo --tool-arg message=hi-<era> --format json).192.168.2.23is this machine's LAN address, standing in for another host on the network.Before: the published
@modelcontextprotocol/server-everything@2.2.1, which hasv2/main'slisten.ts.PORT=3941 … streamableHttp:After: this branch,
HOSTunset,PORT=3942 node src/everything/dist/index.js streamableHttp:After:
HOST=0.0.0.0. The LAN address is reachable again, deliberately:HTTP+SSE (2025 era only), this branch:
HOST=::1:MCP Streamable HTTP Server listening on [::1]:3946,lsof: [::1]:3946.http://localhost:3946/mcpsucceeds in both eras, and127.0.0.1and the LAN address are refused.LLM client: Claude Code 2.1.318 (
claude -p --strict-mcp-config --mcp-config <file>, with{"type":"http","url":…}), against this branch withHOSTunset (PORT=3951). Prompt: "Use the everything server's echo tool to echo the message 'bound to loopback', and tell me exactly what it returned. If the server or tool is unavailable, say so plainly."url: http://localhost:3951/mcp: "The everything server's echo tool worked, and it returned exactly this:Echo: bound to loopback"url: http://192.168.2.23:3951/mcp: "I couldn't run the echo, because the everything MCP server is unavailable. It failed to connect at session start withECONNREFUSED…"Era coverage: this Claude Code version connected on 2026-07-28 (the server created no 2025-era session, only session-less POSTs). No 2025-era LLM client was available on this machine, so the 2025-11-25 era is covered by the Inspector CLI runs above (
--protocol-era legacy, over both Streamable HTTP and SSE). The change is the bind address, below the protocol, so it does not depend on the era.Breaking Changes
Only for anyone who reached an HTTP-transport
everythingserver from another host. They now needHOST=0.0.0.0(or the address to bind). Local clients, stdio, and the Docker image (which setsHOST=0.0.0.0itself) are unaffected. Released as a minor bump, sinceHOSTis a new option.Types of changes
Checklist
npm run changeset) if this changes what a TypeScript server publishes (.changeset/everything-loopback-bind-5371.md, minor)HOSTexits 1 throughlistenOrExit's existing error path, without printing the listening line)Additional context
HOST=localhostbinds whichever addresslocalhostresolves to first, which can be::1alone. The README's examples use127.0.0.1and0.0.0.0for that reason. The default listening line prints the configured host, not a resolved one.🤖 Generated with Claude Code