포스트

Claude + MCP 서버 구현(2026년 8월판): “에이전트 확장 서버”를 프로덕션에 올리는 설계/코드/함정 총정리

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 + 의존성)

1
2
3
4
5
6
7
8
9
# 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 + 최소한의 안전장치

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// 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)

1
2
3
4
5
6
7
8
9
10
# 프로젝트 루트에서
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가 서버를 직접 띄우게 하려면(권장: 팀 정책에 맞춰 명령 허용/차단):

1
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)
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.