oauth-mcp-server

oauth-mcp-server

A production-grade Model Context Protocol (MCP) resource server with OAuth 2.1 authorization, provider-agnostic and supporting multiple MCP specification versions.

Category
访问服务器

README

oauth-mcp-server

A production-grade Model Context Protocol (MCP) resource server with OAuth 2.1 authorization. Written in TypeScript with the official @modelcontextprotocol/sdk.

It is provider-agnostic — swap between Auth0, Keycloak, Google, Entra ID, Okta, or any OAuth 2.1-compliant authorization server by changing one configuration file.

Compatible with both MCP specification versions:

  • 2026-07-28 (primary: per-request _meta, server/discover, stateless auth)
  • 2025-11-25 (backward compat: initialize handshake)

Quick Start

Requires Node.js >= 18. All commands below are PowerShell (Windows). For macOS/Linux, replace $env:VAR = "value" with VAR=value.

# 1. Install dependencies
npm install

# 2. Build the TypeScript
npm run build

# 3. Start the server on port 6000 (dev mode — no OAuth required)
$env:PORT = "6000"; $env:MCP_NO_AUTH = "true"; npm run start:http

Verify it's running:

# Health check
curl http://localhost:6000/health

# Protected Resource Metadata (RFC 9728)
curl http://localhost:6000/.well-known/oauth-protected-resource

Expected output:

{"status":"ok","server":"oauth-mcp-server v1.0.0","auth":"disabled (dev mode)","sessions":0}

Running on a Custom Port

The server reads the PORT environment variable. The config file uses port 6000 by default. To use a different port, set both:

# Pick any free port
$env:PORT = "8080"; npm run start:http

If you change the port, also update these fields in your config file to match (config/default.json):

{
  "baseUrl": "http://localhost:8080",
  "authorization": {
    "validation": {
      "jwks": {
        "uri": "http://localhost:8080/.well-known/jwks.json",
        "issuer": "http://localhost:8080"
      }
    },
    "resourceMetadata": {
      "authorizationServers": ["http://localhost:8080"],
      "resource": "http://localhost:8080"
    }
  }
}

The port appears in 5 places in the config — they must all match the port you actually run on.


Setup Guide: Local Development

For local development, start with OAuth disabled. This unlocks all capabilities without needing any tokens — perfect for testing the server itself.

# Build and start in dev mode
npm run build
$env:PORT = "6000"; $env:MCP_NO_AUTH = "true"; npm run start:http
Variable What it does
PORT Port to listen on (defaults to 6000)
MCP_NO_AUTH Set to "true" to skip ALL OAuth checks — every request gets full access
MCP_CONFIG_PATH Path to a config JSON file (defaults to config/default.json)
HOST Bind address (defaults to 127.0.0.1; Cloud Run auto-sets to 0.0.0.0)

Testing with curl (no auth)

# Discover server capabilities
$body = '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
curl -X POST http://localhost:6000/mcp -H "Content-Type: application/json" -H "MCP-Protocol-Version: 2026-07-28" -d $body

# List tools (all 18 available since auth is off)
$body = '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
curl -X POST http://localhost:6000/mcp -H "Content-Type: application/json" -H "MCP-Protocol-Version: 2026-07-28" -d $body

# Call the echo tool
$body = '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hello world"}}}'
curl -X POST http://localhost:6000/mcp -H "Content-Type: application/json" -H "MCP-Protocol-Version: 2026-07-28" -d $body

Local dev with live reload

# stdio transport (no build step, auto-reloads on code changes)
npm run dev:stdio

# HTTP transport, no OAuth, auto-reloads
$env:MCP_NO_AUTH = "true"; npm run dev:http

Setup Guide: Enabling OAuth (Local Testing with a Real Provider)

When you're ready to test the full OAuth flow, point the server at a real authorization server. Here's the step-by-step for Auth0 (the same pattern works for Keycloak, Okta, etc.).

Step 1: Create an API in Auth0

  1. Go to Auth0 Dashboard → Applications → APIs → Create API
  2. Set Name: MCP Server
  3. Set Identifier: http://localhost:6000 (this becomes your audience)
  4. Set Signing Algorithm: RS256
  5. Click Create

Step 2: Add scopes to the API

In your Auth0 API settings, add these scopes:

mcp:read
mcp:write
mcp:admin

Step 3: Create a Machine-to-Machine client

  1. Go to Applications → Create Application
  2. Name it MCP Client
  3. Choose Machine to Machine
  4. Select the MCP Server API
  5. Grant it mcp:read and mcp:write scopes
  6. Note the Client ID and Client Secret

Step 4: Update the server config

Copy the Auth0 example and fill in your details:

Copy-Item config\auth0.example.json config\production.json

Edit config/production.json — the three fields you must change:

{
  "baseUrl": "http://localhost:6000",
  "authorization": {
    "validation": {
      "jwks": {
        "uri": "https://YOUR_DOMAIN.us.auth0.com/.well-known/jwks.json",
        "issuer": "https://YOUR_DOMAIN.us.auth0.com/",
        "audience": "http://localhost:6000"
      }
    },
    "resourceMetadata": {
      "authorizationServers": ["https://YOUR_DOMAIN.us.auth0.com"],
      "resource": "http://localhost:6000"
    }
  }
}
Field Where to find it
YOUR_DOMAIN Auth0 Dashboard → your tenant domain (e.g. acme-corp.us.auth0.com)
uri https://YOUR_DOMAIN/.well-known/jwks.json
issuer https://YOUR_DOMAIN/ (trailing slash matters — use exactly what Auth0 shows)
audience The API Identifier you set in Step 1

Step 5: Start the server with OAuth enabled

# Omit MCP_NO_AUTH — OAuth is now active
$env:PORT = "6000"; $env:MCP_CONFIG_PATH = "config/production.json"; npm run start:http

Step 6: Get a token and test

# Get a token from Auth0 (Client Credentials flow)
$tokenResponse = curl -X POST https://YOUR_DOMAIN.us.auth0.com/oauth/token `
  -H "Content-Type: application/json" `
  -d '{"client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_CLIENT_SECRET","audience":"http://localhost:6000","grant_type":"client_credentials","scope":"mcp:read"}'

$token = ($tokenResponse | ConvertFrom-Json).access_token

# Now call the server with the token
$body = '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
curl -X POST http://localhost:6000/mcp `
  -H "Content-Type: application/json" `
  -H "MCP-Protocol-Version: 2026-07-28" `
  -H "Authorization: Bearer $token" `
  -d $body

What happens with no token (OAuth enabled)

# Without a token, the server returns 401
curl -X POST http://localhost:6000/mcp `
  -H "Content-Type: application/json" `
  -H "MCP-Protocol-Version: 2026-07-28" `
  -d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{}}'

# Response:
# HTTP 401 Unauthorized
# WWW-Authenticate: Bearer resource_metadata="http://localhost:6000/.well-known/oauth-protected-resource", scope="mcp:read"

Provider-specific notes

Provider JWKS URI pattern Issuer pattern Notes
Auth0 https://<domain>/.well-known/jwks.json https://<domain>/ Trailing slash on issuer
Keycloak https://<host>/realms/<realm>/protocol/openid-connect/certs https://<host>/realms/<realm> No trailing slash
Okta https://<domain>/oauth2/<server-id>/v1/keys https://<domain> Use the authorization server ID
Entra ID https://login.microsoftonline.com/<tenant-id>/discovery/v2.0/keys https://login.microsoftonline.com/<tenant-id>/v2.0 Use tenant ID, not domain

Setup Guide: Production Deployment

Configuration changes for production

The only differences between local and production config are the URLs — everything else stays the same:

{
  "baseUrl": "https://mcp.your-company.com",
  "authorization": {
    "validation": {
      "jwks": {
        "uri": "https://YOUR_DOMAIN.us.auth0.com/.well-known/jwks.json",
        "issuer": "https://YOUR_DOMAIN.us.auth0.com/",
        "audience": "https://mcp.your-company.com"
      }
    },
    "resourceMetadata": {
      "authorizationServers": ["https://YOUR_DOMAIN.us.auth0.com"],
      "resource": "https://mcp.your-company.com"
    }
  }
}
Config field Local value Production value
baseUrl http://localhost:6000 https://mcp.your-company.com
jwks.uri Your provider's JWKS URL Same provider URL (unchanged)
jwks.issuer Your provider's issuer Same (unchanged)
jwks.audience http://localhost:6000 https://mcp.your-company.com
resourceMetadata.resource http://localhost:6000 https://mcp.your-company.com
resourceMetadata.authorizationServers Your provider Same (unchanged)

Key insight: The authorizationServers and JWKS/issuer fields point to your OAuth provider (which doesn't change between local and prod). Only the baseUrl, audience, and resource fields reflect where this server is running.

Deploying to Google Cloud Run

npm run build

Cloud Run sets PORT (usually 8080) and K_SERVICE automatically. The server detects K_SERVICE and binds to 0.0.0.0. Set your config path as an env var in Cloud Run:

MCP_CONFIG_PATH=config/production.json

A Dockerfile is included for container-based deployments. After deployment:

  • MCP endpoint: https://<your-cloud-run-url>/mcp
  • Health: https://<your-cloud-run-url>/health
  • Metadata: https://<your-cloud-run-url>/.well-known/oauth-protected-resource

Deploying to Vercel

api/
├── health.ts     # Health check (no auth)
└── mcp.ts        # Stateless Streamable HTTP handler with OAuth

Deploy via Vercel Git integration — no build step or framework preset needed. Set these env vars in Vercel:

MCP_CONFIG_PATH=config/production.json
MCP_CORS_ORIGIN=https://your-client.example.com

After deployment:

  • MCP endpoint: https://<project>.vercel.app/api/mcp
  • Health: https://<project>.vercel.app/api/health
  • Metadata: https://<project>.vercel.app/.well-known/oauth-protected-resource

How OAuth Works on This Server

This server acts as an OAuth 2.1 Resource Server. It never issues tokens — it only validates tokens issued by an authorization server.

The Flow

MCP Client                         This Server                   Auth Server
    │                                    │                            │
    │  POST /mcp  (no token)             │                            │
    │ ──────────────────────────────────►│                            │
    │                                    │                            │
    │  HTTP 401                          │                            │
    │  WWW-Authenticate: Bearer          │                            │
    │    resource_metadata="...",        │                            │
    │    scope="mcp:read"                │                            │
    │ ◄──────────────────────────────────│                            │
    │                                    │                            │
    │  [Client discovers auth server,    │                            │
    │   gets a Bearer token via OAuth]   │                            │
    │ ───────────────────────────────────────────────────────────────►│
    │ ◄───────────────────────────────────────────────────────────────│
    │                                    │                            │
    │  POST /mcp                         │                            │
    │  Authorization: Bearer <token>     │                            │
    │  MCP-Protocol-Version: 2026-07-28  │                            │
    │ ──────────────────────────────────►│                            │
    │                                    │  Validate JWT signature    │
    │                                    │  Check exp, iss, aud       │
    │                                    │  Resolve scopes→permissions│
    │                                    │  Filter capabilities       │
    │  HTTP 200 + MCP response           │                            │
    │ ◄──────────────────────────────────│                            │

Token Validation Modes

Mode How it works When to use
jwt Fetches JWKS from your provider, verifies signature locally. Fast, no network call per request after JWKS is cached. Auth0, Okta, Keycloak — most providers
introspection Sends the token to the provider's introspection endpoint on every request. Works with opaque tokens. When tokens are not JWTs, or you need real-time revocation
hybrid Tries JWT validation first; falls back to introspection if JWT fails. When your provider issues both JWT and opaque tokens

Scope Model

Different OAuth scopes unlock different subsets of capabilities. The default config defines:

Scope What it unlocks
(no token) echo, current_time, calculate, random_number — public tools only
mcp:read Public tools + search documents, query analytics, list projects/files, read resources
mcp:write Everything in read + create/update documents, create/update projects, upload files
mcp:admin Everything — including get_server_stats, reload_config, list_server_capabilities
mcp:documents:read Just search_documents + docs://* resources
mcp:analytics:read Just query_analytics + analytics://* resources
mcp:files:read / mcp:files:write File system tools
mcp:projects:read / mcp:projects:write Project management tools

Scopes are additive and resolved in the order listed — if a single token has both mcp:read and mcp:files:write, the user gets the union of both permission sets.


Configuration Reference

Environment Variables

Variable Default Description
PORT 6000 HTTP port to listen on
HOST 127.0.0.1 Bind address (0.0.0.0 when K_SERVICE is set, i.e. Cloud Run)
MCP_CONFIG_PATH config/default.json Path to the OAuth config JSON file
MCP_NO_AUTH (not set) Set to "true" to disable all OAuth checks
MCP_DEV_TOKEN (not set) A static JWT to use for local testing (bypasses real OAuth flow)
MCP_LOG_BODY_LIMIT 4000 Max characters logged per request/response body
MCP_CORS_ORIGIN * CORS origin for Vercel/public deployments

Config File Structure

The config file (config/default.json or your own) controls everything about the OAuth integration:

{
  "name": "oauth-mcp-server",
  "version": "1.0.0",
  "baseUrl": "http://localhost:6000",

  "authorization": {
    "validation": {
      "mode": "jwt",
      "jwks": {
        "uri": "https://YOUR_DOMAIN/.well-known/jwks.json",
        "cacheTtlSeconds": 3600,
        "allowedAlgorithms": ["RS256"],
        "issuer": "https://YOUR_DOMAIN/",
        "audience": "http://localhost:6000"
      }
    },
    "resourceMetadata": {
      "authorizationServers": ["https://YOUR_DOMAIN"],
      "scopesSupported": ["mcp:read", "mcp:write", "mcp:admin"],
      "bearerMethodsSupported": ["header"],
      "resourceSupported": true,
      "resource": "http://localhost:6000"
    },
    "scopeMapping": {
      "scopes": {
        "mcp:read": {
          "tools": ["echo", "calculate", "current_time", "..."],
          "resources": ["config://app", "docs://*", "..."],
          "prompts": ["summarize", "code_review", "..."]
        }
      },
      "defaultPermissions": {
        "tools": ["echo", "current_time"],
        "resources": ["config://app", "docs://about"],
        "prompts": []
      }
    }
  },
  "capabilities": {
    "tools": { "listChanged": true },
    "resources": { "subscribe": true, "listChanged": true },
    "prompts": { "listChanged": true }
  },
  "instructions": "OAuth-protected MCP resource server. ..."
}

Tool/resource/prompt names in scopeMapping support glob patterns — "*" matches everything, "docs://*" matches all document resources.

Provider Presets

File Provider Validation
config/default.json Local dev JWKS (dev keys)
config/auth0.example.json Auth0 JWKS RS256
config/keycloak.example.json Keycloak JWKS RS256, ES256

HTTP API Reference

Method Path Auth Required Purpose
POST /mcp Yes (Bearer token) JSON-RPC requests
GET /mcp Session ID Server-Sent Events stream
DELETE /mcp Session ID Terminate a session
GET /health No Health check
GET /.well-known/oauth-protected-resource No RFC 9728 metadata
GET /.well-known/oauth-protected-resource/mcp No Path-specific metadata

Capabilities Inventory

Tools (18)

Category Tool Description Scope
Utility echo Text echo — connectivity check Public
calculate add, subtract, multiply, divide, power, sqrt Public
current_time Server time in any IANA timezone (structured output) Public
random_number Random integer in [min, max] range Public
summarize_list count, sum, min, max, mean, median, stddev Public
Data Ops search_documents Full-text search across 15+ articles mcp:read
query_analytics DAU, revenue, churn, regional data, feature usage mcp:read
create_document Create a new KB article mcp:write
update_document Update an existing KB article mcp:write
list_projects List/filter 12+ mock projects mcp:read
create_project Create a new project mcp:write
update_project Update project status/progress mcp:write
File System list_files Browse mock directory tree mcp:files:read
read_file Read mock file contents mcp:files:read
upload_file Create/overwrite a mock file mcp:files:write
Admin get_server_stats Uptime, requests, memory, auth info mcp:admin
reload_config Hot-reload OAuth config from disk mcp:admin
list_server_capabilities List all tools, resources, prompts mcp:admin

Resources (10)

URI Description Scope
config://app Server config and feature flags (JSON) Public
docs://about About this server (Markdown) Public
docs://articles/{id} KB article by ID mcp:read
docs://articles/recent 10 most recent articles mcp:read
analytics://dashboard Current analytics snapshot mcp:read
analytics://users/{region} Regional user/revenue data mcp:read
projects://{id} Project details by ID (listed) mcp:read
files://{path} Mock file content by path mcp:files:read
user://me Current user profile from token mcp:read
system://health Server health and auth info mcp:admin

Prompts (5)

Prompt Arguments Description Scope
summarize text, style Summarize text (bullet/paragraph/tweet/executive) mcp:read
code_review language, code, focus Structured code review with senior engineer persona mcp:read
brainstorm topic, count, perspective Guided brainstorming session mcp:read
data_explore dataset, question Data exploration with tool guidance mcp:read
refactor language, code, goal Structured refactoring with architect persona mcp:write

Using with Claude Desktop (stdio)

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "oauth-mcp-server": {
      "command": "node",
      "args": ["C:\\Users\\Shylendra\\git\\oauth-mcp-server\\dist\\stdio.js"]
    }
  }
}

The stdio transport does not use OAuth per the MCP spec — it runs in full-access mode. For OAuth, use the HTTP transport.


Testing

npm run test:smoke         # stdio transport — validates all 18 tools, 10 resources, 5 prompts
npm run test:vercel        # Vercel serverless handler

MCP Inspector

npm run inspect            # stdio transport

Or for HTTP: start the server, then open the Inspector and connect with Transport type: Streamable HTTP, URL: http://localhost:6000/mcp.


Project Layout

src/
├── server.ts                    # McpServer factory (auth-aware)
├── stdio.ts                     # stdio transport entry point
├── http.ts                      # Streamable HTTP entry point (OAuth)
├── banner.ts                    # Startup banner
├── logging.ts                   # Structured JSON logging
├── auth/
│   ├── index.ts                 # Public API surface
│   ├── types.ts                 # ValidatedToken, AuthContext, etc.
│   ├── token-validator.ts       # JWT/JWKS + introspection
│   ├── scope-resolver.ts        # OAuth scopes → MCP permissions
│   ├── discovery.ts             # RFC 9728 metadata + WWW-Authenticate
│   └── errors.ts                # 401/403/400 error responses
├── config/
│   ├── types.ts                 # Config type definitions
│   └── loader.ts                # Config loading + Zod validation
├── capabilities/
│   ├── tools/                   # 18 tools in 4 categories
│   ├── resources/index.ts       # 10 resources
│   └── prompts/index.ts         # 5 prompts
└── data/
    ├── users.ts                 # 8 mock users
    ├── documents.ts             # 15+ knowledge base articles
    ├── projects.ts              # 12+ mock projects
    ├── analytics.ts             # 30-day metrics, regional data
    └── files.ts                 # Mock directory tree

config/
├── default.json                 # Dev config (port 6000)
├── auth0.example.json           # Auth0 preset
└── keycloak.example.json        # Keycloak preset

api/
├── health.ts                    # Vercel health endpoint
└── mcp.ts                       # Vercel serverless OAuth handler

scripts/
├── smoke.mjs                    # stdio smoke test
├── smoke-http.mjs               # HTTP smoke test
├── smoke-vercel.ts              # Vercel smoke test
└── gen-jwks.mjs                 # Dev JWKS generator

Standards Compliance

  • MCP 2026-07-28 — Full spec: server/discover, per-request _meta, stateless auth
  • MCP 2025-11-25 — Backward compatible initialize handshake
  • OAuth 2.1 (draft-ietf-oauth-v2-1-13) — Resource server role
  • RFC 6750 — Bearer Token Usage
  • RFC 8414 — OAuth 2.0 Authorization Server Metadata
  • RFC 8707 — Resource Indicators for OAuth 2.0
  • RFC 9207 — Authorization Server Issuer Identification
  • RFC 9728 — OAuth 2.0 Protected Resource Metadata

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

官方
精选