2026년 4월, Claude용 MCP 서버를 “에이전트 확장 서버”로 제대로 구현하는 법: Streamable HTTP, 버전 호환, 그리고 보안까지
들어가며
Model Context Protocol(MCP)은 LLM/에이전트가 “외부 세계(데이터/액션)”와 연결되는 표준 인터페이스입니다. 팀마다 GitHub/DB/사내 API/배포 시스템을 각기 다른 방식으로 붙이던 N×M 통합 지옥을, 서버(툴 제공자)–클라이언트(에이전트/IDE) 구조로 평탄화합니다. Anthropic은 Claude Desktop/Claude Code/Claude.ai에서 MCP를 공식 지원하고 있고, GitHub Copilot SDK나 OpenAI Agents SDK도 MCP 연결을 문서화하기 시작하면서(=클라이언트 다양화) “한 번 서버를 잘 만들면 여러 호스트에서 재사용”이 현실이 됐습니다. (docs.anthropic.com)
언제 쓰면 좋은가:
- 에이전트가 사내 시스템을 반복 호출(티켓 조회/배포 승인/런북 실행/로그 검색)해야 하고, 호출 정책·감사·권한을 서버에서 통제하고 싶을 때
- IDE/CLI(Claude Code, Cursor, Copilot 등)에서 동일한 툴셋을 공유하고 싶을 때(“사내 Tool API”의 표준화)
언제 쓰면 안 되는가:
- 단발성 스크립트 수준(직접 API 호출이 더 단순)
- “모델이 곧바로 prod 권한을 가져야만” 하는 설계(보안·감사·오남용 리스크가 너무 큼). 2026년 4월 기준 MCP 생태계는 tool poisoning / prompt injection / 서버 구현 결함 이슈가 계속 보고되고 있어, 권한 경계 설계를 먼저 해야 합니다. (techradar.com)
🔧 핵심 개념
1) MCP에서 말하는 “서버”는 무엇인가
MCP server는 크게 3가지를 노출합니다.
- tools: 실행 가능한 함수(액션). 예:
deploy_service,search_logs - resources: 읽기 중심의 데이터 핸들(문서/DB/파일)
- prompts: 재사용 가능한 프롬프트 템플릿(조직 표준 운영 절차 등)
호스트(Claude Desktop/Claude Code/Copilot SDK 등)는 MCP server에 붙고, 모델은 “필요 시 tool 호출” 형태로 외부 액션을 실행합니다. 이때 중요한 점은 모델이 직접 네트워크/DB를 치는 게 아니라, 서버가 계약(contract)과 정책을 강제한다는 점입니다. (docs.anthropic.com)
2) Transport: stdio vs (Remote) HTTP 계열
- stdio transport: 호스트가 로컬에서 서버 프로세스를 spawn하고 stdin/stdout으로 통신. Claude Desktop의 전통적 방식(로컬 도구에 강함). (thymer.com)
- HTTP+SSE(legacy) → Streamable HTTP(권장): 원격 서버 연결을 위해 등장. Streamable HTTP는 기존 SSE 방식의 “긴 연결 유지/리줌 어려움” 문제를 줄이고, 더 단순한 HTTP 구현을 목표로 합니다. TypeScript SDK는 Streamable HTTP와 구형 SSE를 호환 모드로 병행하는 예시를 제공합니다. (reddit.com)
실무 판단:
- “개발자 PC에서만” 돌아야 하는 보안 경계(예: 로컬 소스 트리 접근)면 stdio
- 여러 클라이언트가 공유하는 사내 서비스(예: 배포/티켓/관측)면 Streamable HTTP + 강한 AuthN/AuthZ + Audit
3) 버전(프로토콜/SDK) 호환이 실제로 발목 잡는다
클라이언트가 MCP 프로토콜 버전을 올리면(예: Claude Code 업데이트) 구형 SDK 기반 서버가 연결 실패하는 사례가 커뮤니티에서 반복 보고됩니다. 운영 서버라면 “서버가 지원하는 protocol version을 로그로 노출”하고, 배포 파이프라인에 “연결 통합 테스트”를 넣는 게 현실적입니다. (reddit.com)
4) 다른 접근과의 차이점(왜 MCP인가)
- 단순 “function calling”은 특정 벤더 API에 종속되기 쉽고, 각 에이전트/IDE마다 래핑이 중복됩니다.
- MCP는 툴 디스커버리 + 표준 메시지 계약 + transport 추상화까지 포함해, “사내 Tool Platform”을 만들 때 재사용성이 큽니다. (LSP가 IDE 플러그인을 표준화했던 것과 비슷한 포지션) (docs.anthropic.com)
💻 실전 코드
현실적인 시나리오: “배포 승인/실행”을 MCP tool로 만들되,
- 모델이 임의 환경(prod) 배포를 못 하게 정책을 서버에서 강제
- 실행은 비동기(Job)로 돌리고, 상태 조회 tool 제공
- stdio(로컬) + Streamable HTTP(원격) 둘 다 제공(개발/운영 분리)
아래 예시는 TypeScript 기반 MCP 서버(Express) + Streamable HTTP 형태로 작성합니다. (SDK 문서에 Streamable HTTP transport가 명시되어 있고, SSE 레거시와의 병행 패턴도 제공됩니다.) (github.com)
0) 의존성/프로젝트 구성
1
2
3
4
5
6
mkdir mcp-deploy-server && cd mcp-deploy-server
npm init -y
npm i express zod
npm i @modelcontextprotocol/sdk
npm i -D typescript ts-node @types/node @types/express
npx tsc --init
1) 서버: “안전한 배포” 도구 구현 (Streamable HTTP)
deploy_request: 배포 요청 생성(승인 워크플로우/정책 체크 포함)deploy_status: 상태 조회- 핵심은 “모델이 보낸 문자열”을 그대로 shell로 넘기지 않고, 허용된 서비스/환경/버전만 받도록 강제하는 것
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
// src/server.ts
import express from "express";
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
// ---- 현실적인 정책 예시 ----
const AllowedEnv = z.enum(["staging", "prod"]);
const AllowedService = z.enum(["billing-api", "web-frontend", "worker"]);
const SemverLike = z.string().regex(/^\d+\.\d+\.\d+(-[a-z0-9.]+)?$/i);
type JobState = "PENDING_APPROVAL" | "RUNNING" | "SUCCEEDED" | "FAILED" | "REJECTED";
const deployRequestSchema = z.object({
service: AllowedService,
env: AllowedEnv,
version: SemverLike,
changeTicket: z.string().min(5), // JIRA/Linear 등
reason: z.string().min(10),
});
const jobs = new Map<string, { state: JobState; createdAt: number; detail?: string }>();
function newJobId() {
return `dep_${Date.now()}_${Math.random().toString(16).slice(2)}`;
}
// 실제로는 사내 CD 시스템(ArgoCD/Spinnaker/GitHub Actions 등) API 호출.
// 여기서는 “비동기 job” 형태만 유지.
async function startDeployment(jobId: string) {
jobs.set(jobId, { state: "RUNNING", createdAt: Date.now() });
// 예: await fetch("https://cd.internal/deploy", ...)
await new Promise((r) => setTimeout(r, 1500));
jobs.set(jobId, { state: "SUCCEEDED", createdAt: Date.now(), detail: "Rolled out to 3/3 pods" });
}
async function main() {
const mcp = new McpServer({ name: "deploy-control", version: "1.0.0" });
// Tool 1) 배포 요청
mcp.tool(
"deploy_request",
"Request a deployment with policy checks and approval gate. Never executes arbitrary commands.",
deployRequestSchema.shape,
async (args) => {
const parsed = deployRequestSchema.parse(args);
// ---- 정책: prod 배포는 티켓이 'PROD-'로 시작해야 한다(예시) ----
if (parsed.env === "prod" && !parsed.changeTicket.startsWith("PROD-")) {
return {
content: [
{
type: "text",
text:
`REJECTED: prod deployments require changeTicket starting with 'PROD-'. ` +
`Got: ${parsed.changeTicket}`,
},
],
isError: true,
};
}
const jobId = newJobId();
jobs.set(jobId, { state: "PENDING_APPROVAL", createdAt: Date.now() });
// ---- 승인: 실제라면 Slack/Teams 승인, OPA/IGA 연동 등 ----
// 여기선 데모로 staging은 자동 승인, prod는 수동 승인 필요로 둠.
if (parsed.env === "staging") {
// 자동 실행
void startDeployment(jobId);
} else {
jobs.set(jobId, { state: "PENDING_APPROVAL", createdAt: Date.now(), detail: "Waiting for human approval" });
}
return {
content: [
{
type: "text",
text:
`Deployment job created.\n` +
`jobId=${jobId}\nservice=${parsed.service}\nenv=${parsed.env}\nversion=${parsed.version}\n` +
`state=${jobs.get(jobId)!.state}`,
},
],
};
}
);
// Tool 2) 상태 조회
mcp.tool(
"deploy_status",
"Get status of a deployment job.",
{ jobId: z.string().min(1) },
async ({ jobId }) => {
const job = jobs.get(jobId);
if (!job) {
return {
content: [{ type: "text", text: `Not found: ${jobId}` }],
isError: true,
};
}
return {
content: [
{
type: "text",
text: `jobId=${jobId}\nstate=${job.state}\ndetail=${job.detail ?? ""}`,
},
],
};
}
);
// ---- Streamable HTTP endpoint ----
const app = express();
app.use(express.json());
// MCP transport를 라우팅에 연결 (SDK 패턴에 맞춰 구현)
const transport = new StreamableHTTPServerTransport({ endpoint: "/mcp" });
await mcp.connect(transport);
app.use("/mcp", transport.createExpressRouter());
const port = process.env.PORT ? Number(process.env.PORT) : 8787;
app.listen(port, () => {
console.log(`[mcp] deploy-control listening on :${port}`);
console.log(`[mcp] endpoint: http://127.0.0.1:${port}/mcp`);
});
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
실행:
1
npx ts-node src/server.ts
예상 출력:
1
2
[mcp] deploy-control listening on :8787
[mcp] endpoint: http://127.0.0.1:8787/mcp
2) Claude Code에 연결(원격/HTTP)
Claude Code 문서에는 MCP 서버를 HTTP로 추가하는 CLI 예시가 있습니다. (조직 환경에서는 OAuth 설정까지 같이 붙는 패턴이 일반적) (code.claude.com)
1
2
3
4
5
6
# 예시(개념): 로컬에서 돌지만 transport는 HTTP
claude mcp add-json deploy-control '{
"type":"http",
"url":"http://127.0.0.1:8787/mcp"
}'
claude mcp list
3) Claude Desktop(stdio 중심)과의 “현실적” 공존
Claude Desktop은 전통적으로 stdio 기반 설정(JSON에 command/args) 패턴이 많이 쓰였고, 제품/플랜에 따라 remote 지원이 “beta”로 언급됩니다. 즉, 조직 배포는 HTTP로 가더라도 데스크톱 사용자를 위해 stdio wrapper(또는 mcp-remote류 브리지) 를 두는 구성이 자주 나옵니다. (thymer.com)
⚡ 실전 팁 & 함정
Best Practice (2~3개)
1) Tool contract를 “문자열”이 아니라 “스키마”로 잠가라
위 코드처럼 zod로 service/env/version을 제한하면, prompt injection으로 “prod에 최신 main을 배포해” 같은 자연어가 들어와도 서버가 구조적으로 거부할 수 있습니다. (MCP 보안 이슈의 상당수는 “모델이 하라는 대로 툴이 너무 많은 권한으로 실행”에서 시작합니다.) (arxiv.org)
2) 권한 경계를 ‘호스트(클라이언트)’가 아니라 ‘서버’에서 강제
“클라이언트가 조심하겠지”는 깨집니다. 2026년 4월에도 MCP 관련 취약점/설계 논쟁이 계속되고 있고, tool chaining으로 RCE까지 이어질 수 있다는 보고가 반복됩니다. (techradar.com)
3) 버전/호환성: Streamable HTTP 우선 + 레거시 SSE 폴백 전략
TS SDK는 Streamable HTTP 연결 실패 시 SSE로 폴백하는 예시를 공식적으로 제공합니다. 서버도 (필요하면) 구형 클라이언트를 위해 병행 제공을 고려하세요. (github.com)
흔한 함정/안티패턴
- “shell tool” 남발:
exec("kubectl ...")같은 범용 실행 툴은 에이전트 확장 서버가 아니라 “RCE as a Service”가 됩니다. - tool description(메타데이터)에 운영 지침을 길게 써서 모델에 기대기: tool poisoning(툴 설명/메타에 숨은 지시) 계열 리스크가 지적되어 왔습니다. 설명은 짧고, 정책은 코드로. (vulnerablemcp.info)
- 관측성 부재: “왜 모델이 이 툴을 호출했는지/무슨 인자를 넣었는지/실패율”이 없으면 운영이 불가능합니다. (최근 연구/현업 체크리스트들도 이 부분을 반복 강조) (arxiv.org)
비용/성능/안정성 트레이드오프
- stdio는 로컬에서 빠르고 단순하지만, 사용자 PC 의존/배포 난이도가 올라갑니다.
- 원격(Streamable HTTP)은 공유/통제가 쉽지만, AuthN/Z·레이트리밋·감사로그를 갖춘 “미니 플랫폼”이 됩니다(초기 비용↑, 운영 안정성↑).
- 보안은 “추가 옵션”이 아니라 프로토콜 사용 순간 필수 비용으로 잡아야 합니다(최근 공개된 취약점/설계 비판 이슈들을 보면 더더욱). (techradar.com)
🚀 마무리
핵심은 이겁니다: MCP server는 “Claude에 기능을 붙이는 플러그인”이 아니라, 에이전트의 실행 권한을 캡슐화하는 서버입니다. 그래서 구현 포인트도 “툴을 몇 개 제공하느냐”보다 (1) 스키마로 입력을 제한하고 (2) 서버에서 정책/권한을 강제하고 (3) transport/버전 호환과 운영(로그·감사·테스트)을 갖추는지가 도입 성패를 가릅니다. (code.claude.com)
도입 판단 기준(체크리스트):
- 이 툴이 없으면 사람이 하던 “반복 실행/조회”가 명확히 존재하는가?
- 그 작업이 정형화된 입력 스키마로 떨어지는가? (안 떨어지면 위험 신호)
- 서버에서 AuthZ/승인/감사를 강제할 수 있는가?
- Claude Desktop/Claude Code/IDE 등 다중 클라이언트까지 염두에 두고 transport 전략이 있는가?
다음 학습 추천:
- Streamable HTTP로의 마이그레이션/호환 전략(공식 TS SDK 패턴) (github.com)
- MCP 보안(특히 tool poisoning / 간접 prompt injection) 관련 연구·가이드 (arxiv.org)
- Claude Code의 MCP 설정/운영 문서(서버 추가, OAuth 포함) (code.claude.com)