Bridger Tower LogoBridger Tower Logo
Bridger Tower

Design Generalist

How to deploy 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:

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.json

Step 1: Installing Dependencies

Start by installing the required dependencies:

npm install @vercel/mcp-adapter @modelcontextprotocol/sdk
npm install jose redis zod # Optional: for authentication and storage
npm 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:

{
"$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.ts
interface 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.ts
import { 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.ts
export 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.ts
export 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.ts
import { 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 Configuration
META_APP_ID=your_oauth_app_id
META_APP_SECRET=your_oauth_app_secret
META_REDIRECT_URI=https://your-project.vercel.app/api/auth/callback
# JWT Security
JWT_SECRET=your_secure_32_byte_random_key
# Storage
REDIS_URL=redis://user:pass@host:port
# API Configuration
API_BASE_URL=https://api.example.com

Generate 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.ts
export 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 CLI
vercel --prod
# 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:
curl -H "Authorization: Bearer your_session_token" \
-H "Content-Type: application/json" \
-d '{"method":"tools/list"}' \
https://your-project.vercel.app/api/mcp
  1. Configure MCP client (Claude Desktop, etc.) using the mcp-remote package:
{
"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.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