API keys get compromised. Team members leave. Security policies require regular rotation. Rotating keys on a live MCP server without downtime requires a dual-key validation window where both the old and new keys work simultaneously during the transition period. This tutorial implements key rotation for an MCP server that wraps the Scavio API, with graceful deprecation warnings, configurable overlap periods, and automated rotation scheduling.
Prerequisites
- Python 3.9+ installed
- requests library installed
- A Scavio API key from scavio.dev
- An existing MCP server deployment
Walkthrough
Step 1: Design the key rotation state machine
Define key states (active, rotating, deprecated, revoked) and the transition rules between them. During rotation, both old and new keys must work.
import os, requests, json, time, secrets
from datetime import datetime, timedelta
SCAVIO_KEY = os.environ['SCAVIO_API_KEY']
H = {'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json'}
KEY_FILE = 'mcp_keys.json'
def generate_key() -> str:
return f'mcp_{secrets.token_hex(16)}'
def load_keys() -> dict:
if os.path.exists(KEY_FILE):
with open(KEY_FILE) as f:
return json.load(f)
return {'keys': {}}
def save_keys(data: dict):
with open(KEY_FILE, 'w') as f:
json.dump(data, f, indent=2)
# Key states: active -> rotating -> deprecated -> revoked
VALID_TRANSITIONS = {
'active': ['rotating'],
'rotating': ['deprecated'],
'deprecated': ['revoked'],
'revoked': [],
}
print('Key states:', list(VALID_TRANSITIONS.keys()))Step 2: Implement the rotation workflow
Create functions to initiate rotation (generate new key, mark old as rotating), complete rotation (mark old as deprecated), and revoke old keys.
def create_key(name: str) -> str:
data = load_keys()
key = generate_key()
data['keys'][key] = {
'name': name,
'state': 'active',
'created': datetime.now().isoformat(),
'rotated_at': None,
'deprecated_at': None,
}
save_keys(data)
print(f'Created key: {name} ({key[:12]}...)')
return key
def initiate_rotation(old_key: str) -> str:
data = load_keys()
if old_key not in data['keys'] or data['keys'][old_key]['state'] != 'active':
raise ValueError('Can only rotate active keys')
# Mark old key as rotating
data['keys'][old_key]['state'] = 'rotating'
data['keys'][old_key]['rotated_at'] = datetime.now().isoformat()
# Create replacement key
new_key = generate_key()
data['keys'][new_key] = {
'name': data['keys'][old_key]['name'],
'state': 'active',
'created': datetime.now().isoformat(),
'replaces': old_key[:12],
}
save_keys(data)
print(f'Rotation started: {old_key[:12]}... -> {new_key[:12]}...')
print(f'Both keys are now valid. Update clients to use the new key.')
return new_key
def complete_rotation(old_key: str):
data = load_keys()
if data['keys'].get(old_key, {}).get('state') != 'rotating':
raise ValueError('Key is not in rotating state')
data['keys'][old_key]['state'] = 'deprecated'
data['keys'][old_key]['deprecated_at'] = datetime.now().isoformat()
save_keys(data)
print(f'Rotation complete: {old_key[:12]}... is now deprecated')
# Demo
key1 = create_key('production-v1')
key2 = initiate_rotation(key1)
complete_rotation(key1)Step 3: Validate keys with rotation awareness
Build a validation function that accepts active and rotating keys, warns on deprecated keys, and rejects revoked keys.
def validate_with_rotation(api_key: str) -> dict:
data = load_keys()
key_config = data['keys'].get(api_key)
if not key_config:
return {'valid': False, 'error': 'Unknown key'}
state = key_config['state']
if state == 'active':
return {'valid': True, 'warning': None}
elif state == 'rotating':
return {'valid': True, 'warning': 'This key is being rotated. Please switch to the new key.'}
elif state == 'deprecated':
return {'valid': False, 'error': 'Key deprecated. Use the replacement key.', 'deprecated_at': key_config.get('deprecated_at')}
elif state == 'revoked':
return {'valid': False, 'error': 'Key revoked.'}
return {'valid': False, 'error': 'Invalid key state'}
def secure_search_with_rotation(api_key: str, query: str) -> dict:
check = validate_with_rotation(api_key)
if not check['valid']:
return {'error': check['error']}
resp = requests.post('https://api.scavio.dev/api/v1/search', headers=H,
json={'query': query, 'country_code': 'us', 'num_results': 3})
result = {'results': resp.json().get('organic_results', [])}
if check.get('warning'):
result['warning'] = check['warning']
return result
# Test with the active key
result = secure_search_with_rotation(key2, 'test query')
print(f'Results: {len(result.get("results", []))}, Warning: {result.get("warning", "none")}')Python Example
import os, requests, json, secrets
from datetime import datetime
SCAVIO_KEY = os.environ['SCAVIO_API_KEY']
H = {'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json'}
keys = {}
def create_key(name):
key = f'mcp_{secrets.token_hex(16)}'
keys[key] = {'name': name, 'state': 'active', 'created': datetime.now().isoformat()}
return key
def rotate_key(old_key):
keys[old_key]['state'] = 'rotating'
new_key = create_key(keys[old_key]['name'])
print(f'Rotated: {old_key[:12]}... -> {new_key[:12]}...')
return new_key
def validate(key):
k = keys.get(key, {})
if k.get('state') in ('active', 'rotating'):
resp = requests.post('https://api.scavio.dev/api/v1/search', headers=H,
json={'query': 'test', 'country_code': 'us', 'num_results': 1})
print(f'[{k["state"]}] Search OK')
else:
print(f'Key rejected: {k.get("state", "unknown")}')
k1 = create_key('prod')
k2 = rotate_key(k1)
validate(k1) # Still works (rotating)
validate(k2) # Works (active)JavaScript Example
const SCAVIO_KEY = process.env.SCAVIO_API_KEY;
const crypto = require('crypto');
const keys = {};
function createKey(name) {
const key = `mcp_${crypto.randomBytes(16).toString('hex')}`;
keys[key] = { name, state: 'active', created: new Date().toISOString() };
return key;
}
function rotateKey(oldKey) {
keys[oldKey].state = 'rotating';
const newKey = createKey(keys[oldKey].name);
console.log(`Rotated: ${oldKey.slice(0, 12)}... -> ${newKey.slice(0, 12)}...`);
return newKey;
}
async function validate(key) {
const k = keys[key];
if (!k || !['active', 'rotating'].includes(k.state)) {
console.log(`Key rejected: ${k?.state || 'unknown'}`);
return;
}
const resp = await fetch('https://api.scavio.dev/api/v1/search', {
method: 'POST',
headers: { 'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ query: 'test', country_code: 'us', num_results: 1 })
});
console.log(`[${k.state}] Search OK`);
}
const k1 = createKey('prod');
const k2 = rotateKey(k1);
validate(k1).then(() => validate(k2));Expected Output
Key states: ['active', 'rotating', 'deprecated', 'revoked']
Created key: production-v1 (mcp_a1b2c3d4...)
Rotation started: mcp_a1b2c3d4... -> mcp_e5f6g7h8...
Both keys are now valid. Update clients to use the new key.
Rotation complete: mcp_a1b2c3d4... is now deprecated
Results: 3, Warning: none
[rotating] Search OK
[active] Search OK