Skip to content

MCP server separation into Authorization Server (AS) and Resource Server (RS) roles per spec PR #338 #982

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jun 23, 2025

Conversation

ihrpr
Copy link
Contributor

@ihrpr ihrpr commented Jun 18, 2025

Background: modelcontextprotocol/modelcontextprotocol#338
Closes: #921


  • MCP servers now act as Resource Servers (RS) only, not Authorization Servers
  • Implements RFC 9728 Protected Resource Metadata for AS discovery

Implementation:

  • Added TokenVerifier protocol to decouple token validation from OAuth provider logic
  • Resource Servers use token introspection to validate tokens with Authorization Servers
  • Added WWW-Authenticate header with resource_metadata parameter pointing to /.well-known/oauth-protected-resource

Example:

  • Split example into separate AS (auth_server.py) and RS (server.py) implementations
  • AS handles OAuth flows and token issuance
  • RS validates tokens via introspection and serves protected resources

Follow up:

@ihrpr ihrpr requested a review from pcarleton June 18, 2025 21:13
Copy link
Member

@Kludex Kludex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for reviewing a draft, but I'm not sure if I'll have time to help further here.

@ihrpr ihrpr marked this pull request as ready for review June 20, 2025 08:42
@ihrpr ihrpr marked this pull request as draft June 20, 2025 08:43
@ihrpr ihrpr marked this pull request as ready for review June 20, 2025 08:49
"type": "http.response.start",
"status": status_code,
"headers": [
(b"content-type", b"application/json"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense for MCP to be using the low level ASGI API. I know it's not introduced in this PR, but it makes me sad. 😞


This is missing content-length btw.

Comment on lines 230 to 233
@click.command()
@click.option("--port", default=9000, help="Port to listen on")
@click.option("--host", default="localhost", help="Host to bind to")
def main(port: int, host: str) -> int:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will give a lot of maintenance burden. People will ask a lot of uvicorn's parameters to be added here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of this file is demonstrate mcp oauth concepts, not to be a production server. Removed the host as it's not needed, but port is useful

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, my bad. All good here.

# Server settings
host: str = "localhost"
port: int = 8000
server_url: AnyHttpUrl = AnyHttpUrl("http://localhost:8000")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pydantic is appending a trailing slash on AnyHttpUrl - I think we should stop using AnyHttpUrl and use plain str in the whole code source.

Even if you are setting up http://localhost:8000, the real value will be http://localhost:8000/.

Copy link
Contributor Author

@ihrpr ihrpr Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noted, this should be done outside of this PR, added to follow ups

server_url: AnyHttpUrl = AnyHttpUrl("http://localhost:8000")
github_callback_path: str = "http://localhost:8000/github/callback"

def __init__(self, **data):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for? Why is there no type hints?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the __init__, you get the error "Arguments missing for parameters 'github_client_id', 'github_client_secret'" because Pydantic expects these to be explicitly passed, not understanding they should come from environment variables. One option would be to just make then optional

Copy link
Member

@Kludex Kludex Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the nature of BaseSettings. Is the idea that they can either be set programmatically or via env var?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, moved to optional. Using github in the example servers brings too much complexity, like having secrets etc, will remove it in the follow up PR.

@andreasoie
Copy link

@ihrpr Hi! Thanks so much for driving this change, do you have any sense of when this PR might be merged? 🙏 (ballpark estimate)

@@ -15,9 +15,15 @@ class RevocationOptions(BaseModel):
class AuthSettings(BaseModel):
issuer_url: AnyHttpUrl = Field(
...,
description="URL advertised as OAuth issuer; this should be the URL the server " "is reachable at",
description="Base URL where this server is reachable. For AS: OAuth issuer URL. For RS: Resource server URL.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issuer means something specific in OAuth parlance, so I'd prefer we not overload it here if we can avoid it. (mentioned here for AS metadata, and here for PRM metadata)

I put a commit here that changes this around: be4ddd3

I didn't push directly (still need to test it), but curious for your thoughts on that.

That means we'll always do the PRM endpoint, even if the server is an "all-in-one". (Which to clarify, all-in-one is still supported! but in that case we do want both the PRM and the AS metadata endpoints, the PRM will just point to itself.)

so with that change issuer_url will always be the URL of the authorization server.

The most unfortunate thing in that change is crafting the "server" URL, in order to put it in the www-authenticate and as the resource parameter.

I added a function to do it from host + port, but it feels a little jank. The other alternative would be to pass in a new parameter that's the server URL. lmkwyt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other alternative would be to pass in a new parameter that's the server URL. lmkwyt

I think it's cleaner to have a parameter

@ihrpr ihrpr requested a review from pcarleton June 23, 2025 11:54
@rkondra-eightfold
Copy link

@ihrpr @Kludex Is it possible to cleanup AuthSettings and the authorization server provider code ... MCP sdk doesn't need to have utils to implement auth server. This is too much unnecessary complexity, and looks very unclean. Can we please just point to existing libraries which implement oauth very well already https://github.com/authlib/authlib for Authorization server

@ihrpr
Copy link
Contributor Author

ihrpr commented Jun 23, 2025

@ihrpr @Kludex Is it possible to cleanup AuthSettings and the authorization server provider code ... MCP sdk doesn't need to have utils to implement auth server. This is too much unnecessary complexity, and looks very unclean. Can we please just point to existing libraries which implement oauth very well already https://github.com/authlib/authlib for Authorization server

With the new spec, MCP server acts as RS, so there is no need to support AS in the future, for now leaving it for backwards compatibility, and yes, ideally it should be cleaned up.

pcarleton
pcarleton previously approved these changes Jun 23, 2025
Copy link
Contributor

@pcarleton pcarleton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pending doc string, otherwise LGTM

@ihrpr ihrpr merged commit 17f9c00 into main Jun 23, 2025
12 checks passed
@ihrpr ihrpr deleted the ihrpr/auth2 branch June 23, 2025 13:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

auth spec implementation
5 participants