Credential injection middleware for FastMCP servers — keeps secrets out of the LLM
Project description
fastmcp-credentials
Secure credential injection middleware for FastMCP servers.
Keeps secrets completely out of the LLM — credentials are resolved server-side and injected into tools transparently. The AI agent never sees tokens, API keys, or client secrets.
How it works
CredentialMiddlewareintercepts every tool call and resolves credentials via the configured backend.- Credentials are stored in a request-scoped
ContextVar— they never leak between concurrent requests. - Your tool calls
get_credentials()— a plain synchronous function, noawait, noctx— to read them. - After the tool returns (or raises), the
ContextVaris always reset in afinallyblock.
The LLM only ever sees your tool's business parameters. Auth is invisible to it by design.
Installation
pip install fastmcp-credentials
Requires Python 3.11+ and FastMCP 3.x.
Backends
| Backend | Best for |
|---|---|
EnvCredentialBackend |
Local development, self-hosted single-user servers |
HeaderCredentialBackend |
Gateway-managed multi-user deployments |
Quick start — Static credentials (env vars)
Static credentials are arbitrary key/value fields loaded from environment variables. All fields are available on cred.fields.
# Option 1 — JSON object (recommended for multi-field providers):
export MYSERVICE_FIELDS='{"apiKey":"sk-abc123","secretKey":"xyz789"}'
# Option 2 — individual FIELD_<name> vars (useful with secrets managers):
export MYSERVICE_FIELD_apiKey=sk-abc123
export MYSERVICE_FIELD_secretKey=xyz789
import requests
from fastmcp import FastMCP
from fastmcp_credentials import CredentialMiddleware, EnvCredentialBackend, get_credentials
backend = EnvCredentialBackend(prefix="MYSERVICE_")
mcp = FastMCP("My Service", middleware=[CredentialMiddleware(backend, "static")])
@mcp.tool()
def search(query: str) -> list:
creds = get_credentials()
response = requests.get(
"https://api.myservice.com/search",
headers={"Authorization": f"Bearer {creds.fields['apiKey']}"},
params={"q": query},
)
return response.json()
Quick start — OAuth (env vars)
For OAuth tokens, set {PREFIX}CRED_TYPE=oauth:
export MYSERVICE_CRED_TYPE=oauth
export MYSERVICE_ACCESS_TOKEN=ya29...
export MYSERVICE_REFRESH_TOKEN=1//...
export MYSERVICE_CLIENT_ID=your_client_id
export MYSERVICE_CLIENT_SECRET=your_client_secret
export MYSERVICE_TOKEN_URI=https://auth.myservice.com/token
export MYSERVICE_SCOPES=read write
backend = EnvCredentialBackend(prefix="MYSERVICE_")
mcp = FastMCP("My Service", middleware=[CredentialMiddleware(backend, "oauth")])
@mcp.tool()
def list_items(folder_id: str) -> list:
creds = get_credentials()
response = requests.get(
"https://api.myservice.com/items",
headers={"Authorization": f"Bearer {creds.access_token}"},
params={"folder": folder_id},
)
return response.json()
Quick start — Gateway-injected credentials (hosted mode)
For multi-user deployments where a gateway decrypts, refreshes, and injects credentials as HTTP headers before forwarding requests to your MCP server:
import requests
from fastmcp import FastMCP
from fastmcp_credentials import CredentialMiddleware, HeaderCredentialBackend, get_credentials
backend = HeaderCredentialBackend()
mcp = FastMCP("My Service", middleware=[CredentialMiddleware(backend, "oauth")])
@mcp.tool()
def call_api(resource_id: str) -> dict:
creds = get_credentials()
return requests.get(
f"https://api.example.com/resources/{resource_id}",
headers={"Authorization": f"Bearer {creds.access_token}"},
).json()
The gateway sends these headers — no tool parameters, no LLM involvement:
X-MCP-Cred-Access-Token: ya29...
X-MCP-Cred-Fields: {"apiKey":"sk-...","secretKey":"..."}
X-MCP-Cred-Scopes: read write
X-MCP-Cred-Extra: {"tenant_id": "..."}
X-MCP-Cred-Expires-At: 2026-05-04T12:00:00Z
Tools access credentials identically to env-based mode via get_credentials().
OAuth extras
Some OAuth providers include non-sensitive metadata alongside the token — a data-centre region, a workspace identifier, etc. These are collected into cred.extra for OAuth credentials only.
Env vars: use the {PREFIX}EXTRA_{NAME} pattern.
export MYSERVICE_CRED_TYPE=oauth
export MYSERVICE_ACCESS_TOKEN=ya29...
export MYSERVICE_EXTRA_DC=us10
export MYSERVICE_EXTRA_WORKSPACE=my-workspace
@mcp.tool()
def call_api() -> dict:
creds = get_credentials()
base_url = f"https://{creds.extra['dc']}.api.example.com"
return requests.get(base_url, headers={"Authorization": f"Bearer {creds.access_token}"}).json()
Gateway mode: the gateway encodes extras in the X-MCP-Cred-Extra header as a JSON object.
Selecting a backend based on deployment mode
If you need to switch backends at runtime (e.g. env vars locally, header-injected in production), use the get_mode() helper which reads the FASTMCP_CREDENTIAL_MODE environment variable:
from fastmcp_credentials import CredentialMiddleware, EnvCredentialBackend, HeaderCredentialBackend, get_mode, CredentialMode
if get_mode() == CredentialMode.HOSTED:
backend = HeaderCredentialBackend()
else:
backend = EnvCredentialBackend(prefix="MYSERVICE_")
mcp = FastMCP("My Service", middleware=[CredentialMiddleware(backend, "oauth")])
# Local / self-hosted (default — no env var needed)
# FASTMCP_CREDENTIAL_MODE=oss
# Production behind a gateway
export FASTMCP_CREDENTIAL_MODE=hosted
The ResolvedCredential object
get_credentials() always returns a ResolvedCredential dataclass, regardless of which backend is used:
@dataclass
class ResolvedCredential:
type: Literal["static", "oauth"]
# Static auth — all provider fields by name
fields: dict[str, str]
# OAuth
access_token: str | None
refresh_token: str | None
client_id: str | None
client_secret: str | None
token_uri: str | None
scopes: list[str] | None
expires_at: datetime | None
# OAuth metadata only (e.g. dc, workspace). Empty for static credentials.
extra: dict[str, Any]
def is_expired(self) -> bool: ...
is_expired() returns True if the access token has expired or expires within the next 60 seconds.
Environment variable reference
All variables use the prefix you pass to EnvCredentialBackend(prefix="...").
| Variable | Default | Description |
|---|---|---|
{PREFIX}FIELDS |
— | JSON object with all static fields, e.g. {"apiKey":"...","secretKey":"..."} |
{PREFIX}FIELD_{NAME} |
— | Individual static field (key name preserved as-is) → cred.fields["NAME"] |
{PREFIX}EXTRA_{NAME} |
— | OAuth metadata only → cred.extra["name"] |
{PREFIX}ACCESS_TOKEN |
— | OAuth access token |
{PREFIX}REFRESH_TOKEN |
— | OAuth refresh token |
{PREFIX}CLIENT_ID |
— | OAuth client identifier |
{PREFIX}CLIENT_SECRET |
— | OAuth client secret |
{PREFIX}TOKEN_URI |
— | Token refresh endpoint URL |
{PREFIX}SCOPES |
— | Space-separated OAuth scopes |
{PREFIX}EXPIRES_AT |
— | ISO 8601 token expiry (e.g. 2026-05-04T12:00:00+00:00) |
{PREFIX}FIELDS takes priority over individual {PREFIX}FIELD_{NAME} vars when both are set.
Header reference (gateway-injected mode)
When using HeaderCredentialBackend, the gateway injects these headers. At least one of the first two must be present.
| Header | Required | Description |
|---|---|---|
X-MCP-Cred-Access-Token |
One of these | OAuth access token |
X-MCP-Cred-Fields |
One of these | JSON object with all static credential fields |
X-MCP-Cred-Scopes |
No | Space-separated string of OAuth scopes |
X-MCP-Cred-Extra |
No | JSON object with OAuth provider metadata |
X-MCP-Cred-Expires-At |
No | Token expiry as ISO 8601 UTC timestamp |
If neither X-MCP-Cred-Access-Token nor X-MCP-Cred-Fields is present, a MissingCredentialHeaderError is raised.
Running the tests
Clone the repo and install with the dev extras:
git clone https://github.com/AStheTECH/fastmcp-credentials.git
cd fastmcp-credentials
pip install -e ".[dev]"
Run the full suite:
python -m pytest
Run a specific file or test:
python -m pytest tests/backends/test_env.py
python -m pytest tests/backends/test_headers.py::test_parse_scopes
Run with verbose output:
python -m pytest -v
License
Apache-2.0
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fastmcp_credentials-0.1.1.tar.gz.
File metadata
- Download URL: fastmcp_credentials-0.1.1.tar.gz
- Upload date:
- Size: 26.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d3456559db7848513d707e710c7fbd0d6a8e7c6c86b16cb8d05a69dd03011458
|
|
| MD5 |
2357ba847db7830653700c1602093b76
|
|
| BLAKE2b-256 |
46b541e1fef487d3c0b3b89c9b155b7d49847469bd24642e8de680cbfde25149
|
File details
Details for the file fastmcp_credentials-0.1.1-py3-none-any.whl.
File metadata
- Download URL: fastmcp_credentials-0.1.1-py3-none-any.whl
- Upload date:
- Size: 17.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
84a6dd4764a43034b4cdd23b7dd660b0d93a0d044e07244d9cf5a830c4a0e517
|
|
| MD5 |
f4df6eb464a9928b55d479bfcab5e3ef
|
|
| BLAKE2b-256 |
33682e45523883b19275a3e2f617aea722b822e8a174e5e13256ea114132b507
|