diff --git a/docs/integrations/azure.mdx b/docs/integrations/azure.mdx
index a5b316d881..4376a38ce0 100644
--- a/docs/integrations/azure.mdx
+++ b/docs/integrations/azure.mdx
@@ -326,3 +326,135 @@ mcp = FastMCP(name="Azure MI App", auth=auth)
For Azure Government, pass `base_authority="login.microsoftonline.us"` to `AzureJWTVerifier`.
+
+## On-Behalf-Of (OBO)
+
+
+
+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.
+
+
+OBO features require the `azure` extra:
+
+```bash
+pip install 'fastmcp[azure]'
+```
+
+
+### Azure Portal Setup
+
+OBO requires additional configuration in your Azure App registration beyond basic authentication.
+
+
+
+ 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
+
+
+ Only add delegated permissions for OBO. Application permissions bypass user context entirely and are inappropriate for the OBO flow.
+
+
+
+
+ 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.
+
+
+ For development, you can grant consent for just your own account. For production, an Azure AD administrator must grant tenant-wide consent.
+
+
+
+
+### 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.
+
+
+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.
+
+
+### 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.
+
+
+**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.
+
+
+
+For advanced OBO scenarios, use `CurrentAccessToken()` to get the user's token, then construct an `azure.identity.aio.OnBehalfOfCredential` directly with your Azure credentials.
+
+
+
+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).
+
diff --git a/docs/servers/dependency-injection.mdx b/docs/servers/dependency-injection.mdx
index d986bc52a2..27dd3fd0cc 100644
--- a/docs/servers/dependency-injection.mdx
+++ b/docs/servers/dependency-injection.mdx
@@ -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
diff --git a/pyproject.toml b/pyproject.toml
index d1a7c58146..4c0eb22f91 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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",
diff --git a/src/fastmcp/dependencies.py b/src/fastmcp/dependencies.py
index 87d5367a92..b23222e9dc 100644
--- a/src/fastmcp/dependencies.py
+++ b/src/fastmcp/dependencies.py
@@ -26,6 +26,7 @@
CurrentWorker,
Progress,
ProgressLike,
+ TokenClaim,
)
__all__ = [
@@ -39,4 +40,5 @@
"Depends",
"Progress",
"ProgressLike",
+ "TokenClaim",
]
diff --git a/src/fastmcp/server/auth/providers/azure.py b/src/fastmcp/server/auth/providers/azure.py
index cc0d2544e9..b5974d8874 100644
--- a/src/fastmcp/server/auth/providers/azure.py
+++ b/src/fastmcp/server/auth/providers/azure.py
@@ -6,7 +6,7 @@
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.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 @@ def __init__(
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 @@ async def _extract_upstream_claims(
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 @@ def scopes_supported(self) -> list[str]:
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))
diff --git a/src/fastmcp/server/dependencies.py b/src/fastmcp/server/dependencies.py
index ffef2561bb..97cf4fbeff 100644
--- a/src/fastmcp/server/dependencies.py
+++ b/src/fastmcp/server/dependencies.py
@@ -53,6 +53,7 @@
"CurrentWorker",
"Progress",
"TaskContextInfo",
+ "TokenClaim",
"get_access_token",
"get_context",
"get_http_headers",
@@ -991,47 +992,6 @@ async def get_auth_type(headers: dict = CurrentHeaders()) -> 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 @@ async def __aenter__(self) -> ProgressLike:
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))
diff --git a/tests/server/auth/providers/test_azure.py b/tests/server/auth/providers/test_azure.py
index 6bf25a50df..9542c19f99 100644
--- a/tests/server/auth/providers/test_azure.py
+++ b/tests/server/auth/providers/test_azure.py
@@ -1122,3 +1122,99 @@ def test_specific_tenant_sets_issuer(self):
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)
diff --git a/tests/server/test_dependencies.py b/tests/server/test_dependencies.py
index 7d4119c8e4..106babecce 100644
--- a/tests/server/test_dependencies.py
+++ b/tests/server/test_dependencies.py
@@ -1045,3 +1045,115 @@ def my_func(name: str, db: str = Depends(get_db)) -> str:
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__
diff --git a/uv.lock b/uv.lock
index c158b7fb6c..3f329e1f32 100644
--- a/uv.lock
+++ b/uv.lock
@@ -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"