Add Azure OBO dependencies, auth token injection, and documentation (#2918)

This commit is contained in:
Jeremiah Lowin 2026-02-09 20:06:57 -05:00 committed by GitHub
commit 45af482e73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 688 additions and 46 deletions

View file

@ -326,3 +326,135 @@ mcp = FastMCP(name="Azure MI App", auth=auth)
<Note>
For Azure Government, pass `base_authority="login.microsoftonline.us"` to `AzureJWTVerifier`.
</Note>
## On-Behalf-Of (OBO)
<VersionBadge version="3.0.0" />
The On-Behalf-Of (OBO) flow allows your FastMCP server to call downstream Microsoft APIs—like Microsoft Graph—using the authenticated user's identity. When a user authenticates to your MCP server, you receive a token for your API. OBO exchanges that token for a new token that can call other services, maintaining the user's identity and permissions throughout the chain.
This pattern is useful when your tools need to access user-specific data from Microsoft services: reading emails, accessing calendar events, querying SharePoint, or any other Graph API operation that requires user context.
<Note>
OBO features require the `azure` extra:
```bash
pip install 'fastmcp[azure]'
```
</Note>
### Azure Portal Setup
OBO requires additional configuration in your Azure App registration beyond basic authentication.
<Steps>
<Step title="Add API Permissions">
In your App registration, navigate to **API permissions** and add the Microsoft Graph permissions your tools will need.
- Click **Add a permission** → **Microsoft Graph** → **Delegated permissions**
- Select the permissions required for your use case (e.g., `Mail.Read`, `Calendars.Read`, `User.Read`)
- Repeat for any other APIs you need to call
<Warning>
Only add delegated permissions for OBO. Application permissions bypass user context entirely and are inappropriate for the OBO flow.
</Warning>
</Step>
<Step title="Grant Admin Consent">
OBO requires admin consent for the permissions you've added. In the **API permissions** page, click **Grant admin consent for [Your Organization]**.
Without admin consent, OBO token exchanges will fail with an `AADSTS65001` error indicating the user or administrator hasn't consented to use the application.
<Tip>
For development, you can grant consent for just your own account. For production, an Azure AD administrator must grant tenant-wide consent.
</Tip>
</Step>
</Steps>
### Configure AzureProvider for OBO
The `additional_authorize_scopes` parameter tells Azure which downstream API permissions to include during the initial authorization. These scopes establish what your server can request through OBO later.
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.azure import AzureProvider
auth_provider = AzureProvider(
client_id="your-client-id",
client_secret="your-client-secret",
tenant_id="your-tenant-id",
base_url="http://localhost:8000",
required_scopes=["mcp-access"], # Your API scope
# Include Graph scopes for OBO
additional_authorize_scopes=[
"https://graph.microsoft.com/Mail.Read",
"https://graph.microsoft.com/User.Read",
"offline_access", # Enables refresh tokens
],
)
mcp = FastMCP(name="Graph-Enabled Server", auth=auth_provider)
```
Scopes listed in `additional_authorize_scopes` are requested during the initial OAuth flow but aren't validated on incoming tokens. They establish permission for your server to later exchange the user's token for downstream API access.
<Info>
Use fully-qualified scope URIs for downstream APIs (e.g., `https://graph.microsoft.com/Mail.Read`). Short forms like `Mail.Read` work for authorization requests, but fully-qualified URIs are clearer and avoid ambiguity.
</Info>
### EntraOBOToken Dependency
The `EntraOBOToken` dependency handles the complete OBO flow automatically. Declare it as a parameter default with the scopes you need, and FastMCP exchanges the user's token for a downstream API token before your function runs.
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.azure import AzureProvider, EntraOBOToken
import httpx
auth_provider = AzureProvider(
client_id="your-client-id",
client_secret="your-client-secret",
tenant_id="your-tenant-id",
base_url="http://localhost:8000",
required_scopes=["mcp-access"],
additional_authorize_scopes=[
"https://graph.microsoft.com/Mail.Read",
"https://graph.microsoft.com/User.Read",
],
)
mcp = FastMCP(name="Email Reader", auth=auth_provider)
@mcp.tool
async def get_recent_emails(
count: int = 10,
graph_token: str = EntraOBOToken(["https://graph.microsoft.com/Mail.Read"]),
) -> list[dict]:
"""Get the user's recent emails from Microsoft Graph."""
async with httpx.AsyncClient() as client:
response = await client.get(
f"https://graph.microsoft.com/v1.0/me/messages?$top={count}",
headers={"Authorization": f"Bearer {graph_token}"},
)
response.raise_for_status()
data = response.json()
return [
{"subject": msg["subject"], "from": msg["from"]["emailAddress"]["address"]}
for msg in data.get("value", [])
]
```
The `graph_token` parameter receives a ready-to-use access token for Microsoft Graph. FastMCP handles the OBO exchange transparently—your function just uses the token to call the API.
<Warning>
**Scope alignment is critical.** The scopes passed to `EntraOBOToken` must be a subset of the scopes in `additional_authorize_scopes`. If you request a scope during OBO that wasn't included in the initial authorization, the exchange will fail.
</Warning>
<Tip>
For advanced OBO scenarios, use `CurrentAccessToken()` to get the user's token, then construct an `azure.identity.aio.OnBehalfOfCredential` directly with your Azure credentials.
</Tip>
<Tip>
For a complete working example of Azure OBO with FastMCP, see [Pamela Fox's blog post on OBO flow for Entra-based MCP servers](https://blog.pamelafox.org/2026/01/using-on-behalf-of-flow-for-entra-based.html).
</Tip>

View file

@ -237,6 +237,37 @@ The `AccessToken` object provides:
- **`expires_at`**: Token expiration timestamp (if available)
- **`claims`**: Dictionary of all token claims (JWT claims or provider-specific data)
### Token Claims
When you need just one specific value from the token—like a user ID or tenant identifier—`TokenClaim()` extracts it directly without needing the full token object.
```python
from fastmcp import FastMCP
from fastmcp.server.dependencies import TokenClaim
mcp = FastMCP("Demo")
@mcp.tool
async def add_expense(
amount: float,
user_id: str = TokenClaim("oid"), # Azure object ID
) -> dict:
await db.insert({"user_id": user_id, "amount": amount})
return {"status": "created", "user_id": user_id}
```
`TokenClaim()` raises a `RuntimeError` if the claim doesn't exist, listing available claims to help with debugging.
Common claims vary by identity provider:
| Provider | User ID Claim | Email Claim | Name Claim |
|----------|--------------|-------------|------------|
| Azure/Entra | `oid` | `email` | `name` |
| GitHub | `sub` | `email` | `name` |
| Google | `sub` | `email` | `name` |
| Auth0 | `sub` | `email` | `name` |
### Background Task Dependencies
<VersionBadge version="2.3.0" />

View file

@ -52,13 +52,14 @@ classifiers = [
[project.optional-dependencies]
anthropic = ["anthropic>=0.40.0"]
azure = ["azure-identity>=1.16.0"]
openai = ["openai>=1.102.0"]
tasks = ["pydocket>=0.17.2"]
[dependency-groups]
dev = [
"dirty-equals>=0.9.0",
"fastmcp[anthropic,openai,tasks]",
"fastmcp[anthropic,azure,openai,tasks]",
# add optional dependencies for fastmcp dev
"fastapi>=0.115.12",
"opentelemetry-sdk>=1.20.0",

View file

@ -26,6 +26,7 @@ from fastmcp.server.dependencies import (
CurrentWorker,
Progress,
ProgressLike,
TokenClaim,
)
__all__ = [
@ -39,4 +40,5 @@ __all__ = [
"Depends",
"Progress",
"ProgressLike",
"TokenClaim",
]

View file

@ -6,7 +6,7 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast
from key_value.aio.protocols import AsyncKeyValue
@ -16,6 +16,7 @@ from fastmcp.utilities.auth import decode_jwt_payload, parse_scopes
from fastmcp.utilities.logging import get_logger
if TYPE_CHECKING:
from azure.identity.aio import OnBehalfOfCredential
from mcp.server.auth.provider import AuthorizationParams
from mcp.shared.auth import OAuthClientInformationFull
@ -161,6 +162,10 @@ class AzureProvider(OAuthProxy):
if "offline_access" not in parsed_additional_scopes:
parsed_additional_scopes = [*parsed_additional_scopes, "offline_access"]
# Store Azure-specific config for OBO credential creation
self._tenant_id = tenant_id
self._base_authority = base_authority
# Apply defaults
self.identifier_uri = identifier_uri or f"api://{client_id}"
self.additional_authorize_scopes: list[str] = parsed_additional_scopes
@ -453,6 +458,33 @@ class AzureProvider(OAuthProxy):
logger.debug("Failed to extract Azure claims: %s", e)
return None
def create_obo_credential(self, user_assertion: str) -> OnBehalfOfCredential:
"""Create an OnBehalfOfCredential for OBO token exchange.
Uses the AzureProvider's configuration (client_id, client_secret,
tenant_id, authority) to create a credential that can exchange the
user's token for downstream API tokens.
Args:
user_assertion: The user's access token to exchange via OBO.
Returns:
A configured OnBehalfOfCredential ready for get_token() calls.
Raises:
ImportError: If azure-identity is not installed (requires fastmcp[azure]).
"""
_require_azure_identity("OBO token exchange")
from azure.identity.aio import OnBehalfOfCredential
return OnBehalfOfCredential(
tenant_id=self._tenant_id,
client_id=self._upstream_client_id,
client_secret=self._upstream_client_secret.get_secret_value(),
user_assertion=user_assertion,
authority=f"https://{self._base_authority}",
)
class AzureJWTVerifier(JWTVerifier):
"""JWT verifier pre-configured for Azure AD / Microsoft Entra ID.
@ -552,3 +584,117 @@ class AzureJWTVerifier(JWTVerifier):
else:
prefixed.append(f"{self._identifier_uri}/{scope}")
return prefixed
# --- Dependency injection support ---
# These require fastmcp[azure] extra for azure-identity
# Check if DI engine is available
try:
from docket.dependencies import Dependency
except ImportError:
from fastmcp._vendor.docket_di import Dependency
def _require_azure_identity(feature: str) -> None:
"""Raise ImportError with install instructions if azure-identity is not available."""
try:
import azure.identity # noqa: F401
except ImportError as e:
raise ImportError(
f"{feature} requires the `azure` extra. "
"Install with: pip install 'fastmcp[azure]'"
) from e
class _EntraOBOToken(Dependency): # type: ignore[misc]
"""Dependency that performs OBO token exchange for Microsoft Entra.
Uses azure.identity's OnBehalfOfCredential for async-native OBO,
with automatic token caching and refresh.
"""
def __init__(self, scopes: list[str]):
self.scopes = scopes
self._credential: OnBehalfOfCredential | None = None
async def __aenter__(self) -> str:
_require_azure_identity("EntraOBOToken")
from fastmcp.server.dependencies import get_access_token, get_server
access_token = get_access_token()
if access_token is None:
raise RuntimeError(
"No access token available. Cannot perform OBO exchange."
)
server = get_server()
if not isinstance(server.auth, AzureProvider):
raise RuntimeError(
"EntraOBOToken requires an AzureProvider as the auth provider. "
f"Current provider: {type(server.auth).__name__}"
)
self._credential = server.auth.create_obo_credential(
user_assertion=access_token.token,
)
try:
result = await self._credential.get_token(*self.scopes)
except BaseException:
await self._credential.close()
self._credential = None
raise
return result.token
async def __aexit__(self, *args: object) -> None:
if self._credential is not None:
await self._credential.close()
self._credential = None
def EntraOBOToken(scopes: list[str]) -> str:
"""Exchange the user's Entra token for a downstream API token via OBO.
This dependency performs a Microsoft Entra On-Behalf-Of (OBO) token exchange,
allowing your MCP server to call downstream APIs (like Microsoft Graph) on
behalf of the authenticated user.
Args:
scopes: The scopes to request for the downstream API. For Microsoft Graph,
use scopes like ["https://graph.microsoft.com/Mail.Read"] or
["https://graph.microsoft.com/.default"].
Returns:
A dependency that resolves to the downstream API access token string
Raises:
ImportError: If fastmcp[azure] is not installed
RuntimeError: If no access token is available, provider is not Azure,
or OBO exchange fails
Example:
```python
from fastmcp.server.auth.providers.azure import EntraOBOToken
import httpx
@mcp.tool()
async def get_my_emails(
graph_token: str = EntraOBOToken(["https://graph.microsoft.com/Mail.Read"])
):
async with httpx.AsyncClient() as client:
resp = await client.get(
"https://graph.microsoft.com/v1.0/me/messages",
headers={"Authorization": f"Bearer {graph_token}"}
)
return resp.json()
```
Note:
For OBO to work, ensure the scopes are included in the AzureProvider's
`additional_authorize_scopes` parameter, and that admin consent has been
granted for those scopes in your Entra app registration.
"""
return cast(str, _EntraOBOToken(scopes))

View file

@ -53,6 +53,7 @@ __all__ = [
"CurrentWorker",
"Progress",
"TaskContextInfo",
"TokenClaim",
"get_access_token",
"get_context",
"get_http_headers",
@ -991,47 +992,6 @@ def CurrentHeaders() -> dict[str, str]:
return cast(dict[str, str], _CurrentHeaders())
class _CurrentAccessToken(Dependency): # type: ignore[misc]
"""Async context manager for AccessToken dependency."""
async def __aenter__(self) -> AccessToken:
token = get_access_token()
if token is None:
raise RuntimeError(
"No access token found. Ensure authentication is configured "
"and the request is authenticated."
)
return token
async def __aexit__(self, *args: object) -> None:
pass
def CurrentAccessToken() -> AccessToken:
"""Get the current access token for the authenticated user.
This dependency provides access to the AccessToken for the current
authenticated request. Raises an error if no authentication is present.
Returns:
A dependency that resolves to the active AccessToken
Raises:
RuntimeError: If no authenticated user (use get_access_token() for optional)
Example:
```python
from fastmcp.server.dependencies import CurrentAccessToken
from fastmcp.server.auth import AccessToken
@mcp.tool()
async def get_user_id(token: AccessToken = CurrentAccessToken()) -> str:
return token.claims.get("sub", "unknown")
```
"""
return cast(AccessToken, _CurrentAccessToken())
# --- Progress dependency ---
@ -1162,3 +1122,106 @@ class Progress(Dependency): # type: ignore[misc]
async def __aexit__(self, *args: object) -> None:
pass
# --- Access Token dependency ---
class _CurrentAccessToken(Dependency): # type: ignore[misc]
"""Async context manager for AccessToken dependency."""
async def __aenter__(self) -> AccessToken:
token = get_access_token()
if token is None:
raise RuntimeError(
"No access token found. Ensure authentication is configured "
"and the request is authenticated."
)
return token
async def __aexit__(self, *args: object) -> None:
pass
def CurrentAccessToken() -> AccessToken:
"""Get the current access token for the authenticated user.
This dependency provides access to the AccessToken for the current
authenticated request. Raises an error if no authentication is present.
Returns:
A dependency that resolves to the active AccessToken
Raises:
RuntimeError: If no authenticated user (use get_access_token() for optional)
Example:
```python
from fastmcp.server.dependencies import CurrentAccessToken
from fastmcp.server.auth import AccessToken
@mcp.tool()
async def get_user_id(token: AccessToken = CurrentAccessToken()) -> str:
return token.claims.get("sub", "unknown")
```
"""
return cast(AccessToken, _CurrentAccessToken())
# --- Token Claim dependency ---
class _TokenClaim(Dependency): # type: ignore[misc]
"""Dependency that extracts a specific claim from the access token."""
def __init__(self, claim_name: str):
self.claim_name = claim_name
async def __aenter__(self) -> str:
token = get_access_token()
if token is None:
raise RuntimeError(
f"No access token available. Cannot extract claim '{self.claim_name}'."
)
value = token.claims.get(self.claim_name)
if value is None:
raise RuntimeError(
f"Claim '{self.claim_name}' not found in access token. "
f"Available claims: {list(token.claims.keys())}"
)
return str(value)
async def __aexit__(self, *args: object) -> None:
pass
def TokenClaim(name: str) -> str:
"""Get a specific claim from the access token.
This dependency extracts a single claim value from the current access token.
It's useful for getting user identifiers, roles, or other token claims
without needing the full token object.
Args:
name: The name of the claim to extract (e.g., "oid", "sub", "email")
Returns:
A dependency that resolves to the claim value as a string
Raises:
RuntimeError: If no access token is available or claim is missing
Example:
```python
from fastmcp.server.dependencies import TokenClaim
@mcp.tool()
async def add_expense(
user_id: str = TokenClaim("oid"), # Azure object ID
amount: float,
):
# user_id is automatically injected from the token
await db.insert({"user_id": user_id, "amount": amount})
```
"""
return cast(str, _TokenClaim(name))

View file

@ -1122,3 +1122,99 @@ class TestAzureJWTVerifier:
verifier.issuer
== "https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/v2.0"
)
class TestAzureOBOIntegration:
"""Tests for azure.identity OBO integration (create_obo_credential, EntraOBOToken)."""
def test_create_obo_credential_returns_configured_credential(self):
"""Test that create_obo_credential returns a properly configured credential."""
from unittest.mock import MagicMock, patch
provider = AzureProvider(
client_id="test-client-id",
client_secret="test-client-secret",
tenant_id="test-tenant-id",
base_url="https://myserver.com",
required_scopes=["read"],
jwt_signing_key="test-secret",
)
mock_credential = MagicMock()
with patch(
"azure.identity.aio.OnBehalfOfCredential", return_value=mock_credential
) as mock_class:
credential = provider.create_obo_credential(user_assertion="user-token-123")
mock_class.assert_called_once_with(
tenant_id="test-tenant-id",
client_id="test-client-id",
client_secret="test-client-secret",
user_assertion="user-token-123",
authority="https://login.microsoftonline.com",
)
assert credential is mock_credential
def test_create_obo_credential_with_custom_authority(self):
"""Test that create_obo_credential uses custom base_authority."""
from unittest.mock import MagicMock, patch
provider = AzureProvider(
client_id="test-client-id",
client_secret="test-client-secret",
tenant_id="gov-tenant-id",
base_url="https://myserver.com",
required_scopes=["read"],
base_authority="login.microsoftonline.us",
jwt_signing_key="test-secret",
)
mock_credential = MagicMock()
with patch(
"azure.identity.aio.OnBehalfOfCredential", return_value=mock_credential
) as mock_class:
provider.create_obo_credential(user_assertion="user-token")
call_kwargs = mock_class.call_args[1]
assert call_kwargs["authority"] == "https://login.microsoftonline.us"
def test_tenant_and_authority_stored_as_attributes(self):
"""Test that tenant_id and base_authority are stored for OBO credential creation."""
provider = AzureProvider(
client_id="test-client-id",
client_secret="test-client-secret",
tenant_id="my-tenant",
base_url="https://myserver.com",
required_scopes=["read"],
base_authority="login.microsoftonline.us",
jwt_signing_key="test-secret",
)
assert provider._tenant_id == "my-tenant"
assert provider._base_authority == "login.microsoftonline.us"
def test_entra_obo_token_is_importable(self):
"""Test that EntraOBOToken can be imported."""
from fastmcp.server.auth.providers.azure import EntraOBOToken
assert EntraOBOToken is not None
def test_entra_obo_token_creates_dependency(self):
"""Test that EntraOBOToken creates a dependency with scopes."""
from fastmcp.server.auth.providers.azure import EntraOBOToken, _EntraOBOToken
dep = EntraOBOToken(["https://graph.microsoft.com/User.Read"])
assert isinstance(dep, _EntraOBOToken)
assert dep.scopes == ["https://graph.microsoft.com/User.Read"]
def test_entra_obo_token_is_dependency_instance(self):
"""Test that EntraOBOToken is a Dependency instance."""
try:
from docket.dependencies import Dependency
except ImportError:
from fastmcp._vendor.docket_di import Dependency
from fastmcp.server.auth.providers.azure import _EntraOBOToken
dep = _EntraOBOToken(["scope"])
assert isinstance(dep, Dependency)

View file

@ -1045,3 +1045,115 @@ class TestVendoredDI:
db_dep = deps["db"]
assert isinstance(db_dep, _Depends)
assert db_dep.dependency is get_db
class TestAuthDependencies:
"""Tests for authentication dependencies (CurrentAccessToken, TokenClaim)."""
def test_current_access_token_is_importable(self):
"""Test that CurrentAccessToken can be imported."""
from fastmcp.server.dependencies import CurrentAccessToken
assert CurrentAccessToken is not None
def test_token_claim_is_importable(self):
"""Test that TokenClaim can be imported."""
from fastmcp.server.dependencies import TokenClaim
assert TokenClaim is not None
def test_current_access_token_is_dependency(self):
"""Test that CurrentAccessToken is a Dependency instance."""
# Import the Dependency class the same way the code does
# (docket if available, vendored otherwise)
try:
from docket.dependencies import Dependency
except ImportError:
from fastmcp._vendor.docket_di import Dependency
from fastmcp.server.dependencies import _CurrentAccessToken
dep = _CurrentAccessToken()
assert isinstance(dep, Dependency)
def test_token_claim_creates_dependency(self):
"""Test that TokenClaim creates a Dependency instance."""
# Import the Dependency class the same way the code does
try:
from docket.dependencies import Dependency
except ImportError:
from fastmcp._vendor.docket_di import Dependency
from fastmcp.server.dependencies import TokenClaim, _TokenClaim
dep = TokenClaim("oid")
assert isinstance(dep, _TokenClaim)
assert isinstance(dep, Dependency)
assert dep.claim_name == "oid"
async def test_current_access_token_raises_without_token(self):
"""Test that CurrentAccessToken raises when no token is available."""
from fastmcp.server.dependencies import _CurrentAccessToken
dep = _CurrentAccessToken()
with pytest.raises(RuntimeError, match="No access token found"):
await dep.__aenter__()
async def test_token_claim_raises_without_token(self):
"""Test that TokenClaim raises when no token is available."""
from fastmcp.server.dependencies import _TokenClaim
dep = _TokenClaim("oid")
with pytest.raises(RuntimeError, match="No access token available"):
await dep.__aenter__()
async def test_current_access_token_excluded_from_tool_schema(self, mcp: FastMCP):
"""Test that CurrentAccessToken dependency is excluded from tool schema."""
import mcp.types as mcp_types
from fastmcp.server.auth import AccessToken
from fastmcp.server.dependencies import CurrentAccessToken
@mcp.tool()
async def tool_with_token(
name: str,
token: AccessToken = CurrentAccessToken(),
) -> str:
return name
result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())
tool = next(t for t in result.tools if t.name == "tool_with_token")
assert "name" in tool.inputSchema["properties"]
assert "token" not in tool.inputSchema["properties"]
async def test_token_claim_excluded_from_tool_schema(self, mcp: FastMCP):
"""Test that TokenClaim dependency is excluded from tool schema."""
import mcp.types as mcp_types
from fastmcp.server.dependencies import TokenClaim
@mcp.tool()
async def tool_with_claim(
name: str,
user_id: str = TokenClaim("oid"),
) -> str:
return name
result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())
tool = next(t for t in result.tools if t.name == "tool_with_claim")
assert "name" in tool.inputSchema["properties"]
assert "user_id" not in tool.inputSchema["properties"]
def test_current_access_token_exported_from_all(self):
"""Test that CurrentAccessToken is exported from __all__."""
from fastmcp.server import dependencies
assert "CurrentAccessToken" in dependencies.__all__
def test_token_claim_exported_from_all(self):
"""Test that TokenClaim is exported from __all__."""
from fastmcp.server import dependencies
assert "TokenClaim" in dependencies.__all__

65
uv.lock generated
View file

@ -97,6 +97,35 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" },
]
[[package]]
name = "azure-core"
version = "1.38.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/1b/e503e08e755ea94e7d3419c9242315f888fc664211c90d032e40479022bf/azure_core-1.38.0.tar.gz", hash = "sha256:8194d2682245a3e4e3151a667c686464c3786fed7918b394d035bdcd61bb5993", size = 363033, upload-time = "2026-01-12T17:03:05.535Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/d8/b8fcba9464f02b121f39de2db2bf57f0b216fe11d014513d666e8634380d/azure_core-1.38.0-py3-none-any.whl", hash = "sha256:ab0c9b2cd71fecb1842d52c965c95285d3cfb38902f6766e4a471f1cd8905335", size = 217825, upload-time = "2026-01-12T17:03:07.291Z" },
]
[[package]]
name = "azure-identity"
version = "1.25.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "azure-core" },
{ name = "cryptography" },
{ name = "msal" },
{ name = "msal-extensions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/8d/1a6c41c28a37eab26dc85ab6c86992c700cd3f4a597d9ed174b0e9c69489/azure_identity-1.25.1.tar.gz", hash = "sha256:87ca8328883de6036443e1c37b40e8dc8fb74898240f61071e09d2e369361456", size = 279826, upload-time = "2025-10-06T20:30:02.194Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/7b/5652771e24fff12da9dde4c20ecf4682e606b104f26419d139758cc935a6/azure_identity-1.25.1-py3-none-any.whl", hash = "sha256:e9edd720af03dff020223cd269fa3a61e8f345ea75443858273bcb44844ab651", size = 191317, upload-time = "2025-10-06T20:30:04.251Z" },
]
[[package]]
name = "backports-asyncio-runner"
version = "1.2.0"
@ -707,6 +736,9 @@ dependencies = [
anthropic = [
{ name = "anthropic" },
]
azure = [
{ name = "azure-identity" },
]
openai = [
{ name = "openai" },
]
@ -718,7 +750,7 @@ tasks = [
dev = [
{ name = "dirty-equals" },
{ name = "fastapi" },
{ name = "fastmcp", extra = ["anthropic", "openai", "tasks"] },
{ name = "fastmcp", extra = ["anthropic", "azure", "openai", "tasks"] },
{ name = "inline-snapshot", extra = ["dirty-equals"] },
{ name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "ipython", version = "9.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
@ -748,6 +780,7 @@ dev = [
requires-dist = [
{ name = "anthropic", marker = "extra == 'anthropic'", specifier = ">=0.40.0" },
{ name = "authlib", specifier = ">=1.6.5" },
{ name = "azure-identity", marker = "extra == 'azure'", specifier = ">=1.16.0" },
{ name = "cyclopts", specifier = ">=4.0.0" },
{ name = "exceptiongroup", specifier = ">=1.2.2" },
{ name = "httpx", specifier = ">=0.28.1,<1.0" },
@ -770,13 +803,13 @@ requires-dist = [
{ name = "watchfiles", specifier = ">=1.0.0" },
{ name = "websockets", specifier = ">=15.0.1" },
]
provides-extras = ["anthropic", "openai", "tasks"]
provides-extras = ["anthropic", "azure", "openai", "tasks"]
[package.metadata.requires-dev]
dev = [
{ name = "dirty-equals", specifier = ">=0.9.0" },
{ name = "fastapi", specifier = ">=0.115.12" },
{ name = "fastmcp", extras = ["anthropic", "openai", "tasks"] },
{ name = "fastmcp", extras = ["anthropic", "azure", "openai", "tasks"] },
{ name = "inline-snapshot", extras = ["dirty-equals"], specifier = ">=0.27.2" },
{ name = "ipython", specifier = ">=8.12.3" },
{ name = "loq", specifier = ">=0.1.0a3" },
@ -1413,6 +1446,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" },
]
[[package]]
name = "msal"
version = "1.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "pyjwt", extra = ["crypto"] },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cf/0e/c857c46d653e104019a84f22d4494f2119b4fe9f896c92b4b864b3b045cc/msal-1.34.0.tar.gz", hash = "sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f", size = 153961, upload-time = "2025-09-22T23:05:48.989Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/dc/18d48843499e278538890dc709e9ee3dea8375f8be8e82682851df1b48b5/msal-1.34.0-py3-none-any.whl", hash = "sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1", size = 116987, upload-time = "2025-09-22T23:05:47.294Z" },
]
[[package]]
name = "msal-extensions"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "msal" },
]
sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" },
]
[[package]]
name = "openai"
version = "2.16.0"