Model Context Protocol (MCP) lets AI agents call external tools natively. Instead of hand-coding each tool, you can auto-generate an MCP server from any Swagger or OpenAPI specification. This tutorial takes the Scavio API OpenAPI spec and produces a working MCP server that Claude Code can use directly. The same approach works for any API with a Swagger spec -- Stripe, Twilio, GitHub, or your internal services.
Prerequisites
- Node.js 18+ or Python 3.9+
- A Swagger/OpenAPI JSON or YAML spec
- Basic understanding of MCP
- Claude Code installed (for testing)
Walkthrough
Step 1: Parse the OpenAPI spec and extract endpoints
Load the Swagger spec and extract the endpoint definitions that will become MCP tools. Each endpoint becomes one tool with typed parameters.
import json, yaml
from pathlib import Path
def parse_openapi(spec_path: str) -> list:
"""Parse OpenAPI spec into MCP tool definitions."""
raw = Path(spec_path).read_text()
spec = yaml.safe_load(raw) if spec_path.endswith('.yaml') else json.loads(raw)
tools = []
base_url = spec.get('servers', [{}])[0].get('url', '')
for path, methods in spec.get('paths', {}).items():
for method, details in methods.items():
if method not in ('get', 'post', 'put', 'delete'):
continue
params = []
for p in details.get('parameters', []):
params.append({
'name': p['name'],
'type': p.get('schema', {}).get('type', 'string'),
'required': p.get('required', False),
'description': p.get('description', '')
})
# Extract request body schema
body = details.get('requestBody', {}).get('content', {}).get('application/json', {})
body_schema = body.get('schema', {}).get('properties', {})
for name, prop in body_schema.items():
params.append({
'name': name, 'type': prop.get('type', 'string'),
'required': name in body.get('schema', {}).get('required', []),
'description': prop.get('description', '')
})
tool_name = details.get('operationId', f'{method}_{path}'.replace('/', '_'))
tools.append({
'name': tool_name, 'description': details.get('summary', ''),
'method': method.upper(), 'path': path,
'base_url': base_url, 'parameters': params
})
print(f'Parsed {len(tools)} tools from spec')
for t in tools[:5]:
print(f' {t["name"]}: {t["method"]} {t["path"]}')
return toolsStep 2: Generate the MCP server code
Transform the parsed endpoints into a working MCP server. Each endpoint becomes a callable tool with input validation.
def generate_mcp_server(tools: list, output_path: str = 'mcp_server.py'):
"""Generate a Python MCP server from parsed tool definitions."""
lines = [
'import json, os, sys, requests',
'from typing import Any',
'',
'def handle_tool_call(name: str, arguments: dict) -> dict:',
' """Route tool calls to the correct API endpoint."""',
' tools_map = {',
]
for tool in tools:
lines.append(f' "{tool["name"]}": {{"method": "{tool["method"]}", "path": "{tool["path"]}", "base_url": "{tool["base_url"]}"}},')
lines.extend([
' }',
' if name not in tools_map:',
' return {"error": f"Unknown tool: {name}"}',
' spec = tools_map[name]',
' url = spec["base_url"] + spec["path"]',
' headers = {"Content-Type": "application/json",',
' "x-api-key": os.environ.get("API_KEY", "")}',
' if spec["method"] == "GET":',
' resp = requests.get(url, headers=headers, params=arguments)',
' else:',
' resp = requests.post(url, headers=headers, json=arguments)',
' return resp.json()',
'',
])
# Write MCP stdio loop
lines.extend([
'def main():',
' for line in sys.stdin:',
' msg = json.loads(line.strip())',
' if msg.get("method") == "tools/call":',
' result = handle_tool_call(msg["params"]["name"], msg["params"].get("arguments", {}))',
' print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"content": [{"type": "text", "text": json.dumps(result)}]}}))',
' sys.stdout.flush()',
'',
'if __name__ == "__main__":',
' main()',
])
with open(output_path, 'w') as f:
f.write('\n'.join(lines))
print(f'Generated MCP server: {output_path}')
print(f'Tools: {len(tools)}')Step 3: Register the MCP server in Claude Code
Add your generated MCP server to .mcp.json so Claude Code discovers it automatically on startup.
import json
from pathlib import Path
def register_mcp(server_name: str, server_path: str):
mcp_config_path = Path('.mcp.json')
config = json.loads(mcp_config_path.read_text()) if mcp_config_path.exists() else {'mcpServers': {}}
config['mcpServers'][server_name] = {
'command': 'python3',
'args': [server_path],
'env': {
'API_KEY': 'your-api-key-here'
}
}
mcp_config_path.write_text(json.dumps(config, indent=2))
print(f'Registered MCP server: {server_name}')
print(f'Config: {mcp_config_path}')
print(f'Restart Claude Code to load the new tools.')
# Register the generated server
register_mcp('my-api', 'mcp_server.py')Python Example
import json, yaml, requests, os, sys
def parse_spec(spec_url):
resp = requests.get(spec_url)
spec = resp.json()
tools = []
base = spec.get('servers', [{}])[0].get('url', '')
for path, methods in spec.get('paths', {}).items():
for method, details in methods.items():
if method in ('get', 'post'):
tools.append({'name': details.get('operationId', path),
'method': method.upper(), 'path': path, 'base_url': base})
return tools
def call_tool(tool, args):
url = tool['base_url'] + tool['path']
headers = {'x-api-key': os.environ.get('API_KEY', ''), 'Content-Type': 'application/json'}
if tool['method'] == 'GET':
return requests.get(url, headers=headers, params=args).json()
return requests.post(url, headers=headers, json=args).json()
tools = parse_spec('https://api.scavio.dev/api/v1/openapi.json')
print(f'Parsed {len(tools)} tools')JavaScript Example
async function parseSpec(specUrl) {
const resp = await fetch(specUrl);
const spec = await resp.json();
const tools = [];
const base = spec.servers?.[0]?.url || '';
for (const [path, methods] of Object.entries(spec.paths || {})) {
for (const [method, details] of Object.entries(methods)) {
if (['get', 'post'].includes(method)) {
tools.push({ name: details.operationId || path, method: method.toUpperCase(), path, base });
}
}
}
return tools;
}
async function callTool(tool, args) {
const url = tool.base + tool.path;
const opts = { headers: { 'x-api-key': process.env.API_KEY, 'Content-Type': 'application/json' } };
if (tool.method === 'GET') {
return (await fetch(`${url}?${new URLSearchParams(args)}`, opts)).json();
}
return (await fetch(url, { ...opts, method: 'POST', body: JSON.stringify(args) })).json();
}
parseSpec('https://api.scavio.dev/api/v1/openapi.json').then(t => console.log(`${t.length} tools`));Expected Output
Parsed 6 tools from spec
search: POST /api/v1/search
tiktok_search_videos: POST /api/v1/tiktok/search/videos
tiktok_user_info: POST /api/v1/tiktok/user/info
Generated MCP server: mcp_server.py
Registered MCP server: my-api
Restart Claude Code to load the new tools.