Tutorial

How to Implement MCP Auth Key Rotation

Rotate MCP API keys without downtime. Implement dual-key validation, graceful deprecation, and automated rotation for production MCP servers.

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.

Python
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.

Python
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.

Python
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

Python
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

JavaScript
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

JSON
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

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.9+ installed. requests library installed. A Scavio API key from scavio.dev. An existing MCP server deployment. 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

Rotate MCP API keys without downtime. Implement dual-key validation, graceful deprecation, and automated rotation for production MCP servers.