mcp-service
A reusable MCP HTTP server with OAuth 2.1 that turns any JSON-RPC handler into a production-ready endpoint with dynamic client registration, PKCE, refresh tokens, and OpenAPI docs.
README
mcp-service
Reusable MCP HTTP server with OAuth 2.1 — drop in any JSON-RPC handler and get a production-ready, plug-and-play MCP endpoint with dynamic client registration, PKCE, refresh tokens, and OpenAPI docs.
Table of Contents
- Why mcp-service?
- Quick Start
- Installation
- Configuration
- Usage
- OAuth 2.1 Flow
- Docker
- API Reference
- Integration Examples
- Project Structure
- Development
- Troubleshooting
- Contributing
- License
Why mcp-service?
Building an MCP server from scratch is repetitive: OAuth dance, token persistence, PKCE, registration, RFC 8414 metadata, error handling, health checks… mcp-service provides all of that as a tested, documented, configurable FastAPI factory.
You write one function — a JSON-RPC handler — and mcp-service handles everything else:
from mcp_service import run
def my_handler(request: dict) -> dict | None:
if request["method"] == "tools/list":
return {"jsonrpc": "2.0", "id": request["id"], "result": {"tools": []}}
# …
if __name__ == "__main__":
run(my_handler, title="My MCP Server")
That's it. You now have an MCP HTTP server with:
- ✅ OAuth 2.1 Authorization Server (RFC 8414)
- ✅ Dynamic Client Registration (RFC 7591)
- ✅ PKCE / S256 enforcement (RFC 7636)
- ✅ Access + refresh tokens with rotation
- ✅ JWT validation for external IdPs (Google, Auth0, Okta, …)
- ✅ OpenAPI/Swagger docs at
/docs - ✅ Health probes at
/healthand/healthz - ✅ Standardized error responses (RFC 6749 + RFC 6750)
- ✅ In-memory token store with JSON persistence
- ✅ Docker-ready multi-stage image
Quick Start
The fastest way to get a working server in under 60 seconds:
1. Clone & install
git clone https://github.com/AvengerMoJo/mcp-service.git
cd mcp-service
pip install -e ".[dev]"
2. Configure
cp .env.example .env
The defaults work out of the box for local development. No edits required.
3. Run the example server
python example/main.py
4. Verify it's up
curl http://localhost:8000/health
# → {"status":"ok","server":"Example MCP Server"}
5. Hit the MCP endpoint
curl -X POST http://localhost:8000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
6. Explore the API
Open http://localhost:8000/docs in your browser — interactive Swagger UI with every endpoint documented.
Installation
From source (recommended for development)
git clone https://github.com/AvengerMoJo/mcp-service.git
cd mcp-service
pip install -e ".[dev]"
From PyPI (planned for v1.0)
pip install mcp-service
Requirements
- Python 3.11 or newer
- OS any (Linux, macOS, Windows, containers)
- Runtime deps (installed automatically):
fastapi≥ 0.110uvicorn[standard]≥ 0.29pydantic≥ 2.0PyJWT[crypto]≥ 2.8httpx≥ 0.27jinja2≥ 3.1python-multipart≥ 0.0.9
Configuration
All configuration is via environment variables. Copy .env.example to .env and edit as needed.
| Variable | Type | Default | Description |
|---|---|---|---|
MCP_PORT |
int | 8000 |
Server port to bind to. |
MCP_REQUIRE_AUTH |
bool | false |
Enforce OAuth or API key on all requests. |
MCP_API_KEY |
string | change-me |
Static API key accepted as Bearer token (used when OAuth disabled or as fallback). |
OAUTH_ENABLED |
bool | true |
Enable the OAuth 2.1 Authorization Server. |
OAUTH_ENABLE_AS |
bool | true |
Mount AS endpoints (well-known, /oauth/*). |
OAUTH_AUTO_APPROVE |
bool | true |
Skip the consent page. Only enable for personal/headless clients. |
OAUTH_STORAGE_DIR |
path | ~/.mcp_service/oauth |
Directory for persisted tokens & clients. |
OAUTH_ACCESS_TOKEN_TTL |
int | 3600 |
Access token lifetime in seconds (1 hour). |
OAUTH_REFRESH_TOKEN_TTL |
int | 2592000 |
Refresh token lifetime (30 days). |
OAUTH_AUTH_CODE_TTL |
int | 600 |
Authorization code lifetime (10 minutes). |
OAUTH_SUPPORTED_SCOPES |
space-separated string | mcp:read mcp:write mcp:admin |
Allowed OAuth scopes. See Custom scopes below. |
OAUTH_ISSUER |
URL | empty | Expected iss claim for JWT validation. |
OAUTH_AUDIENCE |
string | empty | Expected aud claim for JWT validation. |
OAUTH_JWKS_URI |
URL | empty | JWKS endpoint for external IdP signature verification. |
OAUTH_ALGORITHM |
string | RS256 |
JWT algorithm (RS256, HS256, …). |
OAUTH_VERIFY_SIGNATURE |
bool | true |
Verify JWT signatures. Always keep on in production. |
OAUTH_VERIFY_AUDIENCE |
bool | false |
Enforce aud claim match. |
OAUTH_VERIFY_ISSUER |
bool | false |
Enforce iss claim match. |
OAUTH_VERIFY_EXP |
bool | true |
Enforce JWT exp claim. |
OAUTH_REQUIRED_SCOPE |
string | empty | Scope required for all requests (e.g. mcp:read). |
JWT_SECRET |
string | empty | HMAC secret fallback when no JWKS URI is configured. |
Quick config examples
Local development (no auth):
MCP_PORT=8000
MCP_REQUIRE_AUTH=false
OAUTH_ENABLED=false
Production with built-in OAuth AS:
MCP_PORT=8000
MCP_REQUIRE_AUTH=true
OAUTH_ENABLED=true
OAUTH_AUTO_APPROVE=false
OAUTH_STORAGE_DIR=/var/lib/mcp-service/oauth
MCP_API_KEY=<random-32-bytes>
Production with external JWT IdP (e.g. Auth0):
MCP_REQUIRE_AUTH=true
OAUTH_ENABLED=false
OAUTH_ISSUER=https://your-tenant.auth0.com/
OAUTH_AUDIENCE=https://mcp.yourcompany.com
OAUTH_JWKS_URI=https://your-tenant.auth0.com/.well-known/jwks.json
OAUTH_VERIFY_AUDIENCE=true
OAUTH_VERIFY_ISSUER=true
Custom OAuth Scopes
OAUTH_SUPPORTED_SCOPES accepts any whitespace-separated list of scope names.
The AS treats them as opaque strings — mcp-service does not enforce a
fixed taxonomy. Projects can use domain-specific names and decide for
themselves how to interpret them in their handler.
# Custom scope set for a finance MCP server
OAUTH_SUPPORTED_SCOPES=portfolio:read portfolio:write trades:execute admin
The scopes appear in:
/.well-known/oauth-authorization-server→scopes_supported/.well-known/oauth-protected-resource→scopes_supported- The consent page template (each scope rendered as a list item)
- The
scopeclaim of issued access tokens
The handler receives the granted scopes in the validated OAuthToken.scopes
list, so the application code can enforce them however it wants:
def handler(request):
if request.get("method") == "tools/call":
tool = request["params"]["name"]
if tool == "execute_trade" and "trades:execute" not in request["scopes"]:
return error(-32603, "missing required scope: trades:execute")
Usage
Class-based handlers (MCPHandler)
For new projects, subclass MCPHandler to get method-based dispatch,
tool registration, lifecycle hooks, and discovery metadata — without any
boilerplate:
from mcp_service import MCPHandler, run
class WeatherHandler(MCPHandler):
name = "weather-mcp"
version = "1.0.0"
description = "Read-only weather lookup"
protocol_version = "2024-11-05"
@MCPHandler.register_tool(
name="get_weather",
description="Get current weather for a city",
input_schema={
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
)
def get_weather(self, args: dict) -> dict:
return {"content": [{"type": "text",
"text": f"Sunny in {args['city']}"}]}
async def on_resources_list(self, request):
return self._ok(request, {"resources": []})
async def setup(self):
# Open DB connections, warm caches, etc. Runs once at startup.
...
async def teardown(self):
# Release resources on shutdown.
...
if __name__ == "__main__":
run(WeatherHandler())
What you get for free:
| Built-in JSON-RPC method | Answer |
|---|---|
initialize |
Reports name, version, protocolVersion, and capabilities |
notifications/initialized |
Returns None → HTTP 204 |
ping |
Empty result |
tools/list |
Iterates over registered tools |
tools/call |
Invokes the matching callable; wraps plain results in MCP content |
| any other method | Routed via @register_method("foo/bar") or on_foo_bar(self, req) |
The authenticated subject (when a Bearer token is present) is surfaced
via request["_meta"]["user_id"], so handlers can implement per-user
authorization without threading context through every call.
Function handlers
Plain function handlers remain fully supported:
from mcp_service import run
def my_handler(request: dict) -> dict | None:
method = request.get("method")
if method == "tools/list":
return {
"jsonrpc": "2.0",
"id": request.get("id"),
"result": {"tools": [
{"name": "ping", "description": "Returns pong"},
]},
}
if method == "tools/call":
return {"jsonrpc": "2.0", "id": request.get("id"),
"result": {"content": [{"type": "text", "text": "pong"}]}}
return None
if __name__ == "__main__":
run(my_handler, title="My Server")
Internally, create_app wraps function handlers in a lightweight
MCPHandler adapter so the dispatch path is identical for both styles.
Handler contract
Handler = MCPHandler | Callable[[dict], Optional[dict]]
- Input: a raw JSON-RPC 2.0 request dict (
jsonrpc,id,method,params, plus_metafor context likeuser_id). - Output:
- a dict — wrapped in a 200 response.
None— notification; responded with204 No Content.- raise an exception — wrapped in a 500 JSON-RPC error.
Service discovery
Every server advertises its capabilities at a stable well-known URI. Clients fetch this once on connect to learn what's supported before negotiating OAuth or sending method calls:
curl http://localhost:8000/.well-known/mcp.json
{
"mcp_version": "2024-11-05",
"server": {"name": "weather-mcp", "version": "1.0.0", "description": "Read-only weather lookup"},
"transport": {"type": "http", "endpoint": "/mcp", "methods": ["POST", "GET"]},
"auth": {
"required": false,
"schemes": [
{"type": "bearer", "header": "Authorization", "required": false},
{"type": "api-key", "header": "MCP-API-Key", "required": false}
],
"authorization_server": "http://localhost:8000/.well-known/oauth-authorization-server",
"registration_endpoint": "http://localhost:8000/oauth/register"
},
"scopes_supported": ["mcp:read", "mcp:write", "mcp:admin"],
"capabilities": {"tools": {"listChanged": false}},
"tools": [
{"name": "get_weather", "description": "Get current weather for a city",
"inputSchema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}
],
"endpoints": {
"mcp": "/mcp",
"openapi": "/openapi.json",
"docs": "/docs",
"health": "/health",
"healthz": "/healthz"
}
}
capabilities and tools are populated automatically from
MCPHandler.capabilities() and MCPHandler.list_tools(). Override
those on a subclass to customise.
CLI entry point
If mcp_service is installed, a console script is available:
MCP_HANDLER=example.main:handler mcp-service
OAuth 2.1 Flow
mcp-service implements Authorization Code Flow with PKCE (RFC 7636), the OAuth 2.1 recommended flow for public clients.
┌────────┐ ┌──────────────┐
│ Client │ │ MCP Service │
└───┬────┘ └──────┬───────┘
│ 1. GET /.well-known/oauth-authorization-server │
│ ──────────────────────────────────────────────────►│
│ ◄──────────────────────────────────────────────── │
│ { issuer, authorization_endpoint, token_endpoint, │
│ registration_endpoint, scopes_supported } │
│ │
│ 2. POST /oauth/register (RFC 7591) │
│ ──────────────────────────────────────────────────►│
│ ◄──────────────────────────────────────────────── │
│ { client_id, … } │
│ │
│ 3. Generate code_verifier + code_challenge (S256) │
│ 4. GET /oauth/authorize?response_type=code │
│ &client_id=… │
│ &redirect_uri=… │
│ &code_challenge=… │
│ &code_challenge_method=S256 │
│ &state=… │
│ &scope=… │
│ ──────────────────────────────────────────────────►│
│ ◄──────────────────────────────────────────────── │
│ 302 → redirect_uri?code=…&state=… │
│ │
│ 5. POST /oauth/token │
│ grant_type=authorization_code │
│ &code=… │
│ &code_verifier=… │
│ &redirect_uri=… │
│ ──────────────────────────────────────────────────►│
│ ◄──────────────────────────────────────────────── │
│ { access_token, refresh_token, expires_in, … } │
│ │
│ 6. POST /mcp Authorization: Bearer <access_token>│
│ ──────────────────────────────────────────────────►│
│ ◄──────────────────────────────────────────────── │
│ JSON-RPC 2.0 response │
│ │
│ 7. POST /oauth/token (when access_token expires) │
│ grant_type=refresh_token │
│ &refresh_token=… │
│ ──────────────────────────────────────────────────►│
│ ◄──────────────────────────────────────────────── │
│ { access_token, refresh_token, … } │
Minimal Python client
import secrets, hashlib, base64, httpx
verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode().rstrip("=")
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()
).decode().rstrip("=")
# Register client
client = httpx.post("http://localhost:8000/oauth/register",
json={"client_name": "demo",
"redirect_uris": ["http://localhost:9999/cb"]}).json()
# Authorize (with auto-approve enabled, server returns 302 with code)
auth = httpx.get("http://localhost:8000/oauth/authorize",
params={"response_type": "code",
"client_id": client["client_id"],
"redirect_uri": "http://localhost:9999/cb",
"state": "xyz",
"code_challenge": challenge,
"code_challenge_method": "S256"},
follow_redirects=False)
code = httpx.params(auth.headers["location"])["code"]
# Exchange code for tokens
tokens = httpx.post("http://localhost:8000/oauth/token",
data={"grant_type": "authorization_code",
"code": code,
"redirect_uri": "http://localhost:9999/cb",
"code_verifier": verifier}).json()
# Call MCP
result = httpx.post("http://localhost:8000/mcp",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={"jsonrpc": "2.0", "id": 1,
"method": "tools/list", "params": {}}).json()
See examples/clients/ for full curl, Python, and Node.js clients.
Docker
A multi-stage Dockerfile and docker-compose.yml are provided.
docker-compose up -d
curl http://localhost:8000/health
Build a minimal image:
docker build -t mcp-service:latest .
docker run --rm -p 8000:8000 --env-file .env mcp-service:latest
See docs/quickstart.md for production deployment notes.
API Reference
Interactive docs: /docs (Swagger UI) and /redoc (ReDoc).
Raw schema: /openapi.json.
Full endpoint table and request/response shapes: docs/api-reference.md.
Integration Examples
Ready-to-run examples in examples/clients/:
curl-auth.sh— full OAuth dance with curlpython-client.py— Python with PKCE (httpx)js-integration.mjs— Node.js with built-infetch
Project Structure
mcp-service/
├── mcp_service/ # Library code
│ ├── server.py # FastAPI factory + run()
│ ├── config.py # Environment-based config
│ ├── errors.py # Standardized error helpers
│ └── oauth/ # OAuth 2.1 Authorization Server
│ ├── endpoints.py # /oauth/* routes
│ ├── middleware.py # Bearer token validation
│ ├── models.py # Pydantic schemas
│ ├── pkce.py # RFC 7636
│ ├── storage.py # Token + client persistence
│ ├── token_validator.py
│ └── templates/ # Consent page
├── example/ # Minimal working MCP server
├── examples/clients/ # curl, Python, Node.js examples
├── tests/ # pytest suite (≥85% coverage)
├── docs/ # Detailed documentation
├── Dockerfile
├── docker-compose.yml
├── .env.example
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── pyproject.toml
└── README.md
Development
Setup
git clone https://github.com/AvengerMoJo/mcp-service.git
cd mcp-service
pip install -e ".[dev]"
Run tests
pytest # full suite
pytest --cov=mcp_service # with coverage report
pytest tests/test_oauth.py # single file
pytest -k "pkce" # by keyword
Lint / format
ruff check mcp_service/
ruff format mcp_service/
Run the example
python example/main.py
# in another terminal:
bash examples/clients/curl-auth.sh
Troubleshooting
ModuleNotFoundError: No module named 'mcp_service'
Install in editable mode: pip install -e .
Address already in use on startup
Change MCP_PORT or kill the process holding the port:
lsof -ti:8000 | xargs kill -9
invalid_request: PKCE verification failed
The code_verifier sent to /oauth/token doesn't match the code_challenge from /oauth/authorize. Ensure you're sending the same verifier that produced the challenge (SHA-256, then base64url-stripped).
401 Unauthorized: invalid_token
Token expired or malformed. For JWTs, verify OAUTH_JWKS_URI is reachable and the kid in the JWT header matches a key in the JWKS. For opaque tokens, the token must come from /oauth/token — tokens are tied to the issuing client.
400 Bad Request: redirect_uri mismatch
The redirect_uri sent to /oauth/token must match exactly the one used in /oauth/authorize. This is per OAuth 2.1 spec.
OAUTH_SUPPORTED_SCOPES ignored
Restart the server after editing .env. The config is read once at startup.
Tokens lost on restart
Check OAUTH_STORAGE_DIR is on a persistent volume and writable. Tokens are persisted to <storage_dir>/tokens.json after every issue/refresh.
WWW-Authenticate header missing on errors
This was fixed in v1.0. See CHANGELOG.md. If you're seeing it on an older version, upgrade.
Contributing
See CONTRIBUTING.md for the versioning policy, deprecation timeline, and PR process.
License
MIT — © 2026 AvengerMoJo.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.