Templatetypescriptexpressintermediateβœ“ Production ReadyFree

Express MCP Server with OAuth 2.0

A production-ready TypeScript MCP server using HTTP transport with full OAuth 2.0 Authorization Code flow, built on Express. Features structured Pino logging with correlation IDs, Docker multi-stage builds, Jest test suite, and three realistic GitHub API tools for repository management.

πŸ“– 12 min readβš™οΈ Setup: 20 minutesutilities

Files(8)

{
  "name": "express-mcp-oauth",
  "version": "1.0.0",
  "description": "Production-ready MCP server with Express, HTTP transport, and OAuth 2.0",
  "main": "dist/server.js",
  "scripts": {
    "build": "tsc --project tsconfig.json",
    "start": "node dist/server.js",
    "dev": "ts-node --esm src/server.ts",
    "dev:watch": "nodemon --exec ts-node src/server.ts --ext ts",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "lint": "eslint src --ext .ts",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "1.0.4",
    "axios": "1.7.7",
    "cors": "2.8.5",
    "express": "4.21.1",
    "express-session": "1.18.1",
    "pino": "9.5.0",
    "pino-http": "10.3.0",
    "uuid": "10.0.0",
    "zod": "3.23.8"
  },
  "devDependencies": {
    "@types/cors": "2.8.17",
    "@types/express": "4.17.21",
    "@types/express-session": "1.18.0",
    "@types/jest": "29.5.14",
    "@types/node": "22.9.1",
    "@types/supertest": "6.0.2",
    "@types/uuid": "10.0.0",
    "jest": "29.7.0",
    "nodemon": "3.1.7",
    "pino-pretty": "13.0.0",
    "supertest": "7.0.0",
    "ts-jest": "29.2.5",
    "ts-node": "10.9.2",
    "typescript": "5.6.3"
  },
  "engines": {
    "node": ">=20.0.0"
  },
  "license": "MIT"
}

Express MCP Server with OAuth 2.0

Overview

This template provides a production-ready Model Context Protocol (MCP) server built with TypeScript and Express, using HTTP transport secured by the full OAuth 2.0 Authorization Code flow. It is designed for developers who need to expose authenticated API capabilities to AI assistants like Claude, where the underlying APIs require delegated user authorization rather than static API keys.

The template implements three realistic GitHub API tools β€” repository listing, issue creation, and repository search β€” demonstrating how to build tools that operate on behalf of an authenticated user. The OAuth token lifecycle (authorization, token exchange, refresh, and secure in-memory storage) is fully implemented, so you can study or extend it for any OAuth 2.0-compatible provider such as Google, Slack, or a custom authorization server.

This is an intermediate-level template aimed at TypeScript developers familiar with Node.js and REST APIs who want to understand MCP server construction, OAuth delegation patterns, and production deployment practices including Docker, structured logging, and automated testing.

What You'll Learn

  • How to construct an MCP server using the @modelcontextprotocol/sdk with HTTP/SSE transport on Express
  • Implementing the full OAuth 2.0 Authorization Code flow including PKCE, token exchange, and refresh token rotation
  • Securing MCP tool endpoints with Bearer token middleware that validates OAuth access tokens
  • Registering strongly-typed MCP tools with Zod input schemas and structured error responses
  • Setting up Pino for structured JSON logging with per-request correlation IDs propagated through async context
  • Writing Jest tests for MCP tool handlers, OAuth middleware, and error boundary behaviour
  • Building optimised multi-stage Docker images that separate build and runtime dependencies
  • Configuring docker-compose for local development with hot-reload and environment variable injection
  • Managing OAuth token storage and expiry with in-memory token stores suitable for upgrade to Redis
  • Applying the MCP error taxonomy (InvalidParams, InternalError, MethodNotFound) correctly
  • Reading and validating all configuration from environment variables with typed defaults
  • Deploying the containerised server to Railway or Fly.io with health-check endpoints

Architecture

Claude Desktop / MCP Client
        β”‚  HTTP POST /mcp  (Bearer <access_token>)
        β–Ό
  Express Server
        β”‚
   Auth Middleware  ──► Token Introspection / Validation
        β”‚                        β”‚
        β”‚               Token Store (Memory/Redis)
        β–Ό
   MCP SDK Handler
        β”‚
   Tool Router
    β”œβ”€β”€ list_repositories  ──► GitHub REST API
    β”œβ”€β”€ create_issue       ──► GitHub REST API
    └── search_repositories──► GitHub REST API
        β”‚
  Pino Logger (JSON + correlation ID)
        β”‚
  stdout / log aggregator

OAuth Flow (separate endpoints):
  GET /auth/login  β†’  GitHub Authorization URL
  GET /auth/callback  β†’  Token Exchange  β†’  Token Store
  POST /auth/refresh  β†’  Refresh Token  β†’  Token Store

Project Structure

express-mcp-oauth/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ server.ts                 # Express app + MCP server setup
β”‚   β”œβ”€β”€ config.ts                 # Typed config from environment
β”‚   β”œβ”€β”€ auth.ts                   # OAuth 2.0 flow + Bearer middleware
β”‚   β”œβ”€β”€ logger.ts                 # Pino structured logger + correlation
β”‚   └── __tests__/
β”‚       └── server.test.ts        # Jest tests for tools + middleware
β”œβ”€β”€ Dockerfile                    # Multi-stage production image
β”œβ”€β”€ docker-compose.yml            # Local development stack
β”œβ”€β”€ package.json                  # Dependencies + scripts
β”œβ”€β”€ tsconfig.json                 # TypeScript compiler config
β”œβ”€β”€ jest.config.ts                # Jest configuration
β”œβ”€β”€ .env.example                  # All environment variables documented
└── .gitignore

Prerequisites

  • Node.js 20 or higher
  • npm 10 or higher
  • Docker Desktop (for containerised development)
  • A GitHub OAuth App (create one here) with callback URL set to http://localhost:3000/auth/callback
  • Claude Desktop or another MCP client for end-to-end testing

Quick Start

# 1. Clone or copy this template
git clone https://github.com/your-org/express-mcp-oauth
cd express-mcp-oauth

# 2. Install dependencies
npm install

# 3. Configure environment
cp .env.example .env
# Edit .env with your GitHub OAuth App credentials

# 4. Start in development mode (ts-node with hot reload)
npm run dev

# 5. Initiate OAuth flow in your browser
open http://localhost:3000/auth/login
# Complete GitHub authorization β€” the token is stored in memory

# 6. Run the test suite
npm test

# 7. Build and run with Docker
docker compose up --build

Features

  • Full OAuth 2.0 Authorization Code flow with PKCE, state parameter CSRF protection, token exchange, and refresh token rotation
  • Three GitHub API tools β€” list_repositories, create_issue, search_repositories β€” with Zod input validation
  • Pino structured logging with JSON output, configurable log level, and per-request correlation IDs via AsyncLocalStorage
  • Bearer token middleware that validates OAuth access tokens before routing MCP requests
  • Multi-stage Dockerfile producing a minimal production image (~180 MB) with non-root user
  • docker-compose configuration for local development with volume mounts and health checks
  • Jest test suite covering tool execution, OAuth middleware, error handling, and edge cases
  • Typed configuration loaded from environment variables with validation and sensible defaults
  • Health check endpoint at GET /health for load balancer and orchestrator probes
  • Graceful shutdown handling SIGTERM and SIGINT with in-flight request draining

Works With

ClientTransportStatus
Claude DesktopHTTPβœ… Supported
Claude CodeHTTPβœ… Supported
CursorHTTPβœ… Supported
WindsurfHTTPβœ… Supported
ContinueHTTPβœ… Supported

To add this server to Claude Desktop, append to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "github-oauth": {
      "command": "curl",
      "args": ["-s", "-X", "POST", "http://localhost:3000/mcp"],
      "transport": "http",
      "url": "http://localhost:3000/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_ACCESS_TOKEN"
      }
    }
  }
}

Configuration

VariableDescriptionRequired
PORTHTTP port the server listens onOptional (default: 3000)
NODE_ENVRuntime environment (development/production)Optional (default: development)
LOG_LEVELPino log level: trace debug info warn errorOptional (default: info)
GITHUB_CLIENT_IDOAuth App Client ID from GitHub Developer SettingsRequired
GITHUB_CLIENT_SECRETOAuth App Client Secret from GitHub Developer SettingsRequired
GITHUB_REDIRECT_URICallback URL registered in your GitHub OAuth AppRequired
OAUTH_STATE_SECRETSecret for signing the OAuth state parameter (β‰₯32 chars)Required
SESSION_SECRETSecret for signing session cookies (β‰₯32 chars)Required
TOKEN_STORE_TTL_SECONDSSeconds before cached tokens are evictedOptional (default: 3600)
ALLOWED_ORIGINSComma-separated CORS originsOptional (default: *)
MCP_SERVER_NAMEName advertised in MCP server infoOptional (default: github-mcp)
MCP_SERVER_VERSIONVersion advertised in MCP server infoOptional (default: 1.0.0)

Deployment

Docker (Local)

docker build -t express-mcp-oauth .
docker run -p 3000:3000 --env-file .env express-mcp-oauth

Railway

npm install -g @railway/cli
railway login
railway new
railway link
railway variables set GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=xxx GITHUB_REDIRECT_URI=https://your-app.railway.app/auth/callback OAUTH_STATE_SECRET=xxx SESSION_SECRET=xxx
railway up

Fly.io

npm install -g flyctl
fly auth login
fly launch --name express-mcp-oauth --no-deploy
fly secrets set GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=xxx GITHUB_REDIRECT_URI=https://express-mcp-oauth.fly.dev/auth/callback OAUTH_STATE_SECRET=xxx SESSION_SECRET=xxx
fly deploy

Docker Compose (Production-like)

docker compose -f docker-compose.yml up -d --build
docker compose logs -f mcp-server

Production Checklist

  • GITHUB_CLIENT_SECRET, OAUTH_STATE_SECRET, and SESSION_SECRET are stored in a secrets manager (not in version control)
  • NODE_ENV=production is set in the runtime environment
  • LOG_LEVEL=info or warn in production (not debug or trace)
  • GITHUB_REDIRECT_URI matches exactly the callback URL registered in GitHub OAuth App settings
  • HTTPS is enforced at the reverse-proxy or load balancer layer
  • CORS ALLOWED_ORIGINS is restricted to known client origins
  • Docker image is scanned for CVEs with docker scout or Trivy before deployment
  • Health check endpoint /health is configured in your orchestrator (Railway/Fly.io/K8s)
  • Token store is backed by Redis (not in-memory) for multi-instance deployments
  • Graceful shutdown timeout matches your orchestrator's termination grace period
  • Rate limiting is applied to /auth/ endpoints to prevent abuse
  • Dependency versions are pinned and npm audit reports no high/critical vulnerabilities
  • Container runs as non-root user (already configured in the Dockerfile)
  • Structured logs are forwarded to a log aggregator (Datadog, Logtail, etc.)
  • OAuth refresh token rotation is enabled in your GitHub App settings

Testing

Run the test suite

# All tests
npm test

# Watch mode during development
npm run test:watch

# Coverage report
npm run test:coverage

Manual testing with curl

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

# Start OAuth flow (visit in browser)
open http://localhost:3000/auth/login

# After completing OAuth, copy the access_token from the callback response
export TOKEN=<your_access_token>

# Call the MCP server β€” list tools
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

# Execute list_repositories tool
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_repositories","arguments":{"per_page":5}}}'

Testing with Claude Desktop

  1. Complete the OAuth flow by visiting http://localhost:3000/auth/login
  2. Note the access_token returned on the callback page
  3. Add the server to claude_desktop_config.json with the token in the Authorization header
  4. Restart Claude Desktop and look for the πŸ”Œ tools icon
  5. Ask Claude: "List my GitHub repositories" or "Search for TypeScript MCP servers on GitHub"