Skip to content

Express.js quickstart

Build a production-ready Express.js MCP server with TypeScript, custom middleware for OAuth token validation, and Scalekit authentication.

This guide shows you how to build a production-ready Express.js MCP server with TypeScript and Scalekit’s OAuth authentication. You’ll implement custom middleware for token validation, expose OAuth resource metadata for client discovery, and create MCP tools that enforce authorization using the MCP SDK.

Use this quickstart when you’re building Node.js-based MCP servers and want fine-grained control over request handling. The Express integration gives you flexibility to add custom routes, middleware chains, integrate with existing Express applications, and handle complex authorization requirements. The full code is available on GitHub.

Prerequisites

  • A Scalekit account with permission to manage MCP servers
  • Node.js 20+ installed locally
  • Familiarity with Express.js, TypeScript, and OAuth token validation
  • Basic understanding of MCP server architecture
Review the Express.js MCP authorization flow Express.js MCP with ScalekitMCP ClientMCP ServerScalekit Request to call tool 401 Unauthorized + WWW-Authenticate header Exchange code for access token Issue Bearer token POST /mcp with Bearer token Validate token via Express middleware Tool response
  1. Create a protected resource entry so Scalekit can issue tokens that your custom Express middleware validates.

    1. Navigate to Dashboard > MCP Servers > Add MCP Server.
    2. Enter a descriptive name (for example, Greeting MCP).
    3. Set Server URL to http://localhost:3002/ (keep the trailing slash).
    4. Click Save to create the server.

    Greeting MCP Register

    When you save, Scalekit displays the OAuth-protected resource metadata. Copy this JSON—you’ll use it in your .env file.

    Greeting MCP Protected JSON

  2. Set up a clean directory structure for your TypeScript Express project.

    Terminal
    mkdir express-mcp-node
    cd express-mcp-node
  3. Create a package.json with scripts and all required dependencies for Express, TypeScript, and the MCP SDK.

    Terminal
    cat <<'EOF' > package.json
    {
    "name": "express-mcp-node",
    "version": "1.0.0",
    "type": "module",
    "scripts": {
    "dev": "tsx src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js"
    },
    "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"
    }
    }
    EOF
  4. Add a TypeScript configuration file optimized for ES2022 modules and strict type checking.

    Terminal
    cat <<'EOF' > tsconfig.json
    {
    "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": false,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "outDir": "dist",
    "rootDir": "src",
    "types": ["node"]
    },
    "include": ["src/**/*"]
    }
    EOF
  5. Install all packages declared in package.json.

    Terminal
    npm install
  6. Create a .env file with your Scalekit credentials and the protected resource metadata from step 1.

    Terminal
    cat <<'EOF' > .env
    PORT=3002
    SK_ENV_URL=https://<your-env>.scalekit.com
    SK_CLIENT_ID=<your-client-id>
    SK_CLIENT_SECRET=<your-client-secret>
    MCP_SERVER_ID=<mcp-server-id-from-dashboard>
    PROTECTED_RESOURCE_METADATA='<resource-metadata-json>'
    EXPECTED_AUDIENCE=http://localhost:3002/
    EOF
    open .env
    VariableDescription
    PORTLocal port for the Express server. Must match the Server URL registered in Scalekit (defaults to 3002).
    SK_ENV_URLYour Scalekit environment URL from Dashboard > Settings > API Credentials
    SK_CLIENT_IDClient ID from Dashboard > Settings > API Credentials. Used with SK_CLIENT_SECRET to initialize the SDK.
    SK_CLIENT_SECRETClient secret from Dashboard > Settings > API Credentials. Keep this secret and rotate regularly.
    MCP_SERVER_IDThe MCP server ID from Dashboard > MCP Servers. Not directly used in this implementation but documented for reference.
    PROTECTED_RESOURCE_METADATAThe complete OAuth resource metadata JSON from step 1. Clients use this to discover authorization requirements.
    EXPECTED_AUDIENCEThe audience value that tokens must include. Should match your server’s public URL (e.g., http://localhost:3002/).
  7. Create src/server.ts with the complete server implementation. This includes the Scalekit client initialization, authentication middleware for token validation, CORS configuration, and the greeting MCP tool.

    src/server.ts
    6 collapsed lines
    import 'dotenv/config';
    import cors from 'cors';
    import express, { NextFunction, Request, Response } from 'express';
    import { z } from 'zod';
    import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
    import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
    import { Scalekit } from '@scalekit-sdk/node';
    // Load environment variables
    const PORT = Number(process.env.PORT ?? 3002);
    const SK_ENV_URL = process.env.SK_ENV_URL ?? '';
    const SK_CLIENT_ID = process.env.SK_CLIENT_ID ?? '';
    const SK_CLIENT_SECRET = process.env.SK_CLIENT_SECRET ?? '';
    const EXPECTED_AUDIENCE = process.env.EXPECTED_AUDIENCE ?? '';
    const PROTECTED_RESOURCE_METADATA = process.env.PROTECTED_RESOURCE_METADATA ?? '';
    // Use case: Configure OAuth resource metadata URL for MCP clients
    // This allows MCP clients to discover authorization requirements via WWW-Authenticate header
    // Security: The WWW-Authenticate header signals to clients where to obtain tokens
    const RESOURCE_METADATA_URL = `http://localhost:${PORT}/.well-known/oauth-protected-resource`;
    // WWW-Authenticate header for 401 responses
    const WWW_HEADER_KEY = 'WWW-Authenticate';
    const WWW_HEADER_VALUE = `Bearer realm="OAuth", resource_metadata="${RESOURCE_METADATA_URL}"`;
    // Initialize Scalekit client for token validation
    // Security: Use SDK to validate JWT signatures and claims
    // This prevents accepting forged or tampered tokens
    const scalekit = new Scalekit(SK_ENV_URL, SK_CLIENT_ID, SK_CLIENT_SECRET);
    // Initialize MCP server with greeting tool
    // Context: The McpServer handles MCP protocol details while Express handles HTTP routing
    const server = new McpServer({ name: 'Greeting MCP', version: '1.0.0' });
    // Use case: Simple greeting tool demonstrating OAuth-protected MCP operations
    // Context: This tool is protected by the authentication middleware applied to all routes
    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!`
    }
    ]
    })
    );
    // Initialize Express application
    const app = express();
    // Enable CORS for cross-origin MCP clients
    // Use case: Allow MCP clients from different origins to connect
    app.use(cors({ origin: true, credentials: false }));
    // Parse JSON request bodies
    // Context: MCP protocol uses JSON-RPC format
    app.use(express.json());
    // Use case: Expose OAuth resource metadata for MCP client discovery
    // This endpoint allows clients to discover authorization requirements and server capabilities
    // Context: MCP clients use this metadata to initiate the OAuth flow
    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));
    });
    // Use case: Health check endpoint for monitoring and load balancers
    // Context: Keep this separate from protected endpoints for deployment health checks
    app.get('/health', (_req: Request, res: Response) => {
    res.json({ status: 'healthy' });
    });
    // Security: Validate Bearer tokens on all protected endpoints
    // Public endpoints (health, metadata) are exempt from authentication
    // This prevents unauthorized access to MCP tools and operations
    app.use(async (req: Request, res: Response, next: NextFunction) => {
    // Allow public endpoints without authentication
    // Use case: Health checks for monitoring; metadata for client discovery
    if (req.path === '/.well-known/oauth-protected-resource' || req.path === '/health') {
    next();
    return;
    }
    // Extract Bearer token from Authorization header
    // Use case: OAuth 2.1 Bearer token format (RFC 6750)
    // Security: Reject requests without valid Bearer token prefix
    const header = req.headers.authorization;
    const token = header?.startsWith('Bearer ')
    ? header.slice('Bearer '.length).trim()
    : undefined;
    if (!token) {
    res.status(401)
    .set(WWW_HEADER_KEY, WWW_HEADER_VALUE)
    .json({ error: 'Missing Bearer token' });
    return;
    }
    try {
    // Validate token using Scalekit SDK
    // Security: Verifies signature, expiration, issuer, and audience claims
    // Context: This critical step prevents accepting tokens from other issuers
    await scalekit.validateToken(token, { audience: [EXPECTED_AUDIENCE] });
    next();
    } catch (error) {
    res.status(401)
    .set(WWW_HEADER_KEY, WWW_HEADER_VALUE)
    .json({ error: 'Token validation failed' });
    }
    });
    // Handle MCP protocol requests at root path
    // Use case: Process authenticated MCP tool requests using StreamableHTTPServerTransport
    // Context: The transport layer handles MCP JSON-RPC communication
    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' });
    }
    });
    // Start the Express server
    app.listen(PORT, () => {
    console.log(`MCP server running on http://localhost:${PORT}`);
    });
  8. Start the Express server in development mode with auto-reload enabled. The server will listen on http://localhost:3002/ and display logs indicating Express is ready to receive authenticated MCP requests.

    Terminal
    npm run dev

    The server starts on http://localhost:3002/ and logs indicate Express is ready. The MCP endpoint at / accepts authenticated POST requests, and the metadata endpoint is accessible at /.well-known/oauth-protected-resource.

  9. Test your server end-to-end using the MCP Inspector to verify the OAuth flow works correctly. This allows you to see the authentication handshake and test calling your MCP tools with validated tokens.

    Terminal
    npx @modelcontextprotocol/inspector@latest

    In the Inspector UI:

    1. Enter your MCP Server URL: http://localhost:3002/
    2. Click Connect to initiate the OAuth flow
    3. Authenticate with Scalekit when prompted
    4. Run the greet_user tool with any name

    MCP Inspector

You now have a working Express.js MCP server with Scalekit-protected OAuth authentication. Extend this implementation by adding more MCP tools using server.tool() with Zod schema validation, implementing scope-based authorization using custom middleware, integrating with your existing Express application, or adding features like rate limiting and request logging using Express’s middleware ecosystem.