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 trigger ctx.elicit() or an OAuth flow inside a lifespan function — that blocks the MCP initialize handshake and causes clients like Cursor to time out. Always elicit credentials inside a tool call. See the mcp-startup-patterns skill 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

  1. Never return secret values in tool responses — return "api_key_set": True instead
  2. Never log secrets — log "Credentials loaded" not the actual values
  3. Use serializable=False when storing sensitive objects in context state
  4. Use elicitation to request missing credentials — don't silently fail