포스트

MCP 서버를 “에이전트 확장 레이어”로 쓰는 법: 2026년 6월 기준 Claude 연동까지 끝내는 구현 가이드

MCP 서버를 “에이전트 확장 레이어”로 쓰는 법: 2026년 6월 기준 Claude 연동까지 끝내는 구현 가이드

들어가며

LLM/Agent를 실제 업무 시스템에 붙일 때 가장 큰 비용은 “모델 ↔ 툴/데이터 소스” 연결부가 매번 커스텀으로 증식하는 문제입니다. MCP(Model Context Protocol)는 이 M×N 통합 폭발을 “표준화된 서버/클라이언트” 구조로 줄이려는 프로토콜이고, Claude(Desktop/Claude Code/claude.ai 등)에서 MCP 서버를 붙여 도구(tools)·리소스(resources)·프롬프트(prompts)를 에이전트에게 노출하는 방식이 사실상 업계 표준으로 굳었습니다. (anthropic.com)

언제 쓰면 좋은가

  • 내부 API/DB/티켓/런북/배포 시스템 등 “에이전트가 자주 호출하는 기능”을 일관된 계약(schema)으로 제공하고 싶을 때
  • IDE/Claude Code/데스크톱 클라이언트 등 여러 클라이언트에서 한 번 구현해 재사용하고 싶을 때 (docs.anthropic.com)
  • 에이전트에게 “검색 → 조치”를 시키되, 툴 경계를 명확히 해서 감사/권한통제를 하고 싶을 때

언제 쓰면 안 되는가

  • 툴 호출이 거의 없고 단순 Q&A 위주라서 통합 레이어 유지비가 더 큰 경우
  • 보안 경계가 불명확한 상태에서 “로컬 stdio 서버를 마구 실행”해야 하는 조직(2026년 들어 MCP 생태계에서 RCE/공급망 이슈가 반복적으로 지적됨). 특히 stdio는 “내 머신 권한으로 프로세스를 실행”하는 형태라 운영/감사 체계를 먼저 잡아야 합니다. (techradar.com)

🔧 핵심 개념

1) MCP가 표준화하는 것: “컨텍스트 제공 인터페이스”

MCP 서버는 단순히 함수 엔드포인트를 제공하는 게 아니라, 에이전트가 활용할 컨텍스트를 세 가지 축으로 모델링합니다.

  • Tools: 실행 가능한 액션(예: deploy_service, create_jira_ticket)
  • Resources: 조회 가능한 데이터(예: runbook://payments/rollback, db://analytics/query)
  • Prompts: 특정 작업을 잘 수행하도록 서버가 제공하는 재사용 프롬프트 템플릿

이 구조 덕분에 “API 문서 읽고 맞춰 호출”이 아니라, 서버가 스키마/의미를 명시적으로 노출하고 클라이언트(Claude 등)가 이를 발견(discovery)해서 에이전트에게 제공합니다. (docs.anthropic.com)

2) 내부 작동 흐름(서버 관점)

TypeScript SDK 기준으로 보면, 핵심은 McpServer에 툴/리소스/프롬프트를 등록하고, transport를 붙여 JSON-RPC로 주고받는 모델입니다. (ts.sdk.modelcontextprotocol.io)

에이전트 호출 흐름(실무적으로 중요한 포인트)
1) 클라이언트가 서버에 연결 → 서버 capability/툴 목록을 가져옴
2) 모델이 계획 수립(“어떤 툴을 어떤 입력으로 호출할지”)
3) 툴 호출(JSON-RPC) → 서버는 실제 외부 시스템 호출/검증/로그 기록
4) 결과를 모델에게 반환 → 모델이 후속 툴 호출 또는 최종 응답 작성

여기서 “서버”가 해야 할 일은 단순 실행이 아니라,

  • 입력 검증/권한 체크
  • 외부 호출의 idempotency/timeout/retry
  • 결과 축약(모델 토큰/민감정보 제약)
  • 감사 로그(누가 어떤 툴을 어떤 파라미터로 호출했는지) 까지 포함됩니다. 즉 MCP 서버는 에이전트 확장 서버(Agent Extension Server)로 보는 게 맞습니다.

3) 다른 접근과의 차이점

  • 기존: 각 클라이언트/프레임워크별 Tool Calling 스펙에 맞춰 개별 구현 → 통합 부채 폭발
  • MCP: 서버가 표준 JSON-RPC 계약으로 툴/리소스/프롬프트를 제공 → Claude/IDE 등 다양한 클라이언트가 동일 방식으로 연결 (docs.anthropic.com)
  • “함수 호출”만 제공하는 것보다 한 단계 위: 서버가 컨텍스트 패키징의 책임을 갖기 때문에 에이전트 품질이 재현 가능해집니다.

💻 실전 코드

아래는 “실제 팀에서 흔한” 시나리오로 구성했습니다.

  • 목적: 운영자가 매번 수동으로 하던 Kubernetes 배포/롤백 + 변경 기록(JIRA/티켓 대신 간단히 파일 기록)을 MCP tool로 제공
  • 특징:
    • toy가 아니라 “외부 커맨드(kubectl/helm) 실행”이 들어감
    • 입력 검증, dry-run, timeout, 출력 요약을 포함
    • 로컬에서는 stdio로 Claude Desktop/Claude Code에 붙여 빠르게 검증 후, 원격 Streamable HTTP로 이관 가능한 구조

1) 초기 셋업

1
2
3
4
5
mkdir mcp-ops-server && cd mcp-ops-server
npm init -y
npm i @modelcontextprotocol/sdk zod execa
npm i -D typescript tsx @types/node
npx tsc --init

2) MCP 서버(stdio) 구현: src/server.ts

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
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { execa } from "execa";
import fs from "node:fs/promises";

const server = new McpServer({ name: "ops-mcp", version: "1.0.0" });

/**
 * 현실 포인트:
 * - 모델이 이상한 값을 넣을 수 있으니 zod로 강검증
 * - kubectl/helm 호출은 timeout 필수
 * - 출력은 길어질 수 있으니 요약해서 반환
 */
const DeployInput = z.object({
  namespace: z.string().min(1),
  release: z.string().min(1),
  chart: z.string().min(1),              // 예: "./charts/payments"
  imageTag: z.string().min(1),            // 예: "2026.06.30-1234"
  dryRun: z.boolean().default(false),
});

server.tool(
  "deploy_service",
  {
    description: "Deploy a Helm release to Kubernetes with a specific image tag. Supports dry-run.",
    inputSchema: DeployInput,
  },
  async (input) => {
    const { namespace, release, chart, imageTag, dryRun } = DeployInput.parse(input);

    const args = [
      "upgrade",
      "--install",
      release,
      chart,
      "-n",
      namespace,
      "--set",
      `image.tag=${imageTag}`,
      "--history-max",
      "20",
      "--wait",
      "--timeout",
      "5m",
    ];

    if (dryRun) args.push("--dry-run");

    // kubectl/helm은 stderr에도 정상 로그를 뿌리는 경우가 있어 둘 다 수집
    const startedAt = Date.now();
    const cmd = await execa("helm", args, { timeout: 6 * 60 * 1000, reject: false });

    const durationMs = Date.now() - startedAt;

    // 변경 기록(예: 내부 티켓 시스템 대신 파일로 남김. 실무에선 DB/로그로 교체)
    await fs.appendFile(
      "./deploy-log.ndjson",
      JSON.stringify({
        ts: new Date().toISOString(),
        namespace,
        release,
        chart,
        imageTag,
        dryRun,
        exitCode: cmd.exitCode,
        durationMs,
      }) + "\n",
      "utf-8"
    );

    const output = [cmd.stdout, cmd.stderr].filter(Boolean).join("\n");
    const summarized =
      output.length > 4000 ? output.slice(0, 4000) + "\n...(truncated)" : output;

    return {
      content: [
        {
          type: "text",
          text:
            `helm ${args.join(" ")}\n` +
            `exitCode=${cmd.exitCode}, durationMs=${durationMs}\n\n` +
            summarized,
        },
      ],
      // 모델이 후속 조치를 판단할 수 있게 "성공/실패"를 명확히
      isError: cmd.exitCode !== 0,
    };
  }
);

server.tool(
  "rollback_service",
  {
    description: "Rollback a Helm release to a previous revision.",
    inputSchema: z.object({
      namespace: z.string().min(1),
      release: z.string().min(1),
      revision: z.number().int().positive(),
    }),
  },
  async (input) => {
    const { namespace, release, revision } = z
      .object({
        namespace: z.string().min(1),
        release: z.string().min(1),
        revision: z.number().int().positive(),
      })
      .parse(input);

    const args = ["rollback", release, String(revision), "-n", namespace, "--wait", "--timeout", "5m"];
    const cmd = await execa("helm", args, { timeout: 6 * 60 * 1000, reject: false });

    return {
      content: [{ type: "text", text: `helm ${args.join(" ")}\nexitCode=${cmd.exitCode}\n${cmd.stdout}\n${cmd.stderr}` }],
      isError: cmd.exitCode !== 0,
    };
  }
);

async function main() {
  // stdio transport: 로컬에서 Claude Desktop/Claude Code가 서버 프로세스를 직접 실행
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((err) => {
  // 주의: stdout에 찍으면 JSON-RPC가 깨질 수 있음.
  // stderr만 사용.
  console.error(err);
  process.exit(1);
});

3) 실행 & 예상 출력(서버 단독 테스트)

1
npx tsx src/server.ts

이 상태로는 “클라이언트가 붙어야” 요청이 들어옵니다. (서버가 stdout으로 JSON-RPC를 주고받기 때문)

4) Claude Code에 붙이기(로컬 stdio 서버)

Claude Code는 claude mcp add-json로 stdio 서버를 추가할 수 있습니다. (code.claude.com)

1
2
claude mcp add-json ops-mcp \
'{"type":"stdio","command":"node","args":["--enable-source-maps","./node_modules/.bin/tsx","src/server.ts"],"env":{"NODE_ENV":"development"}}'

이후 Claude Code에서 /mcp를 통해 서버가 인식되면 deploy_service, rollback_service가 도구 목록으로 노출됩니다.


⚡ 실전 팁 & 함정

Best Practice

1) “stdio는 개발용, 원격은 Streamable HTTP”로 분리 TypeScript SDK 문서에서도 원격 서버는 Streamable HTTP를 권장하고, SSE는 deprecated로 분류합니다. (ts.sdk.modelcontextprotocol.io)
팀 확장(여러 명이 같은 툴 사용)까지 고려하면 결국 원격 배포가 필요하니, 초기에 “transport 추상화”를 염두에 두세요.

2) Tool 입력 스키마를 강하게 + 결과는 축약 모델은 종종 “있을 법한 값”을 만들어냅니다. zod 같은 검증을 통과하지 못하면 즉시 실패시키고, stdout 전체를 그대로 반환하지 말고(토큰/민감정보/노이즈) 요약+트렁케이트 전략을 기본값으로 두는 게 운영에 유리합니다.

3) OAuth/headersHelper 같은 인증 전략을 표준 흐름에 얹기 Claude Code는 HTTP 기반 MCP 서버에 대해 OAuth 설정/스코프 제한/메타데이터 discovery 우회 같은 옵션을 제공합니다. 특히 oauth.scopes로 권한 범위를 “보안팀 승인된 최소치”로 고정할 수 있습니다. (code.claude.com)
사내 API는 OAuth가 아니더라도 headersHelper로 런타임 토큰을 주입하는 패턴이 꽤 실용적입니다(다만 임의 쉘 실행이라는 위험을 인지해야 함). (code.claude.com)

흔한 함정/안티패턴

  • stdout에 로그 찍기: stdio transport는 stdout이 프로토콜 채널이라, 서버 시작 시 console.log() 한 줄로도 연결이 깨질 수 있습니다(로그는 stderr로). 실제 사용자 경험담에서도 이 문제가 반복됩니다. (reddit.com)
  • 무권한/무감사로 “배포/삭제” 툴 제공: MCP 서버는 사실상 “에이전트가 사용하는 운영자 권한 API”입니다. 호출자 식별, 승인 흐름(최소한의 human-in-the-loop), 감사 로그 없이는 사고가 납니다.
  • MCP 마켓/예제 서버를 검증 없이 복붙: 2026년에도 공급망/툴 오염(tool poisoning)/RCE 계열 우려가 지속적으로 제기됩니다. “내 PC에서 실행되는 커맨드 목록”으로 취급하고 코드/릴리즈 노트를 리뷰하세요. (techradar.com)

비용/성능/안정성 트레이드오프

  • stdio: 제일 빠른 개발/디버그 vs 로컬 권한 그대로 실행(리스크 큼)
  • 원격 HTTP: 운영/권한/감사에 유리 vs OAuth/네트워크/배포 비용 증가
  • 툴 출력량: 많이 주면 모델이 잘 판단 vs 토큰 비용/응답 지연/민감정보 유출 위험 증가
    → 실무에서는 “짧은 결과 + 필요 시 follow-up resource/tool로 추가 조회” 형태가 안정적입니다.

🚀 마무리

핵심은 MCP를 “Claude에 툴 몇 개 붙이는 플러그인”이 아니라, 에이전트 확장 서버(Agent Extension Server)로 설계하는 겁니다.

  • 로컬에서 stdio로 빠르게 검증하고(단, stdout 오염 금지) (ts.sdk.modelcontextprotocol.io)
  • 원격으로 갈 때는 Streamable HTTP + OAuth/스코프 최소화 + 감사 로그를 기본 구성으로 잡는 게 2026년 현실적인 정답입니다. (ts.sdk.modelcontextprotocol.io)
  • 보안 이슈(RCE/공급망/툴 오염)는 “남 얘기”가 아니라 MCP의 구조적 리스크로 계속 거론되고 있으니, 프로덕션 도입 시 위협 모델링을 선행하세요. (tomshardware.com)

도입 판단 기준(요약)

  • 툴 호출이 잦고, 여러 클라이언트/에이전트에서 재사용해야 한다 → MCP 서버 가치 큼
  • 운영 권한을 다루거나 데이터가 민감하다 → 원격/인증/감사 체계 없으면 도입 보류
  • “일단 붙여보자” 단계 → stdio로 작게 시작하되, 배포/삭제 같은 파괴적 툴은 처음부터 분리(승인 필요)

다음 학습으로는:

  • MCP 공식 문서의 Architecture/Servers 섹션(transport/보안 가이드) (docs.anthropic.com)
  • Claude Code의 MCP 설정(OAuth discovery, scopes, headersHelper) (code.claude.com) 을 순서대로 보면 “내 조직에 맞는 운영 모델”까지 빠르게 설계할 수 있습니다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.