# Express.js quickstart

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 <a href="https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-node" target="_blank" rel="noreferrer">GitHub</a>.

**Prerequisites**

- A [Scalekit account](https://app.scalekit.com) 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

<details>
<summary><IconTdesignSequence style="display: inline; width: 1rem; height: 1rem; vertical-align: middle; margin-right: 0.5rem;" /> Review the Express.js MCP authorization flow</summary>

```d2 pad=36
title: "Express.js MCP with Scalekit" {
  near: top-center
  shape: text
  style.font-size: 18
}

shape: sequence_diagram

MCP Client -> MCP Server: Request to call tool
MCP Server -> MCP Client: 401 Unauthorized + WWW-Authenticate header
MCP Client -> Scalekit: Exchange code for access token
Scalekit -> MCP Client: Issue Bearer token
MCP Client -> MCP Server: POST /mcp with Bearer token
MCP Server -> MCP Server: Validate token via Express middleware
MCP Server -> MCP Client: Tool response
```

</details>

1. ## Register your MCP server in Scalekit

   Create a protected resource entry so Scalekit can issue tokens that your custom Express middleware validates.

1. Navigate to **[Dashboard](https://app.scalekit.com) > 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](@/assets/docs/guides/mcp/greeting-mcp-register.png)

   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](@/assets/docs/guides/mcp/greeting-protected-json.png)

2. ## Create your project directory

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

   ```bash title="Terminal" frame="terminal"
   mkdir express-mcp-node
   cd express-mcp-node
   ```

3. ## Add package dependencies

   Create a `package.json` with scripts and all required dependencies for Express, TypeScript, and the MCP SDK.

   ```bash title="Terminal" frame="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. ## Configure TypeScript

   Add a TypeScript configuration file optimized for ES2022 modules and strict type checking.

   ```bash title="Terminal" frame="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 dependencies

   Install all packages declared in `package.json`.

   ```bash title="Terminal" frame="terminal"
   npm install
   ```
**Package manager choice:** This guide uses <code>npm</code>, but you can also use <code>yarn</code>, <code>pnpm</code>, or <code>bun</code> if you prefer.

6. ## Configure environment variables

   Create a `.env` file with your Scalekit credentials and the protected resource metadata from step 1.

   ```bash title="Terminal" frame="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
   ```

   | Variable                        | Description                                                                   |
   | ------------------------------- | ----------------------------------------------------------------------------- |
   | `PORT`                          | Local port for the Express server. Must match the Server URL registered in Scalekit (defaults to `3002`). |
   | `SK_ENV_URL`                    | Your Scalekit environment URL from **Dashboard > Settings > API Credentials**                   |
   | `SK_CLIENT_ID`                  | Client ID from **Dashboard > Settings > API Credentials**. Used with `SK_CLIENT_SECRET` to initialize the SDK. |
   | `SK_CLIENT_SECRET`              | Client secret from **Dashboard > Settings > API Credentials**. Keep this secret and rotate regularly. |
   | `MCP_SERVER_ID`                 | The MCP server ID from **Dashboard > MCP Servers**. Not directly used in this implementation but documented for reference. |
   | `PROTECTED_RESOURCE_METADATA`   | The complete OAuth resource metadata JSON from step 1. Clients use this to discover authorization requirements. |
   | `EXPECTED_AUDIENCE`             | The audience value that tokens must include. Should match your server's public URL (e.g., `http://localhost:3002/`). |
**Protect your credentials:** Never commit <code>.env</code> to version control. Add it to <code>.gitignore</code> immediately and use a secret manager in production (e.g., AWS Secrets Manager, HashiCorp Vault, or your deployment platform's secrets service).

7. ## Implement the Express MCP server

   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.

   ```typescript title="src/server.ts" wrap collapse={1-6, 219-225} {227-233, 234-236, 286-316} {"Authentication middleware": 286-316} {"Token validation": 306-315}
   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

   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.

   ```bash title="Terminal" frame="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`.
**Production deployment:** For production deployment, build the TypeScript code with <code>npm run build</code>, then start the compiled server with <code>npm start</code> behind a reverse proxy like Nginx or use a process manager like PM2.

9. ## Connect with MCP Inspector

   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.

   ```bash title="Terminal" frame="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](@/assets/docs/guides/mcp/mcp-inspector-google.png)
**Debugging token validation:** The middleware validates every request's token. If you see authentication errors: verify environment variables match dashboard settings, confirm the token audience matches <code>EXPECTED_AUDIENCE</code>, and check token expiration in the Inspector network tab.

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.