Build a Production MCP Server in Next.js — Quick Guide
Step-by-step Next.js 16 guide to create an MCP server for Claude Code using mcp-handler, Upstash Redis, SSE, and…

⚡ 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.
New to MCP? If you want to understand what MCP servers are and why they're useful for your business before diving into the technical implementation, check out Why Your Business Needs an MCP Server first.
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 16 that works both locally during development and in production on Vercel. By the end, you'll have a working MCP server that Claude Code can connect to, with proper Redis-backed SSE streaming and team-shareable configuration.
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.
Troubleshooting Common Issues
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.
What You've Learned
You now have a production-ready MCP server running in Next.js 16 that works both locally and in production. You understand the difference between user, project, and local configuration scopes, and how to use .mcp.json for team collaboration. You've seen how to implement custom tools that give Claude Code direct access to your data sources, and how to deploy everything to Vercel with proper Redis-backed streaming.
This same pattern works for any data source or API you want to expose to Claude Code. You could connect to your database, internal APIs, third-party services, or any other data source. The key is implementing the tool handlers that fetch and format the data Claude needs.
Let me know in the comments if you have questions, and subscribe for more practical development guides. If you'd like me to expand on connecting specific data sources like Sanity CMS, Postgres, or other services, I'd be happy to write a follow-up guide.
Thanks, Matija