MCP(Model Context Protocol) 서버 개발 가이드: LLM과 외부 도구의 완벽한 통합
서론: MCP란 무엇인가?
최근 대규모 언어 모델(LLM)이 인공지능 생태계의 중심으로 급부상하면서, 이러한 모델들이 외부 데이터나 도구와 어떻게 안전하게 상호작용할 수 있는지에 대한 표준이 필요해졌습니다. 이런 필요성에 부응하여 등장한 것이 바로 Model Context Protocol(MCP) 입니다.
MCP는, 간단히 말해서, LLM 애플리케이션이 외부 데이터 소스, 도구, 그리고 다양한 서비스에 안전하게 접근할 수 있게 해주는 표준화된 프로토콜입니다. 이는 웹 API가 다양한 서비스 간 통신을 표준화한 것처럼, AI 모델과 외부 세계 간의 상호작용을 위한 공통 언어를 제공합니다.
저는 지난 며칠 동안 MCP 서버를 개발하고 여러 기업용 애플리케이션에 통합하는 작업을 진행해왔습니다. 그 과정에서 겪었던 도전과 발견한 해결책을 이 글을 통해 공유하고자 합니다.
MCP 아키텍처 이해하기
MCP는 클라이언트-서버 아키텍처를 기반으로 합니다. 이 구조에서 주요 구성 요소는 다음과 같습니다:
- 호스트 애플리케이션: Claude Desktop, VS Code, 또는 사용자 정의 애플리케이션과 같이 MCP 클라이언트를 포함하는 애플리케이션입니다.
- MCP 클라이언트: 호스트 애플리케이션 내에서 서버와 1:1 연결을 유지하는 구성 요소입니다.
- MCP 서버: 클라이언트에게 컨텍스트, 도구 및 프롬프트를 제공하는 구성 요소입니다.
MCP 서버는 LLM 애플리케이션(클라이언트)이 안전하게 외부 리소스와 상호작용할 수 있게 해줍니다. 이런 서버가 제공하는 주요 기능은 다음과 같습니다:
- 도구(Tools): 실행 가능한 함수로, API 호출, 계산, 시스템 작업 등을 수행합니다.
- 리소스(Resources): 파일, 데이터베이스 레코드, 실시간 시스템 데이터 등의 데이터 소스입니다.
- 프롬프트(Prompts): LLM 상호작용을 위한 사전 정의된 템플릿입니다.
실제 개발 현장에서는 이 구조가 매우 유용했습니다. 예를 들어, 한 프로젝트에서는 내부 지식 기반 시스템에 접근할 수 있는 MCP 서버를 구축했는데, 이를 통해 고객 응대 AI가 최신 제품 정보와 FAQ를 항상 참조할 수 있게 되었습니다. 클라이언트와 서버 간의 명확한 분리 덕분에 각 구성 요소를 독립적으로 유지 관리하고 확장할 수 있었습니다.
이제 본격적으로 MCP 서버 개발 환경을 설정하는 방법에 대해 알아보겠습니다.
개발 환경 설정하기
MCP 서버 개발을 시작하기 전에, 필요한 환경을 설정해야 합니다. 이 글에서는 TypeScript SDK를 사용한 서버 개발에 초점을 맞추겠습니다.
- Node.js와 npm 설치하기: 먼저 Node.js(버전 18 이상)와 npm이 설치되어 있는지 확인하세요.
- 프로젝트 디렉토리 생성하기:
- mkdir my-mcp-server cd my-mcp-server
- Node.js 프로젝트 초기화하기:
- npm init -y
- MCP TypeScript SDK 설치하기:
- npm install @modelcontextprotocol/sdk
- TypeScript 및 기타 의존성 설치하기:
- npm install typescript zod
- tsconfig.json 파일 생성하기:
- { "compilerOptions": { "target": "ES2020", "module": "NodeNext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "outDir": "./build" }, "include": ["src/**/*"] }
- 소스 코드를 위한 src 디렉토리 생성하기:
- mkdir src touch src/index.ts
이 설정은 TypeScript로 MCP 서버를 개발하기 위한 기본적인 환경을 제공합니다. 당연히 프로젝트의 구체적인 요구에 따라 추가 패키지가 필요할 수 있습니다.
개발 환경을 설정하면서 실제로 경험했던 문제 중 하나는 SDK 버전 호환성이었습니다. MCP는 계속 발전하고 있는 기술이므로, SDK 버전이 자주 업데이트됩니다. 프로젝트에서 올바른 버전을 사용하고 있는지 확인하고, 가능하다면 패키지 잠금 파일(package-lock.json)을 사용하여 버전을 고정하는 것이 좋습니다.
이제 개발 환경이 설정되었으니, 첫 번째 MCP 서버를 구현해 보겠습니다.
첫 번째 MCP 서버 구현하기
가장 간단한 형태의 MCP 서버를 구현해 보겠습니다. 이 서버는 입력을 그대로 반환하는 기본적인 에코 기능을 제공할 것입니다.
src/index.ts 파일을 다음과 같이 작성해 보세요:
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
// MCP 서버 인스턴스 생성하기
const server = new McpServer({
name: 'EchoServer',
version: '1.0.0',
});
// 입력을 그대로 반환하는 에코 도구 추가하기
server.tool(
'echo',
{ message: z.string() }, // zod를 사용하여 입력 스키마 정의
async ({ message }) => ({
content: [{ type: 'text', text: `에코: ${message}` }],
})
);
// 트랜스포트 생성하기 (이 예제에서는 stdio 사용)
const transport = new StdioServerTransport();
// 서버를 트랜스포트에 연결하기
await server.connect(transport);
이 코드는 다음과 같은 작업을 수행합니다:
- MCP 서버 인스턴스를 생성합니다.
- echo라는 이름의 도구를 정의합니다. 이 도구는 문자열 입력을 받아 그대로 반환합니다.
- stdio 트랜스포트를 사용하여 서버를 시작합니다.
이 예제에서는 StdioServerTransport를 사용하고 있는데, 이는 표준 입출력을 통해 통신하는 방식입니다. 이는 로컬 통합과 명령줄 도구에 적합합니다. 하지만 실제 프로덕션 환경에서는 HTTP를 기반으로 하는 Server-Sent Events(SSE) 트랜스포트가 더 많이 사용됩니다.
이 서버를 실행하려면:
- TypeScript 코드를 컴파일하세요:
- npx tsc
- 컴파일된 JavaScript를 실행하세요:
- node build/index.js
또는 package.json의 scripts 섹션에 다음을 추가하여 간편하게 실행할 수 있습니다:
"scripts": {
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
"start": "node build/index.js"
}
이제 npm run build와 npm start로 서버를 구축하고 시작할 수 있습니다. 실제로 이 기본 서버를 구현하고 나면, 다양한 기능을 추가하여 확장할 수 있습니다.
다음 섹션에서는 MCP 서버가 제공할 수 있는 다양한 기능(도구, 리소스, 프롬프트)에 대해 더 자세히 알아보겠습니다.
MCP 서버의 기능 노출하기
MCP 서버는 세 가지 주요 메커니즘을 통해 기능을 제공합니다: 도구(Tools), 리소스(Resources), 프롬프트(Prompts). 이 기능들은 LLM이 외부 시스템과 상호작용하고, 데이터에 접근하며, 사전 정의된 상호작용 패턴을 활용할 수 있게 해줍니다.
도구(Tools) 정의 및 구현하기
도구는 LLM이 서버를 통해 작업을 수행할 수 있게 해줍니다. 이는 웹 API의 POST 엔드포인트와 유사하며, 모델이 제어할 수 있도록 설계되었습니다.
주요 특징:
- 검색 가능성: 클라이언트는 tools/list 엔드포인트를 사용하여 사용 가능한 도구를 찾을 수 있습니다.
- 호출: 도구는 tools/call 엔드포인트를 통해 호출됩니다.
- 유연성: 도구는 간단한 계산부터 복잡한 API 상호작용까지 다양할 수 있습니다.
다음은 실제로 날씨 정보를 가져오는 도구를 구현한 예입니다:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
const server = new McpServer({
name: "WeatherServer",
version: "1.0.0"
});
// 날씨 조회 도구 추가하기
server.tool(
'get-weather',
{ city: z.string() }, // zod로 입력 스키마 정의
async ({ city }) => {
try {
// 실제 구현에서는 날씨 API를 호출할 것입니다
const mockWeather = {
city,
temperature: Math.round(Math.random() * 30),
conditions: ["맑음", "흐림", "비"]
[Math.floor(Math.random() * 3)]
};
return {
content: [{
type: 'text',
text: JSON.stringify(mockWeather, null, 2)
}]
};
} catch (error: any) {
return {
content: [{
type: 'text',
text: `날씨 조회 오류: ${error.message}`
}],
isError: true
};
}
}
);
실제 프로젝트에서 이와 유사한 도구를 구현했을 때, 도구 호출 과정에서 발생할 수 있는 오류 처리가 중요했습니다. 특히 외부 API 연동 시 타임아웃, 서버 오류 등 다양한 상황에 대비해야 했습니다. 이런 오류 상황에서는 isError 플래그를 true로 설정하고, 오류 메시지를 명확하게 제공하는 것이 중요합니다.
리소스(Resources) 관리하기
리소스는 서버가 LLM에 데이터와 콘텐츠를 노출할 수 있게 해줍니다. 이는 웹 API의 GET 엔드포인트와 유사하며, 응용 프로그램이 제어하도록 설계되었습니다.
주요 특징:
- 리소스 URI: 리소스는 URI로 식별됩니다(예: file:///path/to/file.txt).
- 텍스트 및 이진 리소스: 리소스는 텍스트(UTF-8) 또는 이진 데이터(base64 인코딩)를 포함할 수 있습니다.
- 검색:
- 직접 리소스: 서버는 구체적인 리소스 목록을 노출합니다.
- 리소스 템플릿: 서버는 동적 리소스를 위한 URI 템플릿을 노출합니다.
- 리소스 읽기: 클라이언트는 resources/read 요청을 사용합니다.
다음은 프로젝트 파일에 접근할 수 있는 리소스 구현 예입니다:
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import fs from 'fs/promises';
import path from 'path';
const server = new McpServer({
name: "ProjectFilesServer",
version: "1.0.0"
});
const PROJECT_DIR = './project'; // 프로젝트 디렉토리 경로
server.resource(
"project-files",
new ResourceTemplate("file:///project/{filename}", {
list: async () => {
try {
await fs.mkdir(PROJECT_DIR, { recursive: true });
const files = await fs.readdir(PROJECT_DIR);
return files.map(file => ({
uri: `file:///project/${file}`,
name: file,
mimeType: 'text/plain'
}));
} catch (error) {
console.error(`프로젝트 파일 목록 조회 오류: ${error}`);
return [];
}
}
}),
async (uri, { filename }) => {
const filePath = path.join(PROJECT_DIR, filename);
// 보안 검증: 경로 이탈 방지
if (!filePath.startsWith(PROJECT_DIR) ||
path.relative(PROJECT_DIR, filePath).startsWith('..')) {
throw new Error("접근이 거부되었습니다");
}
try {
const fileContents = await fs.readFile(filePath, 'utf-8');
return {
contents: [{
uri: uri.href,
mimeType: "text/plain",
text: fileContents
}]
};
} catch (error: any) {
throw new Error(`파일 읽기 오류: ${error.message}`);
}
}
);
리소스 구현에서 경험한 주요 도전 중 하나는 경로 이탈(path traversal) 같은 보안 취약점을 방지하는 것이었습니다. 위 예제에서 볼 수 있듯이, 파일 경로가 허용된 디렉토리를 벗어나지 않는지 확인하는 검증 단계가 필수적입니다.
프롬프트(Prompts) 생성 및 공유하기
프롬프트는 클라이언트가 사용자와 LLM에 제공할 수 있는 재사용 가능한 템플릿입니다. 이는 사용자가 제어하도록 설계되었습니다.
주요 특징:
- 동적 인수: 프롬프트는 동적 인수를 받을 수 있습니다.
- 리소스의 컨텍스트: 프롬프트는 리소스의 컨텍스트를 포함할 수 있습니다.
- 다단계 워크플로우: 프롬프트는 여러 상호작용을 연결할 수 있습니다.
- UI 통합: 프롬프트는 UI 요소(예: 슬래시 명령)로 표시될 수 있습니다.
다음은 코드 리뷰 프롬프트를 구현한 예입니다:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
const server = new McpServer({
name: "CodeReviewServer",
version: "1.0.0"
});
server.prompt(
"code-review",
{
code: z.string(),
language: z.string().optional(),
focus: z.enum(["security", "performance", "readability"]).optional()
},
({ code, language, focus }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `다음 ${language || '코드'}를 ${focus || '전반적인 품질'} 관점에서 리뷰해주세요:\n\n${code}`
}
}]
})
);
이 프롬프트는 코드, 언어, 그리고 리뷰 포커스를 매개변수로 받아 LLM에 전달할 메시지를 생성합니다. 실제 현장에서는 이러한 프롬프트가 반복적인 작업을 자동화하는 데 매우 유용했습니다.
다음 섹션에서는 더 복잡한 MCP 서버 기능과 고급 주제를 살펴보겠습니다.
고급 MCP 서버 기능
기본적인 도구, 리소스, 프롬프트 구현을 넘어서면 MCP는 더 강력한 기능들을 제공합니다. 이러한 고급 기능들은 LLM과의 상호작용을 더욱 풍부하게 만들어줍니다.
샘플링(Sampling)
샘플링은 서버가 클라이언트를 통해 LLM에 텍스트 생성을 요청할 수 있게 해줍니다. 이는 서버가 특정 작업에 대해 LLM의 도움을 직접 요청할 수 있음을 의미합니다. 사용자 중심의 설계를 통해 보안과 프라이버시를 유지하면서도 에이전트 같은 행동을 가능하게 합니다.
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
SamplingCreateMessageRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
const server = new Server(
{ name: "SamplingDemoServer", version: "1.0.0" },
{ capabilities: { sampling: {} } }
);
// 파일 요약 도구 구현
server.setRequestHandler(
CallToolRequestSchema,
async (request) => {
if (request.params.name === 'summarize-file') {
const { filePath } = request.params.arguments as { filePath: string };
// 파일 내용 읽기
const fileContent = await fs.readFile(filePath, 'utf-8');
// LLM에 요약 요청하기
const samplingResponse = await server.request(
{
method: 'sampling/createMessage',
params: {
messages: [{
role: 'user',
content: {
type: 'text',
text: `다음 내용을 간결하게 요약해주세요:\n\n${fileContent}`
}
}],
systemPrompt: "당신은 문서 요약 전문가입니다. 핵심만 간결하게 요약해주세요.",
includeContext: "none"
}
},
SamplingCreateMessageRequestSchema
);
// 요약된 내용 반환
return {
content: [{
type: 'text',
text: samplingResponse.completion.content
}]
};
}
throw new Error(`알 수 없는 도구: ${request.params.name}`);
}
);
샘플링 기능은 서버 자체가 LLM의 능력을 활용하여 태스크를 수행할 수 있게 해주는 강력한 기능입니다. 예를 들어, 한 프로젝트에서는 샘플링을 사용하여 서버가 데이터를 분석하고 요약본을 제공하는 기능을 구현했습니다. 이때 중요한 점은 사용자가 샘플링 요청과 완료 내용을 모두 검토하고 수정할 수 있다는 것입니다.
진행 상황 보고(Progress Reporting)
긴 작업을 실행할 때, 서버는 진행 상황을 클라이언트에 보고할 수 있습니다. 이렇게 하면 사용자에게 현재 상태에 대한 피드백을 제공할 수 있습니다:
server.tool(
'long-process',
{ duration: z.number() },
async ({ duration }, progress) => {
const steps = 10;
const stepTime = duration / steps;
for (let i = 1; i <= steps; i++) {
// 각 단계 실행
await new Promise(resolve => setTimeout(resolve, stepTime));
// 진행 상황 보고 (0-100%)
if (progress) {
await progress.report(
(i / steps) * 100,
`단계 ${i}/${steps} 완료`
);
}
}
return {
content: [{ type: 'text', text: '모든 처리가 완료되었습니다.' }]
};
}
);
실제 현장에서는 파일 처리, 데이터 분석, API 호출 등 시간이 많이 걸리는 작업에서 진행 상황 보고가 유용했습니다. 사용자는 작업이 중단된 것이 아니라 진행 중임을 알 수 있어 더 나은 경험을 제공했습니다.
리소스 구독(Resource Subscriptions)
클라이언트는 리소스 업데이트를 구독하여 리소스 내용이 변경될 때 실시간 알림을 받을 수 있습니다. 이는 실시간 업데이트가 필요한 시나리오에서 매우 유용합니다:
// 서버 측 구현
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
ResourceUpdatedNotificationSchema,
SubscribeToResourceRequestSchema,
UnsubscribeFromResourceRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
const server = new Server(
{ name: "SubscriptionServer", version: "1.0.0" },
{ capabilities: { resources: {} } }
);
// 구독 관리를 위한 Map
const subscriptions = new Map();
// 구독 요청 처리
server.setRequestHandler(
SubscribeToResourceRequestSchema,
async (request) => {
const uri = request.params.uri;
// 구독 정보 저장
if (!subscriptions.has(uri)) {
subscriptions.set(uri, new Set());
}
subscriptions.get(uri).add(request.id);
return {};
}
);
// 구독 해제 요청 처리
server.setRequestHandler(
UnsubscribeFromResourceRequestSchema,
async (request) => {
const uri = request.params.uri;
if (subscriptions.has(uri)) {
subscriptions.get(uri).delete(request.id);
}
return {};
}
);
// 리소스가 업데이트되면 알림 보내기
function notifyResourceUpdate(uri) {
if (subscriptions.has(uri)) {
for (const clientId of subscriptions.get(uri)) {
server.sendNotification({
method: 'notifications/resources/updated',
params: { uri }
});
}
}
}
이 기능을 활용하여 실시간 데이터 모니터링 시스템을 구축한 경험이 있습니다. 로그 파일이나 데이터베이스가 변경될 때마다 클라이언트에 알림을 보내 LLM이 항상 최신 정보에 접근할 수 있도록 했습니다. 이는 사용자가 지속적으로 데이터를 다시 로드하지 않아도 되는 효율적인 방법이었습니다.
루트(Roots)
루트는 서버가 작업할 수 있는 범위를 정의합니다. 클라이언트는 서버에 루트 URI를 제안하여 관련 리소스와 그 위치에 대한 정보를 제공합니다:
// 클라이언트 측 구현
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
const client = new Client(
{ name: 'root-aware-client', version: '1.0.0' },
{
capabilities: { resources: {} },
initializationOptions: {
roots: [
{
uri: 'file:///path/to/project',
name: '프로젝트 루트'
},
{
uri: 'git://github.com/username/repo',
name: 'Git 저장소'
}
]
}
}
);
루트는 강제적인 제한이 아니라 정보 제공 목적으로 사용됩니다. 서버는 이 정보를 활용하여 작업 범위를 이해하고 더 관련성 높은 결과를 제공할 수 있습니다.
실제 개발 프로젝트에서는 루트 정보를 활용하여 코드 분석 서버가 프로젝트 구조를 이해하고 관련 파일만 검색하도록 했습니다. 이로 인해 검색 범위가 좁아져 성능이 향상되었습니다.
보안 및 모범 사례
MCP 서버 개발 시 보안은 매우 중요한 고려사항입니다. 다음은 몇 가지 중요한 보안 모범 사례입니다:
입력 검증
모든 입력(리소스 URI, 도구 매개변수, 프롬프트 인수)은 반드시 검증해야 합니다:
import { z } from 'zod';
// 도구 입력 검증
server.tool(
'create-file',
{
filePath: z.string()
.regex(/^[a-zA-Z0-9_\-./]+$/, '허용되지 않는 문자가 포함되어 있습니다'),
content: z.string().max(10000, '파일 크기가 너무 큽니다')
},
async ({ filePath, content }) => {
// 경로 이탈 방지
const absolutePath = path.resolve(BASE_DIR, filePath);
if (!absolutePath.startsWith(BASE_DIR)) {
return {
content: [{ type: 'text', text: '잘못된 파일 경로입니다' }],
isError: true
};
}
// 파일 생성 작업 수행...
}
);
프로젝트 경험상, Zod 라이브러리는 입력 스키마를 정의하고 검증하기에 매우 유용했습니다. 특히 복잡한 입력 구조를 다룰 때 타입 안전성을 제공하여 많은 오류를 사전에 방지할 수 있었습니다.
오류 처리
일관된 오류 처리 전략을 사용하고, 오류 메시지에 민감한 정보가 포함되지 않도록 주의하세요:
// 도구 오류 처리 예시
server.tool(
'database-query',
{ query: z.string() },
async ({ query }) => {
try {
const result = await database.execute(query);
return {
content: [{ type: 'text', text: JSON.stringify(result) }]
};
} catch (error) {
// 민감한 정보 제거 및 일반적인 오류 메시지 반환
console.error('데이터베이스 오류:', error);
return {
content: [{
type: 'text',
text: '데이터베이스 쿼리 실행 중 오류가 발생했습니다'
}],
isError: true
};
}
}
);
오류 처리는 MCP 서버 개발에서 가장 어려웠던 부분 중 하나였습니다. 특히 프로덕션 환경에서는 오류 메시지가 너무 자세하면 보안 취약점을 노출할 수 있고, 너무 모호하면 디버깅이 어려워집니다. 적절한 균형을 찾는 것이 중요합니다.
일반 모범 사례
- 최소 권한 원칙: 클라이언트와 사용자에게 필요한 권한만 부여하세요.
- 심층 방어: 여러 계층의 보안을 구현하세요.
- 정기적인 감사: 서버의 보안과 의존성을 정기적으로 감사하세요.
- 의존성 관리: 잠금 파일을 사용하고 취약점에 대해 의존성을 정기적으로 감사하세요.
- 단순함 유지: 더 단순한 코드는 일반적으로 보호하기 더 쉽습니다.
- 문서화: 서버의 보안 조치와 예상 동작을 철저히 문서화하세요.
- 테스팅: 보안 테스트를 전체 테스트 전략의 일부로 포함하세요.
문제 해결 및 디버깅
MCP 서버 개발 중에 문제가 발생하면 다음과 같은 도구와 기술이 도움이 될 수 있습니다:
MCP 인스펙터
MCP 인스펙터는 MCP 서버를 테스트하기 위한 대화형 디버깅 인터페이스입니다. 이를 통해 모든 MCP 기능(도구, 리소스, 프롬프트)을 테스트할 수 있습니다:
npx @modelcontextprotocol/inspector node build/index.js
MCP 인스펙터는, 실제 개발 과정에서 도구 응답이 예상대로 작동하는지 확인하는 데 매우 유용했습니다. 특히 복잡한 입력과 출력을 테스트할 때 시간을 크게 절약할 수 있었습니다.
로깅 구현
서버 측 로깅은 문제 해결에 필수적입니다:
// stdio 트랜스포트 사용 시 로깅
function logInfo(message, data) {
console.error(`[INFO] ${message}`, data ? JSON.stringify(data) : '');
}
function logError(message, error) {
console.error(`[ERROR] ${message}`, error);
}
// 도구에 로깅 적용
server.tool(
'data-analysis',
{ dataset: z.string() },
async ({ dataset }) => {
logInfo(`데이터 분석 시작: ${dataset}`);
try {
// 작업 수행...
const result = await analyzeData(dataset);
logInfo('데이터 분석 완료', { resultSize: result.length });
return {
content: [{ type: 'text', text: JSON.stringify(result) }]
};
} catch (error) {
logError(`데이터 분석 오류: ${dataset}`, error);
return {
content: [{ type: 'text', text: `분석 오류: ${error.message}` }],
isError: true
};
}
}
);
체계적인 로깅은 복잡한 MCP 서버 디버깅에 큰 도움이 되었습니다. 특히 프로덕션 환경에서 문제가 발생했을 때, 좋은 로그는 문제 원인을 신속하게 파악하는 데 필수적이었습니다.
종합 예제: 완전한 MCP 서버
지금까지 배운 개념을 종합하여 완전한 MCP 서버를 구현해 보겠습니다. 이 예제는 파일 시스템 액세스, 날씨 정보 조회, 코드 리뷰 프롬프트를 제공합니다:
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import fs from 'fs/promises';
import path from 'path';
const BASE_DIR = './data'; // 기본 디렉토리 경로
// MCP 서버 생성
const server = new McpServer({
name: "통합 서버",
version: "1.0.0"
});
// 파일 시스템 리소스
server.resource(
"files",
new ResourceTemplate("file:///{filePath}", {
list: async () => {
try {
await fs.mkdir(BASE_DIR, { recursive: true });
// 기본 디렉토리의 파일 목록 조회
const files = await fs.readdir(BASE_DIR);
return files.map(file => ({
uri: `file:///${file}`,
name: file,
mimeType: getFileMimeType(file)
}));
} catch (error) {
console.error(`파일 목록 조회 오류: ${error}`);
return [];
}
}
}),
async (uri, { filePath }) => {
const fullPath = path.join(BASE_DIR, filePath);
// 경로 이탈 방지
if (!fullPath.startsWith(BASE_DIR)) {
throw new Error("접근이 거부되었습니다");
}
try {
const content = await fs.readFile(fullPath, 'utf-8');
return {
contents: [{
uri: uri.href,
mimeType: getFileMimeType(filePath),
text: content
}]
};
} catch (error) {
throw new Error(`파일 읽기 오류: ${error.message}`);
}
}
);
// 날씨 정보 조회 도구
server.tool(
'get-weather',
{ city: z.string().min(1, "도시 이름은 필수입니다") },
async ({ city }) => {
try {
// 실제 구현에서는 날씨 API 호출
const mockWeather = {
city,
temperature: Math.round(Math.random() * 30),
conditions: ["맑음", "흐림", "비"][Math.floor(Math.random() * 3)]
};
return {
content: [{ type: 'text', text: JSON.stringify(mockWeather, null, 2) }]
};
} catch (error) {
return {
content: [{ type: 'text', text: `날씨 조회 오류: ${error.message}` }],
isError: true
};
}
}
);
// 파일 생성 도구
server.tool(
'create-file',
{
fileName: z.string().regex(/^[a-zA-Z0-9_\-.]+$/, "파일 이름에 잘못된 문자가 있습니다"),
content: z.string()
},
async ({ fileName, content }) => {
const filePath = path.join(BASE_DIR, fileName);
// 경로 이탈 방지
if (!filePath.startsWith(BASE_DIR)) {
return {
content: [{ type: 'text', text: '잘못된 파일 경로입니다' }],
isError: true
};
}
try {
await fs.mkdir(BASE_DIR, { recursive: true });
await fs.writeFile(filePath, content, 'utf-8');
return {
content: [{ type: 'text', text: `파일 ${fileName}이(가) 성공적으로 생성되었습니다` }]
};
} catch (error) {
return {
content: [{ type: 'text', text: `파일 생성 오류: ${error.message}` }],
isError: true
};
}
}
);
// 코드 리뷰 프롬프트
server.prompt(
"code-review",
{
code: z.string(),
language: z.string().optional(),
focus: z.enum(["security", "performance", "readability"]).optional()
},
({ code, language, focus }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `다음 ${language || '코드'}를 ${focus || '전반적인 품질'} 관점에서 리뷰해주세요:\n\n${code}`
}
}]
})
);
// 파일 MIME 타입 판별 유틸리티 함수
function getFileMimeType(filePath) {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes = {
'.txt': 'text/plain',
'.md': 'text/markdown',
'.json': 'application/json',
'.js': 'application/javascript',
'.ts': 'application/typescript',
'.html': 'text/html',
'.css': 'text/css',
'.xml': 'application/xml',
'.csv': 'text/csv'
};
return mimeTypes[ext] || 'text/plain';
}
// 서버 시작
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("서버가 시작되었습니다");
이 서버는 여러 기능을 제공합니다:
- 파일 시스템 접근을 위한 리소스
- 날씨 정보 조회 도구
- 파일 생성 도구
- 코드 리뷰 프롬프트
실제 프로젝트에서는 유사한 구조로 서버를 구성했으며, 여러 팀원이 각 기능을 담당하여 개발했습니다. 모듈화된 접근 방식을 통해 코드 유지보수가 용이해졌고, 새로운 기능을 쉽게 추가할 수 있었습니다.
결론
MCP 서버 개발은 LLM 애플리케이션에 강력한 확장성을 제공합니다. 이 가이드에서는 MCP 서버의 기본 개념부터 고급 기능까지 다루었습니다:
- MCP 아키텍처 이해와 개발 환경 설정
- 기본 MCP 서버 구현
- 도구, 리소스, 프롬프트 구현
- 고급 기능(샘플링, 진행 상황 보고, 리소스 구독 등)
- 보안 및 모범 사례
- 문제 해결 및 디버깅 기법
MCP는 계속 발전하는 기술로, 앞으로 더 많은 기능과 개선사항이 추가될 것입니다. 이 가이드가 MCP 서버 개발에 첫 발을 내딛는 데 도움이 되기를 바랍니다.
'AI' 카테고리의 다른 글
Model Context Protocol (MCP): AI 애플리케이션을 위한 통합 프로토콜 (0) | 2025.04.13 |
---|---|
MCP TypeScript SDK 종합 분석 (0) | 2025.04.13 |
MCP(Model Context Protocol) 클라이언트 개발 가이드: LLM과 외부 도구 통합하기 (0) | 2025.04.11 |
MCP(Model Context Protocol)의 보안 위험과 방지책 (0) | 2025.04.09 |
MCP란? (0) | 2025.04.09 |