vyoniq
AI Development Tools
Business Automation
Case Studies
MCP Servers

Building a Production-Ready MCP Server with Next.js: A Complete Implementation Guide

January 15, 2025
12 min read
Building a Production-Ready MCP Server with Next.js: A Complete Implementation Guide

Building a Production-Ready MCP Server with Next.js: A Complete Implementation Guide

The Model Context Protocol (MCP) is revolutionizing how Large Language Models interact with external systems and data sources. While many tutorials focus on simple implementations, building a production-ready MCP server requires careful attention to schema design, authentication, error handling, and client compatibility.

This guide walks you through building a complete MCP server using Next.js, based on our experience developing the Vyoniq MCP server that successfully integrates with Cursor IDE and other MCP clients.

Why Next.js for MCP Servers?

Next.js provides an excellent foundation for MCP servers due to its:

  • API Routes: Built-in support for serverless functions and API endpoints
  • TypeScript Integration: First-class TypeScript support for type safety
  • Middleware Support: Request/response processing and authentication
  • Flexible Deployment: Works with any hosting provider, not just Vercel
  • Performance: Optimized for production workloads

Project Architecture Overview

Our MCP server follows a modular architecture that separates concerns and maintains scalability:

lib/mcp/
├── server.ts          # Core MCP server implementation
├── init.ts           # Server initialization and tool registration
├── types.ts          # Zod schemas and TypeScript interfaces
├── tools/            # Individual tool implementations
│   ├── blog.ts       # Blog management tools
│   └── analytics.ts  # Analytics tools
└── resources/        # Dynamic resource handlers
    └── blog.ts       # Blog resource providers

This structure ensures clean separation of concerns and makes it easy to add new tools and resources as your MCP server grows.

The Critical Schema Design Pattern

The most important aspect of MCP server implementation is proper schema design. Many implementations fail because they don't provide proper parameter descriptions that MCP clients like Cursor need to display helpful information.

❌ Common Mistake: Raw Schemas Without Descriptions

// This won't show parameter descriptions in Cursor const BadSchema = z.object({ title: z.string().min(1), published: z.boolean().optional() });

✅ Correct Implementation: Always Use .describe()

// This will show proper descriptions in Cursor const GoodSchema = z.object({ title: z.string() .min(1, "Title is required") .describe("The title of the blog post"), published: z.boolean() .optional() .describe("Filter posts by published status (true for published, false for drafts, omit for all posts)") });

Schema Conversion for MCP Clients

The key to Cursor compatibility is proper JSON Schema generation:

import { zodToJsonSchema } from "zod-to-json-schema"; export interface MCPTool { name: string; description?: string; inputSchema: any; // JSON Schema for MCP clients zodSchema?: z.ZodSchema; // Zod schema for server validation } const createBlogPostTool: MCPTool = { name: "create_blog_post", description: "Create a new blog post with specified content, categories, and metadata", // Convert to JSON Schema for MCP clients (like Cursor) inputSchema: zodToJsonSchema(CreateBlogPostSchema, { $refStrategy: "none" // Critical: Use inline schemas, not $ref }), // Keep Zod schema for server-side validation zodSchema: CreateBlogPostSchema, };

The $refStrategy: "none" parameter is crucial - it ensures that complex schema references don't confuse MCP clients.

Implementing Dual Authentication

Production MCP servers need to support both API key authentication (for external clients like Cursor) and session-based authentication (for web UIs):

export async function authenticateRequest( request: NextRequest ): Promise<MCPAuthContext | null> { // 1. Check for API key first (for external MCP clients) const authHeader = request.headers.get("authorization"); if (authHeader?.startsWith("Bearer ")) { return await authenticateApiKey(authHeader.replace("Bearer ", "")); } // 2. Fall back to Clerk session (for web UI) return await authenticateClerkSession(request); }

Secure API Key Management

Store API keys securely using bcrypt hashing:

// API Key Format: vyoniq_sk_<64_hex_characters> const API_KEY_PREFIX = "vyoniq_sk_"; const API_KEY_LENGTH = 64; // Store hashed keys in database const hashedKey = await bcrypt.hash(rawKey, 12); // Validate keys const isValid = await bcrypt.compare(providedKey, storedHashedKey);

Tool Implementation Pattern

Each MCP tool should follow a consistent pattern for reliability and maintainability:

export const createBlogPostTool: MCPTool = { name: "create_blog_post", description: "Create a new blog post with specified content, categories, and metadata", inputSchema: zodToJsonSchema(CreateBlogPostSchema, { $refStrategy: "none" }), zodSchema: CreateBlogPostSchema, }; export async function createBlogPostHandler( args: unknown, auth: MCPAuthContext ): Promise<MCPToolResult> { try { // 1. Validate permissions if (!auth.isAdmin) { return createErrorResponse("Unauthorized: Admin access required"); } // 2. Validate and parse arguments using Zod schema const data = CreateBlogPostSchema.parse(args); // 3. Perform business logic const post = await prisma.blogPost.create({ data: { title: data.title, excerpt: data.excerpt, content: data.content, authorId: auth.userId, }, }); // 4. Return structured success response return createSuccessResponse( `Successfully created blog post: "${post.title}" (ID: ${post.id})` ); } catch (error) { console.error("Error creating blog post:", error); return createErrorResponse( `Failed to create blog post: ${ error instanceof Error ? error.message : "Unknown error" }` ); } }

JSON-RPC Protocol Implementation

MCP uses JSON-RPC 2.0 protocol. Your server must handle the protocol correctly:

export async function POST(request: NextRequest) { try { const body = await request.json(); // Validate JSON-RPC format if (body.jsonrpc !== "2.0") { return createErrorResponse(MCP_ERRORS.INVALID_REQUEST, "Invalid JSON-RPC version"); } // Route to appropriate handler switch (body.method) { case "initialize": return handleInitialize(body); case "tools/list": return await handleToolsList(body, request); case "tools/call": return await handleToolsCall(body, request); case "resources/list": return await handleResourcesList(body, request); case "resources/read": return await handleResourcesRead(body, request); default: return createErrorResponse(MCP_ERRORS.METHOD_NOT_FOUND, `Method not found: ${body.method}`); } } catch (error) { console.error("MCP Server Error:", error); return createErrorResponse(MCP_ERRORS.INTERNAL_ERROR, "Internal server error"); } }

Server Initialization and Tool Registration

Proper server initialization is crucial for reliability:

export function initializeMCPServer() { console.log("Initializing Vyoniq MCP Server..."); // Register blog management tools mcpServer.addTool(createBlogPostTool, createBlogPostHandler); mcpServer.addTool(updateBlogPostTool, updateBlogPostHandler); mcpServer.addTool(publishBlogPostTool, publishBlogPostHandler); mcpServer.addTool(deleteBlogPostTool, deleteBlogPostHandler); mcpServer.addTool(createCategoryTool, createCategoryHandler); mcpServer.addTool(listBlogPostsTool, listBlogPostsHandler); mcpServer.addTool(listCategoriesTool, listCategoriesHandler); mcpServer.addTool(getBlogPostTool, getBlogPostHandler); console.log(`✅ Vyoniq MCP Server initialized successfully`); console.log(`📊 Registered ${mcpServer.listTools().length} tools`); }

Testing Your MCP Server

Create comprehensive tests to ensure your server works correctly:

async function testMCPServer() { const baseUrl = 'http://localhost:3000/api/mcp'; const apiKey = 'your_api_key_here'; // Test 1: Initialize const initResponse = await fetch(baseUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test-client', version: '1.0.0' } } }) }); // Test 2: List tools const toolsResponse = await fetch(baseUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list' }) }); // Validate responses console.log('Initialize:', await initResponse.json()); console.log('Tools:', await toolsResponse.json()); }

Deployment Without Vercel

While Vercel is popular for Next.js deployment, you can deploy your MCP server anywhere:

Docker Deployment

FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . RUN npm run build EXPOSE 3000 CMD ["npm", "start"]

Environment Variables

# Database DATABASE_URL="postgresql://..." # Authentication CLERK_SECRET_KEY="sk_..." NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_..." # MCP Server MCP_SERVER_PORT=3000 MCP_API_KEY_SALT_ROUNDS=12

Reverse Proxy Configuration (Nginx)

server { listen 80; server_name your-mcp-server.com; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; } }

Common Pitfalls and Solutions

1. Missing Parameter Descriptions

Problem: Cursor shows tools but no parameter descriptions Solution: Always use .describe() on every Zod schema field

2. Complex Schema References

Problem: $ref-based schemas confuse MCP clients Solution: Use { $refStrategy: "none" } in zodToJsonSchema()

3. Authentication Issues

Problem: API key validation fails Solution: Use bcrypt.compare() for hashed key validation

4. Tool Registration Errors

Problem: Tools not appearing in clients Solution: Ensure proper tool registration in server initialization

Production Deployment Checklist

Before deploying your MCP server:

  • All tools have parameter descriptions
  • Schema conversion uses inline schemas ($refStrategy: "none")
  • Authentication is properly implemented
  • JSON-RPC protocol is correctly handled
  • Error responses are structured properly
  • API keys are securely generated and stored
  • All tools are registered in server initialization
  • Comprehensive test suite passes
  • Environment variables are configured
  • Database migrations are applied
  • Monitoring and logging are set up

Real-World Results

Our Vyoniq MCP server implementation successfully:

  • Integrates with Cursor IDE: All 8 tools work seamlessly with proper parameter descriptions
  • Handles Authentication: Dual authentication supports both API keys and web sessions
  • Manages Blog Content: Complete CRUD operations for blog posts and categories
  • Provides Resources: Dynamic resource templates for flexible data access
  • Scales Reliably: Handles concurrent requests and maintains performance

Conclusion

Building a production-ready MCP server with Next.js requires attention to detail, especially around schema design and authentication. The patterns and practices outlined in this guide will help you create robust MCP servers that integrate seamlessly with Cursor and other MCP clients.

The key to success is following the established patterns for schema design, implementing proper authentication, and thoroughly testing your implementation. With these foundations in place, you can build powerful MCP servers that unlock new possibilities for LLM integration in your applications.

At Vyoniq, we've successfully implemented these patterns to create a fully functional MCP server that enhances our development workflow and provides seamless integration with modern AI development tools. The investment in proper MCP server implementation pays dividends in developer productivity and system reliability.

Share this post:

About the Author

Javier Gongora

Javier Gongora

Founder & Software Developer

Subscribe

Get the latest insights delivered to your inbox