Claude + MCP 서버 구현(2026년 8월판): “에이전트 확장 서버”를 프로덕션에 올리는 설계/코드/함정 총정리
들어가며
MCP(Model Context Protocol)는 LLM/Agent가 외부 Tool·Resource·Prompt를 표준 인터페이스로 호출하게 만드는 프로토콜입니다. 2026년 현재 Claude 생태계에서는 Claude Desktop/Claude Code가 MCP 클라이언트로서 널리 쓰이고, VS Code도 MCP 서버를 “도구 공급자”로 통합하는 흐름이 강해졌습니다. (code.claude.com)
이 글이 해결하려는 구체적 문제
- “우리 팀 내부 시스템(사내 DB, 배포 파이프라인, 모니터링, 문서/위키)을 Claude/Agent가 직접 쓰게” 만들고 싶은데,
- 단순 REST API 노출이 아니라 (1) 안전한 권한/스코프, (2) 세션/상태, (3) 도구 수가 늘어날 때의 선택 정확도, (4) IDE/로컬/서버 배포까지의 운용을 같이 설계해야 함.
언제 쓰면 좋나
- 에이전트가 “질문→조회→변환→검증→후속 액션”을 반복하며, 툴 호출이 핵심 가치일 때
- 여러 클라이언트(Claude Code, Desktop, VS Code, 기타 MCP 클라이언트)에서 같은 기능을 재사용하고 싶을 때 (modelcontextprotocol.io)
- 툴/리소스 목록이 자주 바뀌어 동적 갱신(list_changed) 이 필요한 경우 (code.claude.com)
언제 쓰면 안 되나
- 단발성 자동화(예: cron 같은 배치)라면 MCP보다 “그냥 스크립트/Job”이 더 단순
- 팀이 아직 권한·감사로그·데이터 노출 정책이 준비 안 됐는데 “에이전트에게 운영 권한”을 주려는 경우(사고 확률이 큼)
- 도구가 너무 많아질 때 오히려 성능/정확도가 떨어질 수 있음(연구에서 “tool-selection accuracy vs tool-count” 저하를 지적) (arxiv.org)
🔧 핵심 개념
1) MCP가 제공하는 primitive: Tools / Resources / Prompts
- Tools: 함수 호출(입력 스키마/설명/결과). 에이전트가 “행동(action)”을 수행하는 단위.
- Resources: 에이전트가 참고할 수 있는 “주소가 있는 컨텍스트”(예:
@server:.../schema,@server:.../runbook). Claude Code는@로 리소스를 탐색/참조하는 UX를 제공합니다. (code.claude.com) - Prompts: 재사용 가능한 프롬프트 템플릿(서버가 “작업 프레임”을 제공).
핵심은 “툴만 던져주면 끝”이 아니라, 리소스로 근거를 제공하고, 프롬프트로 작업 절차를 고정해 에이전트의 일관성을 올리는 구조가 가능하다는 점입니다.
2) 내부 작동 방식(흐름): 세션 + JSON-RPC 스타일 호출 + 알림
MCP는 기본적으로 클라이언트가 서버에 연결(transport는 stdio/SSE/HTTP 등)하고, 서버가 제공하는 도구/리소스/프롬프트를 “카탈로그처럼” 노출합니다. Claude Code 문서에는 stdio/SSE/HTTP 등 여러 transport와, 서버가 도구 목록 변경을 알리는 list_changed notification 같은 동적 갱신이 언급됩니다. (code.claude.com)
운영 관점에서 중요한 포인트:
- stdio: 로컬에서 클라이언트가 서버 프로세스를 서브프로세스로 띄우는 방식(가장 단순, 개발/개인 환경에 최적) (grafana.com)
- SSE / streamable-http: 원격 서버/공유 환경에서 적합(네트워크/인증/로드밸런싱 고려 필요) (grafana.com)
- 디버깅/관찰성: stdio는 stderr 로그, 모든 transport에서 notification 기반 로그도 활용 가능(세션 헤더/스트림 관찰이 중요) (modelcontextprotocol.io)
3) “에이전트 확장 서버” 구축 관점: 단일 툴 제공이 아니라 ‘패턴’ 선택
최근 경험/연구 기반으로 MCP 서버 아키텍처 패턴을 분류하는 흐름이 있습니다. 예를 들어 Resource Gateway / Tool Orchestrator / Stateful Session Server / Proxy Aggregator / Domain-Specific Adapter 같은 패턴이 반복된다고 정리합니다. (arxiv.org)
실무에선 보통:
- 내부 시스템이 많으면 Proxy Aggregator(내부 API들을 감싸는 단일 MCP 서버) 로 시작
- 도구 호출이 연쇄되고 상태가 필요하면 Stateful Session Server 성격이 강해짐
- 특정 도메인(예: 배포/장애대응)만 다루면 Domain-Specific Adapter가 유지보수에 유리
💻 실전 코드
시나리오: “운영자가 Claude Code에서 서비스 장애 원인 추정 → 최근 배포/에러율 조회 → 롤백 PR 생성까지”를 수행하고 싶다.
여기서는 (1) Observability 조회, (2) 배포 이력 조회, (3) 롤백 브랜치/PR 생성을 MCP Tools로 제공하고, runbook/resource를 MCP Resources로 제공합니다.
아래 예제는 TypeScript SDK 기반 MCP 서버(stdio)로, Claude Code가 로컬에서 띄우는 형태입니다. TypeScript SDK의 서버 구성 요소는 공식 가이드에 정리되어 있습니다. (github.com)
1) 초기 셋업 (Node + 의존성)
# Node 20+ 권장(팀 표준에 맞추세요)
mkdir ops-mcp && cd ops-mcp
npm init -y
# MCP TypeScript SDK (서버)
npm i @modelcontextprotocol/server zod
npm i -D typescript tsx @types/node
npx tsc --init
2) 서버 구현: Tools + Resources + 최소한의 안전장치
// src/server.ts
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server";
import { z } from "zod";
type Env = {
GRAFANA_URL: string;
GRAFANA_TOKEN: string;
DEPLOY_API_URL: string;
DEPLOY_API_TOKEN: string;
GIT_API_URL: string;
GIT_TOKEN: string;
};
function mustEnv(name: keyof Env): string {
const v = process.env[name];
if (!v) throw new Error(`Missing env: ${name}`);
return v;
}
const env: Env = {
GRAFANA_URL: mustEnv("GRAFANA_URL"),
GRAFANA_TOKEN: mustEnv("GRAFANA_TOKEN"),
DEPLOY_API_URL: mustEnv("DEPLOY_API_URL"),
DEPLOY_API_TOKEN: mustEnv("DEPLOY_API_TOKEN"),
GIT_API_URL: mustEnv("GIT_API_URL"),
GIT_TOKEN: mustEnv("GIT_TOKEN"),
};
// --- 예시용: 실제론 각 사내 시스템 SDK/클라이언트를 쓰세요 ---
async function fetchJson(url: string, headers: Record<string, string>) {
const res = await fetch(url, { headers });
if (!res.ok) throw new Error(`HTTP ${res.status} ${await res.text()}`);
return res.json();
}
const server = new McpServer({
name: "ops-mcp",
version: "0.1.0",
});
// Resource: 장애 대응 runbook 제공(클라이언트가 @로 참조 가능)
server.resource(
"runbook",
new ResourceTemplate("runbook://{service}", { list: ["payments", "api", "web"] }),
async ({ service }) => {
// 현실: 파일/위키/문서 저장소에서 읽기 + 캐시
const text =
service === "payments"
? `Payments Runbook:
- Check error rate panel
- Check last deploy SHA
- If regression suspected: create rollback PR
- Verify DB latency & queue depth`
: `${service} Runbook: (placeholder)`;
return {
contents: [
{
uri: `runbook://${service}`,
mimeType: "text/plain",
text,
},
],
};
}
);
// Tool 1: 최근 N분 에러율 조회(Grafana/Prometheus 등)
server.tool(
"get_error_rate",
{
description:
"Get error rate for a service in the last N minutes. Use before proposing rollback.",
inputSchema: z.object({
service: z.string().min(1),
minutes: z.number().int().min(1).max(180),
}),
},
async ({ service, minutes }) => {
// 예: Grafana DataSource Query API로 대체 가능(조직에 맞게)
const url = `${env.GRAFANA_URL}/api/fake/error-rate?service=${encodeURIComponent(
service
)}&minutes=${minutes}`;
const data = await fetchJson(url, {
Authorization: `Bearer ${env.GRAFANA_TOKEN}`,
});
return {
content: [
{
type: "text",
text: JSON.stringify(
{
service,
windowMinutes: minutes,
errorRate: data.errorRate,
samples: data.samples,
},
null,
2
),
},
],
};
}
);
// Tool 2: 최근 배포 이력 조회
server.tool(
"get_recent_deploys",
{
description:
"List recent deploys for a service, including commit SHA and timestamp. Use to correlate with spikes.",
inputSchema: z.object({
service: z.string().min(1),
limit: z.number().int().min(1).max(20).default(5),
}),
},
async ({ service, limit }) => {
const url = `${env.DEPLOY_API_URL}/deploys?service=${encodeURIComponent(
service
)}&limit=${limit}`;
const data = await fetchJson(url, {
Authorization: `Bearer ${env.DEPLOY_API_TOKEN}`,
});
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
// Tool 3: 롤백 PR 생성(보수적으로 “제안”을 자동화)
server.tool(
"create_rollback_pr",
{
description:
"Create a rollback pull request from current deploy to previous stable SHA. This is a high-impact tool; use only when evidence supports regression.",
inputSchema: z.object({
repo: z.string().min(1), // "org/repo"
fromSha: z.string().min(7),
toSha: z.string().min(7),
reason: z.string().min(10),
}),
},
async ({ repo, fromSha, toSha, reason }) => {
// 현실: GitHub/GitLab API. 여기서는 사내 Git API를 가정.
const url = `${env.GIT_API_URL}/repos/${encodeURIComponent(repo)}/rollback-pr`;
const body = {
fromSha,
toSha,
title: `Rollback: ${fromSha.slice(0, 7)} -> ${toSha.slice(0, 7)}`,
description: `Automated rollback proposal.\n\nReason:\n${reason}`,
};
const pr = await fetchJson(url, {
Authorization: `Bearer ${env.GIT_TOKEN}`,
"Content-Type": "application/json",
});
return {
content: [
{
type: "text",
text: JSON.stringify(
{
repo,
prUrl: pr.url,
prNumber: pr.number,
fromSha,
toSha,
},
null,
2
),
},
],
};
}
);
server.startStdio();
3) Claude Code에 연결(로컬 stdio)
Claude Code 문서 기준으로 MCP 서버를 stdio로 붙이는 CLI 흐름이 안내되어 있습니다. (code.claude.com)
# 프로젝트 루트에서
export GRAFANA_URL="https://grafana.example.com"
export GRAFANA_TOKEN="***"
export DEPLOY_API_URL="https://deploy.example.com"
export DEPLOY_API_TOKEN="***"
export GIT_API_URL="https://git.example.com"
export GIT_TOKEN="***"
# 서버 실행(개발)
npx tsx src/server.ts
Claude Code가 서버를 직접 띄우게 하려면(권장: 팀 정책에 맞춰 명령 허용/차단):
claude mcp add --transport stdio ops-mcp -- npx tsx /ABS/PATH/ops-mcp/src/server.ts
예상 사용 흐름(Claude Code 프롬프트):
@ops-mcp:runbook://payments를 먼저 읽게 해서 절차를 고정 (code.claude.com)get_error_rate(service="payments", minutes=30)get_recent_deploys(service="payments", limit=5)- 근거가 충분하면
create_rollback_pr(repo="org/payments", fromSha="...", toSha="...", reason="...")
⚡ 실전 팁 & 함정
Best Practice (2~3개)
1) Tool 수를 의도적으로 제한하고 “도메인별 서버 분리”를 고려
- 도구가 10~20개 넘어가면 모델의 tool 선택 정확도가 떨어질 수 있다는 보고가 있습니다. 서버를 “ops / data / docs”처럼 분리하거나, 하나의 서버 안에서도 “entry tool(라우터)”로 묶는 전략이 필요합니다. (arxiv.org)
2) Resources를 ‘근거 저장소’로 만들고, Tool은 ‘행동’에만 집중
- runbook, 스키마, 운영 규칙, 금지사항(예: “DB write 금지”)을 resource로 제공하면, 에이전트가 매번 같은 안전 규칙을 참조하게 만들 수 있습니다(Claude Code의
@참조 UX 활용). (code.claude.com)
3) 관찰성(로그/세션)부터 설계
- 디버깅 문서가 강조하는 것처럼 transport별로 요청/세션/스트림을 추적할 수 있어야 합니다. 특히 stdio는 stderr 로그가 생명줄이고, HTTP 계열은 헤더/세션 ID 관찰이 핵심입니다. (modelcontextprotocol.io)
흔한 함정/안티패턴
- “툴 설명(description) 대충 쓰기”: 에이전트 성능이 툴 설명 품질에 크게 좌우된다는 문제 제기가 있습니다. 입력 스키마뿐 아니라 “언제 이 툴을 써야/말아야 하는지, 어떤 근거가 필요한지”를 description에 넣어야 합니다. (arxiv.org)
- 고위험 툴을 바로 실행형으로 만들기:
delete_user,rollback_prod_now같은 툴을 제공하면 언젠가 사고 납니다. “PR 생성/승인 요청”처럼 인간 승인 단계로 설계하세요(위 코드처럼). - 원격 transport로 가면서 인증/권한을 뒤늦게 붙이기: SSE/HTTP는 공유가 쉬운 만큼, 토큰 스코프·감사로그·레이트리밋이 선행돼야 합니다. (grafana.com)
비용/성능/안정성 트레이드오프
- stdio(로컬): 빠르고 단순하지만 팀 공유/중앙 통제가 어렵고, 각 개발자 환경 의존성이 생김
- SSE/streamable-http(원격): 운영은 표준적이지만 네트워크 지연, 인증, 멀티테넌시, 장애 대응 비용이 증가 (grafana.com)
- Stateful 설계: 세션 캐시로 성능을 얻지만, 서버 재시작/스케일아웃 시 세션 동기화 비용이 생김(필요할 때만)
🚀 마무리
정리하면, 2026년 8월 시점의 MCP 서버 구현은 “툴 몇 개 붙이기”가 아니라 에이전트 확장(Agent Extension) 인프라를 만드는 작업에 가깝습니다. Claude Code/VS Code 등 클라이언트가 MCP를 1급 도구로 다루기 시작하면서, 서버는 곧 “조직의 운영 지식과 액션을 표준화”하는 레이어가 됩니다. (code.claude.com)
도입 판단 기준
- (O) 반복되는 업무가 있고, 조회/검증/후속 액션이 명확하며, 감사/권한/승인 절차를 설계할 수 있다
- (X) 고위험 운영 액션을 무제한 자동화하려 한다 / 도구가 무한히 늘어나는데 정리할 계획이 없다 / 로그·보안이 없다
다음 학습 추천
- 공식 SDK(Server API) 문서로 “tool/resource/prompt + transport”를 정확히 익히고 (github.com)
- Debugging 가이드대로 세션/로그/스트림을 관찰하는 습관을 먼저 만들고 (modelcontextprotocol.io)
- Extensions(확장 메커니즘)는 “호환성 유지(negotiation) + 점진적 도입” 관점에서 접근하세요. (blog.modelcontextprotocol.io)