How I Built an MCP Server in 50 Lines of Python (Auto-Generated from OpenAPI)
Source: Dev.to

What We’re Building
An MCP server that:
- ✅ Auto‑generates tools from any OpenAPI spec
- ✅ Handles per‑request authentication (API keys)
- ✅ Works with Claude Desktop, Cursor, and any MCP client
- ✅ Deploys as a standard web service with health checks
Result: AI assistants can execute LinkedIn actions (posting comments, fetching profiles, managing engagement) through natural language.
The Tech Stack
| Component | Purpose |
|---|---|
| FastMCP | MCP server framework with OpenAPI support |
| FastAPI | Web framework for middleware & health checks |
| httpx | Async HTTP client with event hooks |
| ContextVar | Thread‑safe per‑request state management |
The Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Claude Desktop │────▶│ MCP Server │────▶│ LinkedIn API │
│ / Cursor / AI │ │ (FastMCP) │ │ (OpenAPI) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
│ ?apiKey=xxx │ Authorization: Bearer xxx
└───────────────────────┘
API Key Flow
The middle layer reads an OpenAPI spec and automatically exposes every endpoint as an MCP tool.
The Complete Code
1. Setting Up Per‑Request Authentication
from contextvars import ContextVar
import httpx
# Store API key per request using context variables (thread‑safe)
api_key_context: ContextVar[str] = ContextVar('api_key', default='')
# Event hook to inject Authorization header from context
async def add_auth_header(request: httpx.Request):
"""
Async event hook that injects the Authorization header
from the context variable. Called for every outgoing request.
"""
api_key = api_key_context.get()
if api_key:
request.headers["Authorization"] = f"Bearer {api_key}"
Why ContextVar? Unlike globals, ContextVar maintains separate values for each async context, preventing key leakage between concurrent requests.
2. Creating the HTTP Client with Event Hooks
api_client = httpx.AsyncClient(
base_url="https://api.connectsafely.ai/linkedin",
timeout=300.0, # 5‑minute timeout for long operations
event_hooks={"request": [add_auth_header]}
)
The event_hooks pattern ensures every request automatically receives the correct Authorization header.
3. Loading the OpenAPI Spec
import sys
import httpx
try:
print("Loading OpenAPI spec...")
openapi_spec = httpx.get(
"https://api.connectsafely.ai/linkedin/openapi.json",
timeout=5.0
).json()
print("OpenAPI spec loaded successfully!")
except httpx.ConnectError:
print("ERROR: Could not connect to API")
sys.exit(1)
except Exception as e:
print(f"ERROR: Failed to load OpenAPI spec: {e}")
sys.exit(1)
Benefits:
- Zero manual tool definitions – endpoints become tools automatically
- Always in sync – updating the API updates MCP tools automatically
- Self‑documenting – tool descriptions come from OpenAPI
4. The Magic: FastMCP from OpenAPI
from fastmcp import FastMCP
# Create the MCP server from the OpenAPI specification
mcp = FastMCP.from_openapi(
openapi_spec=openapi_spec,
client=api_client,
name="LinkedIn API Server"
)
A single line converts the entire OpenAPI spec into MCP tools, each with proper parameter validation, type hints, and documentation.
5. FastAPI Integration for Production Features
from fastapi import FastAPI, Request
# Create MCP ASGI app
mcp_app = mcp.http_app(path='/')
# Create FastAPI app with MCP lifespan
app = FastAPI(lifespan=mcp_app.lifespan)
@app.get("/health")
async def health_check():
"""Health check endpoint for Docker and load balancers."""
return {"status": "healthy"}
FastAPI adds health checks, custom middleware, and additional REST endpoints as needed.
6. API Key Extraction Middleware
@app.middleware("http")
async def extract_api_key(request: Request, call_next):
"""
Middleware to extract API key from query parameter
or Authorization header. Supports both formats:
- ?apiKey=your-key
- Authorization: Bearer your-key
"""
# Extract from query parameter first
api_key = request.query_params.get("apiKey", "")
if not api_key:
# Fallback to Authorization header
auth_header = request.headers.get("Authorization", "")
if auth_header.startswith("Bearer "):
api_key = auth_header.split(" ")[1]
# Store in context for downstream use
api_key_context.set(api_key)
response = await call_next(request)
return response
# Mount MCP server at root
app.mount("/", mcp_app)
The middleware captures the API key for each request and stores it in the ContextVar, which the httpx hook later reads.
7. Running the Server
if __name__ == "__main__":
import uvicorn
print("\nStarting MCP server on http://0.0.0.0:3011")
print("Connect with: http://localhost:3011?apiKey=your-api-key")
uvicorn.run(app, host="0.0.0.0", port=3011)
8. Connecting to Claude Desktop
Add the following to claude_desktop_config.json:
{
"mcpServers": {
"connectsafely": {
"url": "http://localhost:3011?apiKey=YOUR_API_KEY"
}
}
}
Claude can now execute LinkedIn automation tasks via natural language, e.g.:
“Post a comment on the latest post from @naval saying ‘Great insight on leverage!‘”
Claude will:
- Search for the user’s posts
- Find the latest one
- Post the comment through the MCP‑generated tool
Why This Pattern Is Powerful
1. Zero Duplication
Your REST API and MCP tools share the same OpenAPI source of truth. Updating the spec updates both automatically.