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
Visit Server

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

Recommended Servers

playwright-mcp

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.

Official
Featured
TypeScript
Audiense Insights MCP Server

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.

Official
Featured
Local
TypeScript
Magic Component Platform (MCP)

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.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

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.

Official
Featured
TypeScript
Kagi MCP Server

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.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

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.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured