mcp-vercel
Deploy MCP Server on Vercel
Create a production-ready remote MCP server on Vercel using Next.js and mcp-handler. The server communicates via Streamable HTTP and works with Claude Desktop, claude.ai, Smithery, and any MCP client.
Why this approach
Vercel's serverless functions are ideal for MCP servers because MCP's Streamable HTTP transport is stateless — each request is independent, which maps perfectly to serverless. No persistent connections needed. The mcp-handler package from Vercel handles all the protocol details.
Quick setup
1. Install dependencies
npm install mcp-handler @modelcontextprotocol/sdk zod
2. Create the MCP route
Create app/api/mcp/route.ts:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { createMcpHandler } from 'mcp-handler';
import { z } from 'zod';
const handler = createMcpHandler(
(server: McpServer) => {
// Register your tools here
server.tool(
'example_tool',
'What this tool does — be specific',
{ query: z.string().describe('What the parameter is for') },
{ readOnlyHint: true, destructiveHint: false, title: 'Example Tool' },
async ({ query }) => ({
content: [{ type: 'text', text: `Result for: ${query}` }],
}),
);
},
{
serverInfo: { name: 'my-server', version: '1.0.0' },
},
{
streamableHttpEndpoint: '/api/mcp',
maxDuration: 60,
},
);
export { handler as GET, handler as POST, handler as DELETE };
3. Deploy and test
vercel deploy --prod
# Verify
curl -X POST https://your-app.vercel.app/api/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'
You should get back serverInfo with your server name and version.
Tool design
Safety annotations (required)
Every tool must have annotations. MCP clients use these to decide how cautiously to invoke tools.
// Read-only (search, get, list, fetch)
{ readOnlyHint: true, destructiveHint: false, title: 'Search Items' }
// Write but not destructive (create, add, update)
{ readOnlyHint: false, destructiveHint: false, title: 'Create Item' }
// Destructive (delete, remove, overwrite)
{ readOnlyHint: false, destructiveHint: true, title: 'Delete Item' }
Parameter descriptions
Every parameter needs a .describe() — this is how MCP clients know what to pass.
{
query: z.string().describe('Search query text'),
limit: z.number().optional().default(10).describe('Max results to return'),
type: z.enum(['artist', 'album', 'track']).describe('Type of content'),
}
MCP prompts (optional but recommended)
Prompt templates improve discoverability on Smithery and give users ready-made starting points.
server.prompt('find_items', 'Search for items by name',
{ name: z.string().describe('Item name') },
({ name }) => ({
messages: [{
role: 'user' as const,
content: { type: 'text' as const, text: `Find ${name} and show details` },
}],
}),
);
Routing — the streamableHttpEndpoint gotcha
Use streamableHttpEndpoint, NOT basePath:
// CORRECT — endpoint at /api/mcp
{ streamableHttpEndpoint: '/api/mcp' }
// WRONG — creates endpoint at /api/mcp/mcp (doubled path)
{ basePath: '/api/mcp' }
The basePath option appends /mcp to whatever you give it. Since your route file is already at app/api/mcp/route.ts, that creates /api/mcp/mcp.
Vercel deployment pitfalls
Root Directory isolation
If your Vercel project uses a Root Directory (like site/), the deployed function CANNOT access files outside that directory. This means import from '../../dist/' will fail at runtime even if it compiles locally.
Solution: Copy compiled files into the site directory and commit them. Use a prebuild script to keep them in sync:
// scripts/copy-deps.js
const fs = require('fs');
const path = require('path');
const src = path.resolve(__dirname, '../../dist');
const dest = path.resolve(__dirname, '../lib/deps');
fs.mkdirSync(dest, { recursive: true });
for (const file of fs.readdirSync(src)) {
if (file.endsWith('.js') || file.endsWith('.d.ts')) {
fs.copyFileSync(path.join(src, file), path.join(dest, file));
}
}
Add to package.json: "prebuild": "node scripts/copy-deps.js"
Serverless read-only filesystem
Vercel functions run on a read-only filesystem with no home directory. If your code writes files (sessions, temp data), wrap in try/catch:
try {
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(filepath, data);
} catch {
// Serverless environment — skip filesystem writes
}
Turbopack CJS/ESM mismatch
If importing CommonJS .js files while the parent package.json doesn't have "type": "module", Turbopack will error. Solution: import from compiled .js files bundled within the site directory, not from TypeScript source files in the parent.
Adding authentication
For OAuth-protected servers, see the mcp-oauth skill which covers the complete OAuth 2.0 PKCE flow with withMcpAuth, including dynamic client registration and token storage.
Publishing to Smithery
After deploying, publish to Smithery for broader distribution:
- Go to https://smithery.ai/new
- Enter your MCP server URL
- Choose a namespace/server-id
- Smithery scans your tools automatically
If your server requires auth, Smithery will prompt you to connect during scanning.
Reference
More from lucaperret/agent-skills
macos-calendar
Create, list, and manage macOS Calendar events via AppleScript. Use when the user asks to add a reminder, schedule an event, create a calendar entry, set a deadline, or anything involving Apple Calendar on macOS. Triggers on requests like "remind me in 3 days", "add to my calendar", "schedule a meeting next Monday at 2pm", "create a recurring weekly event". macOS only.
265macos-reminders
Create, list, and manage macOS Reminders via AppleScript. Use when the user asks to create a reminder, add a to-do, make a task, set a reminder for something, or anything involving Apple Reminders on macOS. Triggers on requests like "remind me to buy milk", "add a to-do to call the dentist", "create a reminder for Friday", "add to my shopping list", "flag this as important". macOS only.
44macos-notes
Create, read, search, and manage macOS Notes via AppleScript. Use when the user asks to take a note, jot something down, save an idea, create meeting notes, read a note, search notes, or anything involving Apple Notes on macOS. Triggers on requests like "note this down", "save this as a note", "create a note about X", "show my notes", "search my notes for X", "what did I write about X". macOS only.
32mcp-oauth
Add OAuth 2.0 PKCE authentication to a remote MCP server. Use this skill whenever the user wants to add authentication to an MCP server, protect MCP tools with OAuth, implement login flow for an MCP connector, add user auth to an MCP endpoint, or set up token-based access for MCP. Also triggers on: 'MCP OAuth', 'MCP authentication', 'withMcpAuth', 'MCP login flow', 'protect MCP endpoint', 'MCP token auth', 'dynamic client registration MCP', 'Claude connector OAuth'. Even if the user just says 'add auth to my MCP server' or 'my MCP server needs login', use this skill.
7anthropic-connector-submit
Prepare and submit an MCP server to Anthropic's Connectors Directory. Use this skill whenever the user wants to list their MCP server on Claude's connector marketplace, submit to Anthropic's directory, get their MCP server featured in Claude Desktop or claude.ai, or prepare a connector submission. Also triggers on: 'submit to Anthropic', 'Claude connector directory', 'list on Claude marketplace', 'Anthropic MCP submission', 'get listed in Claude connectors', 'MCP directory submission form'. Even if the user just says 'how do I get my MCP server in the Claude directory' or 'I want Claude users to find my server', use this skill.
7