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:
- Install
@vercel/mcp-adapterand@modelcontextprotocol/sdk - Create
app/api/[transport]/route.tswith your MCP server logic - 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-adapterfor HTTP transport (using SSE) - Storage Backend: Redis or Vercel KV for user data
Project Structure
A Vercel-deployed MCP server follows this structure:
project/├── api/ # Vercel Functions│ ├── mcp.ts # Main MCP endpoint│ ├── auth/ # Authentication endpoints│ │ ├── login.ts│ │ ├── callback.ts│ │ └── profile.ts│ └── index.ts # Landing page├── src/ # Core MCP logic│ ├── tools/ # MCP tools│ ├── resources/ # MCP resources│ └── utils/ # Authentication & storage├── public/ # Static files├── vercel.json # Vercel configuration└── package.jsonStep 1: Installing Dependencies
Start by installing the required dependencies:
npm install @vercel/mcp-adapter @modelcontextprotocol/sdknpm install jose redis zod # Optional: for authentication and storagenpm install --save-dev @types/node typescriptKey packages:
@vercel/mcp-adapter: Enables MCP over HTTP using SSE transport@modelcontextprotocol/sdk: Official TypeScript SDK for MCPjose: JWT token management (optional, for authentication)redis: Storage backend (optional, or use@vercel/kvfor Vercel KV)zod: Schema validation (included with MCP SDK)
Step 2: Vercel Configuration
Create a vercel.json file to configure the deployment:
{ "$schema": "https://openapi.vercel.sh/vercel.json", "functions": { "app/api/[transport]/route.ts": { "maxDuration": 60 } }}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:
// src/utils/storage.tsinterface StorageAdapter { set(key: string, value: any, options?: { ex?: number }): Promise<void>; get<T>(key: string): Promise<T | null>; del(key: string): Promise<void>;}
class RedisAdapter implements StorageAdapter { private client: any;
constructor() { this.initRedis(); }
private async initRedis() { const { createClient } = await import("redis"); this.client = createClient({ url: process.env.REDIS_URL, }); await this.client.connect(); }
async set(key: string, value: any, options?: { ex?: number }): Promise<void> { const serialized = JSON.stringify(value); if (options?.ex) { await this.client.setEx(key, options.ex, serialized); } else { await this.client.set(key, serialized); } }
async get<T>(key: string): Promise<T | null> { const value = await this.client.get(key); return value ? JSON.parse(value) : null; }
async del(key: string): Promise<void> { await this.client.del(key); }}
function createStorageAdapter(): StorageAdapter { if (process.env.REDIS_URL) { return new RedisAdapter(); } // Add other adapters (Vercel KV, etc.) throw new Error("No storage configuration found");}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:
// src/utils/auth.tsimport { SignJWT, jwtVerify } from "jose";
export class UserAuthManager { private static JWT_SECRET = process.env.JWT_SECRET || ""; private static storage = createStorageAdapter();
static async createSessionToken(userId: string): Promise<string> { const secret = new TextEncoder().encode(this.JWT_SECRET);
return await new SignJWT({ userId }) .setProtectedHeader({ alg: "HS256" }) .setIssuedAt() .setExpirationTime("7d") .sign(secret); }
static async verifySessionToken( token: string ): Promise<{ userId: string } | null> { try { const secret = new TextEncoder().encode(this.JWT_SECRET); const { payload } = await jwtVerify(token, secret);
if (typeof payload.userId === "string") { return { userId: payload.userId }; } return null; } catch (error) { return null; } }
static async storeUserSession(session: UserSession): Promise<void> { const key = `user_session:${session.userId}`; await this.storage.set(key, session, { ex: 7 * 24 * 60 * 60 }); }
static async authenticateUser( authHeader: string | null ): Promise<UserSession | null> { const token = this.extractBearerToken(authHeader); if (!token) return null;
const decoded = await this.verifySessionToken(token); if (!decoded) return null;
return await this.getUserSession(decoded.userId); }
private static extractBearerToken(authHeader: string | null): string | null { if (!authHeader || !authHeader.startsWith("Bearer ")) { return null; } return authHeader.substring(7); }}Step 5: OAuth Endpoints
Create authentication endpoints for the OAuth flow:
// api/auth/login.tsexport default async function handler( req: NextApiRequest, res: NextApiResponse) { if (req.method !== "GET") { return res.status(405).json({ error: "Method not allowed" }); }
try { const state = await UserAuthManager.generateOAuthState();
res.setHeader("Set-Cookie", [ `oauth_state=${state}; HttpOnly; Secure; SameSite=Strict; Max-Age=600; Path=/`, ]);
const authUrl = UserAuthManager.generateMetaOAuthUrl(state);
res.status(200).json({ success: true, authUrl: authUrl, message: "Redirect user to this URL to begin OAuth flow", }); } catch (error) { res.status(500).json({ success: false, error: "Failed to generate authorization URL", }); }}// api/auth/callback.tsexport default async function handler( req: NextApiRequest, res: NextApiResponse) { const { code, state } = req.query;
// Validate state parameter (CSRF protection) // Exchange code for access token // Create user session // Return success with user info and MCP endpoint}Step 6: MCP Server Adaptation
Adapt your existing MCP server to work with Vercel's HTTP transport:
// app/api/[transport]/route.tsimport { createMcpHandler } from "@vercel/mcp-adapter";import { z } from "zod";
const handler = createMcpHandler( (server) => { // Register your MCP tools server.tool( "example_tool", "Description of what this tool does", { param: z.string().describe("Parameter description"), }, async ({ param }) => { // Your tool implementation const result = await someApiCall(param); return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } );
// Add health check tool server.tool("health_check", "Check server health", {}, async () => { return { content: [ { type: "text", text: JSON.stringify( { status: "healthy", timestamp: new Date().toISOString(), }, null, 2 ), }, ], }; }); }, { // Optional server configuration }, { basePath: "/api", // Must match your API route structure maxDuration: 60, verboseLogs: true, });
export { 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:
# OAuth ConfigurationMETA_APP_ID=your_oauth_app_idMETA_APP_SECRET=your_oauth_app_secretMETA_REDIRECT_URI=https://your-project.vercel.app/api/auth/callback
# JWT SecurityJWT_SECRET=your_secure_32_byte_random_key
# StorageREDIS_URL=redis://user:pass@host:port
# API ConfigurationAPI_BASE_URL=https://api.example.comGenerate a secure JWT secret:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Step 8: User Interface
Create a simple landing page for user authentication:
// api/index.tsexport default function handler(req: NextApiRequest, res: NextApiResponse) { const html = `<!DOCTYPE html><html><head> <title>MCP Server</title> <style> body { font-family: system-ui; max-width: 600px; margin: 0 auto; padding: 2rem; } .button { background: #0070f3; color: white; padding: 12px 24px; border: none; border-radius: 6px; cursor: pointer; } </style></head><body> <h1>MCP Server</h1> <p>Authenticate with your account to get started.</p> <button class="button" onclick="startLogin()">Connect Account</button>
<script> async function startLogin() { const response = await fetch('/api/auth/login'); const data = await response.json(); if (data.success) { window.location.href = data.authUrl; } } </script></body></html> `;
res.setHeader("Content-Type", "text/html"); res.status(200).send(html);}Deployment and Testing
Deploy to Vercel:
# Via CLIvercel --prod
# Or connect GitHub repository in Vercel dashboardTest the deployment:
- Visit your deployment URL - should show the login page
- Complete OAuth flow - authenticate with your service
- Test MCP endpoint with Bearer token:
curl -H "Authorization: Bearer your_session_token" \ -H "Content-Type: application/json" \ -d '{"method":"tools/list"}' \ https://your-project.vercel.app/api/mcp- Configure MCP client (Claude Desktop, etc.) using the
mcp-remotepackage:
{ "mcpServers": { "your-server": { "command": "npx", "args": ["mcp-remote", "https://your-project.vercel.app/api/mcp"] } }}For browser-based clients, use the @modelcontextprotocol/sdk:
import { McpClient } from "@modelcontextprotocol/sdk/client";import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse";
const client = new McpClient({ transport: new SSEClientTransport("https://your-project.vercel.app/api/mcp"),});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.htmlfile 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:
- Replacing stdio transport with SSE via
@vercel/mcp-adapter - Adapting the server logic for serverless execution
- Optional: Implementing custom authentication for multi-user scenarios
- 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.