초안 에이전틱 디자인 패턴 - 모델 컨텍스트 프로토콜(MCP)
모델 컨텍스트 프로토콜(MCP)을 구현하여 표준화되고 상호 운용 가능한 AI 에이전트를 생성하는 방법을 알아보세요. 외부 도구, 서비스, 데이터 소스를 동적으로 발견하고 상호작용할 수 있는 에이전트를 Vercel의 서버리스 플랫폼에서 Next.js 15로 구축해보겠습니다.
멘탈 모델: AI 에이전트를 위한 범용 어댑터
MCP를 AI 에이전트를 위한 USB-C 표준으로 생각해보세요. USB-C가 호환 가능한 모든 장치(휴대폰, 노트북, 디스플레이)와 작동하는 범용 커넥터를 제공하는 것처럼, MCP는 LLM이 모든 외부 시스템에 연결할 수 있는 표준화된 프로토콜을 만듭니다. TypeScript/Next.js 환경에서는 모든 외부 서비스에 대해 TypeScript 인터페이스를 자동으로 생성하는 범용 미들웨어를 갖는 것과 같으며, AI 에이전트가 통합을 하드코딩하지 않고도 런타임에 기능을 발견하고 사용할 수 있게 합니다. @vercel/mcp-handler
패키지는 MCP 서버를 Vercel의 인프라와 원활하게 작동하는 서버리스 친화적 엔드포인트로 변환하는 어댑터 역할을 합니다.
기본 예제: Vercel로 MCP 서버 생성하기
기본적인 클라이언트-서버 아키텍처를 보여주는 작업 관리 도구를 제공하는 간단한 MCP 서버를 구축해보겠습니다.
1. MCP 종속성 설치
npm install @vercel/mcp-handler @modelcontextprotocol/sdk zod es-toolkit
서버리스 호환성을 위한 Vercel의 MCP 핸들러, MCP SDK, 그리고 검증 유틸리티를 설치합니다.
2. MCP 서버 도구 정의
// lib/mcp/task-tools.ts
import { z } from 'zod';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { groupBy, sortBy } from 'es-toolkit';
// 타입 안전성을 위한 스키마 정의
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>;
// 인메모리 작업 저장소 (프로덕션에서는 데이터베이스로 교체)
const tasks = new Map<string, Task>();
// 명확한 인터페이스로 MCP 도구 정의
export const taskTools: Tool[] = [
{
name: 'create_task',
description: '지정된 세부 정보로 새로운 작업을 생성합니다',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string', description: '작업 제목' },
priority: {
type: 'number',
minimum: 1,
maximum: 5,
description: '우선순위 레벨 (1-5)'
},
assignee: {
type: 'string',
description: '선택적 담당자 이름'
},
},
required: ['title', 'priority'],
},
},
{
name: 'list_tasks',
description: '선택적 필터링으로 모든 작업을 나열합니다',
inputSchema: {
type: 'object',
properties: {
status: {
type: 'string',
enum: ['pending', 'in-progress', 'completed'],
description: '상태별 필터링'
},
assignee: {
type: 'string',
description: '담당자별 필터링'
},
},
},
},
{
name: 'update_task_status',
description: '기존 작업의 상태를 업데이트합니다',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: '작업 ID' },
status: {
type: 'string',
enum: ['pending', 'in-progress', 'completed'],
description: '새로운 상태'
},
},
required: ['id', 'status'],
},
},
];
// 도구 구현
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());
// es-toolkit을 사용한 필터 적용
if (args.status) {
taskList = taskList.filter(t => t.status === args.status);
}
if (args.assignee) {
taskList = taskList.filter(t => t.assignee === args.assignee);
}
// 우선순위별 정렬
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(`작업 ${args.id}을(를) 찾을 수 없습니다`);
}
task.status = args.status;
tasks.set(args.id, task);
return { success: true, task };
}
default:
throw new Error(`알 수 없는 도구: ${name}`);
}
}
JSON Schema 검증과 es-toolkit 유틸리티를 사용하여 작업 관리 작업을 구현하는 MCP 도구를 정의합니다.
3. MCP 서버 핸들러 생성
// 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: {},
},
}
);
// 도구 발견 등록
server.setRequestHandler('tools/list', async () => {
return { tools: taskTools };
});
// 도구 실행 등록
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 instanceof Error ? error.message : '알 수 없는 오류'}`,
},
],
isError: true,
};
}
});
return server;
}
// 서버리스 사용을 위한 내보내기
export async function startMCPServer() {
const server = await createMCPServer();
const transport = new StdioServerTransport();
await server.connect(transport);
return server;
}
서버리스 환경을 위한 적절한 오류 처리와 함께 도구 발견 및 실행을 처리하는 MCP 서버를 생성합니다.
4. Vercel 서버리스 MCP 핸들러
// 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; // 복잡한 작업을 위한 더 긴 실행 시간 허용
const handler = createMCPHandler({
createServer: createMCPServer,
// Vercel의 서버리스 환경에 맞게 구성
options: {
keepAlive: false, // 서버리스를 위해 비활성화
timeout: 55000, // maxDuration 바로 아래
},
});
export const POST = handler;
서버리스 엔드포인트를 통해 MCP 서버를 노출하는 Vercel 호환 API 라우트를 생성합니다.
5. Langchain과 MCP 클라이언트 통합
// 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) {
// MCP 서버에 연결
const transport = new WebSocketClientTransport(
new URL(serverUrl)
);
await this.mcpClient.connect(transport);
// 사용 가능한 도구 발견
await this.discoverTools();
}
private async discoverTools() {
const response = await this.mcpClient.request(
{ method: 'tools/list' },
{}
);
// MCP 도구를 Langchain 도구로 변환
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 {
// 간소화된 변환 - 프로덕션에서는 확장 필요
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();
}
// 선택적 처리 추가
if (!jsonSchema.required?.includes(key)) {
shape[key] = shape[key].optional();
}
}
return z.object(shape);
}
async execute(input: string) {
// 발견된 도구를 모델과 함께 사용
const response = await this.model.invoke(
input,
{ tools: this.tools }
);
return response;
}
}
서버에서 도구를 발견하고 Langchain 호환 도구로 변환하는 MCP 클라이언트를 생성합니다.
고급 예제: 리소스와 프롬프트를 포함한 프로덕션 MCP 시스템
리소스(데이터 액세스), 도구(작업), 프롬프트(템플릿)를 포함하는 포괄적인 MCP 구현을 구축해보겠습니다.
1. 리소스를 포함한 확장된 MCP 서버
// 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';
// 포괄적인 데이터 스키마 정의
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>;
// 리소스 정의
export const resources: Resource[] = [
{
uri: 'documents://list',
name: '문서 라이브러리',
description: '시스템의 모든 문서에 대한 액세스',
mimeType: 'application/json',
},
{
uri: 'analytics://events',
name: '분석 이벤트',
description: '분석 이벤트 스트림',
mimeType: 'application/x-ndjson',
},
{
uri: 'config://settings',
name: '시스템 구성',
description: '현재 시스템 설정 및 구성',
mimeType: 'application/json',
},
];
// 향상된 도구 정의
export const advancedTools: Tool[] = [
{
name: 'analyze_documents',
description: '문서에 대한 의미 분석을 수행합니다',
inputSchema: {
type: 'object',
properties: {
documentIds: {
type: 'array',
items: { type: 'string' },
description: '분석할 문서 ID',
},
analysisType: {
type: 'string',
enum: ['sentiment', 'summary', 'keywords', 'entities'],
description: '수행할 분석 유형',
},
options: {
type: 'object',
properties: {
maxLength: { type: 'number' },
threshold: { type: 'number' },
},
},
},
required: ['documentIds', 'analysisType'],
},
},
{
name: 'process_batch',
description: '여러 항목을 병렬 배치로 처리합니다',
inputSchema: {
type: 'object',
properties: {
items: {
type: 'array',
description: '처리할 항목',
},
batchSize: {
type: 'number',
default: 10,
description: '각 배치의 크기',
},
operation: {
type: 'string',
enum: ['transform', 'validate', 'enrich'],
},
},
required: ['items', 'operation'],
},
},
];
// 일반적인 작업을 위한 프롬프트 템플릿
export const prompts: Prompt[] = [
{
name: 'document_qa',
description: '문서 컨텍스트를 기반으로 한 질문 답변',
arguments: [
{
name: 'question',
description: '답변할 질문',
required: true,
},
{
name: 'context',
description: '답변을 위한 문서 컨텍스트',
required: true,
},
],
},
{
name: 'data_synthesis',
description: '여러 데이터 소스에서 인사이트를 종합합니다',
arguments: [
{
name: 'sources',
description: '종합할 데이터 소스',
required: true,
},
{
name: 'format',
description: '출력 형식 (report, summary, bullets)',
required: false,
},
],
},
];
// 고급 MCP 서버 생성
export class AdvancedMCPServer {
private server: Server;
private documents = new Map<string, Document>();
private events: AnalyticsEvent[] = [];
// 디바운스된 이벤트 프로세서
private processEvents = debounce(() => {
const grouped = groupBy(this.events, e => e.eventType);
console.log('이벤트 배치 처리:', Object.keys(grouped));
// 그룹화된 이벤트 처리
}, 1000);
constructor() {
this.server = new Server(
{
name: 'advanced-mcp-server',
version: '2.0.0',
},
{
capabilities: {
tools: {},
resources: { subscribe: true },
prompts: {},
},
}
);
this.setupHandlers();
}
private setupHandlers() {
// 리소스 핸들러
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':
// 청크를 사용한 이벤트 스트리밍
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(`알 수 없는 리소스: ${uri}`);
}
});
// 고급 처리를 포함한 도구 핸들러
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(`알 수 없는 도구: ${name}`);
}
});
// 프롬프트 핸들러
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(`알 수 없는 프롬프트: ${name}`);
}
// 템플릿과 인수를 기반으로 프롬프트 생성
return {
messages: [
{
role: 'system',
content: this.generatePromptContent(name, args),
},
],
};
});
}
private async analyzeDocuments(args: any) {
const { documentIds, analysisType, options = {} } = args;
// 문서 가져오기
const docs = documentIds
.map((id: string) => this.documents.get(id))
.filter(Boolean);
// es-toolkit 유틸리티를 사용한 분석 수행
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':
// 키워드 추출 (간소화됨)
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: '분석 완료' };
}
})
);
return {
content: [
{
type: 'text',
text: JSON.stringify({ analysisType, results }, null, 2),
},
],
};
}
private async processBatch(args: any) {
const { items, batchSize = 10, operation } = args;
// 배치 단위로 항목 처리
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 `당신은 질문에 답하기 위해 문서를 분석하고 있습니다.
컨텍스트: ${args.context}
제공된 컨텍스트를 기반으로 다음 질문에 답해주세요:
${args.question}
컨텍스트에 의해 뒷받침되는 명확하고 간결한 답변을 제공해주세요.`;
case 'data_synthesis':
const format = args.format || 'summary';
return `다음 데이터 소스에서 인사이트를 종합하세요:
${JSON.stringify(args.sources, null, 2)}
출력 형식: ${format}
모든 소스에서 패턴, 상관관계, 핵심 인사이트를 분석하세요.`;
default:
return '제공된 인수를 기반으로 요청을 처리하세요.';
}
}
async connect(transport: any) {
await this.server.connect(transport);
}
}
데이터 액세스를 위한 리소스, 고급 도구, 프롬프트 템플릿을 포함한 포괄적인 MCP 서버를 구현합니다.
2. 인증을 포함한 Vercel 핸들러
// 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(),
// 인증 미들웨어 추가
middleware: async (req: NextRequest) => {
const token = req.headers.get('authorization')?.replace('Bearer ', '');
if (!token) {
throw new Error('미인증: 토큰이 제공되지 않았습니다');
}
const valid = await verifyToken(token);
if (!valid) {
throw new Error('미인증: 유효하지 않은 토큰입니다');
}
return req;
},
options: {
keepAlive: false,
timeout: 295000, // maxDuration 바로 아래
maxPayloadSize: 10 * 1024 * 1024, // 10MB
},
});
export const POST = handler;
MCP 서버 엔드포인트에 인증과 보안을 추가합니다.
3. MCP 통합을 포함한 React 프론트엔드
// 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);
// MCP 클라이언트 초기화
useEffect(() => {
const initClient = async () => {
const client = new Client(
{
name: 'web-client',
version: '1.0.0',
},
{
capabilities: {},
}
);
// Vercel 엔드포인트를 통해 서버에 연결
await client.connectToServer('/api/mcp/advanced');
setMcpClient(client);
};
initClient();
}, []);
// 사용 가능한 도구 발견
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,
});
// 사용 가능한 리소스 발견
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,
});
// 도구 실행 뮤테이션
const executeTool = useMutation({
mutationFn: async ({ tool, input }: { tool: Tool; input: any }) => {
if (!mcpClient) throw new Error('MCP 클라이언트가 초기화되지 않았습니다');
const response = await mcpClient.request(
{ method: 'tools/call' },
{
name: tool.name,
arguments: input,
}
);
return response;
},
});
// 리소스 읽기 뮤테이션
const readResource = useMutation({
mutationFn: async (uri: string) => {
if (!mcpClient) throw new Error('MCP 클라이언트가 초기화되지 않았습니다');
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('유효하지 않은 JSON 입력:', error);
}
};
return (
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">MCP 제어판</h2>
{/* 도구 섹션 */}
<div className="divider">사용 가능한 도구</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>
)}
{/* 도구 실행 */}
{selectedTool && (
<div className="mt-6">
<h3 className="text-lg font-bold mb-2">
실행: {selectedTool.name}
</h3>
<div className="form-control">
<label className="label">
<span className="label-text">입력 (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>
실행 중...
</>
) : (
'도구 실행'
)}
</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>실행에 실패했습니다. 입력을 확인해주세요.</span>
</div>
)}
</div>
)}
{/* 리소스 섹션 */}
<div className="divider">사용 가능한 리소스</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">리소스 내용</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()}
>
닫기
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
}
MCP 서버와 상호작용하고, 기능을 발견하며, 도구를 실행하기 위한 포괄적인 React 인터페이스를 생성합니다.
4. 동적 MCP 발견을 포함한 Langchain 에이전트
// 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;
// 메모화된 도구 발견
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, // 5분간 캐시
}
);
// 스로틀된 리소스 읽기
private readResource = throttle(
async (client: Client, uri: string) => {
const response = await client.request(
{ method: 'resources/read' },
{ uri }
);
return response;
},
1000 // 리소스당 초당 최대 1번
);
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: {},
}
);
// 프로토콜에 따른 연결
if (serverUrl.startsWith('ws://') || serverUrl.startsWith('wss://')) {
// WebSocket 연결
const { WebSocketClientTransport } = await import(
'@modelcontextprotocol/sdk/client/websocket.js'
);
const transport = new WebSocketClientTransport(new URL(serverUrl));
await client.connect(transport);
} else {
// Vercel을 통한 HTTP 연결
await client.connectToServer(serverUrl);
}
this.mcpClients.set(serverUrl, client);
return client;
}
async addMCPServers(serverUrls: string[]) {
const allTools: DynamicStructuredTool[] = [];
for (const serverUrl of serverUrls) {
try {
// 각 서버에서 도구 발견
const tools = await this.discoverTools(serverUrl);
const client = this.mcpClients.get(serverUrl)!;
// Langchain 도구로 변환
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);
}
// 리소스 읽기를 위한 도구도 생성
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: `리소스 읽기: ${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(`${serverUrl} 연결 실패:`, error);
}
}
// 발견된 모든 도구로 에이전트 생성
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();
}
// 필수 필드 처리
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('MCP 서버가 연결되지 않았습니다. 먼저 addMCPServers를 호출하세요.');
}
const response = await this.agent.stream({
messages: [{ role: 'user', content: input }],
});
const results = [];
for await (const chunk of response) {
results.push(chunk);
}
return results;
}
}
// API 라우트에서 사용
export async function POST(req: Request) {
const { message, mcpServers } = await req.json();
const agent = new DynamicMCPAgent();
// 여러 MCP 서버에 연결
const toolCount = await agent.addMCPServers(
mcpServers || [
'/api/mcp/advanced',
'ws://localhost:3001/mcp',
'https://external-mcp-server.com/mcp',
]
);
console.log(`MCP 서버에서 ${toolCount}개의 도구에 연결됨`);
const result = await agent.execute(message);
return new Response(
JSON.stringify({ result }),
{ headers: { 'Content-Type': 'application/json' } }
);
}
여러 MCP 서버에서 동시에 도구를 발견하고 사용할 수 있는 동적 MCP 에이전트를 구현합니다.
결론
모델 컨텍스트 프로토콜(MCP)은 도구, 리소스, 프롬프트를 위한 표준화되고 발견 가능한 인터페이스를 제공함으로써 AI 에이전트가 외부 시스템과 상호작용하는 방식을 변화시킵니다. Vercel에서 Next.js 애플리케이션에 MCP를 구현하면 코드 변경 없이도 새로운 기능에 동적으로 적응할 수 있는 에이전트를 생성할 수 있습니다. @vercel/mcp-handler
와 Langchain, es-toolkit의 결합은 모든 MCP 호환 서비스와 원활하게 통합할 수 있는 프로덕션 준비가 완료된 서버리스 AI 시스템을 구축하기 위한 강력한 기반을 제공합니다. 이러한 프로토콜 우선 접근 방식은 AI 도구 생태계가 계속 발전함에 따라 에이전트가 상호 운용 가능하고 미래에도 대비할 수 있도록 보장합니다.