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

Open
wants to merge 22 commits into
base: main
Choose a base branch
from

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)

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
3 participants