
This fix is part of MCP troubleshooting, within The MCP Guide. Still stuck? Request a free MCP readiness review.
A CORS error on an MCP server means a browser blocked the request, not the server. It only happens when the client runs in a web page, such as a web-based agent or the MCP Inspector web UI. The fastest fix is to add CORS middleware to your server that allows your client's origin, allows the Mcp-Method, Mcp-Name and MCP-Protocol-Version request headers, and exposes Mcp-Session-Id if you still serve 2025-era clients. Then make sure the server's own Origin allowlist agrees with the CORS list.
Checked against: the Python SDK "Add to an existing app" guide, the Streamable HTTP page of the 2026-07-28 spec and MDN's CORS guide on 2 November 2026 (Python SDK mcp 2.3.0, TypeScript @modelcontextprotocol/server 2.4.0). eSEOspace did not run these steps.
Key Takeaways
- CORS is enforced by browsers. Desktop and terminal clients such as Claude Code never trigger it.
- Every MCP request is preflighted, because
Content-Type: application/jsonand theMcp-*headers are not on the CORS safelist. - The 2026-07-28 spec requires
MCP-Protocol-Version,Mcp-Methodand, for some methods,Mcp-Nameheaders, so older allow-lists often miss them. - A server's
Origincheck is separate from CORS: an origin it does not trust gets HTTP 403 even after a clean preflight. - Never answer with a
*wildcard on credentialed requests. MDN says browsers reject that.
What this error means
Browsers block a script on one origin from reading a response from another origin unless the server opts in. That opt-in is CORS. MDN explains that for anything beyond a "simple" request, the browser first sends an OPTIONS preflight and only sends the real request if the server approves it. Only three content types count as simple: application/x-www-form-urlencoded, multipart/form-data and text/plain. MCP uses JSON, so every request is preflighted.
The Python SDK guide says a browser client needs two permissions: to send its MCP request headers, and to read the one MCP sends back. It calls allow_headers "the half everyone forgets."
The 2026-07-28 spec made this more likely. The Streamable HTTP page requires an MCP-Protocol-Version header on every POST and Mcp-Method on all requests, with Mcp-Name on tools/call, resources/read and prompts/get. The TypeScript SDK's protocol versions page notes that deployed 2025 servers "commonly have allow-lists that predate the 2026 headers."
Typical browser console messages include "CORS header 'Access-Control-Allow-Origin' missing" or a preflight that fails. A modern client may simply report a TypeError from fetch. Use our MCP error code lookup when you also see a JSON-RPC code.
Quick fix
- Confirm the client really runs in a browser. If the same server works in Claude Code or the Inspector CLI, the problem is CORS, not the server.
- Add CORS middleware in front of your MCP endpoint, with your exact client origin (no wildcard if you send cookies or an
Authorizationheader). - Allow these request headers:
Authorization,Content-Type,Mcp-Method,Mcp-Name,MCP-Protocol-Version. AddMcp-Session-IdandLast-Event-IDonly if you still serve 2025-era clients. - Expose
Mcp-Session-IdinAccess-Control-Expose-Headersif you run a sessionful 2025-era endpoint. MDN notes that only safelisted response headers are visible to scripts by default. - Allow
POST(andGETandDELETEfor 2025-era sessions). - Add the same origin to your MCP server's own
Originallowlist. - Reload the page and watch the
OPTIONSrequest in the browser's Network tab.
Fix by client / OS
Python server (Starlette or FastAPI)
The Python SDK guide gives this pattern, which we copied from its example file (checked against mcp 2.3.0 docs):
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from mcp.server.transport_security import TransportSecuritySettings
security = TransportSecuritySettings(
allowed_hosts=["mcp.example.com", "mcp.example.com:*"],
allowed_origins=["https://app.example.com"],
)
middleware = [
Middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_methods=["GET", "POST", "DELETE"],
allow_headers=[
"Authorization", "Content-Type", "Last-Event-ID",
"Mcp-Method", "Mcp-Name", "Mcp-Protocol-Version", "Mcp-Session-Id",
],
expose_headers=["Mcp-Session-Id"],
)
]
Pass transport_security=security to mcp.streamable_http_app(). The guide warns that "an origin the transport doesn't trust gets a 403 even after a clean preflight."
TypeScript server
The TypeScript v2 Express guide says createMcpExpressApp validates Host and Origin by default on localhost binds. Passing allowedOrigins replaces the default list, and http://* and https://* are not honored. Add your CORS middleware (for example the cors package) with the same headers listed above. A browser extension client sends its extension ID as Origin, so add that ID to the list.
MCP Inspector web UI
The Inspector web app runs in your browser, so it can hit CORS where the CLI does not. The Inspector README points to its TLS page for custom hostnames and ALLOWED_ORIGINS. To confirm the server itself works, run the CLI version, which has no browser in the path:
npx @modelcontextprotocol/inspector --cli https://mcp.example.com/mcp --transport http --method tools/list
Claude Code, Claude Desktop, Cursor and VS Code
These clients do not run your requests inside a web page, so they do not apply CORS. If one of them fails against your server, the cause is something else. See Claude Code MCP failed to connect.
Where to find the logs
- Browser: the Console shows the CORS reason; the Network tab shows the
OPTIONSpreflight and whichAccess-Control-*headers came back. - Python server: a rejected
Originreturns403with the bodyInvalid Origin header, per the Python SDK troubleshooting page. A rejectedHostreturns421 Misdirected Requestand logsInvalid Host headerat WARNING. - Reverse proxy or CDN: check its access log for
OPTIONSrequests that never reached the app.
If it still fails
- Preflight returns 404 or 405. Your proxy or framework does not route
OPTIONSto the CORS middleware. Put CORS before the MCP mount. - Preflight passes, then 403. That is the server's
Origincheck, not CORS. Add the origin toallowed_originsorallowedOrigins. - 421 on every request after deploy. That is the Host allowlist. Add your public hostname with and without
:*. - Works locally, fails in production. A CDN may strip or cache CORS headers. Test the origin server directly.
- Security check. Do not "fix" CORS with
*everywhere. The spec requiresOriginvalidation to prevent DNS rebinding. Our Streamable HTTP reference covers the header rules.
Next step
If you are putting an MCP server in front of browser clients for the first time, eSEOspace's MCP development team can review your transport, CORS and auth setup. For the full list of fix guides, start at the MCP Guide.
More in the MCP Guide
Free tools: MCP Error Code Lookup · MCP Config Generator
Latest research: State of MCP: October 2026
Frequently asked questions
Why does my MCP server work in Claude Code but not in my web app?
Claude Code is not a browser, so it does not enforce CORS. Your web app runs in a browser, which preflights every MCP request and blocks it unless the server allows your origin and headers.
Do I still need to expose Mcp-Session-Id?
Only for 2025-era clients on a sessionful endpoint. The 2026-07-28 spec removed protocol sessions, so a modern-only server does not send that header. The Python SDK guide still exposes it because its example serves both eras.
Can I use Access-Control-Allow-Origin: * for an MCP server?
Not for requests with credentials. MDN states that the server must not use the * wildcard for Access-Control-Allow-Origin or Access-Control-Allow-Headers on a credentialed request. Most MCP servers use an Authorization header, so list origins explicitly.
Is a 403 Invalid Origin header the same as a CORS error?
No. CORS is the browser blocking a response. A 403 Invalid Origin header is your MCP server refusing the request because its own allowlist does not include that origin. You need both lists to match.











