Authsome MCP Proxy
A Model Context Protocol (MCP) proxy that bridges upstream MCP servers protected by token validation or static credentials to MCP clients such as Claude Desktop, Claude Code, Cursor, Codex, MCP Inspector, and Claude.ai. ("Authsome" as in awesome, but with auth.)
The proxy can run as:
-
Web connector (
--transport http, recommended for end users) — a persistent HTTP server that any MCP client reaches by URL. Downstream clients authenticate against the proxy via Dynamic Client Registration or a Client ID Metadata Document; the proxy bridges to your IdP and forwards traffic upstream. A single instance can serve many users simultaneously and lives behind a normal URL (https://mcp.example.com/mcp) — no per-user config files, subprocess launchers, or local Python toolchains. This is the default for non-developer rollouts and the only mode that works with web-only clients like Claude.ai. -
Local stdio proxy (
--transport stdio, the default for backwards compatibility, developer use) — launched as a subprocess by the MCP client (Claude Desktop, Cursor, Codex, Claude Code viaclaude mcp add --transport stdio). Each user runs their own instance and the proxy performs the OAuth flow against an external OIDC IdP on their behalf. Useful when you don't have a server to host the proxy on or when the upstream MCP only validates tokens and you want each developer to authenticate locally.
What Is This For?
Use authsome-mcp-proxy when you need to connect MCP clients to an upstream
MCP server that:
-
Validates inbound bearer tokens against an external IdP but doesn't run its own OAuth authorization server, so MCP clients see no auth metadata and skip authentication entirely (resulting in
401 Unauthorizedon every tool call); or -
Authenticates callers via static credentials (API keys, API tokens, personal access tokens, OAuth client_credentials) while your MCP client is web-based — Claude.ai, for example. Such clients accept only a connector URL in their config and expect the server they point at to be — or pretend to be — a standard OAuth/OIDC authorization server, which an upstream behind raw static credentials isn't. The proxy terminates OIDC inbound and injects the static credential outbound.
The proxy fills these gaps by:
-
Inbound — terminating the standard MCP auth handshake against an IdP the operator configures (Keycloak, OIDC, AWS Cognito, Google, Azure), so downstream MCP clients can perform Dynamic Client Registration and the OAuth code flow without modifying the upstream; and
-
Outbound — forwarding the user's session token to the upstream (the default; works when the upstream and the proxy validate against the same IdP), or substituting a configured static header / service-account token if the upstream uses a separate credential mechanism. See Advanced: Outbound Credential Substitution for the latter case.
The proxy presupposes the upstream is itself an MCP-native server (one
that exposes the MCP protocol — typically a web connector backed by a
REST API in the same codebase). If your upstream is a bare REST API
without MCP, build a full FastMCP server with @mcp.tool()-decorated
handlers instead — this proxy can't synthesize tool definitions for you.
Prerequisites
This tool requires uvx (part of uv). Install it via:
# Windows
winget install --id=astral-sh.uv -e
# macOS
brew install uv
# Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
See the uv installation guide for more options.
Web Connector (Recommended)
Run the proxy as a persistent HTTP server. Downstream MCP clients connect
by URL (e.g. https://mcp.example.com/mcp). A single instance serves
many users concurrently; each MCP session gets an isolated upstream
connection so users never see each other's traffic.
How it works
Browser MCP Client (Claude.ai/Code/Inspector)
│ │
│ OAuth login │ 1. DCR + auth-code + token
│ ◄────────────────────►│ ◄──────────────────────────────┐
│ │ │
│ │ 2. tools, prompts, resources │
│ │ (Authorization: Bearer P, │
│ ▼ issued by the proxy) │
│ ┌─────────────────────┐ │
│ │ authsome-mcp-proxy │ looks up the IdP │
│ │ --transport http │ token U behind P │
│ └─────────────────────┘ in its store │
│ │ │
│ │ 3. Authorization: Bearer U │
│ ▼ (forwarded) │
│ ┌─────────────────────┐ │
│ │ Upstream MCP │ │
│ │ server │ │
│ └─────────────────────┘ │
│ │
└─── Upstream MCP validates U against the same IdP ──────┘
The proxy wears two OAuth 2.0 hats at the same time: it is an OAuth 2.0
client against the upstream IdP / Authorization Server, and a
OAuth 2.0 Authorization Server for the MCP client, which may identify
itself either by Dynamic Client Registration or by
CIMD URL. The MCP client never sees
the upstream IdP's tokens: the proxy keeps them in its store and issues
its own signed tokens that refer to them. On every request it swaps the
client's token for the upstream one, which is what OUTBOUND_AUTH=forward
sends on. For the full sequence diagram, persistence model, and
pod-restart implications, see docs/oauth-flow.md;
for keeping sessions across restarts, see
Surviving restarts.
The proxy is IdP-agnostic — pick the inbound provider that matches your
deployment via --inbound-auth-provider. The following sections give a
working example for each.
Keycloak
For modern Keycloak (≥ 26.6.0) the proxy uses
KeycloakAuthProvider,
a RemoteAuthProvider. Keycloak supports MCP-compatible Dynamic Client
Registration natively, so the proxy holds no IdP credentials of its
own — downstream MCP clients DCR directly against Keycloak. The proxy's
job reduces to JWT verification + protected-resource metadata
advertising.
uvx authsome-mcp-proxy \
--transport http \
--port 8000 \
--proxy-base-url https://mcp.example.com \
--inbound-auth-provider keycloak \
--oidc-issuer-url https://keycloak.example.com/realms/myrealm \
--oidc-scopes "openid email" \
--audience mcp-server \
https://mcp-upstream.example.com/mcp
For Keycloak < 26.6.0, use --inbound-auth-provider oidc
instead — older Keycloak versions don't support MCP-compatible DCR
natively, so the DCR-bridge mode of OIDCProxy is required.
Generic OIDC
Use this for any OIDC-compliant IdP that doesn't support MCP-compatible DCR natively (older Keycloak, Auth0, Okta, etc.). The proxy itself holds a pre-registered static client with the IdP and bridges downstream DCR requests to the IdP's static-client model.
uvx authsome-mcp-proxy \
--transport http \
--port 8000 \
--proxy-base-url https://mcp.example.com \
--inbound-auth-provider oidc \
--oidc-issuer-url https://idp.example.com \
--oidc-client-id mcp-proxy-client \
--oidc-client-secret <secret> \
--oidc-scopes "openid email profile" \
--audience mcp-server \
https://mcp-upstream.example.com/mcp
The OIDC discovery URL is derived as
{oidc-issuer-url}/.well-known/openid-configuration. Register the
proxy's {proxy-base-url}/auth/callback as an allowed redirect URI in
your IdP's client config.
AWS Cognito
uvx authsome-mcp-proxy \
--transport http \
--port 8000 \
--proxy-base-url https://mcp.example.com \
--inbound-auth-provider aws-cognito \
--cognito-user-pool-id eu-central-1_XXXXXXXXX \
--cognito-aws-region eu-central-1 \
--oidc-client-id <cognito-app-client-id> \
--oidc-client-secret <cognito-app-client-secret> \
--oidc-scopes openid \
https://mcp-upstream.example.com/mcp
Cognito requires a confidential app client (one with "Generate a
client secret" enabled) — FastMCP's AWSCognitoProvider mandates a
client_secret. Register {proxy-base-url}/auth/callback as an allowed
callback URL in the app client configuration. Cognito issues refresh
tokens automatically when "Authorization code grant" is enabled — no
offline_access scope needed.
uvx authsome-mcp-proxy \
--transport http \
--port 8000 \
--proxy-base-url https://mcp.example.com \
--inbound-auth-provider google \
--oidc-client-id <google-oauth-client-id>.apps.googleusercontent.com \
--oidc-client-secret <google-oauth-client-secret> \
--oidc-scopes "openid email profile" \
https://mcp-upstream.example.com/mcp
Create the OAuth client via the Google Cloud Console.
Add {proxy-base-url}/auth/callback to the client's authorized redirect
URIs.
Azure (Entra ID)
uvx authsome-mcp-proxy \
--transport http \
--port 8000 \
--proxy-base-url https://mcp.example.com \
--inbound-auth-provider azure \
--azure-tenant-id <tenant-uuid> \
--azure-identifier-uri "api://mcp-server" \
--oidc-client-id <azure-app-client-id> \
--oidc-client-secret <azure-app-client-secret> \
--oidc-scopes "User.Read" \
https://mcp-upstream.example.com/mcp
Azure has two specifics worth knowing: --oidc-scopes are taken literally
and prefixed with --azure-identifier-uri automatically before being sent
to Azure (api://mcp-server/User.Read), and the tenant ID is required
because the issuer URL embeds it.
Connecting downstream MCP clients
Once the proxy is running on https://mcp.example.com:8000/mcp:
Claude.ai: in Settings → Connectors → Add custom connector, paste the URL. Claude will perform DCR against the proxy and walk the user through the OAuth flow in the browser.
Claude Code:
claude mcp add --transport http my-server https://mcp.example.com:8000/mcp
MCP Inspector:
npx -y @modelcontextprotocol/inspector
…then in the UI: set Transport Type to Streamable HTTP, paste
https://mcp.example.com:8000/mcp into URL, and — importantly — set
Connection Type to Via Proxy (the default Direct mode bypasses
Inspector's local OAuth helper and breaks the authorization flow against
the proxy). Then click Connect.
Client ID Metadata Documents (CIMD)
DCR isn't the only way a downstream client can identify itself. With
Client ID Metadata Documents (adopted into MCP as SEP-991)
a client hosts a small JSON document at an HTTPS URL it controls, and
that URL is its client_id. The proxy fetches the document on first
use instead of handing out a freshly registered client.
Two things follow from that. The client keeps one durable identity
across every server it talks to, rather than a different client_id per
proxy instance. And because the metadata lives at the client's own URL,
the proxy can re-fetch it after a restart — where a DCR registration
that lived only in the proxy's client store is simply gone. The client's
session is not covered by that: its tokens live in the proxy's store
like everyone else's, see Surviving restarts.
CIMD is enabled by default and is purely additive: DCR keeps working
exactly as before, so nothing changes for clients that don't use CIMD.
Turn it off with --no-enable-cimd (or ENABLE_CIMD=false).
Support depends on the inbound provider, because it depends on who is acting as the authorization server:
--inbound-auth-provider |
CIMD |
|---|---|
oidc, google, azure, aws-cognito |
✅ the proxy is the authorization server, and handles CIMD itself |
keycloak |
⚠️ not the proxy's call — clients register directly with Keycloak, so CIMD works only if your Keycloak does. ENABLE_CIMD is inert here. |
To try it, generate and check a document with FastMCP's CLI:
uvx fastmcp cimd create --name "My Client" -r "http://localhost:*/callback"
# host the result at e.g. https://client.example.com/oauth/client.json
uvx fastmcp cimd validate https://client.example.com/oauth/client.json
…then connect with that URL as the client id. A successful CIMD
connection reaches /authorize with no /register call ahead of it.
Identity advertised to downstream clients
In stdio mode the proxy relays the upstream MCP server's identity (name, version, instructions, website URL, icons) at startup, so the proxy appears transparent.
In web mode the proxy cannot read the upstream at startup: with
--outbound-auth forward the upstream only accepts a user's token, and
no user has connected yet. So the identity is split:
- Instructions, version, website URL and icons are relayed from the upstream when a client connects. The first handshake fetches them with that client's credential; the result is cached for 10 minutes, so a redeployed upstream is picked up without restarting the proxy. If the fetch fails, the client still connects and sees the proxy's own values. If the upstream cannot be reached and you set no version, FastMCP's own version is shown.
- The name is yours to set. FastMCP also uses the server name
outside the handshake, where a relayed value would not reach it.
Unset, it falls back to FastMCP's auto-generated
FastMCPProxy-xxxx.
Any field you set explicitly wins over the relayed one:
uvx authsome-mcp-proxy ... \
--proxy-name "Awesome tool" \
--proxy-version 1.2.3 \
--proxy-instructions "Use this tool to do amazing things." \
--proxy-website-url https://awesome.tool.example.com
Serving several hostnames from one deployment
Sometimes the same proxy has to be reachable under more than one public hostname — an alias domain for customers whose networks block your primary TLD, a vanity host, or both names running side by side during a migration. Pass them as a comma-separated list:
uvx authsome-mcp-proxy ... --proxy-base-url https://mcp.example.io,https://mcp.example.com
The first entry is canonical: it is the identity served to requests
that arrive with no Host header or an unrecognized one, which is what
container health probes addressing the pod IP do.
Each hostname is served as a fully separate OAuth identity — its own
protected-resource metadata, its own authorization-server metadata, its
own redirect_uri. A client that reaches mcp.example.com is never
handed mcp.example.io to continue its flow with, which is the entire
point when the alias exists because the primary domain is unreachable.
You will need to register {base_url}/auth/callback for every entry
as an allowed redirect URI with your IdP. Two entries sharing a hostname
(differing only in scheme, port or path) are rejected at startup, since
the Host header cannot tell them apart.
Surviving restarts
As the authorization server for its MCP clients (every provider except
keycloak), the proxy keeps state: the clients registered through DCR,
the upstream IdP's access and refresh tokens, and the mapping from the
tokens it issued to them. It signs those tokens with a signing key. Both
decide what a restart costs:
- Store. By default the state lives in an encrypted file store under
${FASTMCP_HOME}/oauth-proxy/(/app/.local/share/fastmcp/oauth-proxy/in the published image). On a VM with a normal disk that is durable. In a container whose filesystem does not survive a restart, such as a KubernetesDeployment, every restart loses all registrations and signs every user out — CIMD clients included, which keep only their registration. A volume at that path helps, but turns the proxy into a single-replicaStatefulSet. - Signing key. Unset, the proxy derives the signing key — and from it
the store's encryption key and directory — from the upstream client
secret (
OIDC_CLIENT_SECRET). Rotating that secret then invalidates every issued token and strands everything stored, volume or not.
A restart-safe deployment therefore sets both a signing key and an external store backend, shared by every replica:
uvx 'authsome-mcp-proxy[dynamodb]' ... \
--jwt-signing-key "$(openssl rand -base64 32)" \
--store-backend dynamodb \
--store-dynamodb-table authsome-mcp-proxy-oauth-state \
--store-dynamodb-region eu-central-1
Keep the signing key in a secret store and stable across deployments —
generating it at startup, as in this one-liner, would defeat the purpose.
With an external backend the proxy refuses to start without one. The
values written to an external store are Fernet-encrypted with a key
derived from it, the same way FastMCP encrypts its own file store, so the
table never holds an upstream token in plaintext. A JWT_SIGNING_KEY is
worth setting with the default file store too: it decouples the stored
state from the upstream client secret. It must be at least 32 characters;
a shorter one is refused at startup. The startup log names the store and
signing key in use. Only the stored values are encrypted: each entry's
collection, key and expiry, among them client IDs and, while they are
valid, authorization codes, are readable to whoever can read the table, so
keep write access to it with the proxy alone. An entry that is not
encrypted is ignored rather than trusted. The settings and the store come
from fastmcp-seaworthy,
so other FastMCP servers built on it read the same variables.
DynamoDB. The backend needs the dynamodb extra, which the published
container image already includes. Credentials and region come from the
AWS SDK's usual chain (IRSA or EKS Pod Identity, instance profile,
AWS_* environment variables); AWS_ENDPOINT_URL_DYNAMODB points it at
DynamoDB Local for testing. Create the table with your infrastructure
code:
| Property | Value |
|---|---|
| Partition key | collection (String) |
| Sort key | key (String) |
| Time to live | enabled on the attribute ttl |
| Billing mode | any; on-demand fits the access pattern |
The proxy needs dynamodb:DescribeTable, dynamodb:DescribeTimeToLive,
dynamodb:GetItem, dynamodb:PutItem and dynamodb:DeleteItem on the
table. It checks the table at startup and refuses to start, naming the
table, if it is missing. --store-dynamodb-auto-create makes it create a
missing table instead (on-demand, TTL enabled); that additionally needs
dynamodb:CreateTable and dynamodb:UpdateTimeToLive, and is off by
default so the proxy can run with the narrower permissions. Without TTL
enabled on the table, the proxy also needs dynamodb:UpdateTimeToLive to
enable it at startup.
All of these settings are inert for --inbound-auth-provider keycloak,
where Keycloak is the authorization server and the proxy keeps no OAuth
state.
Local Stdio Proxy (Developer Use)
The original mode — the proxy is launched as a subprocess by the MCP client, performs the OAuth flow against an external OIDC IdP on the developer's behalf, caches tokens locally, and forwards them upstream.
Use stdio when:
- You don't have a server to host the proxy on.
- You're a developer who wants per-user authentication on their own machine rather than relying on a shared web deployment.
- The upstream is a token-validating MCP server that issues no OAuth metadata of its own, and your MCP client (Claude Desktop, etc.) therefore needs a local OAuth proxy to authenticate against the IdP on its behalf.
Claude Desktop / Cursor / Codex
Add to your Claude Desktop config (accessible via Settings → Developer → Edit Config). Cursor and Codex use the same JSON format in their respective MCP settings files.
{
"mcpServers": {
"company-tools": {
"command": "uvx",
"args": [
"authsome-mcp-proxy",
"https://mcp-upstream.company.com/mcp"
],
"env": {
"OIDC_ISSUER_URL": "https://auth.company.com",
"OIDC_CLIENT_ID": "claude-desktop-client",
"OIDC_CLIENT_SECRET": "your-client-secret", // omit for public OIDC clients that don't require one
"OIDC_SCOPES": "openid profile mcp:read mcp:write"
}
}
}
}
Restart the client to apply changes. On first connection the proxy opens your browser for OIDC login; subsequent runs use the cached token.
To always use the latest version from PyPI (auto-updates), pin
authsome-mcp-proxy@latest:
"args": ["authsome-mcp-proxy@latest", "https://mcp-upstream.company.com/mcp"]
Claude Code
claude mcp add --transport stdio my-server -- \
uvx authsome-mcp-proxy \
--oidc-issuer-url https://auth.example.com \
--oidc-client-id my-client-id \
https://mcp-upstream.example.com/mcp
Defaults to project scope (stored in .mcp.json). Add --scope user to
make the server available across all your projects.
MCP Inspector
Create an mcp.json:
{
"mcpServers": {
"authsome-mcp-proxy": {
"command": "uvx",
"args": ["authsome-mcp-proxy", "https://mcp.example.com/mcp"],
"env": {
"OIDC_ISSUER_URL": "https://auth.example.com",
"OIDC_CLIENT_ID": "inspector-client",
"OIDC_CLIENT_SECRET": "your-client-secret" // omit for public OIDC clients that don't require one
}
}
}
}
Then launch:
npx -y @modelcontextprotocol/inspector --config mcp.json --server authsome-mcp-proxy
Development from source
When developing or testing local changes:
{
"mcpServers": {
"local-dev": {
"command": "uv",
"args": [
"run",
"--with-editable",
"/path/to/authsome-mcp-proxy",
"authsome-mcp-proxy",
"https://mcp.example.com/mcp"
],
"env": {
"OIDC_ISSUER_URL": "https://auth.example.com",
"OIDC_CLIENT_ID": "dev-client",
"OIDC_CLIENT_SECRET": "your-client-secret" // omit for public OIDC clients that don't require one
}
}
}
}
Custom redirect port
If port 8080 is already in use, override the OAuth callback URL:
"env": {
"OIDC_ISSUER_URL": "https://auth.example.com",
"OIDC_CLIENT_ID": "my-client-id",
"OIDC_CLIENT_SECRET": "your-client-secret", // omit for public OIDC clients that don't require one
"OIDC_REDIRECT_URL": "http://localhost:9090/auth/callback"
}
…and add the chosen URL to your OIDC client's allowed redirect URIs.
Advanced: Outbound Credential Substitution
For upstream MCP servers that don't validate tokens against the same
IdP that fronts the proxy. Two outbound mechanisms are supported, both
configured via --outbound-auth. These cover single-tenant deployments
where the upstream uses a tenant-wide service-account token or a per-user
API key / API token; full multi-user identity-bridging with per-user
credential storage is out of scope (see Caveats).
When to use this
You need outbound credential substitution if the upstream MCP server authenticates the caller via something other than a bearer token from your inbound IdP — for example a tenant-wide service-account token, a per-user API key (CoinDesk-style), or a flat API key (Lexware-style).
If the proxy and upstream share the same IdP, don't use this — the
default --outbound-auth forward is what you want.
static mode (API key, API token, PAT)
Injects a literal header value on every outbound request. Use this when the upstream wants a fixed API key, API token, or PAT regardless of who the calling user is.
uvx authsome-mcp-proxy \
--transport http \
--port 8000 \
--proxy-base-url https://mcp.example.com \
--inbound-auth-provider keycloak \
--oidc-issuer-url https://kc.example.com/realms/r \
--outbound-auth static \
--outbound-header-name Authorization \
--outbound-header-value "Bearer ${UPSTREAM_API_KEY}" \
https://mcp-upstream.example.com/mcp
For upstreams that take the key in a non-Authorization header:
--outbound-header-name X-API-Key \
--outbound-header-value "${UPSTREAM_API_KEY}" \
The scope of the credential you paste in determines the effective behaviour: a tenant-wide key terminates identity at the proxy (every user looks the same to the upstream), while a per-user key bridges that single user's identity through to the upstream (single-user instance).
Common shapes:
- Personio-style tenant-scoped credentials (one shared service-account key for the whole org)
- Lexware-style flat tenant API key (no per-user identity at the upstream)
- CoinDesk-style per-user API token (the proxy holds the user's individual key)
- GitLab / GitHub PATs (per-user; same shape as CoinDesk)
oauth-client-credentials mode (service-account OAuth)
Proxy obtains its own access token via the OAuth 2.0 client_credentials grant against an outbound token endpoint that is independent of the inbound IdP. The token is cached and refreshed on expiry.
uvx authsome-mcp-proxy \
--transport http \
--port 8000 \
--proxy-base-url https://mcp.example.com \
--inbound-auth-provider keycloak \
--oidc-issuer-url https://kc.example.com/realms/r \
--outbound-auth oauth-client-credentials \
--outbound-token-url https://upstream-idp.example.com/oauth/token \
--outbound-client-id "${UPSTREAM_SVC_CLIENT_ID}" \
--outbound-client-secret "${UPSTREAM_SVC_CLIENT_SECRET}" \
https://mcp-upstream.example.com/mcp
Equivalent to static with a tenant-scoped credential conceptually, but the token is short-lived and refreshed automatically — preferred over a flat shared API key when the upstream supports it.
Caveats
- Single-tenant only. Identity is terminated at the proxy in
tenant-scoped configurations — no ACL is enforced by the proxy itself,
and every upstream call is indistinguishable to the upstream from any
other authenticated user's. Tenant-scoped policy enforcement needs a
separate PEP layer on top (the planned
fastmcp-edge-authz); multi-user deployments with per-user credentials need a credential vault (the plannedfastmcp-edge-byok). - The proxy presupposes the upstream is itself an MCP server. For a bare REST API, embed MCP tools directly into the API codebase (Spring AI MCP Server Boot Starter, Quarkus MCP Server, official Node/Python SDKs, etc.) rather than fronting it with this proxy. The proxy can't synthesize tool definitions from a REST API.
Configuration Reference
All options can be set via CLI flags or matching environment variables (CLI wins). The full list:
Environment variables and CLI flags
Upstream coordinates — required:
| Env var | CLI flag | Description |
|---|---|---|
UPSTREAM_MCP_URL |
(positional) | URL of the upstream MCP server. |
Proxy runtime — all optional:
| Env var | CLI flag | Default | Description |
|---|---|---|---|
MCP_PROXY_TRANSPORT |
--transport {stdio,http} |
stdio |
Which transport to serve on. |
MCP_PROXY_HOST |
--host |
0.0.0.0 |
HTTP-mode bind address. |
MCP_PROXY_PORT |
--port |
8000 |
HTTP-mode listen port. |
MCP_PROXY_BASE_URL |
--proxy-base-url |
required for http | Publicly reachable URL of the proxy. Used to advertise auth metadata to downstream clients. Accepts a comma-separated list to serve several hostnames, each as its own OAuth identity — see Serving several hostnames from one deployment. |
MCP_PROXY_DEBUG |
--debug |
off | Enable debug logging. |
Server identity advertised to downstream clients (http mode):
| Env var | CLI flag | Description |
|---|---|---|
MCP_PROXY_NAME |
--proxy-name |
Display name (e.g. "Acme Analytics"). |
MCP_PROXY_VERSION |
--proxy-version |
Display version. Optional override: unset, the upstream's version is relayed. |
MCP_PROXY_INSTRUCTIONS |
--proxy-instructions |
Tool-selection hints for the LLM. Optional override: unset, the upstream's instructions are relayed. |
MCP_PROXY_WEBSITE_URL |
--proxy-website-url |
Project URL shown in client UIs. Optional override: unset, the upstream's website URL is relayed. |
Inbound auth (http mode — pick a provider, then supply the fields it needs):
| Env var | CLI flag | Description |
|---|---|---|
INBOUND_AUTH_PROVIDER |
--inbound-auth-provider {oidc,keycloak,aws-cognito,google,azure} |
Which provider class to use. |
OIDC_ISSUER_URL |
--oidc-issuer-url |
OIDC issuer / Keycloak realm URL. |
OIDC_CLIENT_ID |
--oidc-client-id |
OAuth client ID at the IdP. |
OIDC_CLIENT_SECRET |
--oidc-client-secret |
OAuth client secret (required for aws-cognito; optional for oidc/google/azure; unused for keycloak). |
OIDC_SCOPES |
--oidc-scopes |
Space-separated scopes. |
AUDIENCE |
--audience |
JWT aud claim to require (oidc/keycloak; recommended for production). |
COGNITO_USER_POOL_ID |
--cognito-user-pool-id |
AWS Cognito user pool ID. |
COGNITO_AWS_REGION |
--cognito-aws-region |
AWS region for the Cognito user pool. |
AZURE_TENANT_ID |
--azure-tenant-id |
Azure AD tenant ID. |
AZURE_IDENTIFIER_URI |
--azure-identifier-uri |
Azure Application ID URI used for scope prefixing. |
ENABLE_CIMD |
--enable-cimd / --no-enable-cimd |
Accept Client ID Metadata Documents from downstream clients. On by default — see Client ID Metadata Documents (CIMD). |
OAuth proxy state (http mode, inert for keycloak — see
Surviving restarts):
| Env var | CLI flag | Default | Description |
|---|---|---|---|
JWT_SIGNING_KEY |
--jwt-signing-key |
derived from OIDC_CLIENT_SECRET |
Signs the tokens the proxy issues and keys its store's encryption. At least 32 characters. Required with a STORE_BACKEND other than file. |
STORE_BACKEND |
--store-backend {file,dynamodb} |
file |
Where client registrations and sessions are kept. dynamodb needs the dynamodb extra. |
STORE_DYNAMODB_TABLE |
--store-dynamodb-table |
required for dynamodb |
DynamoDB table name. |
STORE_DYNAMODB_REGION |
--store-dynamodb-region |
AWS SDK default | AWS region of the table. |
STORE_DYNAMODB_AUTO_CREATE |
--store-dynamodb-auto-create / --no-store-dynamodb-auto-create |
off | Create the table at startup when it is missing, instead of refusing to start. |
Outbound auth (http mode — forward is the default and needs no
extra fields):
| Env var | CLI flag | Description |
|---|---|---|
OUTBOUND_AUTH |
--outbound-auth {forward,oauth-client-credentials,static} |
Outbound mechanism. |
OUTBOUND_TOKEN_URL |
--outbound-token-url |
Token endpoint for oauth-client-credentials. |
OUTBOUND_CLIENT_ID |
--outbound-client-id |
Client ID for oauth-client-credentials. |
OUTBOUND_CLIENT_SECRET |
--outbound-client-secret |
Client secret for oauth-client-credentials. |
OUTBOUND_HEADER_NAME |
--outbound-header-name |
Header name for static (default Authorization). |
OUTBOUND_HEADER_VALUE |
--outbound-header-value |
Literal header value for static. |
Inbound (stdio mode) — only OIDC_* apply; transport and inbound
auth are not exposed downstream since the MCP client launches the proxy
as a trusted subprocess:
| Env var | CLI flag | Default | Description |
|---|---|---|---|
OIDC_REDIRECT_URL |
--oidc-redirect-url |
http://localhost:8080/auth/callback |
Local OAuth callback URL. |
Run uvx authsome-mcp-proxy --help for the always-current list.
Credential Management
Tokens obtained in stdio mode (the proxy's OAuth code flow) are
cached in ~/.cache/authsome-mcp-proxy-<version>/ (where <version>
is the installed package version) as a SQLite database:
~/.cache/authsome-mcp-proxy-0.5.0/
├── cache.db
├── cache.db-shm
└── cache.db-wal
To force re-authentication (switching accounts, expired tokens, etc.):
# Linux / macOS
rm -rf ~/.cache/authsome-mcp-proxy*
# Windows
rmdir /s %USERPROFILE%\.cache\authsome-mcp-proxy*
In web mode the proxy doesn't cache its own tokens — downstream MCP clients each hold their own session token they obtained via DCR against the proxy. Each client manages its own cache.
Headless stdio first-run: if your stdio deployment target has no browser (Docker, headless VM, CI), authenticate once on a machine with a browser to populate the cache, then copy it over:
# Linux / macOS
scp -r ~/.cache/authsome-mcp-proxy-<version> user@server:~/.cache/
Refresh tokens keep the cache valid for as long as the IdP allows. Add
offline_access to OIDC_SCOPES for Keycloak / Auth0 / Okta to get
long-lived refresh tokens; Cognito issues them automatically.
Troubleshooting
Browser doesn't open (stdio mode) — check that port 8080 (or your custom redirect port) isn't blocked, manually open the URL shown in the proxy logs, and verify your firewall isn't blocking localhost connections.
401 Unauthorized from the upstream — verify OIDC_ISSUER_URL
matches the IdP your upstream validates against, check requested scopes,
and clear the cache (rm -rf ~/.cache/authsome-mcp-proxy*) before
retrying. Run with --debug for token-level diagnostics.
redirect_uri mismatch from the IdP — add the proxy's callback URL
to the OAuth client's allowed redirect URIs at the IdP. For stdio mode
that's http://localhost:8080/auth/callback (or whatever
OIDC_REDIRECT_URL is set to); for web mode it's
{MCP_PROXY_BASE_URL}/auth/callback. The match has to be exact, including
trailing slashes. When MCP_PROXY_BASE_URL lists several hostnames, each
one needs its own callback registered — a hostname whose callback is missing
fails only at the end of the flow, after the user has already logged in.
One hostname advertises another in its OAuth metadata — the proxy is
running with only that other hostname in MCP_PROXY_BASE_URL. Adding a host
at the load balancer is not enough; see Serving several hostnames from one
deployment.
Tokens won't refresh — most IdPs require offline_access in the
scope list to issue refresh tokens (openid profile email offline_access). Cognito is the exception — it issues them automatically
once the app client has "Authorization code grant" enabled.
Web-mode proxy advertises itself as FastMCPProxy-xxxx — set the
--proxy-name flag; the version, instructions and website URL are
relayed from the upstream. See Identity advertised to downstream
clients.
MCP client doesn't recognise the proxy at all (stdio) — JSON
syntax errors are by far the most common cause; double-check trailing
commas and quotes. Confirm uvx is on PATH and restart the MCP client
completely (not just reload).
Connection to the upstream fails — verify the upstream URL is correct and reachable from the host running the proxy. Browser-test the URL directly; check for VPN / corporate-proxy interference.
Debug logging — --debug (CLI) or MCP_PROXY_DEBUG=1 (env) for
detailed flow tracing. In web mode, FastMCP also logs the routes it
serves at startup — useful for confirming that protected-resource
metadata is being advertised at the expected
/.well-known/oauth-protected-resource/mcp path.
Still stuck? Check the examples directory
for a working test setup, and open an issue on GitHub with --debug
output (redacted of any secrets).
Contributing
See CONTRIBUTING.md for development setup, testing, CI/CD workflows, and the release process.
Metadata
Release files for authsome-mcp-proxy 0.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| authsome_mcp_proxy-0.8.0.tar.gz | 305.4 kB | Without provenance | Details for authsome_mcp_proxy-0.8.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| authsome_mcp_proxy-0.8.0-py3-none-any.whl | Python 3 | none | any | Details for authsome_mcp_proxy-0.8.0-py3-none-any.whl |
Total release size: 359.7 kB
Release files / authsome_mcp_proxy-0.8.0.tar.gz
| Download URL | authsome_mcp_proxy-0.8.0.tar.gz |
|---|---|
| Size | 305.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7b24fa45a72e08fac272b21ac6dc7cc9bf94a34f80ffdbf39d0c14c8fd5caa48
|
|
BLAKE2b-256 checksum How to use checksums |
4918c9ae6304b920d7e360151dd2761b7ef7a5c60e88813bcdfbd91bfb074daa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.3 {"installer":{"name":"uv","version":"0.13.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / authsome_mcp_proxy-0.8.0-py3-none-any.whl
| Download URL | authsome_mcp_proxy-0.8.0-py3-none-any.whl |
|---|---|
| Size | 54.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8f38072186660b5030ab69979d61d38ac0230dd33e97f68a1657eb38704386c7
|
|
BLAKE2b-256 checksum How to use checksums |
e8131c25b2c77e1422c93022ef2d391aa74451d6757484c60faa2ec349cf0de2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.3 {"installer":{"name":"uv","version":"0.13.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for authsome-mcp-proxy 0.8.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.