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.
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.
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 rotatedStep 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.
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.
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
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
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
SCAVIO_API_KEY: rotation candidate found
New key validated successfully
Rotated SCAVIO_API_KEY for scavio-search
Config updated with 1 rotated keys