Build an MCP Server in Next.js for Claude Code (Complete Guide)
Create a production-ready MCP server that works locally and on Vercel with Redis-backed SSE. Connect Claude Code directly to your data with mcp-handler.

⚡ Next.js Implementation Guides
In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.
Related Posts:
- •Build a Working MCP Server: Custom JSON-RPC Implementation
- •Build a Claude SEO Agent with Google Search Console MCP Integration
- •Building Secure Multi-User MCP Servers: Claude vs OpenAI's Authentication Gap
- •Persist Google OAuth Refresh Tokens with Next.js & Redis
- •Send Emails from MCP with React Email & Brevo — Guide
Build an MCP Server in Next.js for Claude Code
This is Part 1 of the MCP Server Series. Prerequisites: understand the Why Your Business Needs an MCP Server business case. Start here if you're building from scratch.
I was really struggling with understanding the use cases of MCP for a while. Until I started using shadcn/ui a lot for new components, I didn't quite get it. MCP basically helps your coding tool get up-to-date data in most cases, and more importantly, whenever you're talking with third-party APIs that Claude has no access to.
I was often looking for guides that I wrote in the past to apply them to new codebases. I use my blog at buildwithmatija.com/blog as my own public repository in a way. I reference my guides all the time because I forget implementation details. The current process was visiting my blog, searching for a guide, copying the markdown, and dropping it into my new codebase. This finally clicked: why don't I make an MCP server and wire it into Claude Code directly so I could just say "find if I have anything like..."?
This guide shows you exactly how to build a production-ready MCP server in Next.js that works locally during development and scales to production on Vercel. By the end, you'll have a working MCP server that Claude Code can connect to directly, with proper Redis-backed SSE streaming and team-shareable .mcp.json configuration.
You'll learn:
- How to implement MCP tools with Zod validation
- How to set up local development with
mcp-remote - How to deploy to Vercel with Redis-backed SSE
- How to configure team-wide MCP access
- How to troubleshoot common issues
- How to extend your server with custom data sources
What We're Building
We'll create an MCP server that exposes custom tools to Claude Code. I'll use my blog as an example, implementing two tools:
search_articles- Search blog articles by titleget_article_content- Fetch full article content by slug
The same pattern works for any data source: your database, CMS, internal APIs, or third-party services. If you'd like a deeper dive into connecting specific data sources like Sanity CMS, let me know in the comments and I can expand on that in a separate guide.
Prerequisites
You'll need:
- A Next.js 16 project with App Router
- Node.js and npm/pnpm installed
- Claude Code CLI installed
- A Vercel account (for production deployment)
- An Upstash Redis instance (free tier works fine)
Step 1: Install Dependencies
First, install the required packages. We'll use mcp-handler from Vercel, which handles all the MCP protocol complexity for us, and Zod for schema validation.
pnpm add mcp-handler zod
The mcp-handler library is specifically designed for Next.js and handles both local development and production deployment scenarios. It manages the SSE transport, message routing, and Redis session management automatically.
Step 2: Create Your MCP Route Handler
Create a dynamic route that will handle different transport types. The [transport] segment allows mcp-handler to support multiple connection methods.
// File: src/app/api/mcp/[transport]/route.ts
import { createMcpHandler } from 'mcp-handler'
import { z } from 'zod'
const handler = createMcpHandler(
(server) => {
server.tool(
'search_articles',
'Search blog articles by title (partial match). Returns title and slug.',
{
query: z.string().describe('Search query to match against article titles')
},
async ({ query }) => {
// Your search logic here
const results = await searchArticlesByTitle(query)
return {
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }]
}
}
)
server.tool(
'get_article_content',
'Get full article content by slug. Returns complete markdown content and metadata.',
{
slug: z.string().describe('Article slug (from search results)')
},
async ({ slug }) => {
// Your content fetching logic here
const post = await getPostBySlug(slug)
if (!post) {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'Article not found' }) }],
isError: true
}
}
return {
content: [{
type: 'text',
text: JSON.stringify({
title: post.title,
slug: post.slug,
content: post.markdownContent,
metadata: {
publishedAt: post.publishedAt,
categories: post.categories,
keywords: post.keywords
}
}, null, 2)
}]
}
}
)
},
{},
{
basePath: '/api/mcp',
verboseLogs: true,
redisUrl: process.env.REDIS_URL,
disableSse: false
}
)
export { handler as GET, handler as POST }
This route does several important things. The createMcpHandler function takes three arguments: a server configuration callback where you define your tools, server options (we leave this empty for defaults), and handler options that configure the transport layer.
The server.tool() method registers each tool with a name, description, input schema using Zod, and an async handler function. The Zod schema provides type safety and automatic validation of incoming parameters. When Claude calls your tool, the handler receives validated parameters and returns a response in the MCP format.
The handler options configure how the server communicates. The basePath tells the handler where it's mounted in your app. Setting verboseLogs to true helps during development. The redisUrl enables Redis-backed session management for production SSE streaming, and disableSse: false ensures SSE is enabled when Redis is available.
Step 3: Implement Your Data Fetching Logic
You'll need to implement the actual data fetching functions. Here's a high-level example of what this might look like. The specifics depend on your data source.
// File: src/lib/data/article-search.ts
export async function searchArticlesByTitle(query: string) {
// Example: Query your database or CMS
const results = await yourDataSource.query({
filter: { title: { contains: query } },
select: ['title', 'slug'],
limit: 20
})
return results
}
export async function getPostBySlug(slug: string) {
// Example: Fetch full content
const post = await yourDataSource.getBySlug(slug)
return post
}
In my case, I'm using Sanity CMS with cached queries, but this pattern works with any data source. You could connect to Postgres, MongoDB, REST APIs, or even file-based content. The key is returning data in a format that Claude can understand and work with.
Step 4: Set Up Redis for Production
For production deployment on Vercel, you need Redis to manage SSE sessions. I'm using Upstash's free KV tier, which is perfect for this use case.
Create a free Upstash Redis instance through the Vercel dashboard:
- Go to your Vercel project settings
- Navigate to Storage
- Create a new KV database
- Vercel automatically adds
REDIS_URLto your environment variables
To pull the environment variables to your local development environment:
vercel env pull
This creates a .env.local file with your REDIS_URL. The mcp-handler library automatically uses this for session management when available. Without Redis, the handler falls back to simpler HTTP polling, which works fine for local development but isn't ideal for production.
Step 5: Configure Vercel Firewall Rules
When you deploy to production, Vercel's DDoS protection might block MCP connections. You need to create a firewall bypass rule for your MCP endpoints.
In your Vercel project settings:
- Go to Security → Firewall
- Create a new rule
- Set condition: "Request path contains
/api/mcp" - Set action: "Bypass"
- Save and wait a few minutes for propagation
This allows the MCP client to connect without triggering rate limiting or challenge pages. The rule is specific enough to only affect your MCP endpoints while keeping the rest of your site protected.
Step 6: Configure Claude Code (Local Development)
Now we'll set up Claude Code to connect to your MCP server. There are three ways to configure MCP servers in Claude Code, and understanding the difference is important.
Understanding Configuration Scopes
User scope (formerly "global"): Stored in ~/.claude.json, available across all your projects. Use this for personal utility servers you want everywhere.
Project scope: Stored in .mcp.json at your project root, checked into git, shared with your team. Use this for project-specific servers that everyone on the team should have access to.
Local scope: Stored in ~/.claude.json under your project path, private to you in this specific project. Use this for personal overrides or sensitive configurations.
For team collaboration, project scope is ideal. Create a .mcp.json file in your project root:
{
"mcpServers": {
"build-with-matija-local": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"mcp-remote",
"http://localhost:3000/api/mcp/sse"
],
"env": {}
},
"build-with-matija-prod": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://yourdomain.com/api/mcp/sse"
],
"env": {}
}
}
}
This configuration defines two servers: one for local development and one for production. The mcp-remote package acts as a bridge between Claude Code's stdio interface and your HTTP-based MCP server. This is necessary because Claude Code expects stdio communication, but your Next.js server communicates over HTTP.
Approving Project Servers
When you start Claude Code in a project with a .mcp.json file, you'll be prompted to approve the servers. This is a security feature to prevent malicious servers from being loaded without your knowledge.
If you don't see the prompt, or need to reset your choices:
claude mcp reset-project-choices
Then start a new Claude Code session and you'll be prompted again. Type y to approve the servers.
Alternative: CLI Configuration
You can also add servers directly via the CLI, which writes to your user or local config:
# Add to project scope (writes to .mcp.json)
claude mcp add --transport stdio --scope project my-server \
-- npx -y mcp-remote http://localhost:3000/api/mcp/sse
# Add to user scope (available everywhere)
claude mcp add --transport stdio --scope user my-server \
-- npx -y mcp-remote http://localhost:3000/api/mcp/sse
# Add to local scope (private to you in this project)
claude mcp add --transport stdio --scope local my-server \
-- npx -y mcp-remote http://localhost:3000/api/mcp/sse
The -- separator is important. Everything before it are options for Claude Code, and everything after is the command that gets executed to start the MCP server.
Step 7: Understanding Local vs HTTP Transport
Your MCP server supports two connection methods, and understanding when to use each is important.
Local (stdio) transport: Used during development when running npm run dev. The mcp-remote package connects to your local server at localhost:3000. This is what we configured in .mcp.json. It's fast, doesn't require internet, and perfect for development.
HTTP transport: Used for production or remote servers. Claude Code connects directly to your deployed server via HTTPS. This is what happens when you use the production configuration pointing to your domain.
Both use the same Next.js route handler. The mcp-handler library automatically detects which transport is being used and handles the communication accordingly. The [transport] dynamic segment in your route path is what enables this flexibility.
Step 8: Managing Your MCP Servers
Once configured, you can manage your servers with these commands:
# List all configured servers
claude mcp list
# Get details for a specific server
claude mcp get my-server
# Remove a server
claude mcp remove my-server
# Reset project approval choices
claude mcp reset-project-choices
Within Claude Code, you can also use the /mcp command to see active servers and their status. This is helpful for debugging connection issues or verifying that your tools are loaded correctly.
Step 9: Testing Your MCP Server
Start your Next.js development server:
npm run dev
Then start Claude Code in your project directory:
cd /path/to/your/project
claude
If you configured project-scoped servers, approve them when prompted. Then try using your tools:
> Search for articles about "Next.js"
> Get the content of article with slug "nextjs-deployment"
Claude should now be able to call your MCP tools and receive responses. You'll see the requests in your Next.js console logs if you enabled verboseLogs: true.
Step 10: Deploy to Production
Commit your .mcp.json file to git so your team can use the same configuration:
git add .mcp.json
git commit -m "Add MCP server configuration"
git push
Deploy to Vercel:
vercel --prod
Make sure your REDIS_URL environment variable is set in Vercel (it should be automatic if you created the KV database through Vercel). The production server will now use Redis-backed SSE for efficient streaming.
Your team members can now clone the repo, approve the project servers, and immediately have access to the same MCP tools. The production configuration allows them to use the live server even when not running the app locally.
Connection fails with 404: Make sure your route is at src/app/api/mcp/[transport]/route.ts and you're connecting to the correct URL with /sse at the end.
429 rate limit errors in production: Check your Vercel firewall rules. The bypass rule for /api/mcp must be active and may take a few minutes to propagate.
Tools not appearing in Claude Code: Run claude mcp list to verify the server is configured. Use claude mcp reset-project-choices if you need to re-approve project servers.
Redis connection errors: Verify REDIS_URL is set in your environment. Run vercel env pull to sync production environment variables locally.
mcp-remote throws 406 errors: This means mcp-handler is having issues with SSE. Check out Custom JSON-RPC MCP Implementation for an alternative approach without mcp-handler.
Summary: Your MCP Server is Production-Ready
You now have a fully functional MCP server that:
- ✅ Works locally during development
- ✅ Deploys to Vercel with proper Redis streaming
- ✅ Shares configuration across your team via
.mcp.json - ✅ Gives Claude Code direct access to your data sources
- ✅ Validates inputs with Zod schemas
- ✅ Handles errors gracefully
This same pattern works for any data source: databases, CMSs, REST APIs, internal services. The key is understanding how to expose data through well-defined MCP tools.
Extend Your MCP Server
Now that you have a working foundation, here's what you can build next:
The Series (recommended order):
- ✅ You are here — Build a Production MCP Server (local + Vercel)
- Part 2: Expanding Your MCP Server — Add write operations, cache revalidation, file uploads
- Part 3: OAuth for MCP Servers — Secure endpoints with OAuth 2.1, per-user auth
- Part 4: Send Emails from MCP — Email automation with React Email + Brevo
Alternative Implementations:
- Custom JSON-RPC MCP — Build without mcp-handler (avoids 406 errors, full control)
- MCP Integration: Claude vs OpenAI — Compare platforms, understand OAuth differences, choose what's right for you
Real-World Examples to Learn From:
- Build a Claude SEO Agent — Connect Google Search Console API, run diagnostics, analyze ranking data
- Persist Google OAuth Tokens — Handle OAuth refresh tokens securely (prerequisite for GSC integration)
What You Can Build With This Pattern
Once you understand the basics, here are real-world applications:
- Data Access: Connect to any database, REST API, or CMS directly from Claude Code
- Content Management: Build a personal knowledge base that Claude can search and reference
- Business Tools: Expose internal APIs to Claude for analytics, reporting, or automation
- Custom Integrations: Connect third-party services that Claude doesn't have native access to
- Team Utilities: Share MCP servers across your team via
.mcp.jsonin git
Have questions? Drop them in the comments below. Subscribe for more practical development guides.
Thanks, Matija
Frequently Asked Questions
Comments
You might be interested in

28th December 2025

22nd December 2025

27th December 2025

21st December 2025

20th December 2025