You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
AuthSettings.issuer_url is required in resource-server-only mode but never used #3702
Following up on #3283, where it was noted that token_verifier= shouldn't make you invent an authorization server. I didn't find an issue tracking that, so here's one.
What happens
For a server that only verifies bearer tokens (token_verifier=, no auth_server_provider, resource_server_url=None):
AuthSettings(resource_server_url=None) fails validation: issuer_url field required.
MCPServer(token_verifier=...) without auth= raises "Cannot specify auth_server_provider or token_verifier without auth settings".
With a placeholder such as issuer_url="https://placeholder.invalid", everything works: only /mcp is mounted, a missing token gets 401 and a valid one gets 200. The value is never read. It's only used when auth_server_provider is set (server.py:1218) or when the PRM document is served (server.py:1277).
Minor related point: resource_server_url is typed AnyHttpUrl | None but declared with Field(...), so None has to be passed explicitly.
What I expected
To configure a resource-server-only setup without a made-up URL.
Validate that issuer_url is set when resource_server_url is set, since the PRM authorization_servers needs an entry, and when auth_server_provider is used.
Existing configs behave the same. The only change is that the placeholder is no longer needed.
The alternative would be to allow token_verifier= without auth= at all, but that's a bigger change. Does the first shape fit, or would you prefer something different? I'm happy to implement whichever you prefer.
The shape you propose is right, and it is worth pinning down where the requiredness should live, because the two consumers you found are the whole specification.
issuer_url is read in exactly two places: when an authorization server provider is configured, and when the PRM document is served. So requiredness is conditional, not intrinsic: it is needed if and only if one of those paths can occur. In the token_verifier-only shape neither can, so neither URL should block construction.
Notes on landing that:
The pairing check needs both values visible. If AuthSettings cannot see whether a provider will be supplied, enforce the combination where both are known, next to the existing MCPServer-level error, and keep the model-level fields permissive. That also fixes the message: "provide issuer_url when you serve PRM or configure an auth_server_provider" is actionable, while the current text sends people toward inventing placeholder settings.
Default the fields. resource_server_url is typed AnyHttpUrl | None but declared with Field(...), so the declaration contradicts the annotation; a None default aligns them and removes the explicit-None wart.
Backward compatibility is free: placeholder values keep validating, only the requirement changes, and existing configs read the same.
Test matrix worth encoding: RS-only with no placeholder (401 without a token, 200 with one, only /mcp mounted); provider mode unchanged; PRM-served mode still requires issuer_url, asserted as an error case, which guards against over-loosening.
We run a sizable MCP deployment at https://www.corpusiq.io, and required-but-unread config fields are a debt class we watch for: they acquire placeholder values in templates, the placeholders ship, and nobody can say later why they exist.
AuthSettings(resource_server_url=None) raises ValidationError: 1 validation error for AuthSettings / issuer_url / Field required — reproduces exactly as described.
Audited all issuer_url read sites: src/mcp/server/lowlevel/server.py:803 (inside if auth_server_provider:), :839 (inside if auth and auth.resource_server_url:), src/mcp/server/mcpserver/server.py:1218 (inside if self._auth_server_provider:), :1277 (inside the resource_server_url branch). In resource-server-only mode (token_verifier set, no auth server provider, no resource_server_url) none of them execute — so the mandatory field is never read.
The incremental fix shape from the issue (optional issuer_url/resource_server_url plus a validator requiring issuer_url only when resource_server_url is set or an auth server provider is used) matches this audit.
Note: open PR #3292 takes the alternative, bigger approach (token verifier without AuthSettings at all) — flagging in case maintainers prefer it.
Disclosure: this verification was AI-assisted (per this repo's CONTRIBUTING).
Adding to what @lzif listed, two things to know about RS-only mode without resource_server_url (checked at 754254c):
No protected resource metadata is served, and the 401 carries no resource_metadata in WWW-Authenticate. resource_metadata_url and the PRM routes are only set up when auth.resource_server_url is set (bearer_auth.py L139-140 adds the header only then). The MCP spec says servers "MUST implement one of the following discovery mechanisms to provide authorisation server location information to MCP clients", so in this mode discovery has to come from outside the SDK, for example, a gateway or a separately hosted PRM.
validate_token_resource requires resource_server_url, so the audience check has to live in your TokenVerifier. The spec: "MCP servers MUST validate that access tokens were issued specifically for them as the intended audience".
Also, the validator you proposed matches the MCP spec, which is stricter than RFC 9728. RFC 9728 marks authorization_servers as OPTIONAL, while the MCP spec says the PRM document "MUST include the authorization_servers field containing at least one authorisation server." https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization
Activity
Ben-Home commentedon Oct 14, 2026
The shape you propose is right, and it is worth pinning down where the requiredness should live, because the two consumers you found are the whole specification.
issuer_url is read in exactly two places: when an authorization server provider is configured, and when the PRM document is served. So requiredness is conditional, not intrinsic: it is needed if and only if one of those paths can occur. In the token_verifier-only shape neither can, so neither URL should block construction.
Notes on landing that:
The pairing check needs both values visible. If AuthSettings cannot see whether a provider will be supplied, enforce the combination where both are known, next to the existing MCPServer-level error, and keep the model-level fields permissive. That also fixes the message: "provide issuer_url when you serve PRM or configure an auth_server_provider" is actionable, while the current text sends people toward inventing placeholder settings.
Default the fields. resource_server_url is typed AnyHttpUrl | None but declared with Field(...), so the declaration contradicts the annotation; a None default aligns them and removes the explicit-None wart.
Backward compatibility is free: placeholder values keep validating, only the requirement changes, and existing configs read the same.
Test matrix worth encoding: RS-only with no placeholder (401 without a token, 200 with one, only /mcp mounted); provider mode unchanged; PRM-served mode still requires issuer_url, asserted as an error case, which guards against over-loosening.
We run a sizable MCP deployment at https://www.corpusiq.io, and required-but-unread config fields are a debt class we watch for: they acquire placeholder values in templates, the placeholders ship, and nobody can say later why they exist.
lzif commentedon Oct 14, 2026
Verified on current main (
754254c0, 2026-10-13):AuthSettings(resource_server_url=None)raisesValidationError: 1 validation error for AuthSettings / issuer_url / Field required— reproduces exactly as described.issuer_urlread sites:src/mcp/server/lowlevel/server.py:803(insideif auth_server_provider:),:839(insideif auth and auth.resource_server_url:),src/mcp/server/mcpserver/server.py:1218(insideif self._auth_server_provider:),:1277(inside theresource_server_urlbranch). In resource-server-only mode (token_verifierset, no auth server provider, noresource_server_url) none of them execute — so the mandatory field is never read.The incremental fix shape from the issue (optional
issuer_url/resource_server_urlplus a validator requiringissuer_urlonly whenresource_server_urlis set or an auth server provider is used) matches this audit.Note: open PR #3292 takes the alternative, bigger approach (token verifier without
AuthSettingsat all) — flagging in case maintainers prefer it.Disclosure: this verification was AI-assisted (per this repo's CONTRIBUTING).
sattyamjjain commentedon Oct 15, 2026
Adding to what @lzif listed, two things to know about RS-only mode without
resource_server_url(checked at 754254c):No protected resource metadata is served, and the 401 carries no
resource_metadatain WWW-Authenticate.resource_metadata_urland the PRM routes are only set up whenauth.resource_server_urlis set (bearer_auth.py L139-140 adds the header only then). The MCP spec says servers "MUST implement one of the following discovery mechanisms to provide authorisation server location information to MCP clients", so in this mode discovery has to come from outside the SDK, for example, a gateway or a separately hosted PRM.validate_token_resourcerequiresresource_server_url, so the audience check has to live in your TokenVerifier. The spec: "MCP servers MUST validate that access tokens were issued specifically for them as the intended audience".Also, the validator you proposed matches the MCP spec, which is stricter than RFC 9728. RFC 9728 marks
authorization_serversas OPTIONAL, while the MCP spec says the PRM document "MUST include theauthorization_serversfield containing at least one authorisation server."https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization
AkshayShah03 commentedon Oct 15, 2026
Thanks @lzif, I missed that #3292 already covers this (and more). Closing as a duplicate; I'll follow up there instead.