Joyuno's Dev Blog

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

AI Agent

들어가며

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) 초기 셋업

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

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) 실행 & 예상 출력(서버 단독 테스트)

npx tsx src/server.ts

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

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

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

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) 을 순서대로 보면 “내 조직에 맞는 운영 모델”까지 빠르게 설계할 수 있습니다.