FastMCP 4.1.2 requires an explicit OAuth issuer
Upgrading FastMCP alone does not enable the new OAuth protection: the credential issuer must be configured explicitly.
The risk is not the token itself, but where credentials are sent
A machine-to-machine MCP client needs to request OAuth tokens from an authorization server. If the MCP server it connects to can influence OAuth discovery, the client needs to verify that its credentials will not be presented to an unexpected issuer. FastMCP 4.1.2 adds that safeguard for client-credentials and private_key_jwt flows. (github.com)
This matters when an agent has access to systems that do real work. A self-hosted MCP server may end up calling a Laravel API, reading Git repositories, running Docker tasks, or reaching other parts of a homelab. If the agent reuses a machine identity, sending its secret to the wrong authorization server is more than an authentication failure.
An upgrade does not enable the protection by itself
The important detail is that installing FastMCP 4.1.2 is not enough. The protection only applies when issuer is explicitly configured on ClientCredentialsOAuthProvider or PrivateKeyJWTOAuthProvider. Without that setting, compatibility remains in place, but the client can still use the authorization server found during discovery. (github.com)
The official Python SDK documentation also marks an omitted issuer as deprecated and says it will become required in version 3.0. In practice, this means the update deserves a configuration review instead of a dependency bump alone. If the OAuth provider lives inside an agent service, a CI job, or a sidecar container, it is worth finding it before deploying.
The setting I would review
issuer must be the identifier URL of the authorization server that issued the client_id and client_secret. The right value is the issuer returned by its /.well-known/oauth-authorization-server document, not a URL guessed from its hostname. If MCP discovery points to a different issuer, the flow stops with OAuthFlowError instead of building a token request with those credentials. (github.com)
This is a minimal example using the documented interface. In-memory storage is useful to understand the configuration, but it is not the right final choice for a process that needs to retain tokens across restarts.
import os
from mcp.client.auth.extensions.client_credentials import (
ClientCredentialsOAuthProvider,
)
oauth = ClientCredentialsOAuthProvider(
server_url="https://mcp.internal.example/mcp",
storage=token_storage,
client_id=os.environ["MCP_CLIENT_ID"],
client_secret=os.environ["MCP_CLIENT_SECRET"],
issuer="https://auth.internal.example",
scope="mcp.read mcp.write",
)The provider uses client_secret_basic by default when requesting a token. If the authorization server expects credentials in the form body, the documented option is token_endpoint_auth_method="client_secret_post". I would not change that setting by habit: it needs to match the authentication method supported by the authorization server. (github.com)
What this changes for a home MCP setup
In a self-hosted installation, the value of this protection does not depend on having a large platform. It is enough for an agent to read Git data, operate Docker services, or call an internal API with meaningful permissions. Separating the MCP endpoint from the expected OAuth issuer prevents remote server metadata from deciding where a machine identity is presented.
It is also a good reason to split clients and permissions. An agent that only lists deployments should not share credentials with another one that can delete volumes, administer repositories, or modify Laravel data. Issuer binding limits one specific redirection path, while scopes and separate identities remain the controls that reduce impact when something goes wrong.
The fine print
This does not make an MCP deployment secure by itself. It does not replace server validation, secret handling, HTTPS, or least-privilege permissions. It also cannot fix a bad issuer value: if the wrong authorization server is configured, the client will follow that configuration.
Be careful in environments where OAuth providers differ between development, staging, and production. Each environment should use the issuer that matches its loaded credentials, and secrets should be supplied through environment variables or a secret manager rather than committed to source control. The official documentation specifically advises against storing client_secret in the repository. (github.com)
Finally, this improvement applies to machine OAuth clients using these Python SDK providers. It does not automatically protect non-OAuth integrations, clients built with another library, or secrets that have already leaked. It is a narrow and useful defence, provided that issuer is actually configured.
Source: FastMCP v4.1.2: License to Deny Official documentation: OAuth clients in the MCP Python SDK