-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Description
Enhancement
Context
Some MCP servers do not support OAuth 2.0 Dynamic Client Registration (DCR). For example:
https://api.githubcopilot.com/mcp
The MCP specification explicitly does not require DCR support:
MCP clients and authorization servers SHOULD support the OAuth 2.0 Dynamic Client Registration Protocol (RFC 7591) to allow MCP clients to obtain OAuth client IDs without user interaction.
reference: https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#preregistration
So we should be able to manually register the OAuth client and obtain a client_id and client_secret.
Summary
This PR adds support for providing a pre-registered client_id and client_secret to the OAuth client provider in FastMCP. When provided, these credentials are used to pre-seed the token storage, effectively bypassing the OAuth 2.0 Dynamic Client Registration (DCR) step.
Motivation
The MCP specification suggests that clients and servers SHOULD support RFC 7591 (Dynamic Client Registration). However, some MCP servers (e.g., GitHub Copilot's MCP server) do not support DCR and require clients to be manually registered or use static credentials.
Currently, users wishing to connect to such servers must manually instantiate OAuthClientInformationFull and inject it into the TokenStorage before creating the client, which relies on internal implementation details.
This change exposes client_id and client_secret as first-class arguments in the OAuth class, streamlining the connection process for these servers.
Proposed Changes
src/fastmcp/client/auth/oauth.py
Update OAuth.__init__ to accept client_id and client_secret:
class OAuth(OAuthClientProvider):
def __init__(
self,
# ... existing args ...
client_id: str | None = None,
client_secret: str | None = None,
):
# ... existing setup ...
# 1. Create client_metadata as before
client_metadata = OAuthClientMetadata(
client_name=client_name,
redirect_uris=[AnyHttpUrl(redirect_uri)],
grant_types=["authorization_code", "refresh_token"],
response_types=["code"],
scope=scopes_str,
**(additional_client_metadata or {}),
)
# 2. If client_id/secret are provided, pre-seed the storage
if client_id and client_secret:
logger.info("Using static OAuth client credentials (bypassing DCR)")
# Combine metadata with credentials to create full client info
# This ensures validity against strict schema validation
client_info_data = client_metadata.model_dump()
client_info_data.update({
"client_id": client_id,
"client_secret": client_secret,
})
static_client_info = OAuthClientInformationFull.model_validate(client_info_data)
# Initialize storage adapter
self.token_storage_adapter = TokenStorageAdapter(
async_key_value=token_storage, server_url=mcp_url
)
# Inject into storage (synchronously run the async method)
# This runs the storage set operation to "cache" the credentials
import anyio
anyio.run(
self.token_storage_adapter.set_client_info,
static_client_info
)Example Usage
from fastmcp.client.auth import OAuth
# For a server that doesn't support Dynamic Client Registration
auth = OAuth(
mcp_url="https://api.githubcopilot.com/mcp",
client_id="Iv1...",
client_secret="e9a...",
client_name="My App"
)Considerations
- Sync/Async Execution: The
OAuthclass initializes in__init__(sync). To pre-seed the async storage,anyio.runis used. This works for standard CLI/script usage but should be verified for cases where an event loop is already running. - Metadata Consistency: We ensure that the pre-seeded
OAuthClientInformationFullobject includes the correctredirect_urisand other metadata generated during initialization, ensuring consistency if the SDK validates this later.