mcp-service

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.

Category
访问服务器

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.

License: MIT Python 3.11+ FastAPI OAuth 2.1


Table of Contents


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 /health and /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.110
    • uvicorn[standard] ≥ 0.29
    • pydantic ≥ 2.0
    • PyJWT[crypto] ≥ 2.8
    • httpx ≥ 0.27
    • jinja2 ≥ 3.1
    • python-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 stringsmcp-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-serverscopes_supported
  • /.well-known/oauth-protected-resourcescopes_supported
  • The consent page template (each scope rendered as a list item)
  • The scope claim 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 _meta for context like user_id).
  • Output:
    • a dict — wrapped in a 200 response.
    • None — notification; responded with 204 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 curl
  • python-client.py — Python with PKCE (httpx)
  • js-integration.mjs — Node.js with built-in fetch

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.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选