포스트

MCP 서버로 Claude를 “내 인프라에 붙이는” 방법: 2026년 5월 기준 구현 패턴과 함정 총정리

MCP 서버로 Claude를 “내 인프라에 붙이는” 방법: 2026년 5월 기준 구현 패턴과 함정 총정리

들어가며

LLM을 실무 시스템에 연결할 때 가장 큰 병목은 “모델 호출”이 아니라 컨텍스트/도구 통합의 N×M 지옥입니다. 앱(Claude Desktop, Claude Code, Cursor 등)마다 플러그인 방식이 달라 동일한 내부 API를 여러 번 래핑하게 되고, 권한/감사/에러처리도 제각각이 됩니다. Model Context Protocol(MCP)은 이 부분을 “표준화된 서버 계약(server contract)”으로 분리해, 한 번 MCP server를 만들면 여러 MCP client에서 재사용하게 해줍니다. (anthropic.com)

언제 쓰면 좋나:

  • 에이전트 확장(server-side tools)이 핵심인 프로젝트(사내 DevOps bot, 데이터 조회/티켓 자동화, 레포 분석 등)
  • “클라이언트별 플러그인” 대신 단일 도구 계층을 운영/감사/권한제어 하고 싶은 팀
  • Claude Desktop(로컬 stdio) + Hosted(원격 HTTP) 하이브리드 배포가 필요한 경우 (ts.sdk.modelcontextprotocol.io)

언제 쓰면 안 되나:

  • “모델에 파일 몇 개 넣고 끝” 같은 단발성 유틸(서버 운영비/보안 부담이 더 큼)
  • 안전한 실행 경계(sandbox) 없이 모델이 곧바로 민감 시스템(배포/결제/DB쓰기)을 건드리게 할 계획인 경우
    2026년 들어 MCP 생태계 전반에서 RCE/프롬프트 인젝션/공급망 위험이 실제로 문제화되고 있어(연구/보도 다수) “도구 서버=보안 경계”로 설계하지 않으면 사고가 나기 쉽습니다. (techradar.com)

🔧 핵심 개념

1) MCP에서 말하는 “서버 확장”의 실체

MCP server는 보통 아래 3가지를 노출합니다. (ts.sdk.modelcontextprotocol.io)

  • Tools: 모델이 호출하는 행동(action). 네트워크 호출/계산/파일 변경처럼 side effect가 생길 수 있음
  • Resources: 읽기 전용 데이터(예: 특정 문서, 스키마, 로그 스냅샷)
  • Prompts: 재사용 가능한 프롬프트 템플릿(조직 표준 작업지시 등)

실무 관점에서 “에이전트 확장 서버 구축”은 결국:

  • Tool schema(입력/출력 계약)
  • 권한/인증/사용자 컨텍스트 바인딩
  • 관측(로그/트레이싱), 타임아웃, 실패 모드 설계 이 4개를 제대로 만드는 일입니다(코드는 그 다음). 특히 2026년 현장 사례를 정리한 연구에서도 프로덕션 장애를 “계약/컨텍스트/타임아웃/에러/관측” 축으로 분류합니다. (arxiv.org)

2) 내부 작동 흐름(“왜 이렇게” 동작하나)

클라이언트(Claude Desktop/Code 등)가 MCP server를 붙일 때 대략 흐름은:

  1. 서버 시작/연결
  2. 도구/리소스/프롬프트 목록 교환(discoverability)
  3. 모델이 대화 중 판단하여 tool call 생성 → 서버로 요청
  4. 서버는 외부 시스템 호출/처리 후 tool result 반환
  5. 클라이언트는 결과를 모델 컨텍스트에 반영

여기서 중요한 포인트:

  • MCP는 “모델이 무엇을 할 수 있는지”를 도구 목록 자체로 학습시키므로, 도구 정의가 커지면 토큰/성능/정확도 비용이 바로 증가합니다(도구 난립은 안티패턴). (docs.claude.com)
  • 2026년 기준 Claude API에는 Messages API에서 원격 MCP 서버를 직접 붙이는 MCP connector가 있고, 이 경우 “클라이언트 구현” 없이도 원격 도구 호출이 가능합니다. 단, 이 커넥터는 현재 tool calls만 지원하고 로컬 stdio 서버는 직접 연결할 수 없습니다. (platform.claude.com)

3) 다른 접근과의 차이점(비교 기준)

  • “SDK로 tool calling 직접 구현” vs MCP
    • 직접 구현: 앱마다 커스텀 통합이 늘고, 정책/감사/버전 호환이 분산
    • MCP: 도구 계층을 서버로 모아 클라이언트 다변화에 강함
  • “플러그인 마켓” vs MCP
    • 마켓 기반은 배포는 편하지만 공급망 리스크가 커짐
    • MCP는 표준이지만, 서버 운영자 책임(권한/격리)이 더 커집니다(최근 보안 이슈가 그 근거). (tomshardware.com)

💻 실전 코드

아래는 “현실적인 시나리오”로 사내 Kubernetes 운영 보조 에이전트용 MCP server를 만듭니다.

요구사항(현실성 포인트):

  • 무조건 kubectl 프리패스 금지
  • allowlist된 namespace에서만 동작
  • 변경 작업은 배제하고(읽기 위주), 그래도 위험한 출력은 길이 제한
  • Claude Desktop(로컬)에서 바로 붙여서 검증 가능(stdio)

0) 의존성/실행 준비

1
2
3
4
5
6
7
8
9
# Node 18+ 권장(팀 표준에 맞추세요)
mkdir mcp-k8s-ops && cd mcp-k8s-ops
npm init -y

npm install @modelcontextprotocol/sdk zod
npm install -D tsx typescript @types/node

# kubectl이 로컬에 설치되어 있고, 현재 kube-context가 읽기 권한을 가진다고 가정
kubectl version --client

TypeScript 실행은 tsx로 단순화합니다.

1) MCP 서버(stdio) 구현

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
// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

// 운영 정책: 이 서버가 “보안 경계”가 되게 만드는 핵심
const ALLOWED_NAMESPACES = new Set(
  (process.env.MCP_K8S_NAMESPACES ?? "default,observability")
    .split(",")
    .map((s) => s.trim())
    .filter(Boolean),
);

const MAX_STDOUT_CHARS = Number(process.env.MCP_MAX_STDOUT_CHARS ?? 30_000);

function assertAllowedNamespace(ns: string) {
  if (!ALLOWED_NAMESPACES.has(ns)) {
    throw new Error(
      `namespace '${ns}' is not allowlisted. allowed=${[...ALLOWED_NAMESPACES].join(",")}`,
    );
  }
}

async function kubectl(args: string[]) {
  // shell=true 금지: 인젝션 표면 줄이기
  const { stdout, stderr } = await execFileAsync("kubectl", args, {
    timeout: 15_000,
    maxBuffer: 1024 * 1024 * 5,
  });

  const out = (stdout ?? "").slice(0, MAX_STDOUT_CHARS);
  const err = (stderr ?? "").slice(0, MAX_STDOUT_CHARS);

  // stderr가 있어도 일부 kubectl은 경고를 뿜으므로 함께 반환
  return { out, err };
}

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

// (A) 현실적인 도구 1: 장애 상황에서 제일 먼저 보는 “Pod 목록/상태”
server.tool(
  "k8s_list_pods",
  {
    description:
      "List pods in an allowlisted namespace. Read-only. Use this to triage incidents.",
    inputSchema: z.object({
      namespace: z.string().min(1),
      labelSelector: z.string().optional().describe("kubectl -l selector"),
    }),
  },
  async ({ namespace, labelSelector }) => {
    assertAllowedNamespace(namespace);

    const args = ["get", "pods", "-n", namespace, "-o", "wide"];
    if (labelSelector) args.push("-l", labelSelector);

    const { out, err } = await kubectl(args);

    return {
      content: [
        { type: "text", text: out || "(no stdout)" },
        ...(err ? [{ type: "text", text: `stderr:\n${err}` as const }] : []),
      ],
    };
  },
);

// (B) 현실적인 도구 2: 특정 Pod 이벤트/원인 파악용 describe
server.tool(
  "k8s_describe_pod",
  {
    description:
      "Describe a single pod (events 포함). Read-only. Use after identifying a suspect pod.",
    inputSchema: z.object({
      namespace: z.string().min(1),
      podName: z.string().min(1),
    }),
  },
  async ({ namespace, podName }) => {
    assertAllowedNamespace(namespace);

    const { out, err } = await kubectl(["describe", "pod", podName, "-n", namespace]);

    return {
      content: [
        { type: "text", text: out || "(no stdout)" },
        ...(err ? [{ type: "text", text: `stderr:\n${err}` as const }] : []),
      ],
    };
  },
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((e) => {
  // stdio 서버는 “조용히 죽으면” 디버깅이 지옥입니다.
  console.error("[fatal] MCP server failed:", e);
  process.exit(1);
});

2) Claude Desktop / Claude Code에 붙여서 검증

  • Claude Desktop은 로컬 MCP 서버를 child process + stdio로 띄우는 방식이 일반적입니다. (learn.netdata.cloud)
  • Claude Code 역시 MCP 서버를 구성/관리할 수 있고, Desktop 설정을 가져오는 워크플로도 지원합니다. (docs.claude.com)

(개념 예시) claude_desktop_config.json에 서버 등록(경로/키는 환경에 따라 다를 수 있음):

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "mcpServers": {
    "k8s-ops": {
      "type": "stdio",
      "command": "npx",
      "args": ["tsx", "src/server.ts"],
      "env": {
        "MCP_K8S_NAMESPACES": "default,observability",
        "MCP_MAX_STDOUT_CHARS": "30000"
      }
    }
  }
}

예상 사용 흐름(대화):

  • observability namespace에서 CrashLoopBackOff인 pod 찾아줘”
  • Claude가 k8s_list_pods 호출 → 결과에서 의심 pod 추출
  • 이어서 k8s_describe_pod 호출 → events/이미지풀/Probe 실패 등을 근거로 원인 제시

⚡ 실전 팁 & 함정

Best Practice (현장에서 바로 효과 나는 것)

1) 도구는 ‘원자적’이 아니라 ‘운영 단위’로 설계

  • get pods 같은 원자 도구를 20개 늘리는 대신
    “incident triage에 필요한 최소 묶음”으로 압축(예: list+filter+요약)
  • 이유: 도구 정의가 커질수록 모델의 선택 비용/오판 비용이 올라갑니다(토큰/주의 분산). (docs.claude.com)

2) 권한을 MCP 서버에서 강제(allowlist/denylist), 클라이언트에 기대지 말기

  • “사용자 확인 UI”는 클라이언트마다 다르고(또는 없고), 자동화 환경에서는 우회됩니다.
  • 서버가 정책을 enforce 해야 “확장 서버”가 보안 경계가 됩니다.
    (최근 MCP 관련 보안 이슈들이 공통으로 지적하는 지점이기도 함) (techradar.com)

3) 관측 가능성(Observability) 먼저

  • 최소: tool 호출마다 request-id, 호출자, 대상 리소스, duration, 성공/실패를 구조화 로깅
  • 이유: 에이전트 장애는 “모델이 틀림”보다 “타임아웃/부분 실패/출력 과다”가 더 흔합니다. (arxiv.org)

흔한 함정/안티패턴

  • stdio 서버에 만능 셸 도구 하나 달기: run(command: string) 같은 도구는 결국 “RCE를 제품 기능으로 만든 것”에 가깝습니다. 2026년 보안 논쟁의 핵심도 이런 실행 경계 문제로 번졌습니다. (tomshardware.com)
  • 도구 결과를 무제한으로 반환: 로그/스키마 덤프는 토큰 폭발 → 응답 품질 저하/비용 증가. Claude Code는 큰 출력에 경고/상한을 두는 방향으로 가이드합니다. (docs.claude.com)
  • 버전 핀/클라이언트 매트릭스 없이 배포: 클라이언트가 프로토콜 버전을 올리면 서버가 깨질 수 있습니다(특히 여러 클라이언트를 지원하면 더). “클라이언트 매트릭스 테스트”를 운영 체크리스트에 넣으라는 실전 발표 자료도 있습니다. (jfokus.se)

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

  • 로컬 stdio: 설치/권한은 편하지만 로컬 실행 리스크가 크고, 배포/업데이트가 사용자 단으로 분산
  • 원격 Streamable HTTP: 운영/관측/업데이트는 쉬우나, 인증(OAuth), 네트워크 지연, 멀티테넌시 격리가 과제 (ts.sdk.modelcontextprotocol.io)
  • “Claude API MCP connector”는 클라이언트 구현 부담을 줄이지만, 2026년 5월 기준 제약(예: tool calls 중심, https 공개 필요, ZDR 불가 등)을 감안해야 합니다. (platform.claude.com)

🚀 마무리

정리하면, 2026년 5월의 MCP는 “장난감 플러그인 규격”이 아니라 에이전트 확장 서버를 제품 수준으로 운영하기 위한 표준 계약으로 굳어지는 흐름입니다(공식 SDK/다중 클라이언트/원격 커넥터까지). (modelcontextprotocol.io)
다만 동시에, MCP 서버는 곧 권한과 실행의 경계가 되므로(특히 stdio/local), 보안·격리·정책 강제가 설계의 1순위여야 합니다. (arxiv.org)

도입 판단 기준(현실적인 체크):

  • 우리 조직은 “도구 통합을 여러 클라이언트에서 재사용”할 필요가 있는가?
  • 도구 호출을 감사 가능하게 만들 요구가 있는가?
  • 서버 측에서 권한/출력/타임아웃/관측을 강제할 운영 역량이 있는가?
  • (Yes가 많다면) MCP server를 먼저 “읽기 전용/저위험 도구”부터 깔고, 점진적으로 쓰기 작업을 확장하세요.

다음 학습 추천:

  • MCP 공식 SDK 문서에서 Streamable HTTP(원격) 서버 예제를 직접 돌려보고, 우리 인프라 인증(OAuth)과 로깅을 붙여 “프로덕션 형태”로 확장 (ts.sdk.modelcontextprotocol.io)
  • Claude API의 MCP connector 제약/버전 헤더를 확인한 뒤, “서버 하나로 Desktop+API 양쪽”을 만족하는 배포 전략 수립 (platform.claude.com)
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.