Remote MCP OAuth Template

Remote MCP OAuth Template

A production-ready template for building remote MCP servers with OAuth 2.1 authentication and JWT passthrough. It enables per-user authorization and proxies tool calls to existing backend APIs.

Category
访问服务器

README

Remote MCP Server with OAuth 2.1 — Node.js Template

Node.js >= 18 License: MIT MCP: Streamable HTTP OAuth 2.1 + PKCE PRs Welcome

A production-shaped template for a remote Model Context Protocol (MCP) server that authenticates real users with OAuth 2.1 — authorization code grant + PKCE + dynamic client registration — and proxies tool calls to your existing backend API over Streamable HTTP.

Connect it to Claude as a custom connector (or to any MCP client that supports OAuth, like MCP Inspector), sign in through a browser consent screen, and every tool call runs as that user — with your backend's normal per-user permissions.

Why this exists: remote MCP servers are easy; remote MCP servers with a working OAuth flow are not. Most examples either skip auth entirely or bake an API key into the client config. This template shows the full flow end-to-end, runnable locally in two commands, with no database and no external identity provider.

Features

  • 🔐 Built-in OAuth 2.1 authorization server — authorization code grant, S256 PKCE, public clients, dynamic client registration (RFC 7591), discovery metadata (RFC 8414 + RFC 9728)
  • 🪄 JWT passthroughaccess_token === your backend's JWT, so there is no token store, no refresh rotation, no session database
  • 🌊 Streamable HTTP MCP endpoint at /mcp — stateless, fresh server per request, proper 401 + WWW-Authenticate challenge
  • 👤 Per-user permissions for free — every tool call carries the user's own JWT to your API
  • Local demo in 2 commands — zero-dependency mock backend with a working login + consent page
  • Full-flow smoke test — registration → consent → PKCE exchange → replay protection → a real tools/call
  • 🐳 Deployment configs — multi-stage Dockerfile, docker-compose, Coolify walkthrough

Table of contents

The trick: access_token === your backend's JWT

Your backend already knows how to log users in and issue JWTs. So instead of building a token store, refresh logic, and a session database, the MCP server:

  1. delegates login + consent to your existing web app,
  2. receives the user's JWT from it,
  3. hands that JWT back to the MCP client as the OAuth access token,
  4. forwards it verbatim to your API on every tool call.

The MCP server holds no long-lived state. When the JWT expires, the client just re-runs the OAuth flow. Auth codes live 60 seconds; in-flight sessions live 5 minutes; both are in-memory Maps.

How it fits together

MCP client  ──HTTPS──►  MCP Server  ──redirect──►  Your web app (login + consent)
(Claude)                     ▲                            │
                             │    POST { mcp_session, jwt }│
                             └─────────────────────────────┘
                             ──HTTPS──►  Your backend API

The OAuth flow (one-time per user, then the client caches the token):

  1. Client tries POST /mcp with no token → MCP returns 401 with a WWW-Authenticate header pointing to /.well-known/oauth-protected-resource.
  2. Client reads the discovery metadata and dynamic-registers itself at /oauth/register (RFC 7591).
  3. Client opens the user's browser at /oauth/authorize?...code_challenge=....
  4. MCP validates the params, parks them as a short-lived mcp_session, and 302-redirects the browser to AUTH_APP_URL/oauth/authorize?mcp_session=....
  5. Your web app handles login (or reuses an existing session) and shows an explicit consent screen. On Approve, it POSTs { mcp_session, jwt } to the MCP server's /oauth/complete.
  6. MCP re-verifies the JWT against your backend, mints a single-use auth code, and returns the URL the browser should navigate to (the client's redirect_uri with ?code=...).
  7. Client exchanges the code at /oauth/token with the PKCE verifier and gets back the JWT as the OAuth access_token.
  8. Every subsequent MCP call carries that JWT; the server forwards it to your API.

Three bridge endpoints connect the MCP server to your web app:

Endpoint Method Purpose
/oauth/preflight?mcp_session=... GET Read-only: returns { client_name, client_origin, scopes } so the consent screen can show what's being authorized. Doesn't consume the session.
/oauth/complete POST Consumes the mcp_session, verifies the JWT, returns { redirect_url } carrying the auth code.
/oauth/deny POST Consumes the mcp_session, returns { redirect_url } with error=access_denied.

CORS on these is locked to the auth app origin (ALLOWED_ORIGINS).

Try it locally (2 minutes)

Requirements: Node.js 18+.

npm install
cp .env.example .env      # defaults work out of the box

# Terminal 1 — mock backend (demo API + login/consent page on :3000)
npm run mock-backend

# Terminal 2 — MCP server (:4000)
npm start

Then point MCP Inspector at it:

npx @modelcontextprotocol/inspector
# Transport: Streamable HTTP, URL: http://localhost:4000/mcp
# Click Connect → your browser opens the consent page
# Log in with demo@example.com / demo → Approve

You're through the full OAuth flow. Call whoami, list_notes, or create_note from the Inspector — each request carries the JWT the mock backend issued.

The mock backend (mock-backend/server.js, zero dependencies) plays both roles your real stack plays in production: the API that issues/verifies JWTs and serves data, and the web app that hosts the login + consent page.

Smoke test

npm test

Boots a stub backend + the MCP server on random ports and walks the entire flow: registration, authorize redirect, preflight, complete, PKCE token exchange, replay protection, deny path, bad-JWT rejection, and a real tools/call through the Streamable HTTP endpoint.

Adapting it to your backend

Three steps:

  1. Point it at your API. Set BACKEND_API_URL and JWT_VERIFY_PATH (the endpoint that returns the current user for a Bearer token, e.g. /api/auth/me). That endpoint is all the MCP server needs from your backend.

  2. Add one route to your web app. Implement /oauth/authorize?mcp_session=... in your frontend: call the MCP server's /oauth/preflight to render the consent screen, let the user log in with your normal auth, then POST the JWT to /oauth/complete and redirect the browser to the returned redirect_url. The mock backend's consent page (~100 lines of HTML in mock-backend/server.js) is a working reference. Set AUTH_APP_URL to your app's public URL.

  3. Write your tools. Replace the demo tools in src/tools.js. Each tool is ~5 lines — the shared apiCall(extra, method, path, body?) helper handles auth, fetch, and response shaping:

mcp.registerTool(
  'list_invoices',
  {
    title: 'List invoices',
    description: 'List the authenticated user\'s invoices (GET /api/invoices).',
    inputSchema: { status: z.enum(['paid', 'open']).optional() },
  },
  ({ status }, extra) =>
    apiCall(extra, 'GET', `/api/invoices${status ? `?status=${status}` : ''}`)
);

The JWT arrives in extra.authInfo.token, so per-user permissions apply on your backend exactly as they do for your web app.

Deployment

The MCP server is a plain Node.js Express app. The only hard requirement: it MUST be reachable over HTTPS at MCP_PUBLIC_URL — Claude will not complete OAuth against plain HTTP.

Option A — PM2

npm ci --omit=dev
cp .env.example .env
# edit .env: MCP_PUBLIC_URL=https://mcp.your-domain.com, BACKEND_API_URL, AUTH_APP_URL
pm2 start src/index.js --name mcp-server
pm2 save

Reverse proxy mcp.your-domain.com127.0.0.1:4000 with nginx / Caddy / Cloudflare Tunnel so HTTPS is terminated for you.

Option B — Docker / docker-compose

A multi-stage Alpine Dockerfile and a docker-compose.yml ship with the project. The compose file runs both the MCP server and the mock backend so the demo works in containers too.

docker compose up --build
# or just the MCP server:
docker build -t mcp-server .
docker run --rm -p 4000:4000 \
  -e MCP_PUBLIC_URL=https://mcp.your-domain.com \
  -e BACKEND_API_URL=https://api.your-domain.com \
  -e AUTH_APP_URL=https://app.your-domain.com \
  mcp-server

Option C — Coolify (or similar PaaS)

  1. + New → Resource → Docker Compose, connect this repository.

  2. Attach a domain to the mcp service (e.g. mcp.your-domain.com). The platform terminates HTTPS for you.

  3. Set environment variables in the UI (never commit a .env):

    MCP_PUBLIC_URL=https://mcp.your-domain.com
    BACKEND_API_URL=https://api.your-domain.com
    AUTH_APP_URL=https://app.your-domain.com
    ALLOWED_ORIGINS=https://claude.ai,https://*.anthropic.com,https://*.claude.ai,https://app.your-domain.com
    
  4. Deploy, then sanity-check:

    curl -fsS https://mcp.your-domain.com/healthz
    curl -fsS https://mcp.your-domain.com/.well-known/oauth-authorization-server | jq
    

The issuer in that metadata response MUST equal MCP_PUBLIC_URL exactly, otherwise the client's OAuth flow will refuse to proceed.

Environment variables

Var Required Purpose
PORT no (default 4000) Local listen port
MCP_PUBLIC_URL yes in prod Externally reachable HTTPS URL — used in OAuth discovery metadata
BACKEND_API_URL yes Base URL of your backend API (no trailing slash)
JWT_VERIFY_PATH no (default /api/auth/me) Backend endpoint that returns the current user for a Bearer token
AUTH_APP_URL yes Public URL of the web app hosting login + consent
ALLOWED_ORIGINS no Comma-separated browser origins for CORS; must include the auth app origin

See .env.example for the annotated template. Note there are no credentials here — each user signs in interactively through the browser.

Adding it to Claude (custom connector)

  1. Make sure the MCP server is reachable at https://mcp.your-domain.com.
  2. In Claude → Settings → Connectors → Add custom connector.
  3. URL: https://mcp.your-domain.com/mcp
  4. Claude discovers the OAuth metadata, opens a browser tab at your consent page, and the user logs in with their normal credentials.
  5. Done — the tools appear in Claude, scoped to that user's data.

Each user who adds the connector goes through their own login and gets their own token. No credentials ever live in the MCP client config, and the MCP server never sees a password.

Demo tools

Tool Endpoint Notes
whoami GET /api/auth/me Profile of the authenticated user.
list_notes GET /api/notes The user's notes.
create_note POST /api/notes title required, content optional.

OAuth specifics

  • Only authorization_code grant.
  • Only S256 PKCE.
  • Public clients only (no client_secret).
  • Dynamic client registration (RFC 7591); storage is in-memory.
  • Discovery via RFC 8414 (/.well-known/oauth-authorization-server) and RFC 9728 (/.well-known/oauth-protected-resource).
  • Auth codes: 32 random bytes, single-use, 60s TTL, in-memory.
  • Access token = your backend's JWT verbatim. expires_in mirrors the JWT's exp claim when present.
  • No refresh tokens — the client re-runs the flow when the token expires.

Out of scope (deliberately)

  • Refresh tokens — re-running the flow is a browser round-trip once every token lifetime.
  • Persistent client / auth-code storage — in-memory is fine on a single instance; put the Maps in Redis if you run replicas behind a load balancer.
  • Scope enforcement — the mcp scope is advertised but tools don't check granular scopes; your backend's own permissions are the real boundary.

Project layout

.
├── package.json
├── .env.example
├── Dockerfile
├── docker-compose.yml
├── LICENSE
├── README.md
├── mock-backend/
│   └── server.js       # Zero-dep demo API + login/consent page (replace with your stack)
├── scripts/
│   └── smoketest.mjs   # Full-flow smoke test (npm test)
└── src/
    ├── index.js        # Entry point; boots Express and wires up tools
    ├── server.js       # Express app: OAuth endpoints + Streamable HTTP MCP
    ├── oauth.js        # OAuth core: clients, sessions, codes, PKCE, Bearer middleware
    ├── auth.js         # Backend API client: apiFetch(jwt, ...) + JWT verification
    └── tools.js        # MCP tool registrations (replace the demo tools)

License

MIT

推荐服务器

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 模型以安全和受控的方式获取实时的网络信息。

官方
精选