The short answer
If your MCP server runs on your own machine, prefer the stdio transport, which the spec recommends for local servers because it limits access to the MCP client.3 If you must use HTTP, do three things: bind to 127.0.0.1, reject requests whose Origin header isn’t yours with HTTP 403, and require a token. The MCP spec says servers must validate Origin on all incoming connections to prevent DNS rebinding, must answer 403 when it is present and invalid, should bind only to localhost when running locally, and should authenticate all connections.2 We add a fourth: reject a Host header that isn’t localhost or 127.0.0.1.
Deedglass
A tool call meets your rules before it runs
Asks to run
DELETE FROM visits;
Deletes every row in visits
Deedglass checks your rules
- Reading dataAllowed
- Deleting a whole table (DROP, TRUNCATE)Never
- Changing or deleting rows without saying which (WHERE)This one
- Other changesWait for a person
Why localhost isn’t private
127.0.0.1 is unreachable from other machines. It is reachable from every program on your machine, and that includes your browser, which runs code from every web page you open.
Two routes from a web page to your local server:
- A plain cross-origin request. A page on
evil.examplecan send aPOSTtohttp://127.0.0.1:8765/mcp. The browser may block the page from reading the response, but for many request shapes the request still arrives and your server still acts on it. For a tool call, acting is the damage. - DNS rebinding. The page’s domain first resolves to the attacker’s server, then is switched to resolve to
127.0.0.1. Now the browser thinks requests toevil.example:8765are same-origin, and the page can read responses too. Your server seesHost: evil.example:8765.
The spec puts it directly: without these protections, attackers could use DNS rebinding to interact with local MCP servers from remote websites.2
What the spec asks for
| Check | Spec wording | Why |
|---|---|---|
Validate Origin |
MUST, on all incoming connections; 403 if present and invalid21 | Browsers attach Origin to cross-origin requests; scripts on a page can’t forge it |
| Bind to localhost | SHOULD, rather than all interfaces (0.0.0.0)2 |
Keeps other machines on the network out |
| Authenticate | SHOULD, for all connections2 | Stops other local processes and anything that slips past the browser checks |
| Prefer stdio, or restrict HTTP | SHOULD use stdio; with HTTP, require a token or use unix sockets or other restricted IPC3 | No network listener at all |
The Host check is our addition. It isn’t in the transport’s security warning, but it’s the standard defence against rebinding: after rebinding, the Host header carries the attacker’s name, not localhost.
A 20-line server with all three checks
Node’s standard library only, no framework:
Code Program code, for developers. The text around it says what it does.
import http from 'node:http';
const PORT = 8765;
const ALLOWED_ORIGINS = new Set([`http://localhost:${PORT}`, `http://127.0.0.1:${PORT}`]);
const ALLOWED_HOSTS = new Set([`localhost:${PORT}`, `127.0.0.1:${PORT}`]);
const TOKEN = process.env.MCP_TOKEN;
http.createServer((req, res) => {
const origin = req.headers.origin;
if (origin !== undefined && !ALLOWED_ORIGINS.has(origin)) { res.writeHead(403); return res.end('origin refused\n'); }
if (!ALLOWED_HOSTS.has(req.headers.host ?? '')) { res.writeHead(421); return res.end('host refused\n'); }
if (req.headers.authorization !== `Bearer ${TOKEN}`) { res.writeHead(401); return res.end('token required\n'); }
res.writeHead(200, { 'content-type': 'application/json' });
res.end('{"jsonrpc":"2.0","id":1,"result":{}}\n');
}).listen(PORT, '127.0.0.1');
Three choices worth copying:
- A missing
Originpasses the first check. Non-browser clients such as your MCP client don’t send one. The token check still applies to them. - The
Hostcheck returns 421 (Misdirected Request). Any 4xx works; 421 says what happened. - The token comes from the environment, generated at startup and handed to the client in its config. A fixed token in the source is no token.
What we saw with curl
We started the server with MCP_TOKEN=s3cret and sent five requests:
Code What the screen or file shows, copied as is.
1 no Origin, token 200
2 evil Origin, token 403
3 rebound Host, token 421
4 no token 401
5 local Origin, token 200
Request 2 is what a cross-origin page sends. Request 3 is what a rebound page sends. Request 4 is any other local process without the token. Only the legitimate client (1) and a page on the server’s own origin (5) got through.
lsof confirmed the listener was on 127.0.0.1:8765, not *:8765.
A real case: MCP Inspector
The MCP Inspector, the project’s own debugging tool, shipped a critical flaw in its local proxy. GitHub advisory GHSA-7f8r-222p-6f5g (CVE-2025-49596, CVSS 9.4) says versions below 0.14.1 were vulnerable to remote code execution because of a lack of authentication between the Inspector client and proxy, which allowed unauthenticated requests to launch MCP commands over stdio. The fix was version 0.14.1.4
The lesson generalises. A local process that can start other processes, and listens on a port without authentication, is a remote code execution waiting for a web page. If you run the Inspector, check its version. If you write a local MCP proxy, put the token check in from the first commit.
Where the checks run out
These checks protect the listener. They don’t help with three neighbouring problems:
- A malicious server you installed. If the server itself is hostile, the Origin check protects its port, not you. The spec lists malicious startup commands in a client configuration and malicious payloads inside a server as separate attacks.3 Read what you install, and pin versions.
- Other local software with your token. Anything that can read the client’s config file can read the token. Keep the token out of shared dotfiles and out of the repository.
- Remote servers. A server on the internet needs real OAuth and audience checks, not a shared token; token passthrough and the confused deputy covers the spec’s rules for those.
One more habit helps: when you finish a debugging session, stop the local server. A listener that isn’t running can’t be reached by any page.
A checklist for local servers
- Use stdio unless you have a reason for HTTP.
- Bind to
127.0.0.1, never0.0.0.0. - Reject a present, foreign
Originwith 403. - Reject a
Hostthat isn’tlocalhostor127.0.0.1with your port. - Require a random token, generated at startup, on every request.
- Don’t enable permissive CORS (
Access-Control-Allow-Origin: *) on an MCP endpoint. - Keep debugging tools such as the Inspector updated, and stop them when you’re done.
- If a client offers one-click install of a local server, read the full command first; the spec requires clients to show it untruncated.3
Transport checks keep strangers out; they don’t limit what a connected agent may do. For that, see MCP server security basics and the MCP gateway entry. To grade what the servers configured on your machine can change, paste the config into the check page or run npx deedglass scan (how the grade works); to try the checks in your browser first, open the playground.
Written with AI assistance and checked against our claims register.