mcp-secrets
Manage secrets and credentials in MCP servers using Python keyring. Use this skill when an MCP server needs API keys, tokens, or credentials, when integrating OS keychain storage, or when someone asks "how to store secrets in MCP", "credential management for MCP server", "keychain integration", or "secure API key storage". Covers profile-scoped JSON blob storage, integration patterns, and security guidelines.
MCP Secrets Management
MCP servers frequently need API keys and credentials. Environment variables work
for simple cases but are fragile. The recommended pattern here is direct keyring
usage with one JSON blob per profile in the user's OS keychain.
Quick Start
uv add keyring
import json
import os
import keyring
service_name = "my-mcp-server"
default_profile = "default"
active_profile = os.getenv("CONFIG_PROFILE_NAME", default_profile)
username = f"profile:{active_profile}"
# Load JSON blob for the active profile
raw = keyring.get_password(service_name, username)
secrets_blob = json.loads(raw) if raw else {}
# Store / update
secrets_blob["api"] = {"key": "sk-abc123", "endpoint": "https://api.example.com"}
keyring.set_password(service_name, username, json.dumps(secrets_blob))
# Retrieve
creds = secrets_blob.get("api")
# Delete one key from the blob
secrets_blob.pop("api", None)
keyring.set_password(service_name, username, json.dumps(secrets_blob))
Storage Convention
service_name: stable server namespace (for example,my-api-server)username:profile:<active_profile>password: JSON blob containing secret fields (oauth_token,api_key, etc.)- Active profile:
os.getenv("CONFIG_PROFILE_NAME", default_profile)
MCP Integration Pattern
Startup Safety:
keyring.get_password()is a fast, synchronous local-cache read — safe to call anywhere. However, never triggerctx.elicit()or an OAuth flow inside alifespanfunction — that blocks the MCPinitializehandshake and causes clients like Cursor to time out. Always elicit credentials inside a tool call. See themcp-startup-patternsskill for lazy-singleton and background-warmup patterns.
import json
import os
import keyring
from fastmcp import FastMCP, Context
from fastmcp.dependencies import CurrentContext
from fastmcp.exceptions import ToolError
mcp = FastMCP(name="MyAPI", instructions="...")
service_name = "my-api-server"
default_profile = "default"
def _active_profile() -> str:
return os.getenv("CONFIG_PROFILE_NAME", default_profile)
def _load_blob() -> dict:
username = f"profile:{_active_profile()}"
raw = keyring.get_password(service_name, username)
return json.loads(raw) if raw else {}
def _save_blob(blob: dict) -> None:
username = f"profile:{_active_profile()}"
keyring.set_password(service_name, username, json.dumps(blob))
# ✅ Credentials checked lazily on first tool call — server starts instantly
@mcp.tool
async def call_api(endpoint: str, ctx: Context = CurrentContext()) -> dict:
secrets_blob = _load_blob()
creds = secrets_blob.get("api_credentials")
if not creds:
# ctx.elicit() is safe here — we're inside a tool call, not lifespan
result = await ctx.elicit("API key not found. Enter your API key:", response_type=str)
if result.action != "accept":
raise ToolError("API key required")
secrets_blob["api_credentials"] = {"api_key": result.data}
_save_blob(secrets_blob)
creds = {"api_key": result.data}
# Use credentials...
return {"status": "ok"}
Read references/patterns.md for credential management helpers and security
guidelines.
Security Rules
- Never return secret values in tool responses — return
"api_key_set": Trueinstead - Never log secrets — log
"Credentials loaded"not the actual values - Use
serializable=Falsewhen storing sensitive objects in context state - Use elicitation to request missing credentials — don't silently fail