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-adapter
and@modelcontextprotocol/sdk
- Create
app/api/[transport]/route.ts
with 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-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:
1project/2├── api/ # Vercel Functions3│ ├── mcp.ts # Main MCP endpoint4│ ├── auth/ # Authentication endpoints5│ │ ├── login.ts6│ │ ├── callback.ts7│ │ └── profile.ts8│ └── index.ts # Landing page9├── src/ # Core MCP logic10│ ├── tools/ # MCP tools11│ ├── resources/ # MCP resources12│ └── utils/ # Authentication & storage13├── public/ # Static files14├── vercel.json # Vercel configuration15└── package.json
Step 1: Installing Dependencies
Start by installing the required dependencies:
1npm install @vercel/mcp-adapter @modelcontextprotocol/sdk2npm install jose redis zod # Optional: for authentication and storage3npm install --save-dev @types/node typescript
Key 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/kv
for Vercel KV)zod
: Schema validation (included with MCP SDK)
Step 2: Vercel Configuration
Create a vercel.json
file to configure the deployment:
1{2 "$schema": "https://openapi.vercel.sh/vercel.json",3 "functions": {4 "app/api/[transport]/route.ts": {5 "maxDuration": 606 }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:
1// src/utils/storage.ts2interface 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}78class RedisAdapter implements StorageAdapter {9 private client: any;1011 constructor() {12 this.initRedis();13 }1415 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 }2223 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 }3132 async get<T>(key: string): Promise<T | null> {33 const value = await this.client.get(key);34 return value ? JSON.parse(value) : null;35 }3637 async del(key: string): Promise<void> {38 await this.client.del(key);39 }40}4142function 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:
1// src/utils/auth.ts2import { SignJWT, jwtVerify } from "jose";34export class UserAuthManager {5 private static JWT_SECRET = process.env.JWT_SECRET || "";6 private static storage = createStorageAdapter();78 static async createSessionToken(userId: string): Promise<string> {9 const secret = new TextEncoder().encode(this.JWT_SECRET);1011 return await new SignJWT({ userId })12 .setProtectedHeader({ alg: "HS256" })13 .setIssuedAt()14 .setExpirationTime("7d")15 .sign(secret);16 }1718 static async verifySessionToken(19 token: string20 ): Promise<{ userId: string } | null> {21 try {22 const secret = new TextEncoder().encode(this.JWT_SECRET);23 const { payload } = await jwtVerify(token, secret);2425 if (typeof payload.userId === "string") {26 return { userId: payload.userId };27 }28 return null;29 } catch (error) {30 return null;31 }32 }3334 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 }3839 static async authenticateUser(40 authHeader: string | null41 ): Promise<UserSession | null> {42 const token = this.extractBearerToken(authHeader);43 if (!token) return null;4445 const decoded = await this.verifySessionToken(token);46 if (!decoded) return null;4748 return await this.getUserSession(decoded.userId);49 }5051 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:
1// api/auth/login.ts2export default async function handler(3 req: NextApiRequest,4 res: NextApiResponse5) {6 if (req.method !== "GET") {7 return res.status(405).json({ error: "Method not allowed" });8 }910 try {11 const state = await UserAuthManager.generateOAuthState();1213 res.setHeader("Set-Cookie", [14 `oauth_state=${state}; HttpOnly; Secure; SameSite=Strict; Max-Age=600; Path=/`,15 ]);1617 const authUrl = UserAuthManager.generateMetaOAuthUrl(state);1819 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}
1// api/auth/callback.ts2export default async function handler(3 req: NextApiRequest,4 res: NextApiResponse5) {6 const { code, state } = req.query;78 // Validate state parameter (CSRF protection)9 // Exchange code for access token10 // Create user session11 // Return success with user info and MCP endpoint12}
Step 6: MCP Server Adaptation
Adapt your existing MCP server to work with Vercel's HTTP transport:
1// app/api/[transport]/route.ts2import { createMcpHandler } from "@vercel/mcp-adapter";3import { z } from "zod";45const handler = createMcpHandler(6 (server) => {7 // Register your MCP tools8 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 implementation16 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 );2728 // Add health check tool29 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 241 ),42 },43 ],44 };45 });46 },47 {48 // Optional server configuration49 },50 {51 basePath: "/api", // Must match your API route structure52 maxDuration: 60,53 verboseLogs: true,54 }55);5657export { 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:
1# OAuth Configuration2META_APP_ID=your_oauth_app_id3META_APP_SECRET=your_oauth_app_secret4META_REDIRECT_URI=https://your-project.vercel.app/api/auth/callback56# JWT Security7JWT_SECRET=your_secure_32_byte_random_key89# Storage10REDIS_URL=redis://user:pass@host:port1112# API Configuration13API_BASE_URL=https://api.example.com
Generate a secure JWT secret:
1node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Step 8: User Interface
Create a simple landing page for user authentication:
1// api/index.ts2export 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>1718 <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 `;3031 res.setHeader("Content-Type", "text/html");32 res.status(200).send(html);33}
Deployment and Testing
Deploy to Vercel:
1# Via CLI2vercel --prod34# Or connect GitHub repository in Vercel dashboard
Test the deployment:
- Visit your deployment URL - should show the login page
- Complete OAuth flow - authenticate with your service
- Test MCP endpoint with Bearer token:
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
- Configure MCP client (Claude Desktop, etc.) using the
mcp-remote
package:
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
:
1import { McpClient } from "@modelcontextprotocol/sdk/client";2import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse";34const 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:
- 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.