Machine-to-machine authorization: the OAuth 2.1 client_credentials
grant (the MCP io.modelcontextprotocol/oauth-client-credentials
extension). No user and no browser: the client authenticates as itself
and gets a token for the MCP server. Compiled only with :finch.
{ConduitMcp.Client,
name: :reports,
transport:
{ConduitMcp.Client.Transport.StreamableHTTP,
url: "https://mcp.example.com/mcp",
auth:
{:client_credentials,
client_id: "reporting-service",
client_secret: {:system, "MCP_CLIENT_SECRET"},
scopes: ["reports:read"]}}}Discovery is the same as for the authorization-code flow (Protected
Resource Metadata, then the authorization server's metadata; S256 and
an authorization_endpoint are not required). The token request
carries resource (RFC 8707) and scope (the configured :scopes,
else the server's scopes_supported), and authenticates the client
with:
client_secret- a string, aConduitMcp.Client.Secretor{:system, "VAR"}(read from the environment on every token request, never kept), sent asclient_secret_basic(default) orclient_secret_post(token_endpoint_auth_method:);private_key- a private JWK map or PEM forprivate_key_jwt(RFC 7523): a signed assertion withaud= the token endpoint. Needs the optional:josedependency, checked when the options are validated.
Tokens are cached in the token store under {principal, issuer, resource} like any other, and renewed by requesting a new one when
they expire (within refresh_skew), single-flight. A step-up (403
insufficient_scope) requests a new token for the union of the held and
challenged scopes, within max_step_up_attempts.
ConduitMcp.Client.OAuth.token_for/2 returns the current token; this
module holds the token request itself.