Express mcp server
35 skills that teach AI coding agents to integrate Scalekit auth — agent auth, full-stack login, MCP OAuth 2.1, enterprise SSO, and SCIM. Works with Claude Code, Cursor, Windsurf, and 35+ other agents.
npx -y skills add scalekit-inc/skills --skill express-mcp-serverAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Build a production-ready MCP server using Express.js, TypeScript, and OAuth 2.1 Bearer token authentication via Scalekit. Use when the user wants to build an MCP server with Express.js and needs fine-grained control over HTTP request handling and middleware chains.
SKILL.md
22.2 KB, as published. Nobody here has run it
Express.js MCP OAuth Authentication with Scalekit
Overview
This skill documents the pattern for building production-ready MCP (Model Context Protocol) servers using Express.js, TypeScript, and OAuth 2.1 Bearer token authentication via Scalekit. This approach provides fine-grained control over HTTP request handling, middleware chains, and server behavior for Node.js-based MCP implementations.
When to Use This Pattern
Use this Express.js MCP integration when you need:
- Node.js ecosystem: Leverage existing npm packages, TypeScript tooling, and JavaScript libraries
- Custom middleware chains: Implement rate limiting, request logging, or complex authorization logic with Express middleware
- Existing Express applications: Add MCP capabilities to established Express.js codebases without rewriting
- Fine-grained HTTP control: Manage routing, CORS policies, health checks, and multiple endpoints
- Production flexibility: Deploy on serverless platforms (AWS Lambda, Vercel), containers, or traditional Node.js hosts
Don't use this pattern if you prefer Python's ecosystem or if a simpler MCP server setup (without Express) meets your requirements.
Core Architecture
Token Validation Flow
MCP Client → Express Server (401 + WWW-Authenticate)
MCP Client → Scalekit (Exchange code for token)
Scalekit → MCP Client (Bearer token)
MCP Client → Express Server (POST /mcp + Bearer token)
Express Middleware → Scalekit SDK (Validate token)
McpServer → Tool Handler → Response
Key Components
- Express Middleware: Custom authentication middleware that intercepts requests and validates Bearer tokens before routing to MCP handlers
- Scalekit Node SDK: TypeScript SDK validates JWT signatures, expiration, issuer, and audience claims
- McpServer: Official MCP SDK server that handles protocol details (JSON-RPC, tool registration)
- StreamableHTTPServerTransport: MCP transport layer that bridges Express HTTP requests to MCP protocol
- Zod Schema Validation: Type-safe input validation for MCP tool parameters
- OAuth Resource Metadata Endpoint: Well-known endpoint (
/.well-known/oauth-protected-resource) for client discovery
Implementation Patterns
1. Environment Configuration
Required variables:
SK_ENV_URL: Scalekit environment URL (issuer)SK_CLIENT_ID+SK_CLIENT_SECRET: SDK authentication credentialsEXPECTED_AUDIENCE: The resource identifier that tokens must targetPROTECTED_RESOURCE_METADATA: Complete OAuth discovery metadata JSONPORT: Server listening port (must match registered server URL)
Security:
- Never commit
.envfiles to version control - Add
.envto.gitignoreimmediately - Use secret managers in production (AWS Secrets Manager, Doppler, HashiCorp Vault)
- Rotate
SK_CLIENT_SECRETregularly - Validate
EXPECTED_AUDIENCEmatches your server's public URL exactly (including trailing slash)
2. Scalekit Client Initialization
import { Scalekit } from '@scalekit-sdk/node';
const scalekit = new Scalekit(
SK_ENV_URL,
SK_CLIENT_ID,
SK_CLIENT_SECRET
);
Best practices:
- Initialize once at module level for connection pooling
- SDK handles token caching and JWKS key rotation automatically
- All validation methods are async—always use await
3. MCP Server Setup
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
const server = new McpServer({
name: 'Greeting MCP',
version: '1.0.0'
});
server.tool(
'greet_user',
'Greets the user with a personalized message.',
{
name: z.string().min(1, 'Name is required'),
},
async ({ name }: { name: string }) => ({
content: [
{
type: 'text',
text: `Hi ${name}, welcome to Scalekit!`
}
]
})
);
Tool registration:
- First parameter: Tool name (snake_case recommended for consistency)
- Second parameter: Human-readable description for AI discoverability
- Third parameter: Zod schema for input validation
- Fourth parameter: Async handler function
Zod validation benefits:
- Type-safe parameters with TypeScript inference
- Runtime validation prevents malformed inputs
- Automatic error messages for invalid data
- Composable schemas for complex validation rules
4. Express Middleware Authentication
app.use(async (req: Request, res: Response, next: NextFunction) => {
// Exempt public endpoints
if (req.path === '/.well-known/oauth-protected-resource' || req.path === '/health') {
next();
return;
}
// Extract Bearer token
const header = req.headers.authorization;
const token = header?.startsWith('Bearer ')
? header.slice('Bearer '.length).trim()
: undefined;
if (!token) {
res.status(401)
.set('WWW-Authenticate', WWW_HEADER_VALUE)
.json({ error: 'Missing Bearer token' });
return;
}
try {
// Validate with Scalekit SDK
await scalekit.validateToken(token, {
audience: [EXPECTED_AUDIENCE]
});
next();
} catch (error) {
res.status(401)
.set('WWW-Authenticate', WWW_HEADER_VALUE)
.json({ error: 'Token validation failed' });
}
});
Key principles:
- Use Express
app.use()for middleware that runs on every request - Explicitly exempt public endpoints before token extraction
- Return early with
returnafter sending 401 responses (prevents "headers already sent" errors) - Always set
WWW-Authenticateheader on 401 responses - Use
next()to pass control to subsequent middleware/routes
Common mistake: Forgetting to return after sending a response leads to "Cannot set headers after they are sent" errors.
5. MCP Transport Layer
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
app.post('/', async (req: Request, res: Response) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined
});
await server.connect(transport);
try {
await transport.handleRequest(req, res, req.body);
} catch (error) {
res.status(500).json({ error: 'MCP transport error' });
}
});
Transport responsibilities:
- Converts HTTP requests to MCP JSON-RPC format
- Handles streaming responses for long-running operations
- Manages session state (stateless when
sessionIdGenerator: undefined) - Bridges Express request/response objects to MCP protocol
Stateless design: Setting sessionIdGenerator: undefined ensures each request is independent—suitable for serverless deployments.
6. Resource Metadata Endpoint
app.get('/.well-known/oauth-protected-resource', (_req: Request, res: Response) => {
if (!PROTECTED_RESOURCE_METADATA) {
res.status(500).json({ error: 'PROTECTED_RESOURCE_METADATA config missing' });
return;
}
const metadata = JSON.parse(PROTECTED_RESOURCE_METADATA);
res.type('application/json').send(JSON.stringify(metadata, null, 2));
});
Purpose:
- Enables MCP client discovery of authorization requirements
- Clients fetch this when they receive a 401 response with
WWW-Authenticateheader - Contains authorization server endpoints, supported grant types, and token types
- Must be publicly accessible (no authentication required)
Error handling: Return 500 if metadata is missing to signal misconfiguration (deployment should fail fast).
7. CORS Configuration
import cors from 'cors';
app.use(cors({
origin: true, // Allow all origins
credentials: false // No credentials needed for Bearer tokens
}));
Configuration options:
origin: true: Reflects request origin (development convenience)origin: ['https://app.example.com']: Whitelist specific origins (production)credentials: false: Bearer tokens don't require cookies/credentialsmethods: ['GET', 'POST', 'OPTIONS']: Limit allowed HTTP methods
Production recommendation: Use explicit origin whitelist instead of origin: true.
Security Considerations
Token Validation Requirements
- Always validate issuer: Prevents tokens from other OAuth servers being accepted
- Always validate audience: Ensures token was issued for your specific resource
- Check expiration: Scalekit SDK automatically validates
expclaim - Verify signature: SDK checks JWT signature against Scalekit's public keys (JWKS)
Common Vulnerabilities to Avoid
- Skipping audience validation: Tokens from other Scalekit resources could be used
- Custom JWT parsing: Use SDK validation—don't implement manual
jwt.verify() - Logging tokens: Never log Bearer tokens in middleware, error handlers, or debug output
- Missing CORS configuration: Can enable cross-origin attacks or block legitimate clients
- Hardcoded secrets: Use environment variables and secret managers
- Not returning after response: Causes "headers already sent" errors and potential security issues
Production Hardening
- HTTPS termination: Run behind reverse proxy (Nginx, Caddy, AWS ALB) with TLS
- Process management: Use PM2, systemd, or container orchestration for auto-restart
- Multiple workers: Use Node.js cluster module or container scaling
- Rate limiting: Implement per-client/token rate limits using
express-rate-limit - Request logging: Add structured logging middleware (Winston, Pino) without token values
- Health checks: Separate health endpoint for load balancers and orchestrators
- Error monitoring: Integrate Sentry, Datadog, or similar for production error tracking
Testing Strategy
Local Testing with MCP Inspector
npx @modelcontextprotocol/inspector@latest
Testing workflow:
- Start your Express server:
npm run dev - Launch MCP Inspector
- Connect to
http://localhost:3002/ - Inspector automatically handles OAuth flow
- Test each tool with various inputs
- Verify middleware logs show successful validation
Manual Token Testing with cURL
# Get token from Scalekit (via OAuth flow or test endpoint)
export TOKEN="<your-access-token>"
# Test authenticated MCP request
curl -X POST http://localhost:3002/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "greet_user",
"arguments": {
"name": "Saif"
}
}
}'
# Test missing token (should return 401)
curl -v -X POST http://localhost:3002/
# Test invalid token
curl -X POST http://localhost:3002/ \
-H "Authorization: Bearer invalid-token" \
-H "Content-Type: application/json"
Integration Tests with Jest/Vitest
import request from 'supertest';
import { app } from './server';
describe('MCP Authentication', () => {
test('returns 401 without token', async () => {
const response = await request(app).post('/');
expect(response.status).toBe(401);
expect(response.headers['www-authenticate']).toContain('Bearer');
});
test('returns 401 with invalid token', async () => {
const response = await request(app)
.post('/')
.set('Authorization', 'Bearer invalid-token');
expect(response.status).toBe(401);
});
test('health check is public', async () => {
const response = await request(app).get('/health');
expect(response.status).toBe(200);
expect(response.body).toEqual({ status: 'healthy' });
});
test('metadata endpoint is public', async () => {
const response = await request(app).get('/.well-known/oauth-protected-resource');
expect(response.status).toBe(200);
expect(response.headers['content-type']).toContain('application/json');
});
});
Test dependencies:
{
"devDependencies": {
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
"supertest": "^6.3.4",
"ts-jest": "^29.1.2"
}
}
Load Testing
# Install autocannon for HTTP load testing
npm install -g autocannon
# Test authenticated endpoint throughput
autocannon -c 10 -d 30 \
-m POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-b '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
http://localhost:3002/
Common Pitfalls
1. Mismatched Audience
Symptom: Tokens fail validation with "invalid audience" error
Cause: EXPECTED_AUDIENCE doesn't match the Server URL registered in Scalekit
Fix: Ensure both values are identical including protocol, host, port, and trailing slash
Example:
// ❌ Wrong - missing trailing slash
EXPECTED_AUDIENCE=http://localhost:3002
// ✅ Correct - matches Scalekit registration
EXPECTED_AUDIENCE=http://localhost:3002/
2. Headers Already Sent Error
Symptom: Error: Cannot set headers after they are sent to the client
Cause: Forgetting to return after sending a response in middleware
Fix: Always return immediately after res.json() or res.send()
Example:
// ❌ Wrong - continues to next middleware
if (!token) {
res.status(401).json({ error: 'Missing token' });
}
next(); // This runs even after sending 401
// ✅ Correct - returns after response
if (!token) {
res.status(401).json({ error: 'Missing token' });
return; // Prevents calling next()
}
3. Middleware Order Issues
Symptom: CORS errors, authentication bypassed, or parsing failures Cause: Middleware execution order matters in Express Fix: Correct order is: CORS → body parsing → authentication → routes
Example:
// ✅ Correct order
app.use(cors());
app.use(express.json());
app.use(authMiddleware);
app.get('/public', publicRoute);
app.post('/', protectedRoute);
4. Missing Resource Metadata
Symptom: Clients can't discover how to authenticate
Cause: PROTECTED_RESOURCE_METADATA not set or malformed JSON
Fix: Copy exact JSON from Scalekit dashboard, verify with JSON.parse()
Debugging:
# Test metadata endpoint
curl http://localhost:3002/.well-known/oauth-protected-resource
# Should return valid JSON with authorization_endpoint
5. TypeScript Module Resolution
Symptom: Cannot find module '@modelcontextprotocol/sdk/server/mcp.js'
Cause: Missing .js extension in ES module imports
Fix: Always include .js extension when importing from MCP SDK
Example:
// ❌ Wrong - missing .js
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
// ✅ Correct - includes .js
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6. Token Expiration During Testing
Symptom: Tests pass initially then fail after 1 hour Cause: Access tokens expire (default 3600 seconds) Fix: Implement token refresh before each test run or use shorter test cycles
Extension Patterns
Adding Scope-Based Authorization
import jwt from 'jsonwebtoken';
// Extend Express Request type
declare global {
namespace Express {
interface Request {
tokenPayload?: {
sub: string;
scope: string[];
};
}
}
}
app.use(async (req: Request, res: Response, next: NextFunction) => {
// ... existing token validation ...
// Decode token to access claims (after validation)
const decoded = jwt.decode(token) as any;
req.tokenPayload = {
sub: decoded.sub,
scope: decoded.scope?.split(' ') || []
};
next();
});
// Tool with scope requirement
server.tool(
'admin_action',
'Performs an admin action',
{ action: z.string() },
async ({ action }, { req }) => {
if (!req.tokenPayload?.scope.includes('admin')) {
throw new Error('Requires admin scope');
}
// ... admin logic ...
}
);
Multi-Tenancy Support
app.use(async (req: Request, res: Response, next: NextFunction) => {
// ... validate token ...
const decoded = jwt.decode(token) as any;
req.orgId = decoded.org_id;
next();
});
server.tool(
'get_org_data',
'Retrieves organization-specific data',
{},
async (_params, { req }) => {
const orgId = req.orgId;
const data = await fetchDataForOrg(orgId);
return {
content: [{ type: 'text', text: JSON.stringify(data) }]
};
}
);
Rate Limiting
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per window
message: 'Too many requests, please try again later',
standardHeaders: true,
legacyHeaders: false,
// Rate limit by token subject (user ID)
keyGenerator: (req: Request) => {
const token = req.headers.authorization?.slice('Bearer '.length);
if (!token) return req.ip;
const decoded = jwt.decode(token) as any;
return decoded.sub || req.ip;
}
});
// Apply to MCP endpoint
app.post('/', limiter, async (req: Request, res: Response) => {
// ... MCP transport handling ...
});
Structured Logging
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
redact: ['req.headers.authorization'], // Never log tokens
});
app.use((req: Request, res: Response, next: NextFunction) => {
const start = Date.now();
res.on('finish', () => {
logger.info({
method: req.method,
path: req.path,
status: res.statusCode,
duration: Date.now() - start,
});
});
next();
});
Error Handling Middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
logger.error({ err, path: req.path }, 'Unhandled error');
res.status(500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message
});
});
Dependencies
Required Packages
{
"dependencies": {
"@modelcontextprotocol/sdk": "^1.13.0",
"@scalekit-sdk/node": "^2.0.1",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^5.1.0",
"zod": "^3.25.57"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^4.17.21",
"@types/node": "^20.11.19",
"tsx": "^4.7.0",
"typescript": "^5.4.5"
}
}
Dependency purposes:
@modelcontextprotocol/sdk: Official MCP protocol implementation@scalekit-sdk/node: Scalekit authentication SDK for token validationcors: Cross-Origin Resource Sharing middlewaredotenv: Environment variable loading from.envfilesexpress: Fast, unopinionated web frameworkzod: TypeScript-first schema validationtsx: TypeScript execution for development (faster than ts-node)typescript: TypeScript compiler
Optional Production Dependencies
{
"dependencies": {
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
"pino": "^8.17.2",
"pino-http": "^9.0.0"
}
}
Production enhancements:
express-rate-limit: Rate limiting middlewarehelmet: Security headers middlewarepino: High-performance JSON loggerpino-http: HTTP request logging middleware
Version Pinning Strategy
- Pin exact versions in production
package.json(use"express": "5.1.0"not"^5.1.0") - Use
^for development flexibility - Run
npm auditregularly for security vulnerabilities - Test version upgrades in staging before production
- Use
npm ciin production for reproducible builds
Deployment Patterns
Docker
FROM node:20-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source
COPY . .
# Build TypeScript
RUN npm run build
# Expose port
EXPOSE 3002
# Start server
CMD ["npm", "start"]
PM2 Process Manager
{
"apps": [{
"name": "mcp-server",
"script": "dist/server.js",
"instances": 4,
"exec_mode": "cluster",
"env": {
"NODE_ENV": "production"
}
}]
}
Start with PM2:
npm run build
pm2 start ecosystem.config.json
AWS Lambda (Serverless)
import serverless from 'serverless-http';
// ... existing Express app setup ...
export const handler = serverless(app);
Note: Ensure stateless transport configuration for serverless environments.
Environment-Specific Configuration
const config = {
development: {
port: 3002,
corsOrigin: true,
logLevel: 'debug'
},
production: {
port: parseInt(process.env.PORT || '3002'),
corsOrigin: process.env.ALLOWED_ORIGINS?.split(',') || [],
logLevel: 'info'
}
};
const env = process.env.NODE_ENV || 'development';
const appConfig = config[env];
Complete Working Example
A full production-ready Express.js MCP server is available in the Scalekit MCP Auth Demos repository:
GitHub Repository: scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-node
This example includes:
- Complete server implementation with modular architecture
- OAuth 2.1 authentication middleware
- Tool registration with Zod validation
- CORS configuration and error handling
- Production-ready logging and monitoring
Key Files
src/main.ts- Main server entry pointsrc/lib/auth.ts- OAuth discovery endpoint handlersrc/lib/middleware.ts- Token validation middlewaresrc/lib/transport.ts- MCP transport layer setupsrc/tools/- Tool implementations
Getting Started
cd greeting-mcp-node
npm install
npm run build
npm start
See README.md for complete setup instructions.
Related Resources
- MCP SDK Documentation
- Express.js Guide
- Scalekit Node SDK
- Zod Documentation
- OAuth 2.1 Specification
- Scalekit MCP Authentication Docs
- MCP Protocol Specification
- Scalekit MCP Auth Demos
Changelog
- 2026-02-13: Initial skill documentation based on Express.js MCP quickstart guide