Tutorial

How to Build Automatic API Key Rotation for MCP Configs

Build an automatic API key rotation system for MCP server configurations. Rotate keys on schedule, validate them, and update configs without downtime.

MCP server configurations store API keys in plain JSON files like .mcp.json or claude_desktop_config.json. When a key expires, gets compromised, or hits rate limits, every agent using that MCP server breaks. Manually editing config files across machines is error-prone and slow. This tutorial builds a Python utility that reads MCP configs, rotates API keys from a secure store, validates the new key against the target API, and writes the updated config atomically. The approach works for any MCP server that passes credentials via environment variables or command-line arguments.

Prerequisites

  • Python 3.8 or higher installed
  • requests library installed
  • An existing MCP config file (.mcp.json or equivalent)
  • A Scavio API key for validation testing

Walkthrough

Step 1: Read the current MCP config

Load the MCP config file and identify all servers that use API key environment variables. The script looks for env keys containing API_KEY or TOKEN.

Python
import json
import os

def load_mcp_config(path: str) -> dict:
    with open(path, "r") as f:
        return json.load(f)

config_path = os.path.expanduser("~/.mcp.json")
config = load_mcp_config(config_path)
for name, server in config.get("mcpServers", {}).items():
    env = server.get("env", {})
    key_vars = [k for k in env if "API_KEY" in k or "TOKEN" in k]
    print(f"{name}: {key_vars}")

Step 2: Fetch the rotated key from your secret store

Pull the new API key from an environment variable, a secrets manager, or a local encrypted file. This example reads from environment variables with a _NEW suffix convention.

Python
def get_rotated_key(var_name: str) -> str | None:
    new_var = f"{var_name}_NEW"
    rotated = os.environ.get(new_var)
    if rotated:
        print(f"Rotation candidate found: {new_var}")
    return rotated

Step 3: Validate the new key before writing

Make a lightweight API call with the new key to confirm it works. For Scavio keys, POST a minimal search query. If validation fails, skip rotation for that key.

Python
import requests

def validate_scavio_key(api_key: str) -> bool:
    try:
        r = requests.post(
            "https://api.scavio.dev/api/v1/search",
            headers={"x-api-key": api_key},
            json={"query": "test", "country_code": "us"},
            timeout=10
        )
        return r.status_code == 200
    except Exception:
        return False

new_key = get_rotated_key("SCAVIO_API_KEY")
if new_key and validate_scavio_key(new_key):
    print("New key validated successfully")
else:
    print("Validation failed, skipping rotation")

Step 4: Write the updated config atomically

Write to a temporary file first, then rename it over the original. This prevents partial writes from corrupting the config if the process is interrupted.

Python
import tempfile
import shutil

def rotate_config(config_path: str, config: dict, server_name: str, var_name: str, new_key: str) -> None:
    config["mcpServers"][server_name]["env"][var_name] = new_key
    tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
    json.dump(config, tmp, indent=2)
    tmp.close()
    shutil.move(tmp.name, config_path)
    print(f"Rotated {var_name} for {server_name}")

Python Example

Python
import json
import os
import tempfile
import shutil
import requests

def load_config(path: str) -> dict:
    with open(path, "r") as f:
        return json.load(f)

def validate_key(api_key: str) -> bool:
    try:
        r = requests.post(
            "https://api.scavio.dev/api/v1/search",
            headers={"x-api-key": api_key},
            json={"query": "test", "country_code": "us"},
            timeout=10
        )
        return r.status_code == 200
    except Exception:
        return False

def atomic_write(path: str, data: dict) -> None:
    tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
    json.dump(data, tmp, indent=2)
    tmp.close()
    shutil.move(tmp.name, path)

def rotate_all(config_path: str) -> None:
    config = load_config(config_path)
    rotated = 0
    for name, server in config.get("mcpServers", {}).items():
        env = server.get("env", {})
        for var in list(env.keys()):
            if "API_KEY" not in var and "TOKEN" not in var:
                continue
            new_key = os.environ.get(f"{var}_NEW")
            if not new_key:
                continue
            if validate_key(new_key):
                env[var] = new_key
                rotated += 1
                print(f"Rotated {var} for {name}")
            else:
                print(f"Validation failed for {var}, skipping")
    if rotated > 0:
        atomic_write(config_path, config)
        print(f"Config updated with {rotated} rotated keys")
    else:
        print("No keys rotated")

if __name__ == "__main__":
    rotate_all(os.path.expanduser("~/.mcp.json"))

JavaScript Example

JavaScript
const fs = require("fs");
const os = require("os");
const path = require("path");

async function validateKey(apiKey) {
  try {
    const res = await fetch("https://api.scavio.dev/api/v1/search", {
      method: "POST",
      headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
      body: JSON.stringify({ query: "test", country_code: "us" })
    });
    return res.ok;
  } catch {
    return false;
  }
}

async function rotateAll(configPath) {
  const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
  let rotated = 0;
  for (const [name, server] of Object.entries(config.mcpServers || {})) {
    const env = server.env || {};
    for (const varName of Object.keys(env)) {
      if (!varName.includes("API_KEY") && !varName.includes("TOKEN")) continue;
      const newKey = process.env[`${varName}_NEW`];
      if (!newKey) continue;
      if (await validateKey(newKey)) {
        env[varName] = newKey;
        rotated++;
        console.log(`Rotated ${varName} for ${name}`);
      } else {
        console.log(`Validation failed for ${varName}, skipping`);
      }
    }
  }
  if (rotated > 0) {
    const tmp = path.join(os.tmpdir(), `mcp-${Date.now()}.json`);
    fs.writeFileSync(tmp, JSON.stringify(config, null, 2));
    fs.renameSync(tmp, configPath);
    console.log(`Config updated with ${rotated} rotated keys`);
  }
}

rotateAll(path.join(os.homedir(), ".mcp.json")).catch(console.error);

Expected Output

JSON
SCAVIO_API_KEY: rotation candidate found
New key validated successfully
Rotated SCAVIO_API_KEY for scavio-search
Config updated with 1 rotated keys

Related Tutorials

Frequently Asked Questions

Most developers complete this tutorial in 15 to 30 minutes. You will need a Scavio API key (free tier works) and a working Python or JavaScript environment.

Python 3.8 or higher installed. requests library installed. An existing MCP config file (.mcp.json or equivalent). A Scavio API key for validation testing. A Scavio API key gives you 250 free credits per month.

Yes. The free tier includes 250 credits per month, which is more than enough to complete this tutorial and prototype a working solution.

Scavio has a native LangChain package (langchain-scavio), an MCP server, and a plain REST API that works with any HTTP client. This tutorial uses the raw REST API, but you can adapt to your framework of choice.

Start Building

Build an automatic API key rotation system for MCP server configurations. Rotate keys on schedule, validate them, and update configs without downtime.