Repository navigation
Authentication in High Level MCPServer #3283
Description
Activity
- addedquestionFurther information is requestedFurther information is requested
on Aug 11, 2026 Yeah, I agree this is awkward.
token_verifier=shouldn't make you invent an authorization server, and the docs only ever show the "there's an AS somewhere" shape.To answer the direct question: OAuth is a constructor-shape prerequisite today, not a runtime one. Nothing ever contacts
issuer_url; in resource-server-only mode it's just echoed into the/.well-known/oauth-protected-resourcedocument. Andresource_server_urlis typedAnyHttpUrl | None, so if you passNoneexplicitly that document isn't served at all and an unauthenticated request gets a plain401withWWW-Authenticate: Bearer error="invalid_token"and no discovery pointer, which is exactly the flow you drew.A few things that I think explain what you hit:
- The verifier itself
AccessTokenrequiresclient_idandscopesas well astoken.AccessToken(token=token)raises a validation error insideverify_token, so the correct token gets a 500 and everything else a 401. The static token could never succeed as written.
- The OAuth flow
- The server config doesn't push a client into OAuth; a 401 does.
- That "(or origin)" message is from the TypeScript SDK's client. Clients built on it (Inspector etc.) react to a 401 by fetching the metadata document and chasing
authorization_servers, which for a placeholder issuer is a dead end. - If the client sends
Authorization: Bearer …from the first request, none of that runs. In Inspector that's the custom headers section (leave the OAuth settings empty); most hosts take aheadersmap in the server config.
- localhost vs 127.0.0.1
- That's the client correctly (per RFC 9728) rejecting a metadata document whose
resourcedoesn't match the URL it dialed. - If you keep
resource_server_url, set it to exactly the URL clients connect to. - With
resource_server_url=Nonethere's no document to mismatch.
- That's the client correctly (per RFC 9728) rejecting a metadata document whose
Here's a working example end to end:
server.py
import secrets from pydantic import AnyHttpUrl from mcp.server import MCPServer from mcp.server.auth.middleware.auth_context import get_access_token from mcp.server.auth.provider import AccessToken, TokenVerifier from mcp.server.auth.settings import AuthSettings TOKEN = "alice-token" # read from the environment in real life, and serve over TLS if it's remote class StaticTokenVerifier(TokenVerifier): async def verify_token(self, token: str) -> AccessToken | None: if secrets.compare_digest(token, TOKEN): return AccessToken(token=token, client_id="alice", scopes=[]) return None mcp = MCPServer( "notes", token_verifier=StaticTokenVerifier(), auth=AuthSettings( issuer_url=AnyHttpUrl("https://unused.invalid"), # placeholder, never contacted resource_server_url=None, # no metadata document, plain 401 ), ) @mcp.tool() def whoami() -> str: token = get_access_token() return token.client_id if token else "anonymous" if __name__ == "__main__": mcp.run("streamable-http", port=10000)
client.py
import anyio import httpx2 from mcp import Client from mcp.client.streamable_http import streamable_http_client async def main() -> None: async with httpx2.AsyncClient( headers={"Authorization": "Bearer alice-token"}, timeout=httpx2.Timeout(30.0, read=300.0), follow_redirects=True, ) as http: transport = streamable_http_client("http://127.0.0.1:10000/mcp", http_client=http) async with Client(transport) as client: result = await client.call_tool("whoami", {}) print(result.content[0].text) # alice anyio.run(main)
There's a runnable version of this in
examples/stories/bearer_auth/(it keepsresource_server_urlset so you can see the metadata route too), and the client-side header pattern is under "Bring your ownhttpx2.AsyncClient" in docs/client/transports.md.Hopefully that gets you going, but let me know if it doesn't :)
Reacted by saurabhlalsaxena- The verifier itself
Thanks. I tried running your code and using the mcp inspector to test the MCP, I still get the same error:
Failed to connect to "mcp-server" Protected resource http://localhost:10000/mcp does not match expected http://127.0.0.1:10000/mcp (or origin)Here are the server logs:
INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:10000 (Press CTRL+C to quit) INFO: 127.0.0.1:51525 - "GET / HTTP/1.1" 404 Not Found INFO: 127.0.0.1:51525 - "GET /json/version HTTP/1.1" 404 Not Found INFO: 127.0.0.1:51527 - "GET / HTTP/1.1" 404 Not Found INFO: 127.0.0.1:51542 - "POST /mcp HTTP/1.1" 401 Unauthorized INFO: 127.0.0.1:51545 - "GET /.well-known/oauth-authorization-server HTTP/1.1" 404 Not Found INFO: 127.0.0.1:51545 - "GET /.well-known/openid-configuration HTTP/1.1" 404 Not Found INFO: 127.0.0.1:51542 - "POST /mcp HTTP/1.1" 401 Unauthorized INFO: 127.0.0.1:51545 - "GET /.well-known/oauth-authorization-server HTTP/1.1" 404 Not Found INFO: 127.0.0.1:51545 - "GET /.well-known/openid-configuration HTTP/1.1" 404 Not Found INFO: 127.0.0.1:51568 - "POST /mcp HTTP/1.1" 401 Unauthorized INFO: 127.0.0.1:51569 - "GET /.well-known/oauth-authorization-server HTTP/1.1" 404 Not Found INFO: 127.0.0.1:51569 - "GET /.well-known/openid-configuration HTTP/1.1" 404 Not Found INFO: 127.0.0.1:51568 - "POST /mcp HTTP/1.1" 401 Unauthorized INFO: 127.0.0.1:51569 - "GET /.well-known/oauth-authorization-server HTTP/1.1" 404 Not Found INFO: 127.0.0.1:51569 - "GET /.well-known/openid-configuration HTTP/1.1" 404 Not Found
Ok. So got it to work. Had missed your point on using TypeScript SDK client. Here is what I had to change:
In MCP Inspector, added this under Custom Headers
Authorization: Bearer alice-tokenName | Value Authorization | Bearer alice-token
And left the OAuth configuration empty.
My feedback would still be that the Auth flow is fragmented. Maybe Auth needs to be a separate library (mcp-auth) instead of trying to incorporate it in the MCP SDK, just as you have 'flask-login' for flask.
Also the issues with the TypeScript SDK client seems like a bug.
- addedP3Nice to haves, rare edge casesNice to haves, rare edge casesv1Affects the v1.x maintenance lineAffects the v1.x maintenance linev2Affects the v2 line (2.x on main)Affects the v2 line (2.x on main)
on Aug 12, 2026
Question
I'm trying to understand the authentication model in the new MCP Python SDK v2.
I want to implement the simplest possible authentication for a remote Streamable HTTP MCP server:
Authorization: Bearer <token>.I initially tried using
TokenVerifier:However,
MCPServerrequiresAuthSettingswhenever atoken_verifieris supplied.AuthSettingsin my version requires bothissuer_urlandresource_server_url.I don't understand why
issuer_urlis required for this use case. There is no Authorization Server issuing the token—the token is simply pre-configured on the MCP server.Adding
AuthSettingsalso appears to cause the client to enter the OAuth discovery/authorization flow. For example, I encountered:I also encountered a protected-resource URL validation error when the URL differed only by
localhostvs127.0.0.1:My understanding is that
TokenVerifierand the OAuth discovery/authorization-server configuration are separate concerns. The low-level SDK code appears to supportBearerAuthBackend(token_verifier)independently, whileresource_server_urlis used for protected-resource metadata.Is there a supported way in MCP Python SDK v2 to implement simple bearer-token authentication for Streamable HTTP without configuring an OAuth Authorization Server and without having to implement or manually compose Starlette middleware?
In other words, I'm looking for the equivalent of:
without requiring:
Is this supported by the high-level
MCPServerAPI, or is OAuth intentionally a prerequisite for usingTokenVerifier?