Logo

Bridger Tower / Designer and Software Engineer

How to deploy a Node.js MCP Server on Vercel

This guide covers deploying a Node.js MCP Server on Vercel.

The Model Context Protocol (MCP) enables AI assistants to interact with external tools and data through a standardized interface. While many MCP servers are designed to run locally, deploying them to cloud platforms like Vercel opens up new possibilities for multi-user access and scalable integrations.

This guide walks through the complete process of adapting and deploying a Node.js MCP server to Vercel, covering both simple deployments and more complex multi-user scenarios with authentication and storage.

Quick Start (Simple Deployment)

For a basic deployment without authentication, you only need:

  1. Install @vercel/mcp-adapter and @modelcontextprotocol/sdk
  2. Create app/api/[transport]/route.ts with your MCP server logic
  3. Deploy to Vercel

The rest of this guide covers advanced scenarios for multi-user production deployments.

Understanding the Challenge

Traditional MCP servers use stdio transport for local communication between the AI client and server. However, web deployment requires HTTP-based transport (specifically Server-Sent Events/SSE) and introduces several new requirements:

  • User Authentication: Multiple users need secure access without sharing credentials
  • Session Management: Stateful user sessions across serverless function calls
  • Token Storage: Secure storage for user-specific API tokens
  • CORS and Security: Proper headers and security measures for web access

Architecture Overview

The deployed architecture transforms a local MCP server into a multi-user web service:

AI Client (Claude/Cursor) → HTTPS → Vercel Functions → MCP Tools → External APIs ↓ User Sessions (Redis/KV)

Key components:

  • Authentication Layer: OAuth 2.1 flow for user login
  • Session Management: JWT tokens with secure storage
  • MCP Adapter: Vercel's @vercel/mcp-adapter for HTTP transport (using SSE)
  • Storage Backend: Redis or Vercel KV for user data

Project Structure

A Vercel-deployed MCP server follows this structure:

text
1project/
2├── api/ # Vercel Functions
3│ ├── mcp.ts # Main MCP endpoint
4│ ├── auth/ # Authentication endpoints
5│ │ ├── login.ts
6│ │ ├── callback.ts
7│ │ └── profile.ts
8│ └── index.ts # Landing page
9├── src/ # Core MCP logic
10│ ├── tools/ # MCP tools
11│ ├── resources/ # MCP resources
12│ └── utils/ # Authentication & storage
13├── public/ # Static files
14├── vercel.json # Vercel configuration
15└── package.json

Step 1: Installing Dependencies

Start by installing the required dependencies:

bash
1npm install @vercel/mcp-adapter @modelcontextprotocol/sdk
2npm install jose redis zod # Optional: for authentication and storage
3npm install --save-dev @types/node typescript

Key packages:

  • @vercel/mcp-adapter: Enables MCP over HTTP using SSE transport
  • @modelcontextprotocol/sdk: Official TypeScript SDK for MCP
  • jose: JWT token management (optional, for authentication)
  • redis: Storage backend (optional, or use @vercel/kv for Vercel KV)
  • zod: Schema validation (included with MCP SDK)

Step 2: Vercel Configuration

Create a vercel.json file to configure the deployment:

json
1{
2 "$schema": "https://openapi.vercel.sh/vercel.json",
3 "functions": {
4 "app/api/[transport]/route.ts": {
5 "maxDuration": 60
6 }
7 }
8}

The maxDuration setting is important for MCP servers as they may need to handle long-running operations. The @vercel/mcp-adapter handles the routing automatically through the [transport] dynamic route.

Step 3: Storage Adapter (Optional)

Note: This step is only needed if you're implementing custom authentication and user sessions. For simple deployments, you can skip the storage and authentication sections.

Create a flexible storage system that supports multiple backends:

typescript
1// src/utils/storage.ts
2interface StorageAdapter {
3 set(key: string, value: any, options?: { ex?: number }): Promise<void>;
4 get<T>(key: string): Promise<T | null>;
5 del(key: string): Promise<void>;
6}
7
8class RedisAdapter implements StorageAdapter {
9 private client: any;
10
11 constructor() {
12 this.initRedis();
13 }
14
15 private async initRedis() {
16 const { createClient } = await import("redis");
17 this.client = createClient({
18 url: process.env.REDIS_URL,
19 });
20 await this.client.connect();
21 }
22
23 async set(key: string, value: any, options?: { ex?: number }): Promise<void> {
24 const serialized = JSON.stringify(value);
25 if (options?.ex) {
26 await this.client.setEx(key, options.ex, serialized);
27 } else {
28 await this.client.set(key, serialized);
29 }
30 }
31
32 async get<T>(key: string): Promise<T | null> {
33 const value = await this.client.get(key);
34 return value ? JSON.parse(value) : null;
35 }
36
37 async del(key: string): Promise<void> {
38 await this.client.del(key);
39 }
40}
41
42function createStorageAdapter(): StorageAdapter {
43 if (process.env.REDIS_URL) {
44 return new RedisAdapter();
45 }
46 // Add other adapters (Vercel KV, etc.)
47 throw new Error("No storage configuration found");
48}

Step 4: User Authentication

Note: The authentication implementation shown below is a custom approach for multi-user deployments. The official MCP specification doesn't define authentication mechanisms, as MCP is traditionally used for local, single-user scenarios.

Implement OAuth 2.1 authentication with JWT session management:

typescript
1// src/utils/auth.ts
2import { SignJWT, jwtVerify } from "jose";
3
4export class UserAuthManager {
5 private static JWT_SECRET = process.env.JWT_SECRET || "";
6 private static storage = createStorageAdapter();
7
8 static async createSessionToken(userId: string): Promise<string> {
9 const secret = new TextEncoder().encode(this.JWT_SECRET);
10
11 return await new SignJWT({ userId })
12 .setProtectedHeader({ alg: "HS256" })
13 .setIssuedAt()
14 .setExpirationTime("7d")
15 .sign(secret);
16 }
17
18 static async verifySessionToken(
19 token: string
20 ): Promise<{ userId: string } | null> {
21 try {
22 const secret = new TextEncoder().encode(this.JWT_SECRET);
23 const { payload } = await jwtVerify(token, secret);
24
25 if (typeof payload.userId === "string") {
26 return { userId: payload.userId };
27 }
28 return null;
29 } catch (error) {
30 return null;
31 }
32 }
33
34 static async storeUserSession(session: UserSession): Promise<void> {
35 const key = `user_session:${session.userId}`;
36 await this.storage.set(key, session, { ex: 7 * 24 * 60 * 60 });
37 }
38
39 static async authenticateUser(
40 authHeader: string | null
41 ): Promise<UserSession | null> {
42 const token = this.extractBearerToken(authHeader);
43 if (!token) return null;
44
45 const decoded = await this.verifySessionToken(token);
46 if (!decoded) return null;
47
48 return await this.getUserSession(decoded.userId);
49 }
50
51 private static extractBearerToken(authHeader: string | null): string | null {
52 if (!authHeader || !authHeader.startsWith("Bearer ")) {
53 return null;
54 }
55 return authHeader.substring(7);
56 }
57}

Step 5: OAuth Endpoints

Create authentication endpoints for the OAuth flow:

typescript
1// api/auth/login.ts
2export default async function handler(
3 req: NextApiRequest,
4 res: NextApiResponse
5) {
6 if (req.method !== "GET") {
7 return res.status(405).json({ error: "Method not allowed" });
8 }
9
10 try {
11 const state = await UserAuthManager.generateOAuthState();
12
13 res.setHeader("Set-Cookie", [
14 `oauth_state=${state}; HttpOnly; Secure; SameSite=Strict; Max-Age=600; Path=/`,
15 ]);
16
17 const authUrl = UserAuthManager.generateMetaOAuthUrl(state);
18
19 res.status(200).json({
20 success: true,
21 authUrl: authUrl,
22 message: "Redirect user to this URL to begin OAuth flow",
23 });
24 } catch (error) {
25 res.status(500).json({
26 success: false,
27 error: "Failed to generate authorization URL",
28 });
29 }
30}
typescript
1// api/auth/callback.ts
2export default async function handler(
3 req: NextApiRequest,
4 res: NextApiResponse
5) {
6 const { code, state } = req.query;
7
8 // Validate state parameter (CSRF protection)
9 // Exchange code for access token
10 // Create user session
11 // Return success with user info and MCP endpoint
12}

Step 6: MCP Server Adaptation

Adapt your existing MCP server to work with Vercel's HTTP transport:

typescript
1// app/api/[transport]/route.ts
2import { createMcpHandler } from "@vercel/mcp-adapter";
3import { z } from "zod";
4
5const handler = createMcpHandler(
6 (server) => {
7 // Register your MCP tools
8 server.tool(
9 "example_tool",
10 "Description of what this tool does",
11 {
12 param: z.string().describe("Parameter description"),
13 },
14 async ({ param }) => {
15 // Your tool implementation
16 const result = await someApiCall(param);
17 return {
18 content: [
19 {
20 type: "text",
21 text: JSON.stringify(result, null, 2),
22 },
23 ],
24 };
25 }
26 );
27
28 // Add health check tool
29 server.tool("health_check", "Check server health", {}, async () => {
30 return {
31 content: [
32 {
33 type: "text",
34 text: JSON.stringify(
35 {
36 status: "healthy",
37 timestamp: new Date().toISOString(),
38 },
39 null,
40 2
41 ),
42 },
43 ],
44 };
45 });
46 },
47 {
48 // Optional server configuration
49 },
50 {
51 basePath: "/api", // Must match your API route structure
52 maxDuration: 60,
53 verboseLogs: true,
54 }
55);
56
57export { handler as GET, handler as POST };

Note: For authentication in production deployments, you'll need to implement custom middleware or use Vercel's authentication features, as the MCP adapter itself doesn't include built-in authentication mechanisms.

Step 7: Environment Variables

Configure the required environment variables in Vercel:

bash
1# OAuth Configuration
2META_APP_ID=your_oauth_app_id
3META_APP_SECRET=your_oauth_app_secret
4META_REDIRECT_URI=https://your-project.vercel.app/api/auth/callback
5
6# JWT Security
7JWT_SECRET=your_secure_32_byte_random_key
8
9# Storage
10REDIS_URL=redis://user:pass@host:port
11
12# API Configuration
13API_BASE_URL=https://api.example.com

Generate a secure JWT secret:

bash
1node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Step 8: User Interface

Create a simple landing page for user authentication:

typescript
1// api/index.ts
2export default function handler(req: NextApiRequest, res: NextApiResponse) {
3 const html = `
4<!DOCTYPE html>
5<html>
6<head>
7 <title>MCP Server</title>
8 <style>
9 body { font-family: system-ui; max-width: 600px; margin: 0 auto; padding: 2rem; }
10 .button { background: #0070f3; color: white; padding: 12px 24px; border: none; border-radius: 6px; cursor: pointer; }
11 </style>
12</head>
13<body>
14 <h1>MCP Server</h1>
15 <p>Authenticate with your account to get started.</p>
16 <button class="button" onclick="startLogin()">Connect Account</button>
17
18 <script>
19 async function startLogin() {
20 const response = await fetch('/api/auth/login');
21 const data = await response.json();
22 if (data.success) {
23 window.location.href = data.authUrl;
24 }
25 }
26 </script>
27</body>
28</html>
29 `;
30
31 res.setHeader("Content-Type", "text/html");
32 res.status(200).send(html);
33}

Deployment and Testing

Deploy to Vercel:

bash
1# Via CLI
2vercel --prod
3
4# Or connect GitHub repository in Vercel dashboard

Test the deployment:

  1. Visit your deployment URL - should show the login page
  2. Complete OAuth flow - authenticate with your service
  3. Test MCP endpoint with Bearer token:
bash
1curl -H "Authorization: Bearer your_session_token" \
2 -H "Content-Type: application/json" \
3 -d '{"method":"tools/list"}' \
4 https://your-project.vercel.app/api/mcp
  1. Configure MCP client (Claude Desktop, etc.) using the mcp-remote package:
json
1{
2 "mcpServers": {
3 "your-server": {
4 "command": "npx",
5 "args": ["mcp-remote", "https://your-project.vercel.app/api/mcp"]
6 }
7 }
8}

For browser-based clients, use the @modelcontextprotocol/sdk:

typescript
1import { McpClient } from "@modelcontextprotocol/sdk/client";
2import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse";
3
4const client = new McpClient({
5 transport: new SSEClientTransport("https://your-project.vercel.app/api/mcp"),
6});

Security Considerations

  • User Isolation: Each user's tokens and data are completely isolated
  • Token Security: JWT tokens are signed and validated on every request
  • CSRF Protection: OAuth state parameters prevent cross-site request forgery
  • Secure Storage: User tokens are encrypted in transit and at rest
  • Session Management: Configurable token expiration and refresh

Common Issues and Solutions

Build Errors:

  • Ensure all dependencies are in package.json
  • Don't specify Node.js runtime versions in vercel.json
  • Add a public/index.html file if Vercel expects static assets

Authentication Failures:

  • Verify OAuth redirect URIs match exactly
  • Check that environment variables are set correctly
  • Ensure OAuth app is in "Live" mode or user is added as tester

Storage Connection Issues:

  • Test Redis/KV connectivity independently
  • Check that connection strings are formatted correctly
  • Verify network access and authentication

Conclusion

Deploying MCP servers to Vercel enables powerful integrations accessible over HTTP while leveraging serverless infrastructure. The key architectural changes involve:

  1. Replacing stdio transport with SSE via @vercel/mcp-adapter
  2. Adapting the server logic for serverless execution
  3. Optional: Implementing custom authentication for multi-user scenarios
  4. Optional: Adding persistent storage for user sessions and data

This approach transforms local MCP tools into web-accessible services. For production multi-user deployments, additional considerations around authentication, authorization, and data isolation are necessary.

Additional Resources

© Bridger Tower, 2025