|
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) |
73 | 72 | - [Parsing Tool Results](#parsing-tool-results) |
74 | 73 | - [MCP Primitives](#mcp-primitives) |
75 | 74 | - [Server Capabilities](#server-capabilities) |
@@ -2463,230 +2462,6 @@ _Full example: [examples/snippets/clients/oauth_client.py](https://github.com/mo |
2463 | 2462 |
|
2464 | 2463 | For a complete working example, see [`examples/clients/simple-auth-client/`](examples/clients/simple-auth-client/). |
2465 | 2464 |
|
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 | | -**Token Exchange Flow:** |
2479 | | - |
2480 | | -1. **Obtain ID Token** from your enterprise IdP (e.g., Okta, Azure AD) |
2481 | | -2. **Exchange ID Token for ID-JAG** using RFC 8693 Token Exchange |
2482 | | -3. **Exchange ID-JAG for Access Token** using RFC 7523 JWT Bearer Grant |
2483 | | -4. **Use Access Token** to call protected MCP server tools |
2484 | | - |
2485 | | -**Using the Access Token with MCP Server:** |
2486 | | - |
2487 | | -1. Once you have obtained the access token, you can use it to authenticate requests to the MCP server |
2488 | | -2. The access token is automatically included in all subsequent requests to the MCP server, allowing you to access protected tools and resources based on your enterprise identity and permissions. |
2489 | | - |
2490 | | -**Handling Token Expiration and Refresh:** |
2491 | | - |
2492 | | -Access tokens have a limited lifetime and will expire. When tokens expire: |
2493 | | - |
2494 | | -- **Check Token Expiration**: Use the `expires_in` field to determine when the token expires |
2495 | | -- **Refresh Flow**: When expired, repeat the token exchange flow with a fresh ID token from your IdP |
2496 | | -- **Automatic Refresh**: Implement automatic token refresh before expiration (recommended for production) |
2497 | | -- **Error Handling**: Catch authentication errors and retry with refreshed tokens |
2498 | | - |
2499 | | -**Important Notes:** |
2500 | | - |
2501 | | -- **ID Token Expiration**: If the ID token from your IdP expires, you must re-authenticate with the IdP to obtain a new ID token before performing token exchange |
2502 | | -- **Token Storage**: Store tokens securely and implement the `TokenStorage` interface to persist tokens between application restarts |
2503 | | -- **Scope Changes**: If you need different scopes, you must obtain a new ID token from the IdP with the required scopes |
2504 | | -- **Security**: Never log or expose access tokens or ID tokens in production environments |
2505 | | - |
2506 | | -**Example Usage:** |
2507 | | - |
2508 | | -<!-- snippet-source examples/snippets/clients/enterprise_managed_auth_client.py --> |
2509 | | -```python |
2510 | | -import asyncio |
2511 | | - |
2512 | | -import httpx |
2513 | | -from pydantic import AnyUrl |
2514 | | - |
2515 | | -from mcp import ClientSession |
2516 | | -from mcp.client.auth import TokenStorage |
2517 | | -from mcp.client.auth.extensions import ( |
2518 | | - EnterpriseAuthOAuthClientProvider, |
2519 | | - TokenExchangeParameters, |
2520 | | -) |
2521 | | -from mcp.client.streamable_http import streamable_http_client |
2522 | | -from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken |
2523 | | - |
2524 | | - |
2525 | | -# Placeholder function for IdP authentication |
2526 | | -async def get_id_token_from_idp() -> str: |
2527 | | - """Placeholder function to get ID token from your IdP. |
2528 | | -
|
2529 | | - In production, implement actual IdP authentication flow. |
2530 | | - """ |
2531 | | - raise NotImplementedError("Implement your IdP authentication flow here") |
2532 | | - |
2533 | | - |
2534 | | -# Define token storage implementation |
2535 | | -class SimpleTokenStorage(TokenStorage): |
2536 | | - def __init__(self) -> None: |
2537 | | - self._tokens: OAuthToken | None = None |
2538 | | - self._client_info: OAuthClientInformationFull | None = None |
2539 | | - |
2540 | | - async def get_tokens(self) -> OAuthToken | None: |
2541 | | - return self._tokens |
2542 | | - |
2543 | | - async def set_tokens(self, tokens: OAuthToken) -> None: |
2544 | | - self._tokens = tokens |
2545 | | - |
2546 | | - async def get_client_info(self) -> OAuthClientInformationFull | None: |
2547 | | - return self._client_info |
2548 | | - |
2549 | | - async def set_client_info(self, client_info: OAuthClientInformationFull) -> None: |
2550 | | - self._client_info = client_info |
2551 | | - |
2552 | | - |
2553 | | -async def main() -> None: |
2554 | | - """Example demonstrating enterprise managed authorization with MCP.""" |
2555 | | - # Step 1: Get ID token from your IdP (e.g., Okta, Azure AD) |
2556 | | - id_token = await get_id_token_from_idp() |
2557 | | - |
2558 | | - # Step 2: Configure token exchange parameters |
2559 | | - token_exchange_params = TokenExchangeParameters.from_id_token( |
2560 | | - id_token=id_token, |
2561 | | - mcp_server_auth_issuer="https://auth.mcp-server.example.com", # MCP server's auth issuer |
2562 | | - mcp_server_resource_id="https://mcp-server.example.com", # MCP server resource ID |
2563 | | - scope="mcp:tools mcp:resources", # Optional scopes |
2564 | | - ) |
2565 | | - |
2566 | | - # Step 3: Create enterprise auth provider |
2567 | | - enterprise_auth = EnterpriseAuthOAuthClientProvider( |
2568 | | - server_url="https://mcp-server.example.com", |
2569 | | - client_metadata=OAuthClientMetadata( |
2570 | | - client_name="Enterprise MCP Client", |
2571 | | - redirect_uris=[AnyUrl("http://localhost:3000/callback")], |
2572 | | - grant_types=["urn:ietf:params:oauth:grant-type:jwt-bearer"], |
2573 | | - response_types=["token"], |
2574 | | - ), |
2575 | | - storage=SimpleTokenStorage(), |
2576 | | - idp_token_endpoint="https://your-idp.com/oauth2/v1/token", # Your IdP's token endpoint |
2577 | | - token_exchange_params=token_exchange_params, |
2578 | | - ) |
2579 | | - |
2580 | | - # Step 4: Create authenticated HTTP client |
2581 | | - # The auth provider automatically handles the two-step token exchange: |
2582 | | - # 1. ID Token → ID-JAG (via IDP) |
2583 | | - # 2. ID-JAG → Access Token (via MCP server) |
2584 | | - client = httpx.AsyncClient(auth=enterprise_auth, timeout=30.0) |
2585 | | - |
2586 | | - # Step 5: Connect to MCP server with authenticated client |
2587 | | - async with streamable_http_client(url="https://mcp-server.example.com", http_client=client) as (read, write): |
2588 | | - async with ClientSession(read, write) as session: |
2589 | | - await session.initialize() |
2590 | | - |
2591 | | - # List available tools |
2592 | | - tools_result = await session.list_tools() |
2593 | | - print(f"Available tools: {[t.name for t in tools_result.tools]}") |
2594 | | - |
2595 | | - # Call a tool - auth tokens are automatically managed |
2596 | | - if tools_result.tools: |
2597 | | - tool_name = tools_result.tools[0].name |
2598 | | - result = await session.call_tool(tool_name, {}) |
2599 | | - print(f"Tool result: {result.content}") |
2600 | | - |
2601 | | - # List available resources |
2602 | | - resources = await session.list_resources() |
2603 | | - for resource in resources.resources: |
2604 | | - print(f"Resource: {resource.uri}") |
2605 | | - |
2606 | | - |
2607 | | -async def advanced_manual_flow() -> None: |
2608 | | - """Advanced example showing manual token exchange (for special use cases).""" |
2609 | | - id_token = await get_id_token_from_idp() |
2610 | | - |
2611 | | - token_exchange_params = TokenExchangeParameters.from_id_token( |
2612 | | - id_token=id_token, |
2613 | | - mcp_server_auth_issuer="https://auth.mcp-server.example.com", |
2614 | | - mcp_server_resource_id="https://mcp-server.example.com", |
2615 | | - ) |
2616 | | - |
2617 | | - enterprise_auth = EnterpriseAuthOAuthClientProvider( |
2618 | | - server_url="https://mcp-server.example.com", |
2619 | | - client_metadata=OAuthClientMetadata( |
2620 | | - redirect_uris=[AnyUrl("http://localhost:3000/callback")], |
2621 | | - ), |
2622 | | - storage=SimpleTokenStorage(), |
2623 | | - idp_token_endpoint="https://your-idp.com/oauth2/v1/token", |
2624 | | - token_exchange_params=token_exchange_params, |
2625 | | - ) |
2626 | | - |
2627 | | - # Manual token exchange (for debugging or special use cases) |
2628 | | - async with httpx.AsyncClient() as client: |
2629 | | - # Step 1: Exchange ID token for ID-JAG |
2630 | | - id_jag = await enterprise_auth.exchange_token_for_id_jag(client) |
2631 | | - print(f"Obtained ID-JAG: {id_jag[:50]}...") |
2632 | | - |
2633 | | - # Step 2: Build JWT bearer grant request |
2634 | | - jwt_bearer_request = await enterprise_auth.exchange_id_jag_for_access_token(id_jag) |
2635 | | - print(f"Built JWT bearer grant request to: {jwt_bearer_request.url}") |
2636 | | - |
2637 | | - # Step 3: Execute the request to get access token |
2638 | | - response = await client.send(jwt_bearer_request) |
2639 | | - response.raise_for_status() |
2640 | | - token_data = response.json() |
2641 | | - |
2642 | | - access_token = OAuthToken( |
2643 | | - access_token=token_data["access_token"], |
2644 | | - token_type=token_data["token_type"], |
2645 | | - expires_in=token_data.get("expires_in"), |
2646 | | - ) |
2647 | | - print(f"Access token obtained, expires in: {access_token.expires_in}s") |
2648 | | - |
2649 | | - # Use the access token for API calls |
2650 | | - _ = {"Authorization": f"Bearer {access_token.access_token}"} |
2651 | | - # ... make authenticated requests with headers |
2652 | | - |
2653 | | - |
2654 | | -if __name__ == "__main__": |
2655 | | - asyncio.run(main()) |
2656 | | -``` |
2657 | | - |
2658 | | -_Full example: [examples/snippets/clients/enterprise_managed_auth_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/enterprise_managed_auth_client.py)_ |
2659 | | -<!-- /snippet-source --> |
2660 | | - |
2661 | | -**Working with SAML Assertions:** |
2662 | | - |
2663 | | -If your enterprise uses SAML instead of OIDC, you can exchange SAML assertions: |
2664 | | - |
2665 | | -```python |
2666 | | -token_exchange_params = TokenExchangeParameters.from_saml_assertion( |
2667 | | - saml_assertion=saml_assertion_string, |
2668 | | - mcp_server_auth_issuer="https://your-idp.com", |
2669 | | - mcp_server_resource_id="https://mcp-server.example.com", |
2670 | | - scope="mcp:tools", |
2671 | | -) |
2672 | | -``` |
2673 | | - |
2674 | | -**Decoding and Inspecting ID-JAG Tokens:** |
2675 | | - |
2676 | | -You can decode ID-JAG tokens to inspect their claims: |
2677 | | - |
2678 | | -```python |
2679 | | -from mcp.client.auth.extensions import decode_id_jag |
2680 | | - |
2681 | | -# Decode without signature verification (for inspection only) |
2682 | | -claims = decode_id_jag(id_jag) |
2683 | | -print(f"Subject: {claims.sub}") |
2684 | | -print(f"Issuer: {claims.iss}") |
2685 | | -print(f"Audience: {claims.aud}") |
2686 | | -print(f"Client ID: {claims.client_id}") |
2687 | | -print(f"Resource: {claims.resource}") |
2688 | | -``` |
2689 | | - |
2690 | 2465 | ### Parsing Tool Results |
2691 | 2466 |
|
2692 | 2467 | 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