Unify auth and MCP servers into single modular codebase

- Merge auth-server and mcp-server into unified architecture under src/modules/
- Add configurable auth modes: internal (single process) and external (separate processes)
- Update example scripts to support OAuth discovery and both auth modes
- Consolidate OAuth documentation into comprehensive implementation guide
- Add configuration options for development vs production deployments
- Support both Redis-backed and in-memory session storage
This commit is contained in:
Basil Hosmer 2025-10-14 00:35:21 -04:00
commit bc3619c73c
No known key found for this signature in database
67 changed files with 3946 additions and 17945 deletions

38
.env.example Normal file
View file

@ -0,0 +1,38 @@
# Server Configuration
PORT=3232
BASE_URI=http://localhost:3232
# Auth Mode Configuration
# Options: 'internal' or 'external'
#
# - internal: Run auth server in-process (suitable for demo/development)
# `npm run dev:internal` starts the server in this mode.
# This mode is the default when AUTH_MODE is not set.
#
# - external: Use external auth server (production pattern)
# `npm run dev:external` starts the server in this mode,
# and also starts the demo auth server
#
AUTH_MODE=internal
# External Auth Server URL (required if AUTH_MODE=external,
# ignored if AUTH_MODE=internal)
#
# Examples:
# - Demo auth server running locally: http://localhost:3001
# - Auth0: https://your-tenant.auth0.com
# - Okta: https://your-domain.okta.com
#
AUTH_SERVER_URL=http://localhost:3001
# Redis Configuration (optional)
# If not provided, session management will use in-memory storage
# (sessions will be lost on server restart)
#
REDIS_URL=redis://localhost:6379
REDIS_TLS=0
# NODE_ENV controls Redis connection failure behavior:
# - development: Continue running if Redis connection fails (with warning)
# - production: Exit immediately if Redis connection fails
NODE_ENV=development

667
README.md
View file

@ -1,540 +1,255 @@
# MCP Feature Reference Server
A production-ready example of a Model Context Protocol (MCP) server with OAuth 2.0 authentication, demonstrating best practices for secure, scalable MCP deployments.
This repository provides a complete MCP server implementation that
* demonstrates all MCP protocol features (tools, resources, prompts, sampling)
* implements OAuth 2.0 authentication using the recommended [separate auth server](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#roles) architectural pattern
* serves as a learning resource and starting template for building your own MCP servers
## What is this?
This repository provides a complete implementation of an MCP server that:
- Shows how to add authentication to MCP servers using OAuth 2.0
- Demonstrates all MCP protocol features (tools, resources, prompts)
- Uses a scalable architecture suitable for production deployments
- Serves as a learning resource and starting template for your own MCP servers
The [Model Context Protocol](https://modelcontextprotocol.io) is an open standard that enables seamless integration between AI applications and external data sources, tools, and services.
The [Model Context Protocol](https://modelcontextprotocol.io) enables seamless integration between AI applications and external data sources, tools, and services.
## Table of Contents
- [Getting Started](#getting-started)
- [Quick Start](#quick-start)
- [Installation](#installation)
- [Testing](#testing)
- [Common Issues & Solutions](#common-issues--solutions)
- [Understanding the System](#understanding-the-system)
- [Features](#features)
- [Repository Structure](#repository-structure)
- [Authentication & Authorization](#authentication--authorization)
- [Configuration](#configuration)
- [Customizing for Your Use Case](#customizing-for-your-use-case)
- [Development & Operations](#development--operations)
- [Development](#development)
- [Monitoring & Debugging](#monitoring--debugging)
- [Reference](#reference)
- [API Reference](#api-reference)
- [Technical Details](#technical-details)
- [Security](#security)
- [External Resources](#external-resources)
- [Quick Start](#quick-start)
- [MCP Features](#mcp-features)
- [Development Setup](#development-setup)
- [Project Structure](#project-structure)
- [Documentation](#documentation)
- [Contributing](#contributing)
- [License](#license)
---
# Getting Started
- [License](#license)
## Quick Start
*For detailed installation instructions, see [Installation](#installation) below.*
Get the server running in 5 minutes:
To start exploring ASAP:
```bash
# 1. Prerequisites
brew install orbstack # macOS: Install OrbStack (skip if already installed)
orbctl start # macOS: Start OrbStack daemon
# OR install Docker Desktop and start it (Windows/Linux/macOS alternative)
# 2. Setup
# Clone and install
git clone https://github.com/modelcontextprotocol/example-remote-server.git
cd example-remote-server
npm install # Install dependencies for all workspaces
npm install
# 3. Start services
docker compose up -d # Start Redis
npm run dev # Start both auth-server and mcp-server
# Start the server with in-process auth and in-memory session management
npm run dev:internal
# 4. Test with Inspector
# In another terminal, run MCP Inspector
npx -y @modelcontextprotocol/inspector
# Connect to http://localhost:3232/mcp
# (Make sure to include the http:// prefix!)
# Inspector will open a browser window.
# Connect to http://localhost:3232/mcp to authenticate and explore server features
```
## Installation
The server is now running a lightweight config with everything bundled in a single process:
- authentication is handled by an in-process module, rather than a separate server
- sessions are stored in memory, rather than in Redis
Other configurations are available: see [Development Setup](#development-setup), below.
## MCP Features
This server implements the complete MCP specification:
- **[Tools](https://modelcontextprotocol.io/docs/concepts/tools)**: 7 example tools including echo, add, long-running operations, and LLM sampling
- **[Resources](https://modelcontextprotocol.io/docs/concepts/resources)**: 100+ example resources with pagination and subscription support
- **[Prompts](https://modelcontextprotocol.io/docs/concepts/prompts)**: Simple and complex prompts with argument support
- **[Sampling](https://modelcontextprotocol.io/docs/concepts/sampling)**: LLM interaction capabilities
- **Transports**: Both Streamable HTTP (recommended) and SSE (legacy)
## Development Setup
### Prerequisites
- Node.js >= 16
- npm or yarn
- Docker runtime (for Redis)
- TypeScript (installed automatically via npm install, required for building)
- Docker (optional, for Redis)
### Step 1: Install Docker Runtime
Choose one option:
### Running The Server
The codebase supports a number of configurations ranging from simple/exploratory to something closer to how a production deployment would look.
#### Configuration Options Overview
| | Development/Exploration | Productionesque |
|--------|----------------------|------------|
| **Auto-restart** | `npm run dev:*` <br> • Auto-restarts on file changes <br> • Verbose logging <br> • Source maps enabled | `npm run start:*` <br> • Requires build step first <br> • Optimized performance <br> • No auto-restart |
| **Auth Mode** | `internal` <br> • OAuth in same process <br> • Single port (3232) <br> • Easier to debug | `external` <br> • Separate auth server <br> • Multiple ports (3001 + 3232) <br> • Can point to commercial auth provider instead |
| **Session Storage** | In-memory <br> • No dependencies <br> • Sessions lost on restart <br> • Single instance only | Redis <br> • Requires Docker/Redis <br> • Sessions persist <br> • Multi-instance ready |
Server configuration is determined by environment variables. To set up a non-default configuration, copy [`.env.example`](.env.example) to `.env` and edit as desired, or pass non-defaults on the command line.
Some example commands for different configurations are listed below. See the [Authentication Config](#authentication-config) and [Session Management Config](#session-management-config) sections below for detailed instructions on changing those configurations.
**macOS (Recommended: OrbStack)**
```bash
brew install orbstack
# Start OrbStack daemon (required before using Docker commands)
orbctl start
# Or download from https://orbstack.dev/download
# Development mode - watches for file changes and auto-restarts
npm run dev:internal # Internal auth
# or
npm run dev:external # External auth
# Production mode - optimized build, no auto-restart
npm run build # Build TypeScript to JavaScript first
# then
npm run start:internal # Internal auth
# or
npm run start:external # External auth
# Redis-backed sessions
docker compose up -d # Start Redis first
# configure REDIS_URL or pass on command line - see Session Management Config below - e.g.
REDIS_URL=redis://localhost:6379 npm run dev:internal
# Sessions will now persist across restarts
# Verify Redis is being used
npm run dev:internal 2>&1 | grep -i redis
# Should show: "Redis client connected successfully" or similar
```
**Windows/Linux: Docker Desktop**
- Download from https://www.docker.com/products/docker-desktop
- Start Docker Desktop after installation
## Authentication Config
**Alternative: Local Redis Installation**
```bash
# macOS
brew install redis && brew services start redis
This repo implements the [separate auth server](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#roles) architecture pattern described in the MCP specification, in which the MCP server is the "resource server", and authorization functionality is hosted separately. (The architecture in which resource and authorization server functions are tightly integrated within the MCP server is **deprecated**, and is not demonstrated in this codebase.)
# Ubuntu/Debian
sudo apt-get install redis-server && sudo systemctl start redis
```
For convenience and simplicity during exploration, the server supports an **internal auth mode**, in which OAuth 2.0 endpoints are hosted in the same *process* as the MCP server. However, it remains architecturally separate from the MCP server itself: there is no entanglement of MCP and authorization functionality in the codebase. To run the server in this mode, use `npm run dev:internal`.
### Step 2: Clone and Install Dependencies
```bash
git clone https://github.com/modelcontextprotocol/example-remote-server.git
cd example-remote-server
npm install # Installs dependencies for all workspaces
```
**External auth mode** is the standard configuration in which the MCP server and authentication servers run as separate processes. A demonstration authorization server is provided in this repo, and you can also point to commercial providers like Auth0 or Okta by updating the relevant config options. To run the MCP server in external mode, use `npm run dev:external`: this command will also start the separate demo auth server.
### Step 3: Start Redis
```bash
# Ensure Docker/OrbStack is running first!
docker compose up -d
**Note:** choice of mode and OAuth server does **not** affect the MCP server's interaction with clients during authorization. It simply determines the authorization server endpoints returned in [Protected Resource Metadata](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-server-location).
# Verify Redis is running
docker compose ps
```
**Authentication Environment Variables:**
### Step 4: Start the Servers
```bash
# Start both servers concurrently
npm run dev
- `AUTH_MODE` - Sets the authentication mode:
- `internal` (default) - Auth endpoints run in-process with the MCP server
- `external` - Auth endpoints run on a separate server
# Or start them individually:
npm run dev:auth-server # Auth server on http://localhost:3001
npm run dev:mcp-server # MCP server on http://localhost:3232
```
- `AUTH_SERVER_URL` - URL of the external auth server (required when `AUTH_MODE=external`, ignored when `AUTH_MODE=internal`)
- Example for local demo: `http://localhost:3001`
- Example for Auth0: `https://your-tenant.auth0.com`
- Example for Okta: `https://your-domain.okta.com`
## Testing
## Session Management Config
### With MCP Inspector (Recommended)
By default, the server uses in-memory session storage for development and local single-session testing. This simplifies getting the server up and running for exploration, but confines sessions to a single server instance and destroys them on server restarts.
The easiest way to test the server:
For multi-instance testing and persistent sessions, the server also supports Redis-managed session storage.
**Setting up Redis:**
1. **Install Docker** (if not already installed):
- macOS: [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/)
- Windows: [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/)
- Linux: [Docker Engine](https://docs.docker.com/engine/install/)
2. **Start Redis** using Docker Compose:
```bash
docker compose up -d # Starts Redis in the background
```
To stop Redis later:
```bash
docker compose down
```
3. **Configure the server** to use Redis by setting environment variables:
**Session Storage Environment Variables:**
- `REDIS_URL` - Redis connection URL (optional)
- When set: Sessions are stored in Redis (persistent across restarts)
- When not set: Sessions use in-memory storage (lost on restart)
- Default: Not set (in-memory storage)
- Example: `redis://localhost:6379` (Redis default port)
- `REDIS_TLS` - Enable TLS for Redis connection
- Set to `1` or `true` to enable TLS
- Default: `0` (disabled)
- `REDIS_PASSWORD` - Redis password for authentication (if required)
- `NODE_ENV` - Controls Redis connection failure behavior:
- `development` (default) - Server continues with warning if Redis fails
- `production` - Server exits if Redis connection fails
Note: Docker container config can be found in `.devcontainer/docker-compose.yml`.
### Testing Features With MCP Inspector
As noted above, MCP Inspector is the recommended way to explore the server's capabilities:
```bash
# 1. Ensure servers are running (npm run dev)
# 2. Launch Inspector
# With server running
npx -y @modelcontextprotocol/inspector
# 3. Connect to http://localhost:3232/mcp
# 4. Complete OAuth flow in the Auth tab
# 5. Test tools, resources, and prompts interactively
# 1. Connect to http://localhost:3232/mcp (adjust port to match current config is needed)
# 2. Go through authorization steps
# 3. Explore OAuth authentication in the Auth tab
# 4. Test tools, resources, and prompts interactively
```
### With Example Scripts
### Example Scripts
The `examples/` directory contains runnable code demonstrating OAuth and MCP interactions:
The `examples/` directory contains scripts that interact with MCP endpoints directly, without use of SDK functionality. These can help build intuition for how the protocol works under the hood:
- `client.js` - Node.js client demonstrating OAuth and MCP operations
- `curl-examples.sh` - Shell script showing raw HTTP usage
- **`client.js`**: Complete Node.js example with OAuth flow and MCP operations
- **`curl-examples.sh`**: Shell script showing raw HTTP/curl usage
See [examples/README.md](examples/README.md) for detailed usage.
## Common Issues & Solutions
### "Token validation failed"
- **Cause**: Token expired or servers not communicating
- **Solution**:
- Ensure both servers are running (`npm run dev`)
- Check that Redis is running (`docker compose ps`)
- Verify the token hasn't expired (tokens last 7 days)
### "Cannot connect to MCP server" or "Connection Error - Check if your MCP server is running"
- **Cause**: Incorrect URL format or servers not running
- **Solution**:
- Use the full URL with http:// prefix: `http://localhost:3232/mcp`
- Ensure **both** auth and MCP servers are running (`npm run dev`)
### "Wrong endpoint for SSE transport" or SSE connection fails to `/mcp`
- **Cause**: Using SSE transport with Streamable HTTP endpoint
- **Solution**:
- For **SSE transport** (legacy): Use `http://localhost:3232/sse`
- For **Streamable HTTP** (recommended): Use `http://localhost:3232/mcp`
- In MCP Inspector: Match transport type with the correct endpoint
### "Cannot connect to Docker daemon"
- **Cause**: Docker/OrbStack not running
- **Solution**:
- macOS with OrbStack: `orbctl start` (verify with `orbctl status`)
- Windows/Linux/macOS with Docker Desktop: Start Docker Desktop application
### "Redis connection refused"
- **Cause**: Redis container not running
- **Solution**:
- Check Redis is running: `docker compose ps`
- If not running: `docker compose up -d`
- Ensure Docker/OrbStack is started first
### "Port already in use"
- **Cause**: Another process using port 3001 or 3232
- **Solution**:
- Check for existing processes: `lsof -i :3232` or `lsof -i :3001`
- Kill existing processes or change PORT in .env files
### "Authentication service unavailable" (HTTP 503)
- **Cause**: Auth server is not running or not reachable
- **What happens**: MCP server runs in degraded mode
- Splash page accessible with warning banner
- Health endpoint shows degraded status: `curl http://localhost:3232/health`
- Protected MCP endpoints return 503 with helpful error message
- **Solution**:
- Start the auth server: `npm run dev` (starts both servers)
- Or start manually: `cd auth-server && npm run dev`
- Restart the MCP server to retry connection
### "Authentication flow fails"
- **Cause**: Misconfiguration or servers not communicating
- **Solution**:
- Check server logs for error messages
- Ensure Redis is running: `docker compose ps`
- Verify .env configuration in both server directories
- Check that both servers are running
---
# Understanding the System
## Features
### MCP Protocol Features
- **[Tools](https://modelcontextprotocol.io/docs/concepts/tools)**: 7 demonstration tools including echo, add, long-running operations, LLM sampling, image handling, annotations, and resource references
- **[Resources](https://modelcontextprotocol.io/docs/concepts/resources)**: 100 example resources with pagination, templates, and subscription support
- **[Prompts](https://modelcontextprotocol.io/docs/concepts/prompts)**: Simple and complex prompts with argument support and resource embedding
- **[Sampling](https://modelcontextprotocol.io/docs/concepts/sampling)**: Integration with MCP sampling for LLM interactions
- **Completions**: Auto-completion support for prompt arguments
- **Logging**: Multi-level logging with configurable verbosity
- **Notifications**: Progress updates, resource updates, and stderr messages
### Transport & Infrastructure
- **[Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http)**: Full implementation with GET/POST/DELETE support
- **[SSE Transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#server-sent-events)**: Backwards-compatible Server-Sent Events
- **Redis Integration**: Pub/sub message routing and session state management
- **Session Management**: 5-minute TTL with automatic cleanup
- **Horizontal Scaling**: Any instance can handle any request
### Authentication & Security
- **[OAuth 2.0](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization)**: Complete authorization flow with PKCE support
- **Separate Auth Server**: Demonstrates integration with external OAuth providers
- **Token Introspection**: Validates tokens via [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662)
- **Session Ownership**: User isolation and access control
- **Security Headers**: CSP, HSTS, X-Frame-Options, and more
- **Bearer Token Auth**: Middleware for protected endpoints
## Repository Structure
This repository demonstrates a production-ready MCP deployment pattern with separate authorization and resource servers:
```
auth-server/ # OAuth 2.0 authorization server (demo only - replace in production)
└── src/ # Authorization endpoints and token management
mcp-server/ # MCP resource server (customize tools/resources/prompts)
└── src/ # MCP protocol implementation with external auth
scripts/ # Testing and deployment scripts
docs/ # Architecture and API documentation
examples/ # Example code and usage patterns
```
The architecture separates authentication concerns from MCP functionality, allowing you to integrate with commercial OAuth providers (Auth0, Okta, etc.).
## Authentication & Authorization
Per the [MCP Authorization specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization), this implementation demonstrates a production-ready pattern with separate authorization and resource servers.
### Why OAuth for MCP?
MCP servers often handle sensitive operations and data. This implementation shows how to:
- Authenticate users before granting access to MCP resources
- Integrate with existing identity providers (Auth0, Okta, Google)
- Implement user session isolation in multi-tenant environments
- Scale horizontally while maintaining security
### Authentication Flow
```mermaid
sequenceDiagram
participant User
participant Client as Your App
participant Auth as Auth Server (:3001)
participant MCP as MCP Server (:3232)
User->>Client: Wants to use MCP
Client->>Auth: 1. Register & OAuth flow
Auth->>User: 2. Login page
User->>Auth: 3. Authenticate
Auth->>Client: 4. Access token
Client->>MCP: 5. MCP request + token
MCP->>Auth: 6. Validate token
Auth->>MCP: 7. Token valid ✓
MCP->>Client: 8. MCP response
Client->>User: 9. Results
```
This architecture pattern:
- **Separates concerns**: Auth server handles OAuth, MCP server handles protocol
- **Enables integration**: Can replace auth server with Auth0, Okta, etc.
- **Scales independently**: Auth and MCP servers can scale based on their load
- **Follows standards**: Uses OAuth 2.0 and token introspection (RFC 7662)
### Mock Identity Provider
The `/mock-upstream-idp` endpoints simulate what a real identity provider (Google, GitHub, corporate SSO) would do. In production, users would be redirected to their actual identity provider for login. This mock implementation helps you test the complete flow locally without external dependencies.
For detailed OAuth flow analysis including data storage and TTLs, see [docs/oauth-flow.md](docs/oauth-flow.md).
For alternative patterns like embedded OAuth, see [docs/oauth-architecture-patterns.md](docs/oauth-architecture-patterns.md).
## Configuration
Each server has its own `.env` file:
**Auth Server** (`auth-server/.env`):
```bash
AUTH_SERVER_URL=http://localhost:3001 # Auth server URL
AUTH_SERVER_PORT=3001 # Auth server port
BASE_URI=http://localhost:3232 # MCP server URL (for redirects)
REDIS_URL=redis://localhost:6379 # Redis connection
```
**MCP Server** (`mcp-server/.env`):
```bash
BASE_URI=http://localhost:3232 # MCP server URL
PORT=3232 # MCP server port
AUTH_SERVER_URL=http://localhost:3001 # External auth server URL
REDIS_URL=redis://localhost:6379 # Redis connection
```
## Customizing for Your Use Case
This is a reference implementation with demo tools, resources, and prompts. To adapt it for production:
- **Replace MCP features:** See [Customization Guide](docs/customization-guide.md) for replacing demo functionality with your own
- **Integrate OAuth provider:** See [OAuth Architecture Patterns](docs/oauth-architecture-patterns.md) for production authentication setup
---
# Development & Operations
## Development
### Running Tests
```bash
# Start servers
npm run dev # Both servers concurrently
npm run dev:auth-server # Auth server only
npm run dev:mcp-server # MCP server only
npm run lint # Code linting
npm run typecheck # Type checking
npm test # Unit tests
npm run test:e2e # End-to-end tests
```
### Build & Production
```bash
npm run build # Build all workspaces
npm run start # Run both servers
npm run start:auth-server # Run auth server only
npm run start:mcp-server # Run MCP server only
## Project Structure
```
.
├── src/ # Source code
│ ├── index.ts # Server entry point
│ ├── config.ts # Configuration management
│ ├── interfaces/
│ │ └── auth-validator.ts # Clean auth/MCP boundary
│ ├── modules/
│ │ ├── auth/ # Demo OAuth 2.0 implementation
│ │ │ ├── auth/ # Core auth logic and providers
│ │ │ ├── handlers/ # Mock upstream IdP handler
│ │ │ ├── services/ # Auth and Redis-backed session services
│ │ │ ├── static/ # OAuth frontend assets
│ │ │ ├── index.ts # Auth module router
│ │ │ └── types.ts # Auth type definitions
│ │ ├── mcp/ # MCP protocol implementation
│ │ │ ├── handlers/ # Streamable HTTP and SSE handlers
│ │ │ ├── services/ # MCP core and Redis transport
│ │ │ ├── index.ts # MCP module router
│ │ │ └── types.ts # MCP type definitions
│ │ └── shared/ # Shared utilities
│ │ ├── logger.ts # Logging configuration
│ │ └── redis.ts # Redis client with mock fallback
│ └── static/ # Static web assets
├── examples/ # Example client implementations
│ ├── client.js # Node.js client with OAuth flow
│ └── curl-examples.sh # Shell script with curl examples
├── docs/ # Additional Documentation
├── tests/ # Test files
├── .env.example # Environment variable template
├── docker-compose.yml # Docker setup for Redis
├── package.json # Node.js dependencies
└── tsconfig.json # TypeScript configuration
```
### Testing & Quality
```bash
npm test # All unit tests
npm run lint # Lint all code
npm run typecheck # Typecheck all code
npm run test:e2e # End-to-end test
```
## Documentation
### Automated End-to-End Testing
Additional documentation can be found in the `docs/` directory:
The E2E script in `scripts/test-e2e.sh` verifies the complete OAuth flow and all MCP features:
- [OAuth Implementation](docs/oauth-implementation.md) - Complete OAuth 2.0 + PKCE guide with architecture, flows, and commercial provider integration
- [Session Ownership](docs/session-ownership.md) - Multi-user session isolation and Redis-backed ownership tracking
```bash
npm run test:e2e
```
### Other Resources
The script:
- Automatically manages server lifecycle
- Tests OAuth 2.0 + PKCE flow
- Verifies all MCP features (tools, resources, prompts)
- Validates token introspection between servers
- Tests session management and ownership
## Monitoring & Debugging
### Logging
Structured JSON logging with sanitized outputs:
- HTTP request/response logging
- Authentication events
- Session lifecycle events
- Redis operations
- Error tracking
### Redis Monitoring
```bash
# Monitor session ownership
redis-cli KEYS "session:*:owner"
# Watch real-time operations
redis-cli MONITOR | grep "session:"
# Check active sessions
redis-cli PUBSUB CHANNELS "mcp:shttp:toserver:*"
# Debug specific session
redis-cli GET "session:{sessionId}:owner"
```
### Debug Tools
- MCP Inspector for interactive debugging
- Comprehensive test suite
- Hot-reload development mode
- Source maps for debugging
- Redis monitoring commands
---
# Reference
## API Reference
For a complete listing of all endpoints provided by each server, including OAuth authorization endpoints, MCP resource endpoints, and demo identity provider endpoints, see [docs/endpoints.md](docs/endpoints.md).
## Technical Details
### Scalability Architecture
The server is designed for horizontal scaling using Redis as the backbone:
#### Session State Management
- **Redis Storage**: All session state stored in Redis
- **5-minute TTL**: Automatic session cleanup
- **Session Ownership**: User isolation via Redis keys
- **Stateless Servers**: Any instance can handle any request
#### Message Routing
- **Pub/Sub Channels**: Redis channels for message distribution
- **Message Buffering**: Reliable delivery for disconnected clients
- **Connection State**: Tracked via pub/sub subscription counts
- **Automatic Cleanup**: No explicit cleanup required
#### Redis Key Structure
##### MCP Session Keys
```
session:{sessionId}:owner # Session ownership
mcp:shttp:toserver:{sessionId} # Client→Server messages
mcp:shttp:toclient:{sessionId}:{requestId} # Server→Client responses
mcp:control:{sessionId} # Control messages
```
##### OAuth/Auth Keys
```
auth:client:{clientId} # OAuth client registrations
auth:pending:{authCode} # Pending authorizations
auth:installation:{accessToken} # Active MCP installations
auth:exch:{authCode} # Token exchanges
auth:refresh:{refreshToken} # Refresh tokens
```
Note: The `auth:` prefix ensures complete namespace isolation between auth and MCP functions.
### Transport Methods
#### Streamable HTTP (Recommended)
Modern [transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) supporting bidirectional communication over HTTP:
- Single endpoint for all operations
- Session management via headers
- Efficient message buffering
- Automatic reconnection support
#### Server-Sent Events (Legacy)
Backwards-compatible [transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#server-sent-events) using SSE:
- Separate endpoints for SSE streams and messages
- Session management via URL parameters
- Redis-backed message routing
- Real-time event delivery
## Security
### Implemented Security Measures
- **Authentication**: [OAuth 2.0](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) with bearer tokens
- **Token Validation**: Introspection via [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662)
- **Authorization**: User-based session ownership
- **Session Isolation**: Users can only access their own sessions
- **Security Headers**:
- Content Security Policy (CSP)
- Strict Transport Security (HSTS)
- X-Frame-Options
- X-Content-Type-Options
- **Input Validation**: Zod schemas for all inputs
- **Error Handling**: Sanitized error responses
### Security Best Practices
1. Always use HTTPS in production
2. Configure proper CORS origins
3. Use strong client secrets
4. Enable all security headers
5. Monitor session lifetimes
6. Implement rate limiting
7. Use structured logging
## External Resources
### MCP Documentation
- [Model Context Protocol Documentation](https://modelcontextprotocol.io)
- [MCP Authorization Specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)
- [MCP Specification](https://modelcontextprotocol.io/specification)
- [MCP Concepts](https://modelcontextprotocol.io/docs/concepts)
- [Tools](https://modelcontextprotocol.io/docs/concepts/tools)
- [Resources](https://modelcontextprotocol.io/docs/concepts/resources)
- [Prompts](https://modelcontextprotocol.io/docs/concepts/prompts)
- [Sampling](https://modelcontextprotocol.io/docs/concepts/sampling)
- [Transports](https://modelcontextprotocol.io/docs/concepts/transports)
- [TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
### OAuth 2.0 / 2.1 Resources
- [RFC 6749: OAuth 2.0 Framework](https://datatracker.ietf.org/doc/html/rfc6749)
- [RFC 7662: Token Introspection](https://datatracker.ietf.org/doc/html/rfc7662)
- [OAuth 2.1 Draft](https://oauth.net/2.1/) - Modern security best practices
- [OAuth.net: End User Authentication](https://oauth.net/articles/authentication/)
- [The Resource Server - OAuth 2.0 Simplified](https://www.oauth.com/oauth2-servers/the-resource-server/)
## Contributing
---
# Contributing
We welcome contributions!
### Development Workflow
1. Fork the repository
2. Create a feature branch
3. Implement your changes
4. Add tests for new functionality
5. Ensure all tests pass
6. Run linting and fix issues
7. Submit a pull request
### Code Style
- TypeScript with strict mode
- ESLint configuration included
- Comprehensive type definitions
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
MIT License - see [LICENSE](LICENSE) file for details.

View file

@ -1,123 +0,0 @@
# Auth Server - Demo OAuth 2.0 Provider
Demonstration OAuth 2.0 authorization server representing commercial providers (Auth0, Okta, Google OAuth).
## Purpose
Complete OAuth 2.1 server implementing:
- Client registration (Dynamic Client Registration)
- Authorization flow with PKCE
- Token issuance and refresh
- Token introspection (RFC 7662)
- User authentication (via demo upstream IDP)
In production, replace this server with a commercial OAuth provider.
## Quick Start
```bash
# From this directory:
npm run dev
# Server starts on http://localhost:3001
```
## Flow
```
MCP Client ──OAuth──> Auth Server ──tokens──> MCP Client ──MCP requests──> MCP Server
│
└──validate──> Auth Server /introspect
```
1. MCP Client initiates OAuth flow with this server
2. Server handles authorization and user authentication
3. Server issues access tokens
4. Client uses tokens to access MCP server
5. MCP server validates tokens by calling this server's `/introspect` endpoint
## Key Components
### `src/index.ts`
Main entry point. Sets up:
- OAuth endpoints via SDK's `mcpAuthRouter`
- Token introspection endpoint (`POST /introspect`)
- Mock upstream auth UI for demo
- Rate limiting and CORS
### `src/auth/provider.ts`
**FeatureReferenceAuthProvider** - Implements OAuth server logic:
- Client registration storage
- Authorization code generation
- PKCE challenge validation
- Token issuance and refresh
- Token verification
### `src/services/`
- **auth.ts**: Auth service wrappers
- **redis-auth.ts**: Redis operations for OAuth data
### `src/handlers/mock-upstream-idp.ts`
Mock upstream identity provider. Simulates user authentication that would be handled by corporate SSO or social login in production OAuth providers.
## OAuth Endpoints
```bash
# Discovery
GET /.well-known/oauth-authorization-server
# Client registration
POST /register
# Authorization
GET /authorize?client_id=...&redirect_uri=...&code_challenge=...
# Token exchange
POST /token
# Token introspection (called by MCP server)
POST /introspect
```
## Configuration
Environment variables in `.env`:
```bash
AUTH_SERVER_URL=http://localhost:3001
AUTH_SERVER_PORT=3001
BASE_URI=http://localhost:3232 # MCP server URL
REDIS_URL=redis://localhost:6379
```
## Testing
```bash
npm test # 37 unit tests
npm run lint # Lint code
npm run typecheck # Type checking
npm run build # Build to dist/
```
## Production Usage
This demo server should be replaced with a commercial OAuth provider in production.
See [OAuth Architecture Patterns](../docs/oauth-architecture-patterns.md#using-a-commercial-auth-provider) for detailed integration guidance.
**Supported providers:** Auth0, Okta, Azure AD, AWS Cognito, Google, GitHub, and any RFC 7662-compliant OAuth provider.
## Redis Data
This server stores in Redis:
- `auth:client:{clientId}` - OAuth client registrations (30 day TTL)
- `auth:pending:{code}` - Pending authorizations (10 min TTL)
- `auth:installation:{token}` - Active installations (7 day TTL)
- `auth:refresh:{refreshToken}` - Refresh token mappings (7 day TTL)
- `auth:exch:{code}` - Token exchanges (10 min TTL)
## Related Documentation
- [Main README](../README.md) - Complete project documentation
- [MCP Server README](../mcp-server/README.md) - How MCP server uses these tokens
- [OAuth Architecture Patterns](../docs/oauth-architecture-patterns.md) - OAuth integration options
- [OAuth Flow](../docs/oauth-flow.md) - Detailed OAuth flow analysis

View file

@ -1,19 +0,0 @@
// @ts-check
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
{
linterOptions: {
reportUnusedDisableDirectives: false,
},
rules: {
"@typescript-eslint/no-unused-vars": ["error",
{ "argsIgnorePattern": "^_" }
]
}
}
);

View file

@ -1,19 +0,0 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
export default {
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},
testPathIgnorePatterns: ['/node_modules/', '/dist/', '/scratch/'],
injectGlobals: true,
};

File diff suppressed because it is too large Load diff

View file

@ -1,43 +0,0 @@
{
"name": "mcp-server-feature-reference-external-auth-server",
"version": "0.1.0",
"description": "MCP Feature Reference Server - Standalone Authorization Server",
"type": "module",
"main": "dist/index.js",
"scripts": {
"start": "node dist/index.js",
"dev": "tsx watch --inspect src/index.ts",
"dev:break": "tsx --inspect-brk watch src/index.ts",
"build": "tsc && npm run copy-static",
"copy-static": "mkdir -p dist/static && cp -r src/static/* dist/static/",
"lint": "eslint src/",
"typecheck": "tsc --noEmit",
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",
"@redis/client": "^1.6.0",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express-rate-limit": "^8.0.1",
"raw-body": "^3.0.0"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
"@types/content-type": "^1.1.8",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.0",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"tsx": "^4.19.2",
"typescript": "^5.7.2",
"typescript-eslint": "^8.18.0"
},
"overrides": {
"@types/express": "^5.0.0",
"@types/express-serve-static-core": "^5.0.2"
}
}

View file

@ -1,33 +0,0 @@
import "dotenv/config";
/**
* Port for the MCP server to listen on
*/
export const PORT = Number(process.env.PORT) || 3232;
/**
* Base URI for the MCP server. Used for OAuth callbacks and metadata.
* Should match the port if specified separately.
*/
export const BASE_URI = process.env.BASE_URI || `http://localhost:${PORT}`;
// Validate PORT and BASE_URI consistency
const baseUrl = new URL(BASE_URI);
if (baseUrl.port && parseInt(baseUrl.port) !== PORT) {
console.warn(`Warning: BASE_URI port (${baseUrl.port}) doesn't match PORT (${PORT})`);
}
/**
* Redis connection URL
*/
export const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
/**
* Port for this authorization server
*/
export const AUTH_SERVER_PORT = parseInt(process.env.AUTH_SERVER_PORT || '3001');
/**
* URL where this authorization server is hosted
*/
export const AUTH_SERVER_URL = process.env.AUTH_SERVER_URL || `http://localhost:${AUTH_SERVER_PORT}`;

View file

@ -1,165 +0,0 @@
import express from 'express';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js';
import { FeatureReferenceAuthProvider } from './auth/provider.js';
import { handleMockUpstreamAuthorize, handleMockUpstreamCallback } from './handlers/mock-upstream-idp.js';
import { redisClient } from './redis.js';
import { logger } from './utils/logger.js';
import { AUTH_SERVER_PORT, AUTH_SERVER_URL, BASE_URI } from './config.js';
const app = express();
console.log('=====================================');
console.log('MCP Demonstration Authorization Server');
console.log('=====================================');
console.log('This standalone server demonstrates OAuth 2.0');
console.log('authorization for the MCP resource server');
console.log('');
console.log('This is for demonstration purposes only.');
console.log('In production, you would use a real OAuth provider');
console.log('like Auth0, Okta, Google, GitHub, etc.');
console.log('=====================================');
// CORS for Inspector and MCP server
app.use(cors({
origin: true,
credentials: true
}));
app.use(express.json());
app.use(logger.middleware());
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
mode: 'authorization-server',
endpoints: {
metadata: `${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`,
authorize: `${AUTH_SERVER_URL}/authorize`,
token: `${AUTH_SERVER_URL}/token`,
register: `${AUTH_SERVER_URL}/register`,
introspect: `${AUTH_SERVER_URL}/introspect`
}
});
});
// Create auth provider instance for reuse
const authProvider = new FeatureReferenceAuthProvider();
// OAuth endpoints via SDK's mcpAuthRouter
app.use(mcpAuthRouter({
provider: authProvider,
issuerUrl: new URL(AUTH_SERVER_URL),
tokenOptions: {
rateLimit: { windowMs: 5000, limit: 100 }
},
clientRegistrationOptions: {
rateLimit: { windowMs: 60000, limit: 10 }
}
}));
// Rate limiting for custom endpoints
const introspectRateLimit = rateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
limit: 100, // 100 introspections per 5 minutes
message: { error: 'too_many_requests', error_description: 'Token introspection rate limit exceeded' }
});
const mockUpstreamIdpRateLimit = rateLimit({
windowMs: 60 * 1000, // 1 minute
limit: 20, // 20 auth attempts per minute
message: { error: 'too_many_requests', error_description: 'Authentication rate limit exceeded' }
});
const staticFileRateLimit = rateLimit({
windowMs: 10 * 60 * 1000, // 10 minutes
limit: 25, // 25 requests per 10 minutes for static files
message: { error: 'too_many_requests', error_description: 'Static file rate limit exceeded' }
});
// Token introspection endpoint (RFC 7662)
app.post('/introspect', introspectRateLimit, express.urlencoded({ extended: false }), async (req, res) => {
try {
const { token } = req.body;
if (!token) {
return res.status(400).json({ error: 'invalid_request', error_description: 'Missing token parameter' });
}
// Verify the token using the auth provider
const authInfo = await authProvider.verifyAccessToken(token);
// Return RFC 7662 compliant response
res.json({
active: true,
client_id: authInfo.clientId,
scope: authInfo.scopes.join(' '),
exp: authInfo.expiresAt,
sub: authInfo.extra?.userId || 'unknown',
userId: authInfo.extra?.userId, // Custom field for our implementation
username: authInfo.extra?.username,
iss: AUTH_SERVER_URL,
aud: BASE_URI, // The resource server URL this token is intended for
token_type: 'Bearer'
});
} catch (error) {
logger.debug('Token introspection failed', { error: (error as Error).message });
// Return inactive token response (don't leak error details)
res.json({
active: false
});
}
});
// Fake upstream auth endpoints (for user authentication simulation)
app.get('/mock-upstream-idp/authorize', mockUpstreamIdpRateLimit, cors(), handleMockUpstreamAuthorize);
app.get('/mock-upstream-idp/callback', mockUpstreamIdpRateLimit, cors(), handleMockUpstreamCallback);
// Static assets (for auth page styling)
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
app.get('/mcp-logo.png', staticFileRateLimit, (req, res) => {
const logoPath = path.join(__dirname, 'static/mcp.png');
res.sendFile(logoPath);
});
// Connect to Redis (shared with MCP server in dev)
try {
await redisClient.connect();
logger.info('Connected to Redis', { url: redisClient.options?.url });
} catch (error) {
logger.error('Could not connect to Redis', error as Error);
process.exit(1);
}
app.listen(AUTH_SERVER_PORT, () => {
logger.info('Authorization server started', {
port: AUTH_SERVER_PORT,
url: AUTH_SERVER_URL,
endpoints: {
metadata: `${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`,
authorize: `${AUTH_SERVER_URL}/authorize`,
token: `${AUTH_SERVER_URL}/token`,
register: `${AUTH_SERVER_URL}/register`,
introspect: `${AUTH_SERVER_URL}/introspect`
}
});
console.log('');
console.log('🚀 Auth server ready! Test with:');
console.log(` curl ${AUTH_SERVER_URL}/health`);
console.log(` curl ${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`);
console.log('');
console.log('💡 To test with MCP server:');
console.log(' 1. Keep this server running');
console.log(' 2. In another terminal: cd ../mcp-server && npm run dev');
console.log(' 3. Connect Inspector to http://localhost:3232/mcp');
});

View file

@ -1,289 +0,0 @@
import { createClient, SetOptions } from "@redis/client";
import { logger } from "./utils/logger.js";
import { REDIS_URL } from "./config.js";
/**
* Describes the Redis primitives we use in this application, to be able to mock
* them in tests (so we don't need to actually hit Redis).
*/
export interface RedisClient {
get(key: string): Promise<string | null>;
set(key: string, value: string, options?: SetOptions): Promise<string | null>;
getDel(key: string): Promise<string | null>;
del(key: string): Promise<number>;
expire(key: string, seconds: number): Promise<boolean>;
lpush(key: string, ...values: string[]): Promise<number>;
lrange(key: string, start: number, stop: number): Promise<string[]>;
connect(): Promise<void>;
on(event: string, callback: (error: Error) => void): void;
options?: { url: string };
exists(key: string): Promise<boolean>;
numsub(key: string): Promise<number>;
/**
* Creates a pub/sub subscription. Returns a cleanup function to unsubscribe.
* Handles Redis client duplication and error handling internally.
*/
createSubscription(
channel: string,
onMessage: (message: string) => void,
onError: (error: Error) => void,
): Promise<() => Promise<void>>;
/**
* Publishes a message to a channel.
*/
publish(channel: string, message: string): Promise<void>;
}
export class RedisClientImpl implements RedisClient {
private redis = createClient({
url: REDIS_URL,
password: process.env.REDIS_PASSWORD,
socket: {
tls: process.env.REDIS_TLS === "1",
ca: process.env.REDIS_TLS_CA,
}
});
constructor() {
this.redis.on("error", (error) =>
logger.error("Redis client error", error as Error),
);
}
async numsub(key: string): Promise<number> {
const subs = await this.redis.pubSubNumSub(key);
return subs[key] || 0;
}
async get(key: string): Promise<string | null> {
return await this.redis.get(key);
}
async getDel(key: string): Promise<string | null> {
return await this.redis.getDel(key);
}
async set(key: string, value: string, options?: SetOptions): Promise<string | null> {
return await this.redis.set(
key,
value,
options,
);
}
async del(key: string): Promise<number> {
return await this.redis.del(key);
}
async expire(key: string, seconds: number): Promise<boolean> {
return await this.redis.expire(key, seconds);
}
async lpush(key: string, ...values: string[]): Promise<number> {
return await this.redis.lPush(key, values);
}
async lrange(key: string, start: number, stop: number): Promise<string[]> {
return await this.redis.lRange(key, start, stop);
}
async connect(): Promise<void> {
await this.redis.connect();
}
on(event: string, callback: (error: Error) => void): void {
this.redis.on(event, callback);
}
get options() {
return { url: REDIS_URL };
}
async createSubscription(
channel: string,
onMessage: (message: string) => void,
onError: (error: Error) => void,
): Promise<() => Promise<void>> {
const subscriber = this.redis.duplicate();
subscriber.on("error", (error) => {
onError(error);
});
await subscriber.connect();
await subscriber.subscribe(channel, (message) => {
onMessage(message);
});
return async () => {
await subscriber.disconnect();
};
}
async publish(channel: string, message: string): Promise<void> {
await this.redis.publish(channel, message);
}
async exists(key: string): Promise<boolean> {
const result = await this.redis.exists(key);
return result > 0;
}
}
// Export a mutable reference that can be swapped in tests
export let redisClient: RedisClient = new RedisClientImpl();
// Function to replace the Redis client (used in tests)
export function setRedisClient(client: RedisClient) {
redisClient = client;
}
export class MockRedisClient implements RedisClient {
options = { url: "redis://localhost:6379" };
private store = new Map<string, string>();
private lists = new Map<string, string[]>();
public subscribers = new Map<string, ((message: string) => void)[]>(); // Public for testing access
private errorCallbacks = new Map<string, ((error: Error) => void)[]>();
async get(key: string): Promise<string | null> {
return this.store.get(key) ?? null;
}
async getDel(key: string): Promise<string | null> {
const value = this.store.get(key) ?? null;
this.store.delete(key);
return value;
}
async set(key: string, value: string, options?: SetOptions): Promise<string | null> {
let oldValue: string | null = null;
if (options?.GET) {
oldValue = this.store.get(key) ?? null;
}
this.store.set(key, value);
return oldValue;
}
async del(key: string): Promise<number> {
let deleted = 0;
if (this.store.has(key)) {
this.store.delete(key);
deleted++;
}
if (this.lists.has(key)) {
this.lists.delete(key);
deleted++;
}
return deleted;
}
async expire(key: string, _seconds: number): Promise<boolean> {
// Mock implementation - just return true if key exists
return this.store.has(key) || this.lists.has(key);
}
async lpush(key: string, ...values: string[]): Promise<number> {
const list = this.lists.get(key) || [];
list.unshift(...values);
this.lists.set(key, list);
return list.length;
}
async lrange(key: string, start: number, stop: number): Promise<string[]> {
const list = this.lists.get(key) || [];
if (stop === -1) {
return list.slice(start);
}
return list.slice(start, stop + 1);
}
async connect(): Promise<void> {
// No-op in mock
}
on(event: string, callback: (error: Error) => void): void {
if (event === "error") {
const callbacks = this.errorCallbacks.get("global") ?? [];
callbacks.push(callback);
this.errorCallbacks.set("global", callbacks);
}
}
async createSubscription(
channel: string,
onMessage: (message: string) => void,
onError: (error: Error) => void,
): Promise<() => Promise<void>> {
const callbacks = this.subscribers.get(channel) ?? [];
callbacks.push(onMessage);
this.subscribers.set(channel, callbacks);
const errorCallbacks = this.errorCallbacks.get(channel) ?? [];
errorCallbacks.push(onError);
this.errorCallbacks.set(channel, errorCallbacks);
return async () => {
const callbacks = this.subscribers.get(channel) ?? [];
const index = callbacks.indexOf(onMessage);
if (index !== -1) {
callbacks.splice(index, 1);
}
this.subscribers.set(channel, callbacks);
if (onError) {
const errorCallbacks = this.errorCallbacks.get(channel) ?? [];
const errorIndex = errorCallbacks.indexOf(onError);
if (errorIndex !== -1) {
errorCallbacks.splice(errorIndex, 1);
}
this.errorCallbacks.set(channel, errorCallbacks);
}
};
}
async publish(channel: string, message: string): Promise<void> {
const callbacks = this.subscribers.get(channel) ?? [];
for (const callback of callbacks) {
try {
callback(message);
} catch (error) {
const errorCallbacks = this.errorCallbacks.get(channel) ?? [];
for (const errorCallback of errorCallbacks) {
errorCallback(error as Error);
}
}
}
}
async exists(key: string): Promise<boolean> {
return this.store.has(key) || this.lists.has(key);
}
async numsub(key: string): Promise<number> {
return (this.subscribers.get(key) || []).length;
}
clear() {
this.store.clear();
this.lists.clear();
this.subscribers.clear();
this.errorCallbacks.clear();
}
// Helper method for tests to simulate Redis errors
simulateError(error: Error, channel?: string): void {
if (channel) {
const callbacks = this.errorCallbacks.get(channel) ?? [];
for (const callback of callbacks) {
callback(error);
}
} else {
const callbacks = this.errorCallbacks.get("global") ?? [];
for (const callback of callbacks) {
callback(error);
}
}
}
}

View file

@ -1,97 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MCP Example Server</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div class="container">
<header>
<img src="/mcp-logo.png" alt="MCP Logo" class="logo">
<h1>MCP Example Server</h1>
</header>
<p class="tagline">
A comprehensive reference implementation of the Model Context Protocol (MCP) server
demonstrating all protocol features with full authentication support and horizontal scalability.
</p>
<div class="features">
<div class="feature-card">
<h3>Complete MCP Support</h3>
<p>All MCP features including tools, resources, prompts, sampling, completions, and logging with full protocol compliance.</p>
</div>
<div class="feature-card">
<h3>Multiple Transports</h3>
<p>Streamable HTTP (SHTTP) and Server-Sent Events (SSE) transports for flexible client integration.</p>
</div>
<div class="feature-card">
<h3>OAuth 2.0 Authentication</h3>
<p>Complete OAuth flow with PKCE support and a built-in fake provider for testing and development.</p>
</div>
<div class="feature-card">
<h3>Horizontal Scalability</h3>
<p>Redis-backed session management enables multi-instance deployments with automatic load distribution.</p>
</div>
<div class="feature-card">
<h3>7 Demo Tools</h3>
<p>Echo, add, long-running operations, LLM sampling, image handling, annotations, and resource references.</p>
</div>
<div class="feature-card">
<h3>100+ Resources</h3>
<p>Example resources with pagination, templates, subscriptions, and real-time update notifications.</p>
</div>
</div>
<div class="endpoints">
<h2>API Endpoints</h2>
<div class="endpoint-list">
<div class="endpoint">
<span class="method post">POST</span>
<span>/mcp - Initialize sessions or send messages (Streamable HTTP)</span>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<span>/mcp - Establish SSE streams (Streamable HTTP)</span>
</div>
<div class="endpoint">
<span class="method delete">DELETE</span>
<span>/mcp - Terminate sessions (Streamable HTTP)</span>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<span>/sse - Legacy SSE transport endpoint</span>
</div>
<div class="endpoint">
<span class="method post">POST</span>
<span>/message - Legacy message endpoint for SSE transport</span>
</div>
</div>
</div>
<div class="links">
<a href="https://github.com/modelcontextprotocol/example-remote-server" class="link-button">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
GitHub Repository
</a>
<a href="https://modelcontextprotocol.io" class="link-button secondary">
MCP Documentation
</a>
<a href="https://modelcontextprotocol.io/specification/2025-06-18" class="link-button secondary">
Protocol Specification
</a>
</div>
</div>
<footer>
<p>
Built by the <a href="https://modelcontextprotocol.io">Model Context Protocol</a> team
as a reference implementation for the MCP ecosystem.
</p>
</footer>
</body>
</html>

View file

@ -1,176 +0,0 @@
import { AsyncLocalStorage } from 'async_hooks';
import { Request, Response, NextFunction } from 'express';
// Severity levels as per Google Cloud Logging
export enum LogSeverity {
DEFAULT = 'DEFAULT',
DEBUG = 'DEBUG',
INFO = 'INFO',
NOTICE = 'NOTICE',
WARNING = 'WARNING',
ERROR = 'ERROR',
CRITICAL = 'CRITICAL',
ALERT = 'ALERT',
EMERGENCY = 'EMERGENCY'
}
interface LogContext {
trace?: string;
spanId?: string;
requestId?: string;
userAgent?: string;
method?: string;
path?: string;
[key: string]: string | undefined;
}
interface StructuredLogEntry {
severity: LogSeverity;
message: string;
timestamp: string;
'logging.googleapis.com/trace'?: string;
'logging.googleapis.com/spanId'?: string;
[key: string]: unknown;
}
class StructuredLogger {
private asyncLocalStorage = new AsyncLocalStorage<LogContext>();
private projectId: string | undefined;
constructor() {
// Get project ID from environment or metadata server
this.projectId = process.env.GOOGLE_CLOUD_PROJECT || process.env.GCP_PROJECT;
}
/**
* Run a function with a specific logging context
*/
runWithContext<T>(context: LogContext, fn: () => T): T {
return this.asyncLocalStorage.run(context, fn);
}
/**
* Extract trace context from Cloud Run request
*/
extractTraceContext(req: Request): LogContext {
const context: LogContext = {};
const traceHeader = req.header('X-Cloud-Trace-Context');
if (traceHeader && this.projectId) {
const [trace, spanId] = traceHeader.split('/');
context.trace = `projects/${this.projectId}/traces/${trace}`;
if (spanId) {
context.spanId = spanId.split(';')[0]; // Remove any trace flags
}
}
// Add other useful request context
context.requestId = req.header('X-Request-Id');
context.userAgent = req.header('User-Agent');
context.method = req.method;
context.path = req.path;
return context;
}
/**
* Create Express middleware for request context
*/
middleware() {
return (req: Request, res: Response, next: NextFunction) => {
const context = this.extractTraceContext(req);
this.runWithContext(context, () => {
next();
});
};
}
/**
* Log a structured message
*/
private log(severity: LogSeverity, message: string, metadata?: Record<string, unknown>) {
const context = this.asyncLocalStorage.getStore() || {};
const entry: StructuredLogEntry = {
severity,
message,
timestamp: new Date().toISOString(),
...metadata
};
// Add trace context if available
if (context.trace) {
entry['logging.googleapis.com/trace'] = context.trace;
}
if (context.spanId) {
entry['logging.googleapis.com/spanId'] = context.spanId;
}
// Add any other context fields
Object.keys(context).forEach(key => {
if (key !== 'trace' && key !== 'spanId') {
entry[`context.${key}`] = context[key];
}
});
// Output as JSON for Cloud Logging
console.log(JSON.stringify(entry));
}
// Convenience methods for different severity levels
debug(message: string, metadata?: Record<string, unknown>) {
this.log(LogSeverity.DEBUG, message, metadata);
}
info(message: string, metadata?: Record<string, unknown>) {
this.log(LogSeverity.INFO, message, metadata);
}
notice(message: string, metadata?: Record<string, unknown>) {
this.log(LogSeverity.NOTICE, message, metadata);
}
warning(message: string, metadata?: Record<string, unknown>) {
this.log(LogSeverity.WARNING, message, metadata);
}
error(message: string, error?: Error, metadata?: Record<string, unknown>) {
const errorMetadata = {
...metadata,
error: error ? {
name: error.name,
message: error.message,
stack: error.stack
} : undefined
};
this.log(LogSeverity.ERROR, message, errorMetadata);
}
critical(message: string, metadata?: Record<string, unknown>) {
this.log(LogSeverity.CRITICAL, message, metadata);
}
alert(message: string, metadata?: Record<string, unknown>) {
this.log(LogSeverity.ALERT, message, metadata);
}
emergency(message: string, metadata?: Record<string, unknown>) {
this.log(LogSeverity.EMERGENCY, message, metadata);
}
/**
* Add additional context to the current async context
*/
addContext(context: LogContext) {
const currentContext = this.asyncLocalStorage.getStore();
if (currentContext) {
Object.assign(currentContext, context);
}
}
}
// Export singleton instance
export const logger = new StructuredLogger();
// Re-export for convenience
export type { LogContext, StructuredLogEntry };

View file

@ -1,17 +0,0 @@
{
"compilerOptions": {
"target": "es2018",
"module": "Node16",
"moduleResolution": "Node16",
"sourceMap": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View file

@ -1,117 +0,0 @@
# Customization Guide
This reference implementation includes demo functionality to showcase MCP features. Here's how to adapt it for your own use case.
## Overview: What to Customize vs. Keep
**Replace with your own:**
- MCP tools, resources, and prompts (your business logic)
- Authentication provider (use commercial OAuth provider)
**Keep as-is (infrastructure):**
- Redis transport and session management
- HTTP handlers and routing
- Security middleware and rate limiting
- Logging infrastructure
---
## Customizing MCP Functionality
All MCP feature implementations live in **`mcp-server/src/services/mcp.ts`**. This is where you define what your server can do.
### Tools
Replace the 7 demo tools (echo, add, etc.) with your actual tools:
**Location:** `createMcpServer()` function, look for the `CallToolRequestSchema` handler
**What to change:**
- Tool definitions: name, description, input schema (using Zod)
- Tool execution logic: what happens when the tool is called
- Return format: text, images, or embedded resources
**Pattern:** Each tool validates input with a Zod schema and returns content in MCP format.
### Resources
Replace the 100 fake resources with your actual data sources:
**Location:** `createMcpServer()` function, resource-related handlers:
- `ListResourcesRequestSchema` - List available resources
- `ReadResourceRequestSchema` - Read specific resources
- `SubscribeRequestSchema` / `UnsubscribeRequestSchema` - Resource updates
**What to change:**
- Resource URIs and names
- Data fetching logic
- Pagination if needed
- Update notifications for subscribed resources
**Pattern:** Resources use URIs (e.g., `db://users/123`) and return content as text, JSON, or binary.
### Prompts
Replace the 3 demo prompts with useful prompts for your domain:
**Location:** `createMcpServer()` function, prompt-related handlers:
- `ListPromptsRequestSchema` - List available prompts
- `GetPromptRequestSchema` - Return prompt content
**What to change:**
- Prompt names and descriptions
- Prompt arguments and validation
- Prompt content and embedded resources
**Pattern:** Prompts can include dynamic arguments and reference resources.
---
## Customizing Authentication
**Replace the demo auth server** with a commercial OAuth provider (Auth0, Okta, Azure AD, AWS Cognito, Google, GitHub, etc.).
See [OAuth Architecture Patterns](oauth-architecture-patterns.md#using-a-commercial-auth-provider) for detailed integration steps.
**Do not repurpose the demo auth server** - it's designed for development/testing only. Commercial providers offer better security, reliability, and user management.
---
## Configuration
Update environment variables for your deployment:
**MCP Server** (`mcp-server/.env`):
```bash
BASE_URI=https://your-mcp-server.com
PORT=443
AUTH_SERVER_URL=https://your-tenant.auth0.com
REDIS_URL=redis://your-redis-server:6379
```
**Auth Server** (only if using the demo server for development):
```bash
AUTH_SERVER_URL=http://localhost:3001
BASE_URI=https://your-mcp-server.com
```
---
## Testing Your Customizations
1. **Unit tests:** Add tests for your tools/resources in `mcp-server/src/services/`
2. **Integration tests:** Test with MCP Inspector or client.js
3. **Build and lint:** Run `npm run build && npm run lint`
4. **End-to-end:** Use the examples to verify OAuth and MCP flows
---
## Next Steps
1. Fork/clone this repository
2. Replace tools, resources, and prompts in `mcp-server/src/services/mcp.ts`
3. Integrate with your OAuth provider (see OAuth Architecture Patterns doc)
4. Update environment variables for your deployment
5. Test thoroughly before deploying
For questions about the MCP protocol itself, see the [MCP specification](https://modelcontextprotocol.io/specification).

View file

@ -1,129 +0,0 @@
# Endpoint Reference
Complete listing of all endpoints provided by each server in the architecture.
## Auth Server
Standalone OAuth 2.0 authorization server that handles authentication and token management.
### OAuth Authorization Endpoints
Provided by `mcpAuthRouter` from MCP SDK:
- `GET /.well-known/oauth-authorization-server` - OAuth metadata discovery
- `POST /register` - Dynamic client registration
- `GET /authorize` - Authorization request (starts OAuth flow)
- `POST /token` - Token exchange (authorization code → tokens) and token refresh
- `POST /revoke` - Token revocation
### Token Introspection
Custom implementation for resource server token validation:
- `POST /introspect` - Token introspection ([RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662))
- Called by MCP server to validate tokens
- Returns token status, scopes, expiry, user info
- Protected endpoint (not public)
### Mock Upstream Identity Provider Endpoints
Local simulation of upstream IDP (would be external in production):
- `GET /mock-upstream-idp/authorize` - Mock user authentication page
- `GET /mock-upstream-idp/callback` - IDP callback handler (returns userId)
**Note**: In production, this would redirect to external providers like Auth0, Okta, Google, GitHub, etc. These endpoints simulate that functionality for demonstration purposes.
### Utility Endpoints
- `GET /health` - Health check (returns server status)
- `GET /mcp-logo.png` - Logo asset for auth pages
---
## MCP Server
MCP resource server that implements the Model Context Protocol with delegated authentication.
### OAuth Metadata (Read-Only)
- `GET /.well-known/oauth-authorization-server` - Returns metadata pointing to external auth server
- Tells clients to use auth server at :3001
- Returns 503 if auth server is unavailable (degraded mode)
- Read-only - no token issuance happens here
### MCP Resource Endpoints
#### Streamable HTTP Transport (Recommended)
- `GET /mcp` - Establish SSE stream for session
- `POST /mcp` - Initialize session or send messages
- `DELETE /mcp` - Terminate session
#### SSE Transport (Legacy)
- `GET /sse` - Establish SSE connection
- `POST /message` - Send messages to session
All MCP endpoints require `Authorization: Bearer <token>` header. Tokens are validated by calling the auth server's `/introspect` endpoint.
### Utility Endpoints
- `GET /health` - Health check endpoint
- Returns status: `healthy` (auth available) or `degraded` (auth unavailable)
- Shows operational status of MCP, auth, and Redis services
### Static Assets
- `GET /` - Splash page (HTML with degraded mode warning if auth unavailable)
- `GET /mcp-logo.png` - MCP logo
- `GET /styles.css` - Stylesheet
---
## Architecture Overview
| Endpoint Type | Auth Server | MCP Server |
|---------------|-------------|------------|
| OAuth authorization (`/authorize`, `/token`) | ✅ Full implementation | ❌ None (delegates to auth server) |
| Token introspection (`/introspect`) | ✅ Provides service | ❌ Consumes service |
| OAuth metadata discovery | ✅ Authoritative | ✅ Read-only redirect |
| Mock IDP (`/mock-upstream-idp`) | ✅ Yes | ❌ No |
| MCP resources (`/mcp`, `/sse`) | ❌ No | ✅ Yes |
---
## Headers
### Required Headers
**For MCP endpoints**:
- `Authorization: Bearer <access_token>` - OAuth access token (required)
- `Mcp-Session-Id: <session_id>` - Session identifier (for Streamable HTTP, after initialization)
**For OAuth endpoints**:
- `Content-Type: application/json` (for POST requests)
- `Content-Type: application/x-www-form-urlencoded` (for `/token` endpoint)
### Response Headers
**Streamable HTTP**:
- `Mcp-Session-Id` - Session ID (returned on initialization)
- `Content-Type: text/event-stream` - For SSE responses
**SSE Transport**:
- `Content-Type: text/event-stream`
- `Cache-Control: no-store, max-age=0`
- `Connection: keep-alive`
---
## Authentication Flow
1. **Client discovers auth server**: GET `/.well-known/oauth-authorization-server` from MCP server
2. **Client registers**: POST to auth server's `/register` endpoint
3. **User authorizes**: Redirected to auth server's `/authorize` endpoint
4. **Token exchange**: POST to auth server's `/token` endpoint
5. **Access MCP resources**: Use bearer token with MCP server endpoints
6. **Token validation**: MCP server validates tokens via auth server's `/introspect` endpoint
---
## References
- [MCP Transport Specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports)
- [MCP Authorization Specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)
- [RFC 7662: Token Introspection](https://datatracker.ietf.org/doc/html/rfc7662)
- [OAuth 2.0 Endpoints](https://www.oauth.com/oauth2-servers/definitions/)

View file

@ -1,151 +0,0 @@
# OAuth Architecture Patterns
This document describes different OAuth 2.0 architecture patterns for MCP servers, as specified in the [MCP Authorization specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization).
## Overview
Per the MCP specification, "the authorization server may be hosted with the resource server or as a separate entity." This leads to two primary patterns for implementing OAuth in MCP servers.
## Pattern 1: Separate Authorization Server (Recommended)
The current implementation demonstrates this pattern with separate authorization and resource servers. (Note: the authorization server is for demonstration purposes only.)
### Architecture
```
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ │ OAuth │ │ Token │ │
│ MCP Client │────────>│ Auth Server │<────────│ MCP Server │
│ │ │ (port 3001) │validate │ (port 3232) │
│ │<────────│ │ │ │
│ │ token │ Issues tokens │ │ Serves MCP │
│ │ └──────────────────┘ │ resources │
│ │────────────────────────────────────> │ │
│ │ MCP requests with token │ │
└─────────────┘ └─────────────────┘
```
### Components
1. **Demo Authorization Server** (port 3001)
- Handles OAuth 2.0 authorization flow
- Issues and manages tokens
- Provides token introspection endpoint (RFC 7662)
- Should be replaced with Auth0, Okta, Google OAuth, etc.
2. **MCP Resource Server** (port 3232)
- Serves MCP protocol resources
- Validates tokens via introspection
- Contains no OAuth authorization code
- Focuses purely on MCP functionality
### Using a Commercial Auth Provider
The demo auth server should be replaced with a commercial OAuth provider in production.
**Supported providers:**
- Auth0, Okta, Azure AD/Microsoft Entra
- AWS Cognito, Google Identity Platform
- GitHub OAuth
- Any RFC 7662-compliant OAuth provider
#### Integration Steps
1. **Configure provider**: Set up OAuth app in your provider
- Register your MCP server as a resource server
- Configure allowed redirect URIs
- Enable token introspection endpoint
2. **Update MCP server environment** (`mcp-server/.env`):
```bash
AUTH_SERVER_URL=https://your-tenant.auth0.com
# or https://your-domain.okta.com
# or https://login.microsoftonline.com/your-tenant
```
3. **Adjust token introspection** if needed (`mcp-server/src/auth/external-verifier.ts`):
```typescript
// Most providers use RFC 7662 standard format, but some may differ
const response = await fetch(`${this.authServerUrl}/oauth/introspect`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
// Some providers require authentication here
'Authorization': `Basic ${Buffer.from('client_id:client_secret').toString('base64')}`
},
body: `token=${token}`
});
```
4. **Update redirect URIs**: Configure your provider's allowed callbacks to match your deployment URLs
5. **Test the integration**: Verify the full OAuth flow with your provider
**Note on token introspection:** Most providers use the RFC 7662 standard format. If your provider uses a non-standard format, you may need to adjust the response parsing in `mcp-server/src/auth/external-verifier.ts`.
The MCP server code otherwise remains unchanged - it only needs to know where to validate tokens.
---
## Pattern 2: Embedded Authorization Server (Alternative/Legacy)
The MCP spec describes an alternative pattern where the OAuth server is embedded within the MCP server. However, this pattern is not recommended in the general case, and is not demonstrated in this codebase.
### Possible Use Cases
- **Enterprise Deployments**: Organizations needing control over OAuth token issuance while leveraging existing corporate identity infrastructure (LDAP, Active Directory, SAML)
- **Proof of Concepts**: Quick prototypes and demonstrations
- **Small Deployments**: Internal tools with simple auth needs
- **Isolated Systems**: Air-gapped environments without external connectivity
- **Custom Auth Requirements**: Specialized authentication needs not met by standard providers
---
## Comparison
| Aspect | Separate Servers | Embedded Server |
|--------|------------------|-----------------|
| **Architecture** | 2+ servers | 1 server |
| **OAuth endpoints** | On auth server | On MCP server |
| **Token validation** | Remote (introspection) | In-process |
| **Upstream IDP support** | Via auth server | Direct integration |
| **Production readiness** | ✅ Recommended | ⚠️ Limited use cases |
---
## Best Practices
1. **Use separate servers** for production deployments
2. **Implement token caching** to reduce introspection calls
3. **Use PKCE** for all OAuth flows (prevents code interception)
4. **Validate token audience** to prevent token substitution
5. **Implement proper session management** with Redis or similar
6. **Use HTTPS** in production for all endpoints
7. **Monitor token expiration** and implement refresh logic
8. **Log authentication events** for security auditing
9. **Consider upstream delegation** for enterprise deployments
10. **Document auth flow** for your specific implementation
---
## Testing Considerations
When implementing either pattern, ensure comprehensive testing:
- **Unit tests**: Test OAuth flows, token validation, session management
- **Integration tests**: Test auth server and MCP server interaction
- **E2E tests**: Test complete user flows from authorization to resource access
- **Security tests**: Test PKCE validation, token expiration, session isolation
- **Performance tests**: Test token validation caching, concurrent sessions
---
## References
- [MCP Authorization Specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)
- [RFC 6749: OAuth 2.0 Framework](https://datatracker.ietf.org/doc/html/rfc6749)
- [RFC 7662: Token Introspection](https://datatracker.ietf.org/doc/html/rfc7662)
- [RFC 7636: PKCE](https://datatracker.ietf.org/doc/html/rfc7636)
- [OAuth 2.0 Security Best Practices](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics)
- [OAuth 2.1 Draft](https://oauth.net/2.1/) - Modern security requirements
- [The OAuth 2.0 Authorization Framework in Practice](https://www.oauth.com/)

View file

@ -1,202 +0,0 @@
# OAuth 2.0 + PKCE Flow Analysis
This document details the complete OAuth 2.0 authorization code flow with PKCE as implemented in this reference server with separate authorization and resource servers.
## Flow Overview
The server implements OAuth 2.1 with PKCE (Proof Key for Code Exchange) for secure authorization. The architecture separates the authorization server (port 3001) from the MCP resource server (port 3232).
## 1. Client Registration
**Purpose**: Register the application with the OAuth server (happens once during app setup).
**Flow**:
```
App → Auth Server: POST /register {"client_name": "...", "redirect_uris": [...]}
Auth Server → App: {"client_id": "XYZ", "client_secret": "ABC", ...}
```
**Storage**:
- Redis key: `auth:client:{clientId}`
- Expiry: 30 days (long-lived app credentials)
- Server: Auth Server (port 3001)
---
## 2. Authorization Request
**Purpose**: User initiates connection to MCP server.
**Flow**:
```
App → Auth Server: GET /authorize?
client_id=XYZ
&redirect_uri=http://localhost:3000/callback
&code_challenge=<SHA256 of verifier>
&code_challenge_method=S256
&state=<CSRF token>
Auth Server: Saves pending authorization, shows authorization page
```
**Storage**:
- Redis key: `auth:pending:{authCode}`
- Expiry: 10 minutes (short-lived temporary state)
- Data: redirect_uri, code_challenge, code_challenge_method, client_id, state
- Server: Auth Server (port 3001)
---
## 3. User Authentication & Authorization
**Purpose**: Authenticate the user and obtain consent.
**Flow**:
```
Auth Server → User: Shows auth page with "Continue to Authentication" button
User → Auth Server: Clicks button
Auth Server → Upstream IDP: Redirects to /mock-upstream-idp/authorize
Upstream IDP → User: Shows user selection UI
User → Upstream IDP: Selects/creates user ID
Upstream IDP → Auth Server: Redirects to /mock-upstream-idp/callback?userId=X
Auth Server: Validates user, issues authorization code
Auth Server → App: Redirects to app's redirect_uri with code
```
**Note**: In production, the upstream IDP would be an external provider (Auth0, Okta, Google, GitHub, etc.). The `/mock-upstream-idp/*` endpoints simulate this for demonstration purposes.
---
## 4. Authorization Code Exchange
**Purpose**: Exchange authorization code for access and refresh tokens.
**Flow**:
```
App → Auth Server: POST /token
grant_type=authorization_code
&client_id=XYZ
&client_secret=ABC
&code=<authorization code>
&redirect_uri=http://localhost:3000/callback
&code_verifier=<original random string>
Auth Server:
1. Validates code_verifier matches code_challenge (PKCE)
2. Validates redirect_uri matches original
3. Validates client credentials
4. Issues tokens
Auth Server → App:
{
"access_token": "...",
"refresh_token": "...",
"expires_in": 604800,
"token_type": "Bearer"
}
```
**Storage**:
- `auth:exch:{authCode}` - Token exchange record (prevents replay attacks)
- `auth:installation:{accessToken}` - Active MCP installation
- `auth:refresh:{refreshToken}` - Refresh token mapping
- Expiry: 10 minutes for exchange record, 7 days for installation
- Server: Auth Server (port 3001)
---
## 5. Using Access Tokens
**Purpose**: Access MCP resources with the token.
**Flow**:
```
App → MCP Server: POST /mcp
Authorization: Bearer <access_token>
Mcp-Session-Id: <session_id>
{MCP request}
MCP Server → Auth Server: POST /introspect
token=<access_token>
Auth Server → MCP Server:
{
"active": true,
"userId": "...",
"exp": ...,
"aud": "http://localhost:3232"
}
MCP Server: Validates audience, serves MCP resource
```
**Token Validation Process**:
1. MCP Server receives request with bearer token
2. Calls Auth Server's `/introspect` endpoint (RFC 7662)
3. Auth Server validates token and returns metadata
4. MCP Server validates audience matches `BASE_URI`
5. Request proceeds with authenticated user context
This separation allows the Auth Server to be replaced with commercial providers (Auth0, Okta) while keeping the MCP server unchanged.
---
## 6. Token Refresh
**Purpose**: Obtain new access token when current one expires.
**Flow**:
```
App → Auth Server: POST /token
grant_type=refresh_token
&refresh_token=<refresh token>
&client_id=XYZ
&client_secret=ABC
Auth Server:
1. Validates refresh_token
2. Issues new access_token (and optionally new refresh_token)
3. Updates installation record
Auth Server → App: {"access_token": "...", ...}
```
**Storage**:
- Reads: `auth:refresh:{refreshToken}` → gets access token
- Reads: `auth:installation:{accessToken}` → gets installation
- Writes: New `auth:installation:{newAccessToken}`
- Writes: New `auth:refresh:{newRefreshToken}`
- Expiry: 7 days
- Server: Auth Server (port 3001)
---
## Data Lifecycle Hierarchy
**Timeline (shortest to longest expiry):**
1. **OAuth flow state** (10 minutes) - `auth:pending`, `auth:exch`
2. **User sessions** (7 days) - `auth:installation`, `auth:refresh`
3. **Client credentials** (30 days) - `auth:client`
This hierarchy ensures each layer outlives the layers it supports.
---
## Security: PKCE (Proof Key for Code Exchange)
PKCE prevents authorization code interception attacks:
1. **Authorization request**: Client sends `code_challenge` (SHA256 hash of random string)
2. **Token exchange**: Client must provide original `code_verifier`
3. **Server validates**: SHA256(code_verifier) must equal stored code_challenge
This ensures only the client that initiated the flow can exchange the code, even if the code is intercepted.
---
## References
- [RFC 6749: OAuth 2.0 Framework](https://datatracker.ietf.org/doc/html/rfc6749)
- [RFC 7636: PKCE](https://datatracker.ietf.org/doc/html/rfc7636)
- [RFC 7662: Token Introspection](https://datatracker.ietf.org/doc/html/rfc7662)
- [MCP Authorization Spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)

View file

@ -0,0 +1,501 @@
# OAuth Implementation Guide
This comprehensive guide covers the OAuth 2.0 + PKCE implementation in the MCP Feature Reference Server, including architecture patterns, detailed flow analysis, and integration guidance.
**Quick Start**: If you're new to this codebase, start with [Implementation Modes](#implementation-modes) to understand the difference between internal (development) and external (production) auth modes.
## Table of Contents
- [Architecture Overview](#architecture-overview)
- [Implementation Modes](#implementation-modes)
- [OAuth Flow Details](#oauth-flow-details)
- [Commercial Provider Integration](#commercial-provider-integration)
- [Error Handling](#error-handling)
- [Testing & Troubleshooting](#testing--troubleshooting)
- [Best Practices](#best-practices)
---
## Architecture Overview
This implementation uses the **separate authorization server pattern** as recommended by the [MCP Authorization specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization). The authorization server is architecturally separate from the MCP resource server, ensuring clean separation of concerns and enabling integration with commercial OAuth providers.
### Key Principles
1. **Separation of Concerns**: OAuth logic is isolated from MCP functionality
2. **Token-Based Security**: All MCP requests require valid bearer tokens
3. **PKCE Protection**: Prevents authorization code interception attacks
4. **Flexible Deployment**: Supports both development and production configurations
---
## Implementation Modes
The server supports two modes while maintaining the same architectural pattern:
### Internal Mode (Development)
In internal mode, OAuth endpoints run in the same process as the MCP server for convenience during development and exploration.
**Configuration:**
```bash
AUTH_MODE=internal # or leave unset (default)
# Server runs on port 3232
# OAuth and MCP endpoints share the same port
```
**Architecture:**
```
┌─────────────┐ ┌───────────────────────────────────────┐
│ │ OAuth │ Single Process (port 3232) │
│ MCP Client │────────>│ ┌──────────────┐ ┌──────────────┐ │
│ │ │ │ Auth Module │ │ MCP Module │ │
│ │<────────│ │ │ │ │ │
│ │ token │ │Issues tokens │────│ Serves MCP │ │
│ │ │ └──────────────┘ └──────────────┘ │
│ │────────>│ MCP requests with token │
└─────────────┘ └───────────────────────────────────────┘
```
### External Mode (Production)
In external mode, the authorization server runs as a separate process, following production best practices.
**Configuration:**
```bash
AUTH_MODE=external
AUTH_SERVER_URL=http://localhost:3001 # or commercial provider URL
# Auth server on port 3001
# MCP server on port 3232
```
**Architecture:**
```
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ │ OAuth │ │ Token │ │
│ MCP Client │────────>│ Auth Server │<────────│ MCP Server │
│ │ │ (port 3001) │validate │ (port 3232) │
│ │<────────│ │ │ │
│ │ token │ Issues tokens │ │ Serves MCP │
│ │ └──────────────────┘ │ resources │
│ │────────────────────────────────────> │ │
│ │ MCP requests with token │ │
└─────────────┘ └─────────────────┘
```
### Mode Comparison
| Aspect | Internal Mode | External Mode |
|--------|--------------|---------------|
| **Process Architecture** | Single process | Multiple processes |
| **Port Usage** | One port (3232) | Two ports (3001 + 3232) |
| **OAuth Endpoints** | Same port as MCP | Different port/server |
| **Token Validation** | In-process call | HTTP introspection |
| **Production Ready** | Development only | ✅ Production recommended |
| **Commercial Auth Providers** | Not applicable | ✅ Supported |
---
## OAuth Flow Details
The implementation follows OAuth 2.0 with PKCE (RFC 7636) for secure authorization.
### 1. Client Registration
**Purpose**: Register the application with the OAuth server (one-time setup)
```http
POST /register
Content-Type: application/json
{
"client_name": "My MCP Client",
"redirect_uris": ["http://localhost:3000/callback"]
}
```
**Response:**
```json
{
"client_id": "abc123",
"client_secret": "secret456",
"client_id_issued_at": 1234567890,
"client_secret_expires_at": 0
}
```
**Storage**: Redis key `auth:client:{clientId}` (30-day expiry)
### 2. Authorization Request
**Purpose**: Initiate OAuth flow with PKCE
```http
GET /authorize?
client_id=abc123&
redirect_uri=http://localhost:3000/callback&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256&
state=xyz789
```
**PKCE Security**:
- Client generates random `code_verifier` (43-128 characters)
- Sends SHA256 hash as `code_challenge`
- Must provide original verifier during token exchange
**Storage**: Redis key `auth:pending:{authCode}` (10-minute expiry)
### 3. User Authentication
The auth server authenticates the user and obtains consent:
1. Shows authorization page
2. User authenticates (via upstream IDP in production)
3. Issues authorization code
4. Redirects to client's `redirect_uri` with code
### 4. Token Exchange
**Purpose**: Exchange authorization code for tokens
```http
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
client_id=abc123&
client_secret=secret456&
code=auth_code_here&
redirect_uri=http://localhost:3000/callback&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
```
**PKCE Validation**:
```javascript
// Server verifies: SHA256(code_verifier) === stored code_challenge
const challenge = base64url(sha256(code_verifier));
if (challenge !== stored_code_challenge) {
throw new Error('Invalid PKCE verifier');
}
```
**Response:**
```json
{
"access_token": "eyJhbGc...",
"refresh_token": "refresh_xyz",
"token_type": "Bearer",
"expires_in": 604800
}
```
### 5. Using Access Tokens
**Purpose**: Access MCP resources with bearer token
```http
POST /mcp
Authorization: Bearer eyJhbGc...
Mcp-Session-Id: session-123
{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 1
}
```
**Token Validation Process**:
1. MCP server extracts bearer token
2. Calls auth server's `/introspect` endpoint
3. Validates token is active and not expired
4. Extracts user ID from `sub` claim
5. Processes MCP request with user context
### 6. Token Refresh
**Purpose**: Obtain new access token when current expires
```http
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=refresh_xyz&
client_id=abc123&
client_secret=secret456
```
**Response**: New access token (and optionally new refresh token)
---
## Commercial Provider Integration
The demo auth server should be replaced with a commercial OAuth provider in production.
### Supported Providers
- **Auth0** - Full OAuth 2.0 + OIDC support
- **Okta** - Enterprise identity management
- **Azure AD/Microsoft Entra** - Microsoft identity platform
- **AWS Cognito** - AWS managed authentication
- **Google Identity Platform** - Google OAuth
- **GitHub OAuth** - Developer-friendly OAuth
- Any RFC 7662-compliant OAuth provider
### Integration Steps
#### 1. Configure the Provider
In your OAuth provider's dashboard:
- Create new OAuth application/client
- Set redirect URIs (e.g., `http://localhost:3000/callback`)
- Enable token introspection endpoint (if required)
- Note client ID and secret
#### 2. Update Server Configuration
```bash
# .env file
AUTH_MODE=external
AUTH_SERVER_URL=https://your-tenant.auth0.com
# For Okta: https://your-domain.okta.com
# For Azure: https://login.microsoftonline.com/your-tenant
```
#### 3. Adjust Token Introspection (if needed)
Most providers follow RFC 7662, but some require authentication. Edit `src/interfaces/auth-validator.ts`:
```typescript
// In ExternalTokenValidator.introspect() method
const response = await fetch(`${this.authServerUrl}/oauth/introspect`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
// Some providers require authentication
'Authorization': `Basic ${Buffer.from('client_id:client_secret').toString('base64')}`
},
body: `token=${encodeURIComponent(token)}`
});
```
#### 4. Provider-Specific Examples
**Auth0:**
```bash
AUTH_SERVER_URL=https://your-tenant.auth0.com
# Introspection endpoint: /oauth/token_info
```
**Okta:**
```bash
AUTH_SERVER_URL=https://your-domain.okta.com/oauth2/default
# Introspection endpoint: /v1/introspect
```
**Azure AD:**
```bash
AUTH_SERVER_URL=https://login.microsoftonline.com/your-tenant-id
# May require additional configuration for introspection
```
---
## Error Handling
### Common OAuth Errors
| Error | Cause | Solution |
|-------|-------|----------|
| `invalid_client` | Wrong client credentials | Verify client_id and client_secret |
| `invalid_grant` | Expired/invalid auth code | Ensure code is used within 10 minutes |
| `invalid_request` | Missing required parameters | Check all OAuth parameters are provided |
| `unauthorized_client` | Client not authorized for grant type | Verify client registration settings |
| `invalid_token` | Token expired or revoked | Refresh token or re-authenticate |
### Token Validation Errors
```typescript
// Token expired
if (result.exp && result.exp < Date.now() / 1000) {
throw new InvalidTokenError('Token has expired');
}
// Token not active
if (!result.active) {
throw new InvalidTokenError('Token is not active');
}
// Wrong audience
if (result.aud !== expectedAudience) {
throw new InvalidTokenError('Token audience mismatch');
}
```
### Error Response Format
```json
{
"error": "invalid_token",
"error_description": "The access token expired",
"error_uri": "https://tools.ietf.org/html/rfc6750#section-3.1"
}
```
---
## Testing & Troubleshooting
### Testing OAuth Flows
```bash
# Test internal mode
npm run dev:internal
npx @modelcontextprotocol/inspector
# Connect to http://localhost:3232/mcp
# Test external mode
npm run dev:external
npx @modelcontextprotocol/inspector
# Connect to http://localhost:3232/mcp
# Run automated tests
npm test -- --testNamePattern="OAuth"
npm run test:e2e
```
### Common Issues and Solutions
**Issue: "Cannot connect to auth server"**
- Check `AUTH_MODE` and `AUTH_SERVER_URL` in `.env`
- Verify auth server is running (external mode)
- Check network connectivity to auth server
**Issue: "Token validation failed"**
- Verify tokens haven't expired (7-day default)
- Check Redis connection if using Redis storage
- Ensure auth server introspection endpoint is accessible
**Issue: "PKCE validation failed"**
- Ensure code_verifier is 43-128 characters
- Verify SHA256 hashing is correct
- Check code_challenge_method is 'S256'
**Issue: "Session not found"**
- Verify Redis is running if configured
- Check session hasn't expired
- Ensure same user is accessing the session
### Debug Logging
Enable detailed logging for troubleshooting:
```bash
# Enable debug logs
DEBUG=* npm run dev:internal
# Check auth flow metadata
curl -v http://localhost:3232/.well-known/oauth-authorization-server
# Test introspection (external mode only, on auth server)
# Note: In internal mode, introspection is handled internally
curl -X POST http://localhost:3001/introspect \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=YOUR_TOKEN"
```
### Monitoring Token Usage
```bash
# Monitor Redis for token operations (when Redis is configured)
redis-cli MONITOR | grep auth:
# Check active sessions
redis-cli KEYS "auth:installation:*"
# Check refresh tokens
redis-cli KEYS "auth:refresh:*"
```
---
## Best Practices
### Security
1. **Always use HTTPS in production** - Prevents token interception
2. **Implement PKCE for all flows** - Required by OAuth 2.1
3. **Validate token audience** - Prevents token substitution attacks
4. **Use short token expiries** - Limits exposure window
5. **Rotate refresh tokens** - Issue new refresh token on use
6. **Log authentication events** - For security auditing
7. **Rate limit auth endpoints** - Prevent brute force attacks
### Performance
1. **Cache introspection results** - Reduce auth server load
2. **Use connection pooling** - For Redis and HTTP connections
3. **Implement token refresh ahead of expiry** - Prevent interruptions
4. **Batch token validations** - When processing multiple requests
5. **Monitor auth server latency** - Set appropriate timeouts
### Development
1. **Use internal mode for development** - Faster iteration
2. **Test with external mode before production** - Catch integration issues
3. **Implement comprehensive error handling** - Better debugging
4. **Document your OAuth configuration** - For team members
5. **Use environment variables** - Never hardcode credentials
### Production Deployment
1. **Use commercial OAuth provider** - Better security and reliability
2. **Enable Redis for sessions** - Support multiple instances
3. **Implement health checks** - Monitor auth availability
4. **Set up monitoring and alerting** - Track auth failures
5. **Plan for token migration** - When changing providers
6. **Document disaster recovery** - Auth server failure procedures
---
## Data Lifecycle
### Storage Hierarchy
When Redis is configured, the following data is stored with automatic expiry:
| Data Type | Redis Key Pattern | Default Expiry | Purpose |
|-----------|------------------|----------------|---------|
| OAuth flow state | `auth:pending:{code}` | 10 minutes | Temporary auth state |
| Token exchange | `auth:exch:{code}` | 10 minutes | Prevent replay attacks |
| User sessions | `auth:installation:{token}` | 7 days | Active sessions |
| Refresh tokens | `auth:refresh:{token}` | 7 days | Token refresh |
| Client credentials | `auth:client:{id}` | 30 days | App registration |
**Note**: When Redis is not configured (in-memory storage), all data is lost on server restart.
### Cleanup
Expired data is automatically cleaned up by Redis TTL. For manual cleanup:
```bash
# Remove all auth data (CAUTION: will log out all users)
redis-cli --scan --pattern "auth:*" | xargs redis-cli DEL
# Remove specific user session
redis-cli DEL "auth:installation:ACCESS_TOKEN"
```
---
## References
- [MCP Authorization Specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)
- [RFC 6749: OAuth 2.0 Framework](https://datatracker.ietf.org/doc/html/rfc6749)
- [RFC 7636: PKCE](https://datatracker.ietf.org/doc/html/rfc7636)
- [RFC 7662: Token Introspection](https://datatracker.ietf.org/doc/html/rfc7662)
- [OAuth 2.0 Security Best Practices](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics)
- [OAuth 2.1 Draft](https://oauth.net/2.1/) - Modern security requirements

View file

@ -2,7 +2,9 @@
## Overview
The MCP server implements multi-user session isolation to ensure users can only access their own MCP sessions. This is accomplished using user identifiers and Redis-backed ownership tracking.
The MCP server implements multi-user session isolation to ensure users can only access their own MCP sessions. This is accomplished using user identifiers and session ownership tracking.
**Note**: While this document describes Redis-backed storage, the server also supports in-memory storage for development. When Redis is not configured, session data is stored in memory and lost on restart.
## How It Works
@ -17,7 +19,7 @@ User ID sources:
- **Social/federated identities**: Provider links to Google, Facebook (example: `google-oauth2|112233445566778899`)
- **Enterprise connections**: SAML, LDAP, Active Directory (example: `samlp|ad|john.doe@company.com`)
**This implementation**: Mock upstream IDP returns HTML with JavaScript that generates a random UUID in the browser, stores it in `localStorage.mcpUserId`, and sends it to the server as a query parameter. This simulates user authentication for testing - different browsers/tabs get different UUIDs, allowing multi-user testing.
**Demo mode**: The demo auth server simulates user authentication by generating unique user IDs. This allows testing multi-user scenarios without a real identity provider.
### Token Validation
@ -76,7 +78,7 @@ When an MCP server starts handling a session, it subscribes to the channel. When
## Implementation
### Core Functions (mcp-server/src/services/redisTransport.ts)
### Core Functions (src/modules/mcp/services/redisTransport.ts)
```typescript
export async function setSessionOwner(sessionId: string, userId: string): Promise<void>
@ -135,11 +137,11 @@ redis-cli MONITOR | grep "session:"
## Testing
```bash
# Run from mcp-server directory to test session isolation
cd mcp-server && npm test -- --testNamePattern="User Session Isolation"
# Test session isolation
npm test -- --testNamePattern="User Session Isolation"
# Run from mcp-server directory to test session ownership
cd mcp-server && npm test -- --testNamePattern="Session Ownership"
# Test session ownership
npm test -- --testNamePattern="Session Ownership"
```
## References

View file

@ -1,98 +0,0 @@
# MCP Server Examples
These examples demonstrate interaction with the MCP server, covering OAuth authentication, session management, and MCP operations (tools, resources, prompts).
**Authentication is the most complex part** of using an MCP server with OAuth. The examples demonstrate authentication interactions using two different step-by-step approaches:
- **client.js**: runs the server's end-to-end auth flow in the browser
- **curl-examples.sh**: uses raw HTTP interactions (requires obtaining an access token separately)
See below for details.
## Prerequisites
Before running any examples, ensure:
1. Redis is running: `docker compose up -d`
2. Both servers are running: `npm run dev`
3. Servers are accessible at:
- Auth Server: http://localhost:3001
- MCP Server: http://localhost:3232
## Available Examples
### client.js
Node.js client showing programmatic interaction with the MCP server.
**Features:**
- Complete OAuth flow demonstration
- Automatic client registration
- MCP session management
- Tool and resource operations
**Usage:**
```bash
# Run the example
node client.js
# Or make it executable
chmod +x client.js
./client.js
```
**Note:** When you complete the OAuth flow in your browser, you'll be redirected to `http://localhost:8080/callback` which will show "site can't be reached". This is expected! Simply copy the authorization code from the URL in your browser's address bar (the long string after `code=`). The script will exchange this for an access token and display it for use with other tools.
### curl-examples.sh
Shell script demonstrating API interactions using curl.
**Features:**
- OAuth client registration
- MCP session initialization
- Tool calls (echo, add)
- Resource listing and reading
- Prompt operations
**Three-step workflow:**
```bash
# Make executable
chmod +x curl-examples.sh
# Step 1: Setup - Register OAuth client and get instructions
./curl-examples.sh
# Step 2: Create session - Get an access token (via Inspector or client.js), then:
./curl-examples.sh YOUR_ACCESS_TOKEN
# → Initializes MCP session and displays session ID
# Step 3: Run examples - Use both access token and session ID:
./curl-examples.sh YOUR_ACCESS_TOKEN YOUR_SESSION_ID
# → Demonstrates all MCP features (tools, resources, prompts)
```
**Quick reference:**
- Run `./curl-examples.sh --help` for detailed usage
- Requires an access token (get from MCP Inspector or `node client.js`)
- Each step explains what to do next
## Troubleshooting
### "401 Unauthorized"
- Token may be expired (7-day TTL)
- Token may be invalid
- Get a new token via OAuth flow
### "Session not found"
- Session expired (5-minute TTL)
- Re-initialize with the same token
### "Cannot connect"
- Ensure servers are running: `npm run dev`
- Check Redis is running: `docker compose ps`
- Verify URLs are correct
## Additional Resources
- [MCP Protocol Documentation](https://modelcontextprotocol.io)
- [OAuth Flow Documentation](../docs/oauth-flow.md)
- [API Endpoints Reference](../docs/endpoints.md)
- [Main README](../README.md)

Some files were not shown because too many files have changed in this diff Show more