PrefectHQ/fastmcp · error · RuntimeError

No access token available. Cannot perform OBO exchange.

Error message

No access token available. Cannot perform OBO exchange.

What it means

EntraOBOToken's __aenter__ exchanges the current request's access token for a downstream token via OBO. It reads the token from FastMCP's request-scoped context via get_access_token(); if no token is in context (None), there is nothing to exchange, so it raises RuntimeError.

Source

Thrown at fastmcp_slim/fastmcp/server/auth/providers/azure.py:851

    """Dependency that performs OBO token exchange for Microsoft Entra.

    Uses azure.identity's OnBehalfOfCredential for async-native OBO,
    with automatic token caching and refresh. Credentials are cached on
    the AzureProvider so repeated tool calls reuse existing credentials
    and benefit from the Azure SDK's internal token cache.
    """

    def __init__(self, scopes: list[str]):
        self.scopes = scopes

    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()
        azure_provider = _find_azure_provider(server.auth)
        if azure_provider is None:
            raise RuntimeError(
                "EntraOBOToken requires an AzureProvider as the auth provider. "
                f"Current provider: {type(server.auth).__name__}"
            )

        credential = await azure_provider.get_obo_credential(
            user_assertion=access_token.token,
        )

        result = await credential.get_token(*self.scopes)
        return result.token

View on GitHub (pinned to 1f02114297)

Solutions

  1. Only use EntraOBOToken inside request-scoped code (tool/resource handlers) where an authenticated access token exists.
  2. Ensure the server's auth provider is configured and the client is actually sending a valid token so get_access_token() returns a token.
  3. For non-request contexts, obtain the user assertion explicitly and call provider.get_obo_credential(user_assertion=...) instead of relying on context.

Example fix

// before
@app.on_event("startup")
async def warm():
    async with EntraOBOToken(scopes=["api"]) as t: ...  # no request context
// after
@.tool
async def my_tool():
    async with EntraOBOToken(scopes=["api"]) as t: ...  # inside request context
Defensive patterns

Strategy: try-catch

Validate before calling

from fastmcp.server.dependencies import get_access_token
def assert_request_token_available() -> bool:
    return get_access_token() is not None

Type guard

def has_access_token() -> bool:
    from fastmcp.server.dependencies import get_access_token
    t = get_access_token()
    return t is not None and bool(t.token)

Try / catch

try:
    async with EntraOBOToken(scopes=["api"]) as t:
        ...
except RuntimeError as e:
    if "No access token" in str(e):
        raise RuntimeError("EntraOBOToken used outside an authenticated request context") from e
    raise

Prevention

When it happens

Trigger: Using `async with EntraOBOToken(...) as t:` in code that runs outside an authenticated MCP request — e.g. at server startup, in a background task, in a tool without auth, or when the auth middleware did not populate the context.

Common situations: Calling OBO from a non-request context (startup/shutdown hooks, scheduled jobs); running a tool while the server has no auth provider wired for the incoming request; testing tools outside the FastMCP request lifecycle.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/d8fdb82228c5a1c6. Report an issue: GitHub.