|
69 | 69 | - [Writing MCP Clients](#writing-mcp-clients) |
70 | 70 | - [Client Display Utilities](#client-display-utilities) |
71 | 71 | - [OAuth Authentication for Clients](#oauth-authentication-for-clients) |
| 72 | + - [Enterprise Managed Authorization](#enterprise-managed-authorization) |
72 | 73 | - [Parsing Tool Results](#parsing-tool-results) |
73 | 74 | - [MCP Primitives](#mcp-primitives) |
74 | 75 | - [Server Capabilities](#server-capabilities) |
@@ -2462,6 +2463,140 @@ _Full example: [examples/snippets/clients/oauth_client.py](https://github.com/mo |
2462 | 2463 |
|
2463 | 2464 | For a complete working example, see [`examples/clients/simple-auth-client/`](examples/clients/simple-auth-client/). |
2464 | 2465 |
|
| 2466 | +#### Enterprise Managed Authorization |
| 2467 | + |
| 2468 | +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: |
| 2469 | + |
| 2470 | +- **RFC 8693**: OAuth 2.0 Token Exchange (ID Token → ID-JAG) |
| 2471 | +- **RFC 7523**: JSON Web Token (JWT) Profile for OAuth 2.0 Authorization Grants (ID-JAG → Access Token) |
| 2472 | +- Integration with enterprise identity providers (Okta, Azure AD, etc.) |
| 2473 | + |
| 2474 | +**Key Components:** |
| 2475 | + |
| 2476 | +The `EnterpriseAuthOAuthClientProvider` class extends the standard OAuth provider to implement the enterprise authorization flow: |
| 2477 | + |
| 2478 | +```python |
| 2479 | +from mcp.client.auth.extensions import ( |
| 2480 | + EnterpriseAuthOAuthClientProvider, |
| 2481 | + TokenExchangeParameters, |
| 2482 | + IDJAGClaims, |
| 2483 | + decode_id_jag, |
| 2484 | +) |
| 2485 | +from mcp.shared.auth import OAuthClientMetadata, OAuthToken |
| 2486 | +from mcp.client.auth import TokenStorage |
| 2487 | +``` |
| 2488 | + |
| 2489 | +**Token Exchange Flow:** |
| 2490 | + |
| 2491 | +1. **Obtain ID Token** from your enterprise IdP (e.g., Okta, Azure AD) |
| 2492 | +2. **Exchange ID Token for ID-JAG** using RFC 8693 Token Exchange |
| 2493 | +3. **Exchange ID-JAG for Access Token** using RFC 7523 JWT Bearer Grant |
| 2494 | +4. **Use Access Token** to call protected MCP server tools |
| 2495 | + |
| 2496 | +**Example Usage:** |
| 2497 | + |
| 2498 | +```python |
| 2499 | +import asyncio |
| 2500 | +import httpx |
| 2501 | +from pydantic import AnyUrl |
| 2502 | + |
| 2503 | +from mcp.client.auth.extensions import ( |
| 2504 | + EnterpriseAuthOAuthClientProvider, |
| 2505 | + TokenExchangeParameters, |
| 2506 | +) |
| 2507 | +from mcp.shared.auth import OAuthClientMetadata, OAuthToken |
| 2508 | +from mcp.client.auth import TokenStorage |
| 2509 | + |
| 2510 | +# Define token storage implementation |
| 2511 | +class SimpleTokenStorage(TokenStorage): |
| 2512 | + def __init__(self): |
| 2513 | + self._tokens = None |
| 2514 | + self._client_info = None |
| 2515 | + |
| 2516 | + async def get_tokens(self): |
| 2517 | + return self._tokens |
| 2518 | + |
| 2519 | + async def set_tokens(self, tokens): |
| 2520 | + self._tokens = tokens |
| 2521 | + |
| 2522 | + async def get_client_info(self): |
| 2523 | + return self._client_info |
| 2524 | + |
| 2525 | + async def set_client_info(self, client_info): |
| 2526 | + self._client_info = client_info |
| 2527 | + |
| 2528 | +async def main(): |
| 2529 | + # Step 1: Get ID token from your IdP (example with Okta) |
| 2530 | + id_token = await get_id_token_from_idp() # Your IdP authentication |
| 2531 | + |
| 2532 | + # Step 2: Configure token exchange parameters |
| 2533 | + token_exchange_params = TokenExchangeParameters.from_id_token( |
| 2534 | + id_token=id_token, |
| 2535 | + mcp_server_auth_issuer="https://your-idp.com", # IdP issuer URL |
| 2536 | + mcp_server_resource_id="https://mcp-server.example.com", # MCP server resource ID |
| 2537 | + scope="mcp:tools mcp:resources", # Optional scopes |
| 2538 | + ) |
| 2539 | + |
| 2540 | + # Step 3: Create enterprise auth provider |
| 2541 | + enterprise_auth = EnterpriseAuthOAuthClientProvider( |
| 2542 | + server_url="https://mcp-server.example.com", |
| 2543 | + client_metadata=OAuthClientMetadata( |
| 2544 | + client_name="Enterprise MCP Client", |
| 2545 | + client_id="your-client-id", |
| 2546 | + redirect_uris=[AnyUrl("http://localhost:3000/callback")], |
| 2547 | + grant_types=["urn:ietf:params:oauth:grant-type:jwt-bearer"], |
| 2548 | + response_types=["token"], |
| 2549 | + ), |
| 2550 | + storage=SimpleTokenStorage(), |
| 2551 | + idp_token_endpoint="https://your-idp.com/oauth2/v1/token", |
| 2552 | + token_exchange_params=token_exchange_params, |
| 2553 | + ) |
| 2554 | + |
| 2555 | + # Step 4: Perform token exchange and get access token |
| 2556 | + async with httpx.AsyncClient() as client: |
| 2557 | + # Exchange ID token for ID-JAG |
| 2558 | + id_jag = await enterprise_auth.exchange_token_for_id_jag(client) |
| 2559 | + print(f"Obtained ID-JAG: {id_jag[:50]}...") |
| 2560 | + |
| 2561 | + # Exchange ID-JAG for access token |
| 2562 | + access_token = await enterprise_auth.exchange_id_jag_for_access_token( |
| 2563 | + client, id_jag |
| 2564 | + ) |
| 2565 | + print(f"Access token obtained, expires in: {access_token.expires_in}s") |
| 2566 | + |
| 2567 | +if __name__ == "__main__": |
| 2568 | + asyncio.run(main()) |
| 2569 | +``` |
| 2570 | + |
| 2571 | +**Working with SAML Assertions:** |
| 2572 | + |
| 2573 | +If your enterprise uses SAML instead of OIDC, you can exchange SAML assertions: |
| 2574 | + |
| 2575 | +```python |
| 2576 | +token_exchange_params = TokenExchangeParameters.from_saml_assertion( |
| 2577 | + saml_assertion=saml_assertion_string, |
| 2578 | + mcp_server_auth_issuer="https://your-idp.com", |
| 2579 | + mcp_server_resource_id="https://mcp-server.example.com", |
| 2580 | + scope="mcp:tools", |
| 2581 | +) |
| 2582 | +``` |
| 2583 | + |
| 2584 | +**Decoding and Inspecting ID-JAG Tokens:** |
| 2585 | + |
| 2586 | +You can decode ID-JAG tokens to inspect their claims: |
| 2587 | + |
| 2588 | +```python |
| 2589 | +from mcp.client.auth.extensions import decode_id_jag |
| 2590 | + |
| 2591 | +# Decode without signature verification (for inspection only) |
| 2592 | +claims = decode_id_jag(id_jag) |
| 2593 | +print(f"Subject: {claims.sub}") |
| 2594 | +print(f"Issuer: {claims.iss}") |
| 2595 | +print(f"Audience: {claims.aud}") |
| 2596 | +print(f"Client ID: {claims.client_id}") |
| 2597 | +print(f"Resource: {claims.resource}") |
| 2598 | +``` |
| 2599 | + |
2465 | 2600 | ### Parsing Tool Results |
2466 | 2601 |
|
2467 | 2602 | 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. |
|
0 commit comments