Skip to main content
Glama
Shylendra

oauth-mcp-server

by Shylendra

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.


Related MCP server: MCP REST API Server

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

Install Server
F
license - not found
B
quality
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    D
    quality
    D
    maintenance
    A ready-to-use starter implementation of the Model Context Protocol (MCP) server that enables applications to provide standardized context for LLMs with sample resources, tools, and prompts.
    2
    178
    1
  • A
    license
    -
    quality
    D
    maintenance
    A self-hostable OAuth 2.0 server designed for the Model-Context-Protocol (MCP) that enables you to secure your MCP applications with a robust implementation you control.
    2,868
    112
    ISC
  • A
    license
    -
    quality
    D
    maintenance
    A comprehensive reference implementation demonstrating all features of the Model Context Protocol (MCP) specification, serving as documentation, learning resource, and testing tool for MCP implementations.
    1
    MIT

View all related MCP servers

Related MCP Connectors

  • Self-hosted federated MCP gateway: one OAuth 2.1 MCP server in front of N apps, user-level scopes.

  • MCP (Model Context Protocol) server for Appwrite

  • MCP server for verifying EUDI/Talao wallet data via OIDC4VP (pull) for AI agents.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Shylendra/oauth-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server