DRAFT Agentic Design Patterns - Model Context Protocol (MCP)
Learn how to implement the Model Context Protocol (MCP) to create standardized, interoperable AI agents that can dynamically discover and interact with external tools, services, and data sources in Next.js 15 on Vercel's serverless platform.
Mental Model: Universal Adapter for AI Agents
Think of MCP as the USB-C standard for AI agents. Just as USB-C provides a universal connector that works with any compatible device (phones, laptops, displays), MCP creates a standardized protocol for LLMs to plug into any external system. In the TypeScript/Next.js world, it's like having a universal middleware that automatically generates TypeScript interfaces for any external service, allowing your AI agents to discover and use capabilities at runtime without hardcoding integrations. The @vercel/mcp-handler
package acts as your adapter, transforming MCP servers into serverless-friendly endpoints that work seamlessly with Vercel's infrastructure.
Basic Example: Creating an MCP Server with Vercel
Let's build a simple MCP server that exposes tools for task management, demonstrating the fundamental client-server architecture.
1. Install MCP Dependencies
npm install @vercel/mcp-handler @modelcontextprotocol/sdk zod es-toolkit
Installs Vercel's MCP handler for serverless compatibility, the MCP SDK, and validation utilities.
2. Define MCP Server Tools
// lib/mcp/task-tools.ts
import { z } from 'zod';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { groupBy, sortBy } from 'es-toolkit';
// Define schemas for type safety
const TaskSchema = z.object({
id: z.string(),
title: z.string(),
status: z.enum(['pending', 'in-progress', 'completed']),
priority: z.number().min(1).max(5),
createdAt: z.date(),
assignee: z.string().optional(),
});
type Task = z.infer<typeof TaskSchema>;
// In-memory task store (replace with database in production)
const tasks = new Map<string, Task>();
// Define MCP tools with clear interfaces
export const taskTools: Tool[] = [
{
name: 'create_task',
description: 'Create a new task with specified details',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string', description: 'Task title' },
priority: {
type: 'number',
minimum: 1,
maximum: 5,
description: 'Priority level (1-5)'
},
assignee: {
type: 'string',
description: 'Optional assignee name'
},
},
required: ['title', 'priority'],
},
},
{
name: 'list_tasks',
description: 'List all tasks with optional filtering',
inputSchema: {
type: 'object',
properties: {
status: {
type: 'string',
enum: ['pending', 'in-progress', 'completed'],
description: 'Filter by status'
},
assignee: {
type: 'string',
description: 'Filter by assignee'
},
},
},
},
{
name: 'update_task_status',
description: 'Update the status of an existing task',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Task ID' },
status: {
type: 'string',
enum: ['pending', 'in-progress', 'completed'],
description: 'New status'
},
},
required: ['id', 'status'],
},
},
];
// Tool implementations
export async function executeTool(name: string, args: any) {
switch (name) {
case 'create_task': {
const task: Task = {
id: `task_${Date.now()}`,
title: args.title,
status: 'pending',
priority: args.priority,
createdAt: new Date(),
assignee: args.assignee,
};
tasks.set(task.id, task);
return { success: true, task };
}
case 'list_tasks': {
let taskList = Array.from(tasks.values());
// Apply filters using es-toolkit
if (args.status) {
taskList = taskList.filter(t => t.status === args.status);
}
if (args.assignee) {
taskList = taskList.filter(t => t.assignee === args.assignee);
}
// Sort by priority
taskList = sortBy(taskList, [(t) => -t.priority]);
return { tasks: taskList, count: taskList.length };
}
case 'update_task_status': {
const task = tasks.get(args.id);
if (!task) {
throw new Error(`Task ${args.id} not found`);
}
task.status = args.status;
tasks.set(args.id, task);
return { success: true, task };
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
Defines MCP tools with JSON Schema validation, implementing task management operations with es-toolkit utilities.
3. Create MCP Server Handler
// lib/mcp/server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { taskTools, executeTool } from './task-tools';
export async function createMCPServer() {
const server = new Server(
{
name: 'task-manager',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// Register tool discovery
server.setRequestHandler('tools/list', async () => {
return { tools: taskTools };
});
// Register tool execution
server.setRequestHandler('tools/call', async (request) => {
const { name, arguments: args } = CallToolRequestSchema.parse(request);
try {
const result = await executeTool(name, args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
});
return server;
}
// Export for serverless usage
export async function startMCPServer() {
const server = await createMCPServer();
const transport = new StdioServerTransport();
await server.connect(transport);
return server;
}
Creates an MCP server that handles tool discovery and execution with proper error handling for serverless.
4. Vercel Serverless MCP Handler
// app/api/mcp/route.ts
import { createMCPHandler } from '@vercel/mcp-handler';
import { createMCPServer } from '@/lib/mcp/server';
export const runtime = 'nodejs';
export const maxDuration = 60; // Allow longer execution for complex operations
const handler = createMCPHandler({
createServer: createMCPServer,
// Configure for Vercel's serverless environment
options: {
keepAlive: false, // Disable for serverless
timeout: 55000, // Just under maxDuration
},
});
export const POST = handler;
Creates a Vercel-compatible API route that exposes the MCP server through a serverless endpoint.
5. MCP Client Integration with Langchain
// lib/agents/mcp-client-agent.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
export class MCPClientAgent {
private model: ChatGoogleGenerativeAI;
private mcpClient: Client;
private tools: DynamicStructuredTool[] = [];
constructor() {
this.model = new ChatGoogleGenerativeAI({
modelName: 'gemini-2.5-flash',
temperature: 0,
});
this.mcpClient = new Client(
{
name: 'langchain-agent',
version: '1.0.0',
},
{
capabilities: {},
}
);
}
async connect(serverUrl: string) {
// Connect to MCP server
const transport = new WebSocketClientTransport(
new URL(serverUrl)
);
await this.mcpClient.connect(transport);
// Discover available tools
await this.discoverTools();
}
private async discoverTools() {
const response = await this.mcpClient.request(
{ method: 'tools/list' },
{}
);
// Convert MCP tools to Langchain tools
this.tools = response.tools.map(tool =>
new DynamicStructuredTool({
name: tool.name,
description: tool.description,
schema: this.convertToZodSchema(tool.inputSchema),
func: async (input) => {
const result = await this.mcpClient.request(
{ method: 'tools/call' },
{ name: tool.name, arguments: input }
);
return result.content[0].text;
},
})
);
}
private convertToZodSchema(jsonSchema: any): z.ZodSchema {
// Simplified conversion - extend for production
const shape: Record<string, z.ZodSchema> = {};
for (const [key, prop] of Object.entries(jsonSchema.properties || {})) {
const propSchema = prop as any;
if (propSchema.type === 'string') {
shape[key] = propSchema.enum
? z.enum(propSchema.enum)
: z.string();
} else if (propSchema.type === 'number') {
shape[key] = z.number();
}
// Add optional handling
if (!jsonSchema.required?.includes(key)) {
shape[key] = shape[key].optional();
}
}
return z.object(shape);
}
async execute(input: string) {
// Use discovered tools with the model
const response = await this.model.invoke(
input,
{ tools: this.tools }
);
return response;
}
}
Creates an MCP client that discovers tools from the server and converts them to Langchain-compatible tools.
Advanced Example: Production MCP System with Resources and Prompts
Let's build a comprehensive MCP implementation that includes resources (data access), tools (actions), and prompts (templates).
1. Extended MCP Server with Resources
// lib/mcp/advanced-server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { Resource, Tool, Prompt } from '@modelcontextprotocol/sdk/types.js';
import { groupBy, chunk, debounce } from 'es-toolkit';
import { z } from 'zod';
// Define comprehensive data schemas
const DocumentSchema = z.object({
id: z.string(),
title: z.string(),
content: z.string(),
metadata: z.object({
author: z.string(),
createdAt: z.date(),
tags: z.array(z.string()),
version: z.number(),
}),
});
const AnalyticsEventSchema = z.object({
eventType: z.string(),
userId: z.string(),
timestamp: z.date(),
properties: z.record(z.unknown()),
});
type Document = z.infer<typeof DocumentSchema>;
type AnalyticsEvent = z.infer<typeof AnalyticsEventSchema>;
// Resource definitions
export const resources: Resource[] = [
{
uri: 'documents://list',
name: 'Document Library',
description: 'Access to all documents in the system',
mimeType: 'application/json',
},
{
uri: 'analytics://events',
name: 'Analytics Events',
description: 'Stream of analytics events',
mimeType: 'application/x-ndjson',
},
{
uri: 'config://settings',
name: 'System Configuration',
description: 'Current system settings and configuration',
mimeType: 'application/json',
},
];
// Enhanced tool definitions
export const advancedTools: Tool[] = [
{
name: 'analyze_documents',
description: 'Perform semantic analysis on documents',
inputSchema: {
type: 'object',
properties: {
documentIds: {
type: 'array',
items: { type: 'string' },
description: 'Document IDs to analyze',
},
analysisType: {
type: 'string',
enum: ['sentiment', 'summary', 'keywords', 'entities'],
description: 'Type of analysis to perform',
},
options: {
type: 'object',
properties: {
maxLength: { type: 'number' },
threshold: { type: 'number' },
},
},
},
required: ['documentIds', 'analysisType'],
},
},
{
name: 'process_batch',
description: 'Process multiple items in parallel batches',
inputSchema: {
type: 'object',
properties: {
items: {
type: 'array',
description: 'Items to process',
},
batchSize: {
type: 'number',
default: 10,
description: 'Size of each batch',
},
operation: {
type: 'string',
enum: ['transform', 'validate', 'enrich'],
},
},
required: ['items', 'operation'],
},
},
];
// Prompt templates for common operations
export const prompts: Prompt[] = [
{
name: 'document_qa',
description: 'Question answering based on document context',
arguments: [
{
name: 'question',
description: 'The question to answer',
required: true,
},
{
name: 'context',
description: 'Document context for answering',
required: true,
},
],
},
{
name: 'data_synthesis',
description: 'Synthesize insights from multiple data sources',
arguments: [
{
name: 'sources',
description: 'Data sources to synthesize',
required: true,
},
{
name: 'format',
description: 'Output format (report, summary, bullets)',
required: false,
},
],
},
];
// Create advanced MCP server
export class AdvancedMCPServer {
private server: Server;
private documents = new Map<string, Document>();
private events: AnalyticsEvent[] = [];
// Debounced event processor
private processEvents = debounce(() => {
const grouped = groupBy(this.events, e => e.eventType);
console.log('Processing event batches:', Object.keys(grouped));
// Process grouped events
}, 1000);
constructor() {
this.server = new Server(
{
name: 'advanced-mcp-server',
version: '2.0.0',
},
{
capabilities: {
tools: {},
resources: { subscribe: true },
prompts: {},
},
}
);
this.setupHandlers();
}
private setupHandlers() {
// Resource handlers
this.server.setRequestHandler('resources/list', async () => {
return { resources };
});
this.server.setRequestHandler('resources/read', async (request) => {
const { uri } = request;
switch (uri) {
case 'documents://list':
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(
Array.from(this.documents.values()),
null,
2
),
},
],
};
case 'analytics://events':
// Stream events using chunks
const eventChunks = chunk(this.events, 100);
return {
contents: eventChunks.map((chunk, idx) => ({
uri: `${uri}#chunk-${idx}`,
mimeType: 'application/x-ndjson',
text: chunk.map(e => JSON.stringify(e)).join('\n'),
})),
};
case 'config://settings':
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify({
maxBatchSize: 100,
timeout: 30000,
features: {
streaming: true,
batching: true,
caching: true,
},
}),
},
],
};
default:
throw new Error(`Unknown resource: ${uri}`);
}
});
// Tool handlers with advanced processing
this.server.setRequestHandler('tools/list', async () => {
return { tools: advancedTools };
});
this.server.setRequestHandler('tools/call', async (request) => {
const { name, arguments: args } = request;
switch (name) {
case 'analyze_documents':
return await this.analyzeDocuments(args);
case 'process_batch':
return await this.processBatch(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
});
// Prompt handlers
this.server.setRequestHandler('prompts/list', async () => {
return { prompts };
});
this.server.setRequestHandler('prompts/get', async (request) => {
const { name, arguments: args } = request;
const prompt = prompts.find(p => p.name === name);
if (!prompt) {
throw new Error(`Unknown prompt: ${name}`);
}
// Generate prompt based on template and arguments
return {
messages: [
{
role: 'system',
content: this.generatePromptContent(name, args),
},
],
};
});
}
private async analyzeDocuments(args: any) {
const { documentIds, analysisType, options = {} } = args;
// Fetch documents
const docs = documentIds
.map((id: string) => this.documents.get(id))
.filter(Boolean);
// Perform analysis using es-toolkit utilities
const results = await Promise.all(
docs.map(async (doc) => {
switch (analysisType) {
case 'summary':
return {
docId: doc!.id,
summary: doc!.content.substring(0, options.maxLength || 200),
};
case 'keywords':
// Extract keywords (simplified)
const words = doc!.content.toLowerCase().split(/\s+/);
const wordGroups = groupBy(words, w => w);
const keywords = Object.entries(wordGroups)
.sort((a, b) => b[1].length - a[1].length)
.slice(0, 10)
.map(([word]) => word);
return {
docId: doc!.id,
keywords,
};
default:
return { docId: doc!.id, result: 'Analysis complete' };
}
})
);
return {
content: [
{
type: 'text',
text: JSON.stringify({ analysisType, results }, null, 2),
},
],
};
}
private async processBatch(args: any) {
const { items, batchSize = 10, operation } = args;
// Process items in batches
const batches = chunk(items, batchSize);
const results = [];
for (const batch of batches) {
const batchResults = await Promise.all(
batch.map(async (item: any) => {
switch (operation) {
case 'transform':
return { ...item, transformed: true };
case 'validate':
return { item, valid: true };
case 'enrich':
return { ...item, enriched: { timestamp: new Date() } };
default:
return item;
}
})
);
results.push(...batchResults);
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
operation,
processedCount: results.length,
batchCount: batches.length,
results,
}, null, 2),
},
],
};
}
private generatePromptContent(name: string, args: any): string {
switch (name) {
case 'document_qa':
return `You are analyzing documents to answer questions.
Context: ${args.context}
Please answer the following question based on the provided context:
${args.question}
Provide a clear, concise answer supported by the context.`;
case 'data_synthesis':
const format = args.format || 'summary';
return `Synthesize insights from the following data sources:
${JSON.stringify(args.sources, null, 2)}
Output format: ${format}
Analyze patterns, correlations, and key insights across all sources.`;
default:
return 'Process the request based on the provided arguments.';
}
}
async connect(transport: any) {
await this.server.connect(transport);
}
}
Implements a comprehensive MCP server with resources for data access, advanced tools, and prompt templates.
2. Vercel Handler with Authentication
// app/api/mcp/advanced/route.ts
import { NextRequest } from 'next/server';
import { createMCPHandler } from '@vercel/mcp-handler';
import { AdvancedMCPServer } from '@/lib/mcp/advanced-server';
import { verifyToken } from '@/lib/auth';
export const runtime = 'nodejs';
export const maxDuration = 300;
const handler = createMCPHandler({
createServer: async () => new AdvancedMCPServer(),
// Add authentication middleware
middleware: async (req: NextRequest) => {
const token = req.headers.get('authorization')?.replace('Bearer ', '');
if (!token) {
throw new Error('Unauthorized: No token provided');
}
const valid = await verifyToken(token);
if (!valid) {
throw new Error('Unauthorized: Invalid token');
}
return req;
},
options: {
keepAlive: false,
timeout: 295000, // Just under maxDuration
maxPayloadSize: 10 * 1024 * 1024, // 10MB
},
});
export const POST = handler;
Adds authentication and security to the MCP server endpoint.
3. React Frontend with MCP Integration
// components/MCPInterface.tsx
'use client';
import { useState, useEffect } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
interface Tool {
name: string;
description: string;
inputSchema: any;
}
interface Resource {
uri: string;
name: string;
description: string;
}
export default function MCPInterface() {
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
const [toolInput, setToolInput] = useState('{}');
const [mcpClient, setMcpClient] = useState<Client | null>(null);
// Initialize MCP client
useEffect(() => {
const initClient = async () => {
const client = new Client(
{
name: 'web-client',
version: '1.0.0',
},
{
capabilities: {},
}
);
// Connect to server through Vercel endpoint
await client.connectToServer('/api/mcp/advanced');
setMcpClient(client);
};
initClient();
}, []);
// Discover available tools
const { data: tools, isLoading: toolsLoading } = useQuery({
queryKey: ['mcp-tools'],
queryFn: async () => {
if (!mcpClient) return [];
const response = await mcpClient.request(
{ method: 'tools/list' },
{}
);
return response.tools as Tool[];
},
enabled: !!mcpClient,
});
// Discover available resources
const { data: resources } = useQuery({
queryKey: ['mcp-resources'],
queryFn: async () => {
if (!mcpClient) return [];
const response = await mcpClient.request(
{ method: 'resources/list' },
{}
);
return response.resources as Resource[];
},
enabled: !!mcpClient,
});
// Execute tool mutation
const executeTool = useMutation({
mutationFn: async ({ tool, input }: { tool: Tool; input: any }) => {
if (!mcpClient) throw new Error('MCP client not initialized');
const response = await mcpClient.request(
{ method: 'tools/call' },
{
name: tool.name,
arguments: input,
}
);
return response;
},
});
// Read resource mutation
const readResource = useMutation({
mutationFn: async (uri: string) => {
if (!mcpClient) throw new Error('MCP client not initialized');
const response = await mcpClient.request(
{ method: 'resources/read' },
{ uri }
);
return response;
},
});
const handleToolExecution = () => {
if (!selectedTool) return;
try {
const input = JSON.parse(toolInput);
executeTool.mutate({ tool: selectedTool, input });
} catch (error) {
console.error('Invalid JSON input:', error);
}
};
return (
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">MCP Control Panel</h2>
{/* Tools Section */}
<div className="divider">Available Tools</div>
{toolsLoading ? (
<div className="loading loading-spinner"></div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{tools?.map((tool) => (
<div
key={tool.name}
className={`card bg-base-200 cursor-pointer hover:bg-base-300 ${
selectedTool?.name === tool.name ? 'ring-2 ring-primary' : ''
}`}
onClick={() => setSelectedTool(tool)}
>
<div className="card-body compact">
<h3 className="font-bold">{tool.name}</h3>
<p className="text-sm opacity-70">{tool.description}</p>
</div>
</div>
))}
</div>
)}
{/* Tool Execution */}
{selectedTool && (
<div className="mt-6">
<h3 className="text-lg font-bold mb-2">
Execute: {selectedTool.name}
</h3>
<div className="form-control">
<label className="label">
<span className="label-text">Input (JSON)</span>
</label>
<textarea
className="textarea textarea-bordered h-32 font-mono text-sm"
value={toolInput}
onChange={(e) => setToolInput(e.target.value)}
placeholder={JSON.stringify(
selectedTool.inputSchema.properties,
null,
2
)}
/>
</div>
<button
className="btn btn-primary mt-4"
onClick={handleToolExecution}
disabled={executeTool.isPending}
>
{executeTool.isPending ? (
<>
<span className="loading loading-spinner"></span>
Executing...
</>
) : (
'Execute Tool'
)}
</button>
{executeTool.data && (
<div className="alert alert-success mt-4">
<pre className="text-xs overflow-auto">
{JSON.stringify(executeTool.data, null, 2)}
</pre>
</div>
)}
{executeTool.isError && (
<div className="alert alert-error mt-4">
<span>Execution failed. Please check your input.</span>
</div>
)}
</div>
)}
{/* Resources Section */}
<div className="divider">Available Resources</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{resources?.map((resource) => (
<div
key={resource.uri}
className="card bg-base-200 cursor-pointer hover:bg-base-300"
onClick={() => readResource.mutate(resource.uri)}
>
<div className="card-body compact">
<h3 className="font-bold">{resource.name}</h3>
<p className="text-sm opacity-70">{resource.description}</p>
<p className="text-xs font-mono">{resource.uri}</p>
</div>
</div>
))}
</div>
{readResource.data && (
<div className="modal modal-open">
<div className="modal-box max-w-3xl">
<h3 className="font-bold text-lg">Resource Content</h3>
<pre className="text-xs overflow-auto mt-4">
{JSON.stringify(readResource.data, null, 2)}
</pre>
<div className="modal-action">
<button
className="btn"
onClick={() => readResource.reset()}
>
Close
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
}
Creates a comprehensive React interface for interacting with MCP servers, discovering capabilities, and executing tools.
4. Langchain Agent with Dynamic MCP Discovery
// lib/agents/dynamic-mcp-agent.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { createReactAgent } from '@langchain/langgraph/prebuilt';
import { DynamicStructuredTool } from '@langchain/core/tools';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { z } from 'zod';
import { memoize, throttle } from 'es-toolkit';
export class DynamicMCPAgent {
private model: ChatGoogleGenerativeAI;
private mcpClients: Map<string, Client> = new Map();
private agent: any;
// Memoized tool discovery
private discoverTools = memoize(
async (serverUrl: string) => {
const client = await this.connectToServer(serverUrl);
const response = await client.request(
{ method: 'tools/list' },
{}
);
return response.tools;
},
{
getCacheKey: (serverUrl) => serverUrl,
ttl: 300000, // Cache for 5 minutes
}
);
// Throttled resource reading
private readResource = throttle(
async (client: Client, uri: string) => {
const response = await client.request(
{ method: 'resources/read' },
{ uri }
);
return response;
},
1000 // Max once per second per resource
);
constructor() {
this.model = new ChatGoogleGenerativeAI({
modelName: 'gemini-2.5-pro',
temperature: 0,
streaming: true,
});
}
async connectToServer(serverUrl: string): Promise<Client> {
if (this.mcpClients.has(serverUrl)) {
return this.mcpClients.get(serverUrl)!;
}
const client = new Client(
{
name: 'dynamic-agent',
version: '1.0.0',
},
{
capabilities: {},
}
);
// Connect based on protocol
if (serverUrl.startsWith('ws://') || serverUrl.startsWith('wss://')) {
// WebSocket connection
const { WebSocketClientTransport } = await import(
'@modelcontextprotocol/sdk/client/websocket.js'
);
const transport = new WebSocketClientTransport(new URL(serverUrl));
await client.connect(transport);
} else {
// HTTP connection through Vercel
await client.connectToServer(serverUrl);
}
this.mcpClients.set(serverUrl, client);
return client;
}
async addMCPServers(serverUrls: string[]) {
const allTools: DynamicStructuredTool[] = [];
for (const serverUrl of serverUrls) {
try {
// Discover tools from each server
const tools = await this.discoverTools(serverUrl);
const client = this.mcpClients.get(serverUrl)!;
// Convert to Langchain tools
for (const tool of tools) {
const langchainTool = new DynamicStructuredTool({
name: `${serverUrl}_${tool.name}`.replace(/[^a-zA-Z0-9_]/g, '_'),
description: `[${serverUrl}] ${tool.description}`,
schema: this.convertMCPSchemaToZod(tool.inputSchema),
func: async (input) => {
const result = await client.request(
{ method: 'tools/call' },
{
name: tool.name,
arguments: input,
}
);
return result.content[0].text;
},
});
allTools.push(langchainTool);
}
// Also create tools for reading resources
const resourcesResponse = await client.request(
{ method: 'resources/list' },
{}
);
for (const resource of resourcesResponse.resources || []) {
const resourceTool = new DynamicStructuredTool({
name: `read_${resource.uri}`.replace(/[^a-zA-Z0-9_]/g, '_'),
description: `Read resource: ${resource.description}`,
schema: z.object({}),
func: async () => {
const result = await this.readResource(client, resource.uri);
return JSON.stringify(result.contents[0], null, 2);
},
});
allTools.push(resourceTool);
}
} catch (error) {
console.error(`Failed to connect to ${serverUrl}:`, error);
}
}
// Create agent with all discovered tools
this.agent = createReactAgent({
llm: this.model,
tools: allTools,
});
return allTools.length;
}
private convertMCPSchemaToZod(schema: any): z.ZodSchema {
if (!schema || !schema.properties) {
return z.object({});
}
const shape: Record<string, z.ZodSchema> = {};
for (const [key, prop] of Object.entries(schema.properties)) {
const propSchema = prop as any;
let zodSchema: z.ZodSchema;
switch (propSchema.type) {
case 'string':
zodSchema = propSchema.enum
? z.enum(propSchema.enum as [string, ...string[]])
: z.string();
if (propSchema.description) {
zodSchema = zodSchema.describe(propSchema.description);
}
break;
case 'number':
zodSchema = z.number();
if (propSchema.minimum !== undefined) {
zodSchema = (zodSchema as z.ZodNumber).min(propSchema.minimum);
}
if (propSchema.maximum !== undefined) {
zodSchema = (zodSchema as z.ZodNumber).max(propSchema.maximum);
}
break;
case 'boolean':
zodSchema = z.boolean();
break;
case 'array':
if (propSchema.items) {
zodSchema = z.array(this.convertMCPSchemaToZod(propSchema.items));
} else {
zodSchema = z.array(z.unknown());
}
break;
case 'object':
zodSchema = propSchema.properties
? this.convertMCPSchemaToZod(propSchema)
: z.record(z.unknown());
break;
default:
zodSchema = z.unknown();
}
// Handle required fields
if (!schema.required?.includes(key)) {
zodSchema = zodSchema.optional();
}
shape[key] = zodSchema;
}
return z.object(shape);
}
async execute(input: string) {
if (!this.agent) {
throw new Error('No MCP servers connected. Call addMCPServers first.');
}
const response = await this.agent.stream({
messages: [{ role: 'user', content: input }],
});
const results = [];
for await (const chunk of response) {
results.push(chunk);
}
return results;
}
}
// Usage in API route
export async function POST(req: Request) {
const { message, mcpServers } = await req.json();
const agent = new DynamicMCPAgent();
// Connect to multiple MCP servers
const toolCount = await agent.addMCPServers(
mcpServers || [
'/api/mcp/advanced',
'ws://localhost:3001/mcp',
'https://external-mcp-server.com/mcp',
]
);
console.log(`Connected to ${toolCount} tools across MCP servers`);
const result = await agent.execute(message);
return new Response(
JSON.stringify({ result }),
{ headers: { 'Content-Type': 'application/json' } }
);
}
Implements a dynamic MCP agent that can discover and use tools from multiple MCP servers simultaneously.
Conclusion
The Model Context Protocol (MCP) transforms how AI agents interact with external systems by providing a standardized, discoverable interface for tools, resources, and prompts. By implementing MCP in your Next.js applications on Vercel, you create agents that can dynamically adapt to new capabilities without code changes. The combination of @vercel/mcp-handler
with Langchain and es-toolkit provides a powerful foundation for building production-ready, serverless AI systems that can seamlessly integrate with any MCP-compliant service. This protocol-first approach ensures your agents remain interoperable and future-proof as the ecosystem of AI tools continues to evolve.