코딩 지식이 없어도 Anakin AI를 사용하여 쉽게 AI 워크플로우를 만들 수 있습니다. GPT-4, Claude 3.5 Sonnet, Uncensored Dolphin-Mixtral, Stable Diffusion, DALLE, 웹 스크래핑 등과 같은 LLM API에 연결하여 하나의 워크플로우로 통합하세요!
복잡한 코딩을 잊고 Anakin AI로 당신의 평범한 작업을 자동화하세요!
한정된 시간 동안 Google Gemini 1.5와 Stable Diffusion을 무료로 사용할 수 있습니다!

소개
모델 컨텍스트 프로토콜(MCP)은 AI 생태계의 중요한 발전을 나타내며, 대규모 언어 모델과의 표준화된 통신 방법을 제공합니다. 각 AI 플랫폼이 메시지를 위한 고유한 형식을 구현하는 대신, MCP는 다양한 모델과 플랫폼에서 프롬프트, 응답, 함수 호출을 위한 일관된 인터페이스를 제공하는 것을 목표로 합니다.
프로토콜 자체는 발전하고 있지만, 기본적인 MCP 호환 서버를 구축하는 것은 간단할 수 있습니다. 이 가이드에서 저는 현대 AI 시스템에서 메시지 처리를 위한 핵심 원칙을 준수하는 간단하지만 기능적인 서버를 만드는 방법을 안내할 것입니다. 우리의 구현은 나중에 더 고급 기능으로 확장할 수 있는 기초를 만드는 데 집중할 것입니다.
10분이면 충분할까요? 프로덕션 준비가 된 시스템에는 확실히 그렇지 않습니다. 그러나 핵심 개념을 시연하는 작동 프로토타입에는 충분합니다. 그럼 시작해볼까요!
전제 조건
시작하기 전에 필요한 것은:
- 시스템에 설치된 Node.js (v16+)
- JavaScript/TypeScript에 대한 기본 지식
- Express.js 또는 유사한 웹 프레임워크에 대한 친숙함
- 코드 편집기 (VS Code 추천)
- 터미널/명령 줄 접근
- npm 또는 yarn 패키지 관리자
1단계: 프로젝트 설정 (2분)
우선 새로운 디렉토리를 만들고 프로젝트를 초기화합시다:
mkdir mcp-server
cd mcp-server
npm init -y
이제 필요한 종속성을 설치합니다:
npm install express cors typescript ts-node @types/node @types/express @types/cors
npm install --save-dev nodemon
타입스크립트 구성 파일을 생성합니다:
npx tsc --init
생성된 tsconfig.json
를 수정하여 다음과 같은 필수 설정을 포함합니다:
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
package.json
의 스크립트 섹션을 업데이트합니다:
"scripts": {
"start": "node dist/index.js",
"dev": "nodemon --exec ts-node src/index.ts",
"build": "tsc"
}
2단계: 핵심 서버 만들기 (3분)
mkdir -p src/handlers
touch src/index.ts
touch src/handlers/messageHandler.ts
touch src/types.ts
src/types.ts
에서 먼저 타입을 정의해 봅시다:
// 기본 메시지 구조
export interface Content {
type: string;
text?: string;
}
export interface Message {
role: "user" | "assistant" | "system";
content: Content[] | string;
}
// 요청 및 응답 구조
export interface ModelRequest {
messages: Message[];
max_tokens?: number;
temperature?: number;
stream?: boolean;
}
export interface ModelResponse {
message: Message;
}
// 도구 호출 인터페이스
export interface Tool {
name: string;
description: string;
input_schema: {
type: string;
properties: Record<string, any>;
required?: string[];
};
}
export interface ToolCall {
type: "tool_call";
id: string;
name: string;
input: Record<string, any>;
}
export interface ToolResult {
type: "tool_result";
tool_call_id: string;
content: string;
}
이제 src/index.ts
에 기본 서버를 구현합니다:
import express from 'express';
import cors from 'cors';
import { handleMessageRequest } from './handlers/messageHandler';
const app = express();
const PORT = process.env.PORT || 3000;
// 미들웨어
app.use(cors());
app.use(express.json({ limit: '10mb' }));
// 메시지 처리의 주요 엔드포인트
app.post('/v1/messages', handleMessageRequest);
// 상태 점검 엔드포인트
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
// 서버 시작
app.listen(PORT, () => {
console.log(`서버가 ${PORT} 포트에서 실행 중입니다.`);
console.log(`상태 점검: http://localhost:${PORT}/health`);
console.log(`메시지 엔드포인트: http://localhost:${PORT}/v1/messages`);
});
다음으로 src/handlers/messageHandler.ts
에서 메시지 핸들러를 구현합니다:
import { Request, Response } from 'express';
import { ModelRequest, ModelResponse, Message, Content } from '../types';
export async function handleMessageRequest(req: Request, res: Response) {
try {
const request = req.body as ModelRequest;
// 기본 유효성 검사
if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
return res.status(400).json({ error: '유효하지 않은 요청 형식입니다. 메시지 배열이 필요합니다.' });
}
// 수신한 요청 로그 (디버깅 용)
console.log('수신한 요청:', request.messages.length, '메시지');
// 메시지 처리
const response = processMessages(request.messages);
// 응답 반환
return res.status(200).json(response);
} catch (error) {
console.error('요청 처리 중 오류:', error);
return res.status(500).json({ error: '서버 내부 오류' });
}
}
function processMessages(messages: Message[]): ModelResponse {
// 마지막 사용자 메시지를 추출
const lastUserMessage = findLastUserMessage(messages);
if (!lastUserMessage) {
return createErrorResponse("대화에서 사용자 메시지를 찾을 수 없습니다.");
}
const userQuery = extractTextContent(lastUserMessage);
// 간단한 응답 생성 로직
let responseText = "";
if (userQuery.toLowerCase().includes('안녕하세요') || userQuery.toLowerCase().includes('안녕')) {
responseText = "안녕하세요! 오늘 무엇을 도와드릴까요?";
} else if (userQuery.toLowerCase().includes('날씨')) {
responseText = "제가 실시간 날씨 데이터에 접근할 수는 없지만, 일반적으로 날씨 패턴을 이해하는 데 도움을 드릴 수 있습니다.";
} else if (userQuery.toLowerCase().includes('시간')) {
responseText = `현재 서버 시간은 ${new Date().toLocaleTimeString()}입니다.`;
} else {
responseText = "귀하의 메시지를 받았습니다. 이는 간단한 모델 서버 응답입니다.";
}
// 응답 구성 및 반환
return {
message: {
role: "assistant",
content: [{
type: "text",
text: responseText
}]
}
};
}
function findLastUserMessage(messages: Message[]): Message | undefined {
// 역할이 'user'인 마지막 메시지 찾기
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === 'user') {
return messages[i];
}
}
return undefined;
}
function extractTextContent(message: Message): string {
if (typeof message.content === 'string') {
return message.content;
} else if (Array.isArray(message.content)) {
return message.content
.filter(item => item.type === 'text' && item.text)
.map(item => item.text)
.join(' ');
}
return '';
}
function createErrorResponse(errorMessage: string): ModelResponse {
return {
message: {
role: "assistant",
content: [{
type: "text",
text: `오류: ${errorMessage}`
}]
}
};
}
3단계: 도구 호출 기능 추가 (3분)
도구 정의 및 구현을 위한 새로운 파일을 생성합니다:
touch src/tools.ts
src/tools.ts
에서 기본 도구를 구현합니다:
import { Tool } from './types';
// 도구 정의
export const availableTools: Tool[] = [
{
name: "get_current_time",
description: "현재 서버 시간을 가져옵니다.",
input_schema: {
type: "object",
properties: {
timezone: {
type: "string",
description: "선택적인 시간대 (기본값은 서버 시간대)"
}
}
}
},
{
name: "calculate",
description: "수학 계산을 수행합니다.",
input_schema: {
type: "object",
properties: {
expression: {
type: "string",
description: "평가할 수학 표현식"
}
},
required: ["expression"]
}
}
];
// 도구 구현
export function executeToolCall(name: string, params: Record<string, any>): string {
switch (name) {
case "get_current_time":
return getTime(params.timezone);
case "calculate":
return calculate(params.expression);
default:
throw new Error(`알 수 없는 도구: ${name}`);
}
}
function getTime(timezone?: string): string {
const options: Intl.DateTimeFormatOptions = {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZoneName: 'short'
};
try {
if (timezone) {
options.timeZone = timezone;
}
return new Date().toLocaleTimeString('ko-KR', options);
} catch (error) {
return `${new Date().toLocaleTimeString()} (서버 시간)`;
}
}
function calculate(expression: string): string {
try {
// 주의: 실제 애플리케이션에서는 보다 안전한 평가 방법을 사용해야 합니다.
// 이는 단순한 예시입니다.
const sanitizedExpression = expression.replace(/[^0-9+\-*/().\s]/g, '');
const result = eval(sanitizedExpression);
return `${expression} = ${result}`;
} catch (error) {
return `오류: ${expression} 계산 중 오류: ${error instanceof Error ? error.message : String(error)}`;
}
}
이제 메시지 핸들러를 업데이트하여 도구 호출을 지원하게 합니다 src/handlers/messageHandler.ts
를 수정합니다:
// 상단에 다음 가져오기 추가
import { availableTools, executeToolCall } from '../tools';
import { ToolCall, ToolResult } from '../types';
// handleMessageRequest 함수 업데이트
export async function handleMessageRequest(req: Request, res: Response) {
try {
const request = req.body as ModelRequest;
// 기본 유효성 검사
if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
return res.status(400).json({ error: '유효하지 않은 요청 형식입니다. 메시지 배열이 필요합니다.' });
}
// 도구 결과가 있는 요청인지 확인
const lastMessage = request.messages[request.messages.length - 1];
if (lastMessage.role === 'assistant' && Array.isArray(lastMessage.content)) {
const toolCalls = lastMessage.content.filter(item =>
item.type === 'tool_call') as ToolCall[];
if (toolCalls.length > 0) {
// 이것은 도구 결과에 대한 후속 요청입니다
return handleToolResultsResponse(request, res);
}
}
// 일반 메시지로 처리
const response = processMessages(request.messages);
// 응답 반환
return res.status(200).json(response);
} catch (error) {
console.error('요청 처리 중 오류:', error);
return res.status(500).json({ error: '서버 내부 오류' });
}
}
// 도구 호출을 처리하는 이 함수를 추가합니다
function handleToolResultsResponse(request: ModelRequest, res: Response) {
const messages = request.messages;
const lastAssistantMessage = messages[messages.length - 1];
if (lastAssistantMessage.role !== 'assistant' || !Array.isArray(lastAssistantMessage.content)) {
return res.status(400).json({ error: '유효하지 않은 도구 결과 형식' });
}
// 도구 호출 및 결과 찾기
const toolCalls = lastAssistantMessage.content.filter(
item => item.type === 'tool_call'
) as ToolCall[];
const toolResults = lastAssistantMessage.content.filter(
item => item.type === 'tool_result'
) as ToolResult[];
// 결과 처리
let finalResponse = "다음 정보를 처리했습니다:\n\n";
toolResults.forEach(result => {
const relatedCall = toolCalls.find(call => call.id === result.tool_call_id);
if (relatedCall) {
finalResponse += `- ${relatedCall.name}: ${result.content}\n`;
}
});
return res.status(200).json({
message: {
role: "assistant",
content: [{ type: "text", text: finalResponse }]
}
});
}
// processMessages 함수 수정하여 도구 호출 가능성 처리
function processMessages(messages: Message[]): ModelResponse {
const lastUserMessage = findLastUserMessage(messages);
if (!lastUserMessage) {
return createErrorResponse("대화에서 사용자 메시지를 찾을 수 없습니다.");
}
const userQuery = extractTextContent(lastUserMessage);
// 도구 사용을 트리거할 수 있는 키워드 찾기
if (userQuery.toLowerCase().includes('시간')) {
return {
message: {
role: "assistant",
content: [
{ type: "tool_call", id: "call_001", name: "get_current_time", input: {} },
{
type: "text",
text: "현재 시간을 확인해볼게요."
}
]
}
};
} else if (userQuery.toLowerCase().match(/계산|구해봐|무엇이 \d+[\+\-\*\/]/)) {
// 잠재적 계산 추출
const expression = userQuery.match(/(\d+[\+\-\*\/\(\)\.]*\d+)/)?.[0] || "1+1";
return {
message: {
role: "assistant",
content: [
{
type: "tool_call",
id: "call_002",
name: "calculate",
input: { expression }
},
{
type: "text",
text: `${expression}를 계산해볼게요.`
}
]
}
};
}
// 기타 쿼리에 대한 기본 응답
return {
message: {
role: "assistant",
content: [{
type: "text",
text: `귀하의 메시지를 받았습니다: "${userQuery}". 추가로 어떻게 도와드릴까요?`
}]
}
};
}
4단계: 테스트 및 배포 (2분)
서버를 확인하기 위해 간단한 테스트 스크립트를 만듭니다. 루트 디렉토리에 test.js
라는 파일을 생성합니다:
const fetch = require('node-fetch');
async function testServer() {
const url = 'http://localhost:3000/v1/messages';
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
messages: [
{
role: "user",
content: [
{
type: "text",
text: "지금 몇 시인가요?"
}
]
}
]
})
});
const data = await response.json();
console.log(JSON.stringify(data, null, 2));
}
testServer().catch(console.error);
서버를 테스트하려면:
# 서버 시작
npm run dev
# 다른 터미널에서 테스트 실행
node test.js
빠른 배포를 위해 간단한 Dockerfile을 생성합니다:
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
컨테이너를 빌드하고 실행합니다:
docker build -t mcp-server .
docker run -p 3000:3000 mcp-server
결론
- 구조화된 메시지 요청을 처리할 수 있습니다.
- 표준 형식으로 응답할 수 있습니다.
- 기본 도구 호출 기능을 처리할 수 있습니다.
- 도구 결과를 처리하고 응답할 수 있습니다.
이 구현은 단순화되었지만, 향후 개발을 위한 탄탄한 기초를 제공합니다. 이 서버를 프로덕션 사용을 위해 확장하려면 다음을 고려하십시오:
- 인증 및 속도 제한 추가
- 적절한 오류 처리 및 유효성 검사 구현
- 처리를 위한 실제 AI 모델에 연결
- 더 정교한 도구 추가
- 스트리밍 응답 구현
- 포괄적인 로깅 및 모니터링 추가
우리의 구현이 현대 AI 메시징 프로토콜의 일반 원칙을 따르지만, OpenAI의 API 또는 Anthropic의 Claude API와 같은 특정 구현은 추가 요구 사항이나 예상 형식의 약간의 변동이 있을 수 있음을 기억하십시오. 통합할 특정 서비스의 공식 문서를 항상 참조하십시오.
AI 메시징 프로토콜 분야는 빠르게 발전하고 있으므로 최신 동향을 유지하고 표준이 발전함에 따라 구현을 조정할 준비를 하십시오. 이 가이드에서 보여준 핵심 개념을 이해함으로써, 이 흥미로운 분야에서 등장하는 새로운 발전과 함께 작업할 준비가 잘 되어 있을 것입니다.