|
61 | 61 | - [Writing MCP Clients](#writing-mcp-clients) |
62 | 62 | - [Client Display Utilities](#client-display-utilities) |
63 | 63 | - [OAuth Authentication for Clients](#oauth-authentication-for-clients) |
| 64 | + - [Enterprise Managed Authorization](#enterprise-managed-authorization) |
64 | 65 | - [Parsing Tool Results](#parsing-tool-results) |
65 | 66 | - [MCP Primitives](#mcp-primitives) |
66 | 67 | - [Server Capabilities](#server-capabilities) |
@@ -2356,6 +2357,140 @@ _Full example: [examples/snippets/clients/oauth_client.py](https://github.com/mo |
2356 | 2357 |
|
2357 | 2358 | For a complete working example, see [`examples/clients/simple-auth-client/`](examples/clients/simple-auth-client/). |
2358 | 2359 |
|
| 2360 | +#### Enterprise Managed Authorization |
| 2361 | + |
| 2362 | +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: |
| 2363 | + |
| 2364 | +- **RFC 8693**: OAuth 2.0 Token Exchange (ID Token → ID-JAG) |
| 2365 | +- **RFC 7523**: JSON Web Token (JWT) Profile for OAuth 2.0 Authorization Grants (ID-JAG → Access Token) |
| 2366 | +- Integration with enterprise identity providers (Okta, Azure AD, etc.) |
| 2367 | + |
| 2368 | +**Key Components:** |
| 2369 | + |
| 2370 | +The `EnterpriseAuthOAuthClientProvider` class extends the standard OAuth provider to implement the enterprise authorization flow: |
| 2371 | + |
| 2372 | +```python |
| 2373 | +from mcp.client.auth.extensions import ( |
| 2374 | + EnterpriseAuthOAuthClientProvider, |
| 2375 | + TokenExchangeParameters, |
| 2376 | + IDJAGClaims, |
| 2377 | + decode_id_jag, |
| 2378 | +) |
| 2379 | +from mcp.shared.auth import OAuthClientMetadata, OAuthToken |
| 2380 | +from mcp.client.auth import TokenStorage |
| 2381 | +``` |
| 2382 | + |
| 2383 | +**Token Exchange Flow:** |
| 2384 | + |
| 2385 | +1. **Obtain ID Token** from your enterprise IdP (e.g., Okta, Azure AD) |
| 2386 | +2. **Exchange ID Token for ID-JAG** using RFC 8693 Token Exchange |
| 2387 | +3. **Exchange ID-JAG for Access Token** using RFC 7523 JWT Bearer Grant |
| 2388 | +4. **Use Access Token** to call protected MCP server tools |
| 2389 | + |
| 2390 | +**Example Usage:** |
| 2391 | + |
| 2392 | +```python |
| 2393 | +import asyncio |
| 2394 | +import httpx |
| 2395 | +from pydantic import AnyUrl |
| 2396 | + |
| 2397 | +from mcp.client.auth.extensions import ( |
| 2398 | + EnterpriseAuthOAuthClientProvider, |
| 2399 | + TokenExchangeParameters, |
| 2400 | +) |
| 2401 | +from mcp.shared.auth import OAuthClientMetadata, OAuthToken |
| 2402 | +from mcp.client.auth import TokenStorage |
| 2403 | + |
| 2404 | +# Define token storage implementation |
| 2405 | +class SimpleTokenStorage(TokenStorage): |
| 2406 | + def __init__(self): |
| 2407 | + self._tokens = None |
| 2408 | + self._client_info = None |
| 2409 | + |
| 2410 | + async def get_tokens(self): |
| 2411 | + return self._tokens |
| 2412 | + |
| 2413 | + async def set_tokens(self, tokens): |
| 2414 | + self._tokens = tokens |
| 2415 | + |
| 2416 | + async def get_client_info(self): |
| 2417 | + return self._client_info |
| 2418 | + |
| 2419 | + async def set_client_info(self, client_info): |
| 2420 | + self._client_info = client_info |
| 2421 | + |
| 2422 | +async def main(): |
| 2423 | + # Step 1: Get ID token from your IdP (example with Okta) |
| 2424 | + id_token = await get_id_token_from_idp() # Your IdP authentication |
| 2425 | + |
| 2426 | + # Step 2: Configure token exchange parameters |
| 2427 | + token_exchange_params = TokenExchangeParameters.from_id_token( |
| 2428 | + id_token=id_token, |
| 2429 | + mcp_server_auth_issuer="https://your-idp.com", # IdP issuer URL |
| 2430 | + mcp_server_resource_id="https://mcp-server.example.com", # MCP server resource ID |
| 2431 | + scope="mcp:tools mcp:resources", # Optional scopes |
| 2432 | + ) |
| 2433 | + |
| 2434 | + # Step 3: Create enterprise auth provider |
| 2435 | + enterprise_auth = EnterpriseAuthOAuthClientProvider( |
| 2436 | + server_url="https://mcp-server.example.com", |
| 2437 | + client_metadata=OAuthClientMetadata( |
| 2438 | + client_name="Enterprise MCP Client", |
| 2439 | + client_id="your-client-id", |
| 2440 | + redirect_uris=[AnyUrl("http://localhost:3000/callback")], |
| 2441 | + grant_types=["urn:ietf:params:oauth:grant-type:jwt-bearer"], |
| 2442 | + response_types=["token"], |
| 2443 | + ), |
| 2444 | + storage=SimpleTokenStorage(), |
| 2445 | + idp_token_endpoint="https://your-idp.com/oauth2/v1/token", |
| 2446 | + token_exchange_params=token_exchange_params, |
| 2447 | + ) |
| 2448 | + |
| 2449 | + # Step 4: Perform token exchange and get access token |
| 2450 | + async with httpx.AsyncClient() as client: |
| 2451 | + # Exchange ID token for ID-JAG |
| 2452 | + id_jag = await enterprise_auth.exchange_token_for_id_jag(client) |
| 2453 | + print(f"Obtained ID-JAG: {id_jag[:50]}...") |
| 2454 | + |
| 2455 | + # Exchange ID-JAG for access token |
| 2456 | + access_token = await enterprise_auth.exchange_id_jag_for_access_token( |
| 2457 | + client, id_jag |
| 2458 | + ) |
| 2459 | + print(f"Access token obtained, expires in: {access_token.expires_in}s") |
| 2460 | + |
| 2461 | +if __name__ == "__main__": |
| 2462 | + asyncio.run(main()) |
| 2463 | +``` |
| 2464 | + |
| 2465 | +**Working with SAML Assertions:** |
| 2466 | + |
| 2467 | +If your enterprise uses SAML instead of OIDC, you can exchange SAML assertions: |
| 2468 | + |
| 2469 | +```python |
| 2470 | +token_exchange_params = TokenExchangeParameters.from_saml_assertion( |
| 2471 | + saml_assertion=saml_assertion_string, |
| 2472 | + mcp_server_auth_issuer="https://your-idp.com", |
| 2473 | + mcp_server_resource_id="https://mcp-server.example.com", |
| 2474 | + scope="mcp:tools", |
| 2475 | +) |
| 2476 | +``` |
| 2477 | + |
| 2478 | +**Decoding and Inspecting ID-JAG Tokens:** |
| 2479 | + |
| 2480 | +You can decode ID-JAG tokens to inspect their claims: |
| 2481 | + |
| 2482 | +```python |
| 2483 | +from mcp.client.auth.extensions import decode_id_jag |
| 2484 | + |
| 2485 | +# Decode without signature verification (for inspection only) |
| 2486 | +claims = decode_id_jag(id_jag) |
| 2487 | +print(f"Subject: {claims.sub}") |
| 2488 | +print(f"Issuer: {claims.iss}") |
| 2489 | +print(f"Audience: {claims.aud}") |
| 2490 | +print(f"Client ID: {claims.client_id}") |
| 2491 | +print(f"Resource: {claims.resource}") |
| 2492 | +``` |
| 2493 | + |
2359 | 2494 | ### Parsing Tool Results |
2360 | 2495 |
|
2361 | 2496 | 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