-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Implement SEP-990 Enterprise Managed OAuth #1721
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
BinoyOza-okta
wants to merge
14
commits into
modelcontextprotocol:main
Choose a base branch
from
BinoyOza-okta:feature/sep-990-enterprise-oauth
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,681
−0
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
f0de9e7
- Implemented SEP-990 feature for providing support for Enterprise Ma…
BinoyOza-okta 9759c7a
Added test cases for missing lines of code.
BinoyOza-okta 7f80d32
- Added tests cases for few of the missing lines. src/mcp/client/auth…
BinoyOza-okta 1ea72c5
- Fixed pre-commit errors.
BinoyOza-okta c07b7b9
- Tried to fix the ruff error.
BinoyOza-okta f431b54
- Fixed ruff errors.
BinoyOza-okta d4392ae
- Removed server side changes for enterprise_managed_auth.py
BinoyOza-okta db2f02c
- Added README.md changes for SEP-990 implementation for enterprise m…
BinoyOza-okta 5fb2c0f
- Resolved pyright checks error.
BinoyOza-okta 005bad4
- Resolved README.md file fixes for removing unused imports.
BinoyOza-okta 73b12b7
- Resolved pyright errors.
BinoyOza-okta 8214778
- Added new test cases for the missing code lines.
BinoyOza-okta 04ffe5a
- Fixed the failing test cases.
BinoyOza-okta 09c05aa
- Fixed the test cases.
BinoyOza-okta File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,6 +61,7 @@ | |
| - [Writing MCP Clients](#writing-mcp-clients) | ||
| - [Client Display Utilities](#client-display-utilities) | ||
| - [OAuth Authentication for Clients](#oauth-authentication-for-clients) | ||
| - [Enterprise Managed Authorization](#enterprise-managed-authorization) | ||
| - [Parsing Tool Results](#parsing-tool-results) | ||
| - [MCP Primitives](#mcp-primitives) | ||
| - [Server Capabilities](#server-capabilities) | ||
|
|
@@ -2356,6 +2357,129 @@ _Full example: [examples/snippets/clients/oauth_client.py](https://github.com/mo | |
|
|
||
| For a complete working example, see [`examples/clients/simple-auth-client/`](examples/clients/simple-auth-client/). | ||
|
|
||
| #### Enterprise Managed Authorization | ||
|
|
||
| The SDK includes support for Enterprise Managed Authorization (SEP-990), which enables MCP clients to connect to protected servers using enterprise Single Sign-On (SSO) systems. This implementation supports: | ||
|
|
||
| - **RFC 8693**: OAuth 2.0 Token Exchange (ID Token → ID-JAG) | ||
| - **RFC 7523**: JSON Web Token (JWT) Profile for OAuth 2.0 Authorization Grants (ID-JAG → Access Token) | ||
| - Integration with enterprise identity providers (Okta, Azure AD, etc.) | ||
|
|
||
| **Key Components:** | ||
|
|
||
| The `EnterpriseAuthOAuthClientProvider` class extends the standard OAuth provider to implement the enterprise authorization flow: | ||
|
|
||
| **Token Exchange Flow:** | ||
|
|
||
| 1. **Obtain ID Token** from your enterprise IdP (e.g., Okta, Azure AD) | ||
| 2. **Exchange ID Token for ID-JAG** using RFC 8693 Token Exchange | ||
| 3. **Exchange ID-JAG for Access Token** using RFC 7523 JWT Bearer Grant | ||
| 4. **Use Access Token** to call protected MCP server tools | ||
|
|
||
| **Example Usage:** | ||
|
|
||
| ```python | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this instead be put in the snippets folder and then use the |
||
| import asyncio | ||
| import httpx | ||
| from pydantic import AnyUrl | ||
|
|
||
| from mcp.client.auth.extensions import ( | ||
| EnterpriseAuthOAuthClientProvider, | ||
| TokenExchangeParameters, | ||
| ) | ||
| from mcp.shared.auth import OAuthClientMetadata | ||
| from mcp.client.auth import TokenStorage | ||
|
|
||
| # Define token storage implementation | ||
| class SimpleTokenStorage(TokenStorage): | ||
| def __init__(self): | ||
| self._tokens = None | ||
| self._client_info = None | ||
|
|
||
| async def get_tokens(self): | ||
| return self._tokens | ||
|
|
||
| async def set_tokens(self, tokens): | ||
| self._tokens = tokens | ||
|
|
||
| async def get_client_info(self): | ||
| return self._client_info | ||
|
|
||
| async def set_client_info(self, client_info): | ||
| self._client_info = client_info | ||
|
|
||
| async def main(): | ||
| # Step 1: Get ID token from your IdP (example with Okta) | ||
| id_token = await get_id_token_from_idp() # Your IdP authentication | ||
|
|
||
| # Step 2: Configure token exchange parameters | ||
| token_exchange_params = TokenExchangeParameters.from_id_token( | ||
| id_token=id_token, | ||
| mcp_server_auth_issuer="https://your-idp.com", # IdP issuer URL | ||
| mcp_server_resource_id="https://mcp-server.example.com", # MCP server resource ID | ||
| scope="mcp:tools mcp:resources", # Optional scopes | ||
| ) | ||
|
|
||
| # Step 3: Create enterprise auth provider | ||
| enterprise_auth = EnterpriseAuthOAuthClientProvider( | ||
| server_url="https://mcp-server.example.com", | ||
| client_metadata=OAuthClientMetadata( | ||
| client_name="Enterprise MCP Client", | ||
| client_id="your-client-id", | ||
| redirect_uris=[AnyUrl("http://localhost:3000/callback")], | ||
| grant_types=["urn:ietf:params:oauth:grant-type:jwt-bearer"], | ||
| response_types=["token"], | ||
| ), | ||
| storage=SimpleTokenStorage(), | ||
| idp_token_endpoint="https://your-idp.com/oauth2/v1/token", | ||
| token_exchange_params=token_exchange_params, | ||
| ) | ||
|
|
||
| # Step 4: Perform token exchange and get access token | ||
| async with httpx.AsyncClient() as client: | ||
| # Exchange ID token for ID-JAG | ||
| id_jag = await enterprise_auth.exchange_token_for_id_jag(client) | ||
| print(f"Obtained ID-JAG: {id_jag[:50]}...") | ||
|
|
||
| # Exchange ID-JAG for access token | ||
| access_token = await enterprise_auth.exchange_id_jag_for_access_token( | ||
| client, id_jag | ||
| ) | ||
| print(f"Access token obtained, expires in: {access_token.expires_in}s") | ||
|
|
||
| if __name__ == "__main__": | ||
| asyncio.run(main()) | ||
| ``` | ||
|
|
||
| **Working with SAML Assertions:** | ||
|
|
||
| If your enterprise uses SAML instead of OIDC, you can exchange SAML assertions: | ||
|
|
||
| ```python | ||
| token_exchange_params = TokenExchangeParameters.from_saml_assertion( | ||
| saml_assertion=saml_assertion_string, | ||
| mcp_server_auth_issuer="https://your-idp.com", | ||
| mcp_server_resource_id="https://mcp-server.example.com", | ||
| scope="mcp:tools", | ||
| ) | ||
| ``` | ||
|
|
||
| **Decoding and Inspecting ID-JAG Tokens:** | ||
|
|
||
| You can decode ID-JAG tokens to inspect their claims: | ||
|
|
||
| ```python | ||
| from mcp.client.auth.extensions import decode_id_jag | ||
|
|
||
| # Decode without signature verification (for inspection only) | ||
| claims = decode_id_jag(id_jag) | ||
| print(f"Subject: {claims.sub}") | ||
| print(f"Issuer: {claims.iss}") | ||
| print(f"Audience: {claims.aud}") | ||
| print(f"Client ID: {claims.client_id}") | ||
| print(f"Resource: {claims.resource}") | ||
| ``` | ||
|
|
||
| ### Parsing Tool Results | ||
|
|
||
| When calling tools through MCP, the `CallToolResult` object contains the tool's response in a structured format. Understanding how to parse this result is essential for properly handling tool outputs. | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| """MCP Client Auth Extensions.""" | ||
|
|
||
| from mcp.client.auth.extensions.enterprise_managed_auth import ( | ||
| EnterpriseAuthOAuthClientProvider, | ||
| IDJAGClaims, | ||
| TokenExchangeParameters, | ||
| TokenExchangeResponse, | ||
| decode_id_jag, | ||
| validate_token_exchange_params, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "EnterpriseAuthOAuthClientProvider", | ||
| "IDJAGClaims", | ||
| "TokenExchangeParameters", | ||
| "TokenExchangeResponse", | ||
| "decode_id_jag", | ||
| "validate_token_exchange_params", | ||
| ] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.