MCP(Model Context Protocol)란 무엇인가
Claude를 사용하다 보면 종종 이런 생각이 듭니다. "내 도구를 Claude에 직접 연결할 수 있으면 얼마나 좋을까?" MCP(Model Context Protocol)는 바로 이 문제를 해결하는 오픈 스탠다드입니다.
MCP는 Claude(또는 다른 AI 모델)와 로컬 도구, API, 데이터베이스 같은 외부 시스템 사이의 표준화된 통신 프로토콜입니다. 간단히 말해, Claude가 당신의 커스텀 도구를 마치 자신의 기본 기능인 것처럼 사용할 수 있게 해줍니다.
왜 직접 만들어야 할까요?
- 기존 MCP 서버로 충분하지 않은 특수한 비즈니스 로직이 필요할 때
- 레거시 시스템이나 비표준 API와 Claude를 연결해야 할 때
- 회사 내부 데이터베이스, 파일 시스템, 또는 독점 서비스에 접근해야 할 때
- Claude의 능력을 확장하는 것이 곧 경쟁력이 되는 애플리케이션을 만들 때
요약하면: MCP는 "Claude가 당신의 세계에 진입"할 수 있도록 하는 브릿지입니다.
개발 환경 설정 — 필수 도구와 설치
MCP 서버는 Node.js 환경에서 TypeScript로 작성하는 것이 가장 표준적입니다. 시작해봅시다.
1단계: Node.js 설치 확인
node --version # v18 이상 권장
npm --version # v9 이상
설치되지 않았다면 nodejs.org에서 LTS 버전을 다운로드하세요.
2단계: 프로젝트 폴더 생성 및 초기화
mkdir my-mcp-server
cd my-mcp-server
npm init -y
3단계: TypeScript 및 필수 패키지 설치
npm install --save-dev typescript @types/node ts-node
npm install @modelcontextprotocol/sdk
@modelcontextprotocol/sdk는 Anthropic에서 공식 제공하는 MCP SDK로, 서버와 클라이언트 구현의 모든 기본 클래스와 타입을 포함합니다.
4단계: TypeScript 설정 파일 생성
프로젝트 루트에 tsconfig.json을 다음과 같이 작성합니다:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
5단계: package.json에 빌드 스크립트 추가
{
"scripts": {
"build": "tsc",
"dev": "ts-node src/index.ts",
"start": "node dist/index.js"
}
}
MCP 서버의 기본 구조 이해하기
이제 실제로 작동하는 MCP 서버를 만들어봅시다. 먼저 폴더 구조를 정리합니다:
my-mcp-server/
├── src/
│ ├── index.ts # 진입점
│ ├── tools/
│ │ └── fileTool.ts # 도구 구현
│ └── resources/
│ └── dataResource.ts # 리소스 구현
├── tsconfig.json
├── package.json
└── .gitignore
진입점 코드 (src/index.ts)
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequest, ListToolsRequest, Tool } from "@modelcontextprotocol/sdk/types.js";
const server = new Server({
name: "my-custom-server",
version: "1.0.0"
});
// 이곳에 도구와 리소스를 등록합니다 (다음 섹션 참조)
// 표준 입출력을 통해 Claude와 통신합니다
const transport = new StdioServerTransport();
await server.connect(transport);
이 코드의 핵심:
Server: MCP 서버 인스턴스를 생성합니다StdioServerTransport: Claude와 표준 입출력(stdin/stdout)을 통해 통신합니다server.connect(transport): 통신 채널을 시작합니다
도구(Tool) 구현 — 커스텀 함수 등록하기
MCP에서 "도구"는 Claude가 호출할 수 있는 함수입니다. 파일 읽기, API 호출, 데이터베이스 쿼리 등 무엇이든 가능합니다.
간단한 예: 파일 읽기 도구
import fs from "fs/promises";
import path from "path";
// src/tools/fileTool.ts
export async function registerFileTools(server: Server) {
server.setRequestHandler(
ListToolsRequest,
async () => ({
tools: [
{
name: "read_file",
description: "특정 경로의 파일을 읽습니다",
inputSchema: {
type: "object",
properties: {
file_path: {
type: "string",
description: "읽을 파일의 절대 또는 상대 경로"
}
},
required: ["file_path"]
}
},
{
name: "list_files",
description: "특정 디렉터리의 파일 목록을 반환합니다",
inputSchema: {
type: "object",
properties: {
directory: {
type: "string",
description: "탐색할 디렉터리 경로"
}
},
required: ["directory"]
}
}
]
})
);
server.setRequestHandler(
CallToolRequest,
async (request: CallToolRequest) => {
const { name, arguments: args } = request;
if (name === "read_file") {
try {
const filePath = args.file_path as string;
const content = await fs.readFile(filePath, "utf-8");
return {
content: [
{
type: "text",
text: content
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `오류: ${(error as Error).message}`
}
],
isError: true
};
}
}
if (name === "list_files") {
try {
const dir = args.directory as string;
const files = await fs.readdir(dir);
return {
content: [
{
type: "text",
text: files.join("\n")
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `오류: ${(error as Error).message}`
}
],
isError: true
};
}
}
return {
content: [
{
type: "text",
text: `알 수 없는 도구: ${name}`
}
],
isError: true
};
}
);
}
코드 설명
ListToolsRequest: Claude가 "이 서버가 어떤 도구를 제공하는가?"라고 묻을 때 응답합니다- 각 도구는 이름, 설명, 입력 스키마(JSON Schema)를 정의합니다
CallToolRequest: Claude가 실제로 도구를 호출할 때, 요청을 처리하고 결과를 반환합니다- 에러 처리는 필수입니다.
isError: true를 반환하면 Claude가 오류를 인식합니다
더 실무적인 예: API 호출 래퍼
export async function registerApiTool(server: Server) {
server.setRequestHandler(
ListToolsRequest,
async () => ({
tools: [
{
name: "fetch_api",
description: "HTTP API를 호출하고 JSON 결과를 반환합니다",
inputSchema: {
type: "object",
properties: {
url: {
type: "string",
description: "호출할 API URL"
},
method: {
type: "string",
enum: ["GET", "POST", "PUT", "DELETE"],
description: "HTTP 메서드"
},
headers: {
type: "object",
description: "요청 헤더 (선택사항)"
},
body: {
type: "object",
description: "요청 본문 (POST/PUT 시)"
}
},
required: ["url", "method"]
}
}
]
})
);
server.setRequestHandler(
CallToolRequest,
async (request: CallToolRequest) => {
if (request.name === "fetch_api") {
try {
const { url, method, headers, body } = request.arguments as {
url: string;
method: string;
headers?: Record<string, string>;
body?: object;
};
const response = await fetch(url, {
method,
headers: {
"Content-Type": "application/json",
...headers
},
body: body ? JSON.stringify(body) : undefined
});
const data = await response.json();
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2)
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `API 호출 실패: ${(error as Error).message}`
}
],
isError: true
};
}
}
}
);
}
리소스(Resource) 구현 — 데이터 노출하기
도구가 "행동"이라면, 리소스는 "정보"입니다. 파일, 데이터베이스 레코드, API 응답 등 Claude가 읽을 수 있는 정적 또는 동적 데이터입니다.
예: 로컬 파일을 리소스로 노출
import { ResourceContents, ReadResourceRequest, ListResourcesRequest } from "@modelcontextprotocol/sdk/types.js";
export async function registerFileResources(server: Server) {
// 리소스 목록 제공
server.setRequestHandler(
ListResourcesRequest,
async () => ({
resources: [
{
uri: "file:///docs/README.md",
name: "프로젝트 README",
description: "프로젝트 설명서"
},
{
uri: "file:///config/settings.json",
name: "설정 파일",
description: "애플리케이션 설정"
}
]
})
);
// 특정 리소스 읽기
server.setRequestHandler(
ReadResourceRequest,
async (request: ReadResourceRequest) => {
const uri = request.uri;
if (uri === "file:///docs/README.md") {
const content = await fs.readFile("./README.md", "utf-8");
return {
contents: [
{
uri,
mimeType: "text/markdown",
text: content
}
]
};
}
if (uri === "file:///config/settings.json") {
const content = await fs.readFile("./config/settings.json", "utf-8");
return {
contents: [
{
uri,
mimeType: "application/json",
text: content
}
]
};
}
throw new Error(`알 수 없는 리소스: ${uri}`);
}
);
}
동적 리소스 예: 데이터베이스 쿼리 결과
// 데이터베이스 접근이 필요한 경우
export async function registerDatabaseResources(server: Server) {
server.setRequestHandler(
ListResourcesRequest,
async () => ({
resources: [
{
uri: "db://users/all",
name: "모든 사용자",
description: "데이터베이스의 사용자 목록"
},
{
uri: "db://stats/daily",
name: "일일 통계",
description: "오늘의 사용 통계"
}
]
})
);
server.setRequestHandler(
ReadResourceRequest,
async (request: ReadResourceRequest) => {
const uri = request.uri;
if (uri === "db://users/all") {
// 실제로는 데이터베이스 쿼리
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
];
return {
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(users, null, 2)
}
]
};
}
throw new Error(`리소스를 찾을 수 없습니다: ${uri}`);
}
);
}
Claude Desktop에 MCP 서버 연결하기
만든 MCP 서버를 Claude Desktop에 연결해야 합니다. 이는 claude_desktop_config.json 파일을 통해 설정합니다.
1단계: 서버 빌드
npm run build
이 명령은 TypeScript 파일을 JavaScript로 컴파일하여 dist/ 폴더에 저장합니다.
2단계: claude_desktop_config.json 찾기 및 편집
claudeDesktop 설정 파일 위치:
- macOS:
~/Library/Application\ Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
3단계: 서버 설정 추가
{
"mcpServers": {
"my-custom-server": {
"command": "node",
"args": [
"/absolute/path/to/my-mcp-server/dist/index.js"
]
}
}
}
중요한 점:
- 경로는 반드시 절대 경로여야 합니다 (
/absolute/path형태) command는 실행 파일(여기선 node)args는 실행 인자 배열
4단계: Claude Desktop 재시작
완전히 종료한 후 다시 실행합니다. 설정이 로드됩니다.
5단계: 연결 확인
Claude와 대화할 때, 도구 아이콘(왼쪽 하단)을 클릭하면 등록된 도구들을 볼 수 있습니다. 만약 나타나지 않으면, 로그를 확인해야 합니다:
# macOS의 경우
cat ~/Library/Logs/Claude/mcp-server.log
실전 활용: 완전한 예제 작성하기
지금까지 배운 내용을 종합한 실제 사용 가능한 MCP 서버를 만들어봅시다. 이 예제는 프로젝트 문서를 읽고, 메모를 관리하는 도구입니다.
src/index.ts (완전판)
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequest,
ListToolsRequest,
ListResourcesRequest,
ReadResourceRequest
} from "@modelcontextprotocol/sdk/types.js";
import fs from "fs/promises";
import path from "path";
const server = new Server({
name: "project-assistant",
version: "1.0.0"
});
// 도구 목록
server.setRequestHandler(ListToolsRequest, async () => ({
tools: [
{
name: "create_note",
description: "프로젝트 메모를 생성합니다",
inputSchema: {
type: "object",
properties: {
title: {
type: "string",
description: "메모 제목"
},
content: {
type: "string",
description: "메모 내용"
}
},
required: ["title", "content"]
}
},
{
name: "list_notes",
description: "저장된 모든 메모를 나열합니다",
inputSchema: {
type: "object",
properties: {}
}
}
]
}));
// 도구 실행
server.setRequestHandler(CallToolRequest, async (request) => {
const { name, arguments: args } = request;
if (name === "create_note") {
try {
const { title, content } = args as { title: string; content: string };
const timestamp = new Date().toISOString();
const fileName = `note_${Date.now()}.md`;
const notePath = path.join("./notes", fileName);
await fs.mkdir("./notes", { recursive: true });
await fs.writeFile(
notePath,
`# ${title}\n\n${content}\n\n**생성일**: ${timestamp}`
);
return {
content: [
{
type: "text",
text: `메모가 생성되었습니다: ${fileName}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `오류: ${(error as Error).message}`
}
],
isError: true
};
}
}
if (name === "list_notes") {
try {
const notesDir = "./notes";
const files = await fs.readdir(notesDir).catch(() => []);
const noteList = files.map(f => `- ${f}`).join("\n");
return {
content: [
{
type: "text",
text: noteList || "저장된 메모가 없습니다"
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `오류: ${(error as Error).message}`
}
],
isError: true
};
}
}
return {
content: [
{
type: "text",
text: `알 수 없는 도구: ${name}`
}
],
isError: true
};
});
// 리소스 목록
server.setRequestHandler(ListResourcesRequest, async () => ({
resources: [
{
uri: "file:///project/README.md",
name: "프로젝트 설명",
description: "프로젝트 개요"
}
]
}));
// 리소스 읽기
server.setRequestHandler(ReadResourceRequest, async (request) => {
const { uri } = request;
if (uri === "file:///project/README.md") {
try {
const content = await fs.readFile("./README.md", "utf-8");
return {
contents: [
{
uri,
mimeType: "text/markdown",
text: content
}
]
};
} catch (error) {
throw new Error(`파일을 읽을 수 없습니다: ${(error as Error).message}`);
}
}
throw new Error(`알 수 없는 리소스: ${uri}`);
});
// 서버 시작
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP 서버 실행 중...");
}
main().catch((err) => {
console.error("서버 오류:", err);
process.exit(1);
});
이 예제의 활용 시나리오
- Claude: "메모를 만들어줘"
- Claude가
create_note도구를 호출 - 로컬
./notes폴더에 마크다운 파일 생성 - Claude: "저장된 메모를 보여줘"
- Claude가
list_notes도구로 목록 조회 - Claude: "README를 읽어줄래?"
- Claude가 리소스로 README.md 접근
디버깅 및 트러블슈팅
문제 1: "도구가 나타나지 않습니다"
해결책:
- Claude Desktop을 완전히 종료하고 다시 시작
- 절대 경로를 다시 확인
- 빌드가 최신인지 확인:
npm run build - 로그 파일 확인 (위의 경로 참조)
문제 2: "타입 오류가 발생합니다"
해결책:
tsconfig.json에서"strict": true확인@types/node설치 확인:npm list @types/node- 비동기 함수는 항상 Promise를 반환해야 함
문제 3: "Claude가 도구를 호출했지만 응답이 없습니다"
해결책:
- 모든 요청 핸들러에 try-catch 추가
- 반드시 응답 객체를 반환해야 함 (null이 아님)
console.error()로 디버깅 메시지 출력하고 로그 확인- 입력 스키마가 실제 입력과 일치하는지 확인
로컬 테스트 방법
Claude Desktop 연결 전에 로컬에서 테스트할 수 있습니다:
npm run dev
이 명령으로 서버가 시작되면, 다른 터미널에서 표준 입력을 통해 JSON 메시지를 보낼 수 있습니다 (advanced 사용자용).
성능 최적화 팁
- 도구가 느린 작업을 수행할 때, 진행 상황을 피드백으로 전달하는 것이 좋습니다
- 캐싱을 활용하여 반복되는 작업 최소화
- 타임아웃 처리: 네트워크 요청이나 DB 쿼리에는 타임아웃 설정
- 에러 처리를 완벽하게 하면, Claude가 재시도할 수 있습니다
핵심 요약 및 다음 단계
지금까지 다룬 내용을 정리하면:
- MCP의 본질: Claude와 외부 도구 간의 표준화된 통신 프로토콜
- 개발 환경: Node.js, TypeScript, @modelcontextprotocol/sdk
- 기본 구조: Server 객체 → setRequestHandler → StdioServerTransport
- 도구: 호출 가능한 함수 (Tool)
- 리소스: Claude가 읽을 수 있는 데이터 (Resource)
- 연결: claude_desktop_config.json을 통해 설정
- 디버깅: 로그 확인, 타입 체크, try-catch 활용
다음 도전 과제:
- 실제 데이터베이스와 연결 (PostgreSQL, MongoDB 등)
- 외부 API 통합 (OAuth, 인증 처리)
- MCP 서버 배포 및 보안 (환경 변수, 권한 제어)
- 테스트 코드 작성 (도구의 신뢰성 확보)
- 프로덕션 MCP 서버 모니터링 및 로깅
MCP는 Claude를 단순한 채팅 도구에서 당신의 비즈니스 로직과 깊게 통합된 AI 어시스턴트로 변신시킵니다. 투자할 가치가 있는 기술입니다.
이 글에 소개된 서비스와 도구는 작성 시점 기준이며, 업데이트에 따라 변경될 수 있습니다.
'AI 개발 활용' 카테고리의 다른 글
| Cursor Agent 모드 완전 정복 — 자율 코딩 에이전트로 복잡한 작업 자동화하기 (0) | 2026.04.11 |
|---|---|
| LangChain으로 RAG 파이프라인 구축하기 — 문서 검색 AI 앱 만드는 완전 가이드 (0) | 2026.04.11 |
| Claude Code Max 플랜 완벽 가이드 — Ultraplan 설정·토큰 최적화·실전 활용법 (0) | 2026.04.09 |
| Perplexity API 연동해서 실시간 AI 검색 앱 만들기 (0) | 2026.04.09 |
| Gemini CLI 설치하고 터미널에서 AI 코딩 어시스턴트 쓰기 (0) | 2026.04.07 |