---
title: Machine-to-Machine Authentication
sidebarTitle: Client Credentials
description: Authenticate your FastMCP client to a protected server without a browser.
icon: robot
---

import { VersionBadge } from "/snippets/version-badge.mdx"

<VersionBadge version="4.0.0" />

<Tip>
Machine-to-machine authentication is only relevant for HTTP-based transports.
</Tip>

When a FastMCP client runs without a human present — a backend service, a scheduled job, a CI pipeline, one MCP server calling another — it cannot complete the browser-based [OAuth](/clients/auth/oauth) flow. Instead it authenticates as itself using the OAuth 2.0 **client credentials** grant: the client presents its own credentials directly to the authorization server, receives an access token, and attaches that token to every request. There is no redirect, no consent screen, and no user.

FastMCP provides two providers for this, both implementing the `httpx2.Auth` interface so they drop into the same `auth=` parameter as every other client auth option. You pass the **MCP server URL**, not a token endpoint — the token endpoint is discovered from the server's OAuth metadata, exactly as the interactive `OAuth` helper does. As with `OAuth`, you can omit the URL entirely and let the transport supply it.

## Client ID and Secret

The common case is a pre-registered client with an ID and a secret. Use `ClientCredentialsOAuthProvider` and pass it to the `auth` parameter of your `Client` or transport:

```python {2, 4-8, 10}
from fastmcp import Client
from fastmcp.client.auth import ClientCredentialsOAuthProvider

auth = ClientCredentialsOAuthProvider(
    client_id="my-client-id",
    client_secret="my-client-secret",
    scopes=["read", "write"],
)

async with Client("https://example.com/mcp", auth=auth) as client:
    await client.list_tools()
```

The provider discovers the authorization server, exchanges the credentials for an access token, and caches the token in memory for the life of the client. When the token expires it is re-acquired automatically on the next request. Because re-acquiring a token is a single non-interactive request, tokens are held in memory by default with no warning — unlike the interactive `OAuth` flow, losing the cache on restart costs nothing.

### `ClientCredentialsOAuthProvider` Parameters

- **`mcp_url`** (`str`, optional): Full URL to the MCP endpoint. Omit it when passing the provider to `Client(auth=...)` — the transport supplies the URL automatically.
- **`client_id`** (`str`, required): The pre-registered OAuth client ID.
- **`client_secret`** (`str`, required): The OAuth client secret.
- **`scopes`** (`str | list[str]`, optional): Scopes to request, as a space-separated string or a list.
- **`token_endpoint_auth_method`** (`"client_secret_basic" | "client_secret_post"`, optional): How the credentials are presented to the token endpoint. Defaults to `"client_secret_basic"` (an HTTP Basic `Authorization` header); use `"client_secret_post"` to send them in the request body instead.
- **`token_storage`** (`AsyncKeyValue`, optional): A key-value store for the acquired token. Defaults to in-memory storage.

## Private Key JWT

Some authorization servers require the client to prove its identity with a signed JWT assertion (RFC 7523 `private_key_jwt`) instead of a shared secret. This is common with workload identity federation, where the assertion comes from a cloud identity provider. Use `PrivateKeyJWTOAuthProvider` and supply an `assertion_provider` — an async callback that receives the authorization server's issuer identifier (the required JWT audience) and returns the assertion.

For a locally signed assertion, build the callback with `SignedJWTParameters`:

```python {4-7, 9, 11-15, 17-20, 22}
from pathlib import Path

from fastmcp import Client
from fastmcp.client.auth import (
    PrivateKeyJWTOAuthProvider,
    SignedJWTParameters,
)

private_key_pem = Path("client-signing-key.pem").read_text()

jwt_params = SignedJWTParameters(
    issuer="my-client-id",
    subject="my-client-id",
    signing_key=private_key_pem,
)

auth = PrivateKeyJWTOAuthProvider(
    client_id="my-client-id",
    assertion_provider=jwt_params.create_assertion_provider(),
)

async with Client("https://example.com/mcp", auth=auth) as client:
    await client.list_tools()
```

If you already have a JWT from an identity provider, wrap it with `static_assertion_provider`, or pass your own `async def provider(audience: str) -> str` callback to fetch one on demand.

### `PrivateKeyJWTOAuthProvider` Parameters

- **`mcp_url`** (`str`, optional): Full URL to the MCP endpoint. Omit it when passing the provider to `Client(auth=...)`.
- **`client_id`** (`str`, required): The OAuth client ID.
- **`assertion_provider`** (`Callable[[str], Awaitable[str]]`, required): Async callback that receives the authorization server's issuer identifier and returns a signed JWT assertion.
- **`scopes`** (`str | list[str]`, optional): Scopes to request, as a space-separated string or a list.
- **`token_storage`** (`AsyncKeyValue`, optional): A key-value store for the acquired token. Defaults to in-memory storage.
