Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

CORSMiddleware on /register and /token forwards any non-preflight OPTIONS request straight to the body-reading handler

Open Beginner friendly
#3,652 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
84/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python
Domain
api, backend, security

Research direction

Start at src/mcp/server/auth/routes.py: read _cors() (~lines 57-63) and the /token and /register route definitions (~118-136), then confirm how starlette's CORSMiddleware decides a request is preflight. Add a guard before dispatch that a non-preflight OPTIONS (no origin, or no access-control-request-method header) answers 405 with an Allow header instead of reaching RegistrationHandler/TokenHandler. Reproduce first with the standalone httpx/ASGITransport script in the issue; done means both OPTIONS cases no longer return 201/500 and the existing auth tests under tests/ still pass.

Written by the indexing model from the issue text.

Description

v1 v2

Affected versions: mcp 1.30.0 (confirmed); still present in the latest release, mcp 2.3.0, same code at src/mcp/server/auth/routes.py:119-120 (/token) and :132-136 (/register).

Where

  • routes.py:57-63: _cors() wraps the handler in starlette.middleware.cors.CORSMiddleware with allow_methods=["POST","OPTIONS"].
  • routes.py:118-123 and :130-136: both routes list OPTIONS and are wrapped by _cors.
  • starlette/middleware/cors.py:86 (starlette 1.7.0): CORSMiddleware.__call__ only treats the request as a preflight when origin is not None AND method == "OPTIONS" AND "access-control-request-method" in headers; any other OPTIONS (including one with no CORS headers at all) falls through to simple_response, which calls the wrapped handler.

Repro (standalone, mcp + starlette + httpx only)

import anyio
from pydantic import AnyHttpUrl
from starlette.applications import Starlette
from mcp.server.auth.routes import create_auth_routes
from mcp.server.auth.settings import ClientRegistrationOptions
from httpx import ASGITransport, AsyncClient

class DummyProvider:
    def __init__(self): self.clients = {}
    async def register_client(self, client_info): self.clients[client_info.client_id] = client_info
    async def get_client(self, client_id): return self.clients.get(client_id)
    async def authorize(self, *a, **k): ...
    async def load_authorization_code(self, *a, **k): ...
    async def exchange_authorization_code(self, *a, **k): ...
    async def load_refresh_token(self, *a, **k): ...
    async def exchange_refresh_token(self, *a, **k): ...
    async def revoke_token(self, *a, **k): ...

async def main():
    provider = DummyProvider()
    routes = create_auth_routes(provider, issuer_url=AnyHttpUrl("https://example.test"), client_registration_options=ClientRegistrationOptions(enabled=True))
    app = Starlette(routes=routes)
    transport = ASGITransport(app=app, raise_app_exceptions=False)
    async with AsyncClient(transport=transport, base_url="https://example.test") as client:
        r1 = await client.options("/register")
        print("OPTIONS /register, no body, no CORS headers ->", r1.status_code)
        r2 = await client.request("OPTIONS", "/register", content=b'{"redirect_uris": ["https://client.example/cb"], "client_name": "demo"}', headers={"Content-Type": "application/json"})
        print("OPTIONS /register, JSON body, no CORS headers ->", r2.status_code, r2.text[:200])
        print("Clients stored:", list(provider.clients.keys()))

anyio.run(main)

Expected vs actual

  • Expected: a non-preflight OPTIONS is rejected (405) or answered empty, never reaching the handler.
  • Actual: the empty-body OPTIONS reaches RegistrationHandler.handle, which calls request.json() on an empty body and raises an uncaught JSONDecodeError (unhandled 500), and the JSON-body OPTIONS is parsed, assigned a client_id/client_secret, stored via provider.register_client, and answered 201 — a registration created over what looked like a CORS preflight, bypassing rate limiting.

Suggested fix

Check origin is not None and "access-control-request-method" in headers before dispatch, in create_auth_routes/cors_middleware, answering a non-preflight OPTIONS with 405 + Allow header; no Starlette change required.

Dominant language
Python
Stars
24.5k
Forks
4k
Avg merge
1d 13h
Merged PRs (30d)
35

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from modelcontextprotocol/python-sdk

All issues in modelcontextprotocol/python-sdk

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.