Model Context Protocol (MCP): 진짜 동작하는 AI 에이전트 만들기 완벽 가이드
2025년에 AI 앱 만들어봤으면, 다들 비슷한 경험 있을 거예요. LLM이 글 쓰는 건 진짜 잘하는데, 막상 DB 연결하거나 API 호출하려면? API 억지로 이어붙이고 프롬프트 기도 메타 시전하게 되죠.
그래서 나온 게 **Model Context Protocol (MCP)**예요. AI 개발에서 REST API급으로 중요해지고 있는 오픈 표준입니다. Anthropic이 만들어서 이제 업계 전반이 쓰고 있고, AI 엔지니어링 최대 난제를 해결해요: AI한테 외부 세계 접근권을 어떻게 안전하게 줄 것인가?
이 글에서 MCP가 뭔지, 왜 중요한지, 어떻게 돌아가는지, 그리고 직접 구현하는 법까지 싹 다 다룹니다.
MCP가 해결하는 문제
MCP를 파기 전에, 어떤 고통을 해결하는지 먼저 알아볼게요.
인테그레이션 지옥
기존 AI 애플리케이션 개발은 대충 이렇게 생겼어요:
┌─────────────────────────────────────────────────────────────┐
│ 당신의 AI 애플리케이션 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ OpenAI │ │ Database │ │ Slack │ │
│ │ API │ │ Queries │ │ API │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ 커스텀 파서 커스텀 파서 커스텀 파서 │
│ │ │ │ │
│ 프롬프트 꼼수 프롬프트 꼼수 프롬프트 꼼수 │
│ │ │ │ │
│ 에러 핸들러 에러 핸들러 에러 핸들러 │
│ │ │ │ │
│ └───────────────┴───────────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ 오케스트레이션 │ │
│ │ 스파게티 코드 │ │
│ └───────────────────┘ │
└─────────────────────────────────────────────────────────────┘
인테그레이션마다 필요한 것들:
- 커스텀 인증 처리
- 전용 응답 파싱
- LLM에게 툴을 설명하는 프롬프트 엔지니어링
- 인테그레이션마다 다른 에러 핸들링
- 수동 스키마 관리
일반적인 AI 에이전트가 10개 이상 인테그레이션이 필요하다고 치면, 유지보수 지옥이에요.
Function Calling의 한계
OpenAI의 function calling 같은 기능이 도움이 되긴 하는데, 근본적으로 특정 LLM 벤더에 종속돼요. GPT-4용으로 공들여 만든 function 정의가 Claude, Gemini, 막 나온 핫한 오픈소스 모델에서는 안 돌아가요.
// OpenAI에서는 이렇게... const tools = [{ type: "function", function: { name: "get_weather", description: "현재 날씨 조회", parameters: { type: "object", properties: { location: { type: "string" } } } } }]; // 근데 Claude는 다른 포맷... // Gemini도 또 다르고... // Llama는 또 다르고...
우리가 진짜 필요한 것
이상적인 솔루션은:
- 범용성: LLM 프로바이더 상관없이 동작
- 표준화: 모든 데이터 소스에 하나의 인테그레이션 패턴
- 양방향: AI가 데이터 조회도 하고 업데이트 받기도
- 보안: 내장된 인증과 권한 관리
- 발견 가능: AI가 런타임에 어떤 툴이 있는지 학습
MCP가 정확히 이걸 제공합니다.
Model Context Protocol이 뭔가요?
MCP는 AI 애플리케이션이 외부 데이터 소스와 툴에 연결하는 방식을 표준화하는 오픈 프로토콜이에요. **"AI용 USB"**라고 생각하면 돼요. 어떤 AI 모델이든 어떤 데이터 소스나 툴에든 꽂을 수 있는 범용 커넥터.
아키텍처
MCP는 클라이언트-서버 아키텍처를 따라요:
┌────────────────────────────────────────────────────────────────┐
│ MCP 아키텍처 │
├────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────────────────┐ │
│ │ MCP Client │ │ MCP Servers │ │
│ │ │ │ │ │
│ │ ┌───────────┐ │ │ ┌─────────┐ ┌─────────┐ │ │
│ │ │ AI Model │ │ JSON-RPC│ │ GitHub │ │ Slack │ │ │
│ │ │(GPT/Claude│◄─┼─────────┼─►│ Server │ │ Server │ │ │
│ │ │ /Gemini) │ │ over │ └─────────┘ └─────────┘ │ │
│ │ └───────────┘ │ stdio/ │ │ │
│ │ │ SSE/WS │ ┌─────────┐ ┌─────────┐ │ │
│ │ ┌───────────┐ │ │ │Database │ │ Custom │ │ │
│ │ │Host App │ │ │ │ Server │ │ Server │ │ │
│ │ │(Your App) │ │ │ └─────────┘ └─────────┘ │ │
│ │ └───────────┘ │ │ │ │
│ └─────────────────┘ └─────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
핵심 컴포넌트:
- MCP Client: 내 AI 앱 안에 있음. MCP 서버 찾아서 연결해줌.
- MCP Server: DB, API 같은 걸 표준 형식으로 열어줌.
- Transport Layer: JSON-RPC 2.0 (stdio, SSE, WebSocket으로 통신).
세 가지 기본 요소
MCP는 AI-외부세계 상호작용의 거의 모든 것을 커버하는 세 가지 핵심 요소를 정의해요:
1. Resources
AI가 읽을 수 있는 정적 또는 동적 데이터. AI가 접근할 수 있는 "파일"이라고 생각하면 돼요.
{ "uri": "file:///project/README.md", "name": "Project README", "mimeType": "text/markdown" }
2. Tools
AI가 액션을 수행하기 위해 호출할 수 있는 함수.
{ "name": "create_github_issue", "description": "GitHub 저장소에 새 이슈 생성", "inputSchema": { "type": "object", "properties": { "repo": { "type": "string" }, "title": { "type": "string" }, "body": { "type": "string" } }, "required": ["repo", "title"] } }
3. Prompts
파라미터와 함께 호출할 수 있는 재사용 가능한 프롬프트 템플릿.
{ "name": "code_review", "description": "베스트 프랙티스 코드 리뷰", "arguments": [ { "name": "code", "description": "리뷰할 코드", "required": true } ] }
왜 지금 MCP가 중요한가
AI 에이전트 폭발
2025년은 AI 에이전트의 해예요. OpenAI의 Operator부터 Claude의 컴퓨터 사용 기능까지, AI가 채팅을 넘어 자율 행동으로 이동하고 있어요. 근데 여기 더러운 비밀이 있어요: 자율 AI는 실제 세계 접근만큼만 똑똑해요.
다음을 신뢰성 있게 못하는 AI 에이전트는:
- 코드베이스 읽기
- 데이터베이스 쿼리
- 캘린더 확인
- 팀에 메시지 보내기
...그냥 아주 비싼 챗봇이에요.
MCP는 이런 인테그레이션을 신뢰성 있고, 일관되고, 유지보수 가능하게 만들어요.
표준화의 순간
2000년대 초반 웹 서비스 시대와 비슷한 변곡점에 있어요. 그때는 CORBA, DCOM, 독점 프로토콜이 서로 경쟁했죠. 그러다 REST가 이겼고, 갑자기 모두가 상호운용 가능한 웹 서비스를 만들 수 있게 됐어요.
MCP가 AI 인테그레이션의 REST가 되려고 해요. 주요 플레이어가 이미 합류했어요:
- Anthropic: 프로토콜 만들고 유지
- Microsoft: Copilot에 통합 중
- Cursor: AI IDE에서 네이티브 MCP 지원
- Sourcegraph: 코드 인텔리전스용 MCP 서버
MCP 서버 돌리는 게 새로운 웹 서버 돌리기
대담한 예측: 2026년이면 "MCP 서버 돌릴 수 있어요?"가 오늘의 "REST API 만들 수 있어요?"만큼 흔한 개발자 면접 질문이 될 거예요.
왜? 가치 있는 데이터를 가진 모든 회사가 AI 에이전트에게 통제되고 표준화된 방식으로 노출하고 싶어할 테니까요. 그 말은:
- 내부 문서용 MCP 서버
- 고객 데이터용 (적절한 인가와 함께)
- 비즈니스 프로세스용
- 도메인 특화 툴용
첫 MCP 서버 만들기
직접 해봅시다. 간단한 투두 리스트 API를 노출하는 MCP 서버를 만들어 볼게요.
프로젝트 세팅
# 새 프로젝트 생성 mkdir mcp-todo-server cd mcp-todo-server npm init -y # 의존성 설치 npm install @modelcontextprotocol/sdk zod npm install -D typescript @types/node tsx
기본 서버 구조
src/index.ts 생성:
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; // 인메모리 투두 저장소 interface Todo { id: string; title: string; completed: boolean; createdAt: Date; } const todos: Map<string, Todo> = new Map(); // MCP 서버 생성 const server = new Server( { name: "todo-server", version: "1.0.0", }, { capabilities: { tools: {}, resources: {}, }, } ); // 사용 가능한 툴 목록 server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "create_todo", description: "새 투두 아이템 생성", inputSchema: { type: "object", properties: { title: { type: "string", description: "투두 아이템 제목", }, }, required: ["title"], }, }, { name: "complete_todo", description: "투두 아이템 완료 표시", inputSchema: { type: "object", properties: { id: { type: "string", description: "완료할 투두 아이템 ID", }, }, required: ["id"], }, }, { name: "delete_todo", description: "투두 아이템 삭제", inputSchema: { type: "object", properties: { id: { type: "string", description: "삭제할 투두 아이템 ID", }, }, required: ["id"], }, }, ], }; }); // 툴 호출 처리 server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; switch (name) { case "create_todo": { const id = crypto.randomUUID(); const todo: Todo = { id, title: args.title as string, completed: false, createdAt: new Date(), }; todos.set(id, todo); return { content: [ { type: "text", text: JSON.stringify({ success: true, todo }, null, 2), }, ], }; } case "complete_todo": { const todo = todos.get(args.id as string); if (!todo) { return { content: [ { type: "text", text: JSON.stringify({ error: "투두를 찾을 수 없음" }) }, ], isError: true, }; } todo.completed = true; return { content: [ { type: "text", text: JSON.stringify({ success: true, todo }, null, 2), }, ], }; } case "delete_todo": { const deleted = todos.delete(args.id as string); return { content: [ { type: "text", text: JSON.stringify({ success: deleted }, null, 2), }, ], }; } default: return { content: [ { type: "text", text: JSON.stringify({ error: "알 수 없는 툴" }) }, ], isError: true, }; } }); // 사용 가능한 리소스 목록 server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: [ { uri: "todo://list", name: "투두 리스트", description: "현재 모든 투두 아이템 목록", mimeType: "application/json", }, ], }; }); // 리소스 읽기 server.setRequestHandler(ReadResourceRequestSchema, async (request) => { if (request.params.uri === "todo://list") { const todoList = Array.from(todos.values()); return { contents: [ { uri: "todo://list", mimeType: "application/json", text: JSON.stringify(todoList, null, 2), }, ], }; } throw new Error(`알 수 없는 리소스: ${request.params.uri}`); }); // 서버 시작 async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Todo MCP server running on stdio"); } main().catch(console.error);
Claude Desktop 설정
Claude Desktop에서 사용하려면 claude_desktop_config.json에 추가:
{ "mcpServers": { "todo": { "command": "npx", "args": ["tsx", "/path/to/mcp-todo-server/src/index.ts"] } } }
이제 Claude가:
- 투두 생성: "장보기 투두 추가해줘"
- 투두 완료: "장보기 투두 완료 처리해줘"
- 투두 조회: "내 투두 리스트 뭐 있어?"
고급 MCP 패턴
패턴 1: 데이터베이스 통합
가장 강력한 MCP 활용 중 하나가 AI에게 데이터베이스 읽기 (때로는 쓰기) 접근 권한 주기예요:
import { Pool } from 'pg'; const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "query_database") { const { query } = request.params.arguments as { query: string }; // 중요: 쿼리 검증 및 새니타이즈 if (!isReadOnlyQuery(query)) { return { content: [{ type: "text", text: "SELECT 쿼리만 허용됩니다" }], isError: true, }; } try { const result = await pool.query(query); return { content: [{ type: "text", text: JSON.stringify(result.rows, null, 2), }], }; } catch (error) { return { content: [{ type: "text", text: `쿼리 에러: ${error.message}` }], isError: true, }; } } }); function isReadOnlyQuery(query: string): boolean { const normalized = query.trim().toLowerCase(); return normalized.startsWith('select') && !normalized.includes('into') && !normalized.includes('update') && !normalized.includes('delete') && !normalized.includes('insert') && !normalized.includes('drop') && !normalized.includes('alter'); }
패턴 2: OAuth 통합
사용자 인증이 필요한 API용:
import { OAuth2Client } from 'google-auth-library'; const oauth2Client = new OAuth2Client( process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, 'http://localhost:3000/callback' ); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "list_calendar_events", description: "다가오는 캘린더 이벤트 목록", inputSchema: { type: "object", properties: { maxResults: { type: "number", description: "반환할 최대 이벤트 수", default: 10, }, }, }, }, ], }; });
패턴 3: 오래 걸리는 작업과 진행 상태
시간이 걸리는 작업은 MCP가 진행 상태 알림을 지원해요:
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { if (request.params.name === "analyze_codebase") { const files = await getAllFiles(request.params.arguments.path); const total = files.length; for (let i = 0; i < files.length; i++) { // 진행 상태 업데이트 전송 await extra.sendNotification({ method: "notifications/progress", params: { progressToken: request.params._meta?.progressToken, progress: i, total, }, }); await analyzeFile(files[i]); } return { content: [{ type: "text", text: `${total}개 파일 분석 완료`, }], }; } });
MCP 보안 베스트 프랙티스
1. 모든 입력 검증
AI에서 오는 데이터를 절대 신뢰하지 마세요. 항상 검증:
import { z } from 'zod'; const CreateTodoSchema = z.object({ title: z.string().min(1).max(200), priority: z.enum(['low', 'medium', 'high']).optional(), }); server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "create_todo") { const parsed = CreateTodoSchema.safeParse(request.params.arguments); if (!parsed.success) { return { content: [{ type: "text", text: `검증 에러: ${parsed.error.message}`, }], isError: true, }; } // parsed.data는 이제 타입 안전하고 검증됨 } });
2. Rate Limiting 구현
폭주하는 AI 에이전트로부터 보호:
import { RateLimiter } from 'limiter'; const limiter = new RateLimiter({ tokensPerInterval: 100, interval: 'minute', }); server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!await limiter.tryRemoveTokens(1)) { return { content: [{ type: "text", text: "Rate limit 초과. 나중에 다시 시도하세요.", }], isError: true, }; } // 요청 처리... });
3. 감사 로깅
보안과 디버깅을 위해 모든 툴 호출 로깅:
function logToolInvocation(name: string, args: unknown, result: unknown) { console.log(JSON.stringify({ timestamp: new Date().toISOString(), tool: name, arguments: args, result: result, })); }
4. 최소 권한 원칙
필요한 것만 노출:
// 나쁨: 원시 데이터베이스 접근 노출 tools: [{ name: "execute_sql", description: "모든 SQL 쿼리 실행", // 보안 악몽! }] // 좋음: 특정 범위의 작업 노출 tools: [ { name: "get_user_orders", description: "특정 사용자의 주문 조회", inputSchema: { type: "object", properties: { userId: { type: "string" }, limit: { type: "number", maximum: 100 }, }, required: ["userId"], }, }, ]
프로덕션 MCP: 배운 교훈
교훈 1: 실패 대비 설계
AI 에이전트는 예상치 못한 방식으로 툴을 호출해요. 방어적으로 만드세요:
server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const result = await handleTool(request); return result; } catch (error) { // AI가 복구할 수 있게 구조화된 에러 반환 return { content: [{ type: "text", text: JSON.stringify({ error: error.message, suggestion: "다른 파라미터로 시도해보세요", validExamples: [ { title: "장보기" }, { title: "엄마한테 전화" }, ], }), }], isError: true, }; } });
교훈 2: 풍부한 설명 제공
툴 설명의 품질이 AI가 얼마나 잘 사용하는지 직접 영향 미쳐요:
// 나쁨 { name: "search", description: "아이템 검색", } // 좋음 { name: "search_products", description: `제품 카탈로그 검색. 쿼리에 일치하는 최대 20개 제품 반환. 카테고리, 가격대, 재고 여부 필터 지원. 결과에 제품명, 가격, 재고 상태, 썸네일 URL 포함. 최상의 결과를 위해 일반적인 단어보다 구체적인 제품명이나 카테고리 사용 권장.`, inputSchema: { type: "object", properties: { query: { type: "string", description: "검색 쿼리. 예시: '무선 헤드폰', '100만원 이하 노트북'", }, }, }, }
교훈 3: 서버 버전 관리
MCP 서버가 진화할 때 하위 호환성 유지:
const server = new Server( { name: "my-server", version: "2.1.0", // 시맨틱 버저닝 }, { capabilities: { tools: {}, resources: {}, }, } ); // 마이그레이션 중 새/구 툴 이름 둘 다 지원 server.setRequestHandler(CallToolRequestSchema, async (request) => { const name = request.params.name; // 레거시 툴 이름 처리 if (name === "old_tool_name") { console.warn("Deprecated: 'new_tool_name' 사용하세요"); return handleNewTool(request); } if (name === "new_tool_name") { return handleNewTool(request); } });
MCP의 미래
다가오는 것들
- 스트리밍 응답: 툴 출력 스트리밍 일급 지원
- 멀티모달 툴: 이미지, 오디오, 비디오 반환하는 툴
- 툴 조합: 여러 툴을 워크플로우로 결합
- 강화된 보안: 내장 OAuth 플로우와 권한 스코프
MCP vs. 대안들
| 기능 | MCP | OpenAI Functions | LangChain Tools |
|---|---|---|---|
| 벤더 독립적 | ✅ | ❌ | ✅ |
| 표준화된 프로토콜 | ✅ | ❌ | ❌ |
| 리소스 접근 내장 | ✅ | ❌ | ❌ |
| 진행 상태 알림 | ✅ | ❌ | 부분 |
| 커뮤니티 서버 | 성장 중 | N/A | 제한적 |
정리: MCP 배우기 딱 좋은 타이밍
MCP 아직 초기예요. 근데 방향은 확실해요. 2010년대에 REST 몰랐으면 웹 개발자 아니었던 것처럼, 2020년대 AI 개발자는 MCP 알아야 해요.
핵심만 정리하면:
- MCP = AI랑 외부 세계 연결하는 표준
- 일단 간단한 서버부터 — SDK가 쉬움
- 보안 신경 쓰세요 — AI는 예측 불가
- 설명 잘 써야 함 — AI가 읽는 API 문서임
- 에러 대응 잘 만들어야 — AI가 복구할 수 있게
MCP 빨리 익힌 팀이 AI 시대에 앞서갈 거예요. 아직도 API 덕테이핑하는 팀보다 훨씬 빠르게 움직일 테니까요.
질문은 "MCP 배워야 하나?"가 아니라, 앞서갈 건지 뒤따라갈 건지예요.
빠른 참조: MCP 개념
| 개념 | 설명 | 예시 |
|---|---|---|
| Server | 툴과 리소스 노출 | 데이터베이스 접근 서버 |
| Client | 서버에 연결, AI가 사용 | Claude Desktop |
| Tool | 호출 가능한 함수 | create_github_issue |
| Resource | 읽을 수 있는 데이터 | file:///project/README.md |
| Prompt | 재사용 가능한 템플릿 | 코드 리뷰 템플릿 |
| Transport | 통신 레이어 | stdio, SSE, WebSocket |
리소스:
관련 도구 둘러보기
Pockit의 무료 개발자 도구를 사용해 보세요