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.
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:
initializehandshake)
Quick Start
Requires Node.js >= 18. All commands below are PowerShell (Windows). For macOS/Linux, replace
$env:VAR = "value"withVAR=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
- Go to Auth0 Dashboard → Applications → APIs → Create API
- Set Name:
MCP Server - Set Identifier:
http://localhost:6000(this becomes youraudience) - Set Signing Algorithm:
RS256 - 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
- Go to Applications → Create Application
- Name it
MCP Client - Choose Machine to Machine
- Select the
MCP ServerAPI - Grant it
mcp:readandmcp:writescopes - 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
authorizationServersand JWKS/issuer fields point to your OAuth provider (which doesn't change between local and prod). Only thebaseUrl,audience, andresourcefields 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
initializehandshake - 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
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.
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.
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.
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.