MCP 프로덕션 서버 구축 2026: Streamable HTTP·OAuth 2.1로 안전한 AI 에이전트 연동
MCP 최신 스펙(2025-11-25)을 기준으로 Streamable HTTP 전송과 OAuth 2.1 리소스 서버 인증을 적용해 프로덕션 등급 AI 에이전트 서버를 구축하는 방법을 코드와 함께 정리합니다.
왜 지금 MCP 서버의 "프로덕션 강건성"이 화두인가
MCP(Model Context Protocol)는 더 이상 실험적 프로토콜이 아닙니다. 공식 집계 기준 2026년 3월 시점에 월간 SDK 다운로드 9,700만 회, GitHub 스타 8만 1천 개를 넘어서며 사실상 AI 에이전트–도구 연동의 표준으로 자리 잡았습니다. 문제는 대부분의 팀이 여전히 로컬 stdio 데모 수준에 머물러 있다는 점입니다.
에이전트를 실제 서비스에 올리는 순간 전혀 다른 질문이 쏟아집니다. 어떻게 인증할 것인가, 어떻게 수평 확장할 것인가, 토큰을 안전하게 다루는가. 2025년 한 해 동안 MCP 스펙이 가장 크게 바뀐 영역도 정확히 이 지점, 전송(transport)과 인증(authorization) 입니다.
이 글에서는 현재 안정 스펙인 2025-11-25 리비전을 기준으로, Streamable HTTP 전송과 OAuth 2.1 리소스 서버 모델을 적용해 프로덕션 등급 MCP 서버를 구축하는 방법을 코드와 함께 정리합니다.
배경: MCP 스펙은 어떻게 진화해 왔는가
MCP 스펙은 날짜 기반 리비전으로 관리됩니다. 프로덕션 관점에서 의미 있는 변경점만 추리면 다음과 같습니다.
| 리비전 | 핵심 변경 | 프로덕션 영향 |
|---|---|---|
| 2024-11-05 | 최초 공개, HTTP+SSE 전송 | 두 개의 엔드포인트(SSE+POST), 상태 유지 부담 |
| 2025-03-26 | Streamable HTTP 도입(HTTP+SSE 대체) | 단일 엔드포인트, 수평 확장 용이 |
| 2025-06-18 | OAuth 2.1 리소스 서버 모델 확정 | PRM(RFC 9728)·Resource Indicators(RFC 8707) 필수 |
| 2025-11-25 | 현재 안정 스펙, Tasks 등 정리 | 장기 실행 작업·거버넌스 강화 |
가장 중요한 두 가지는 전송과 인증입니다. 먼저 전송 방식을 비교합니다.
| 항목 | HTTP+SSE (구) | Streamable HTTP (현재) |
|---|---|---|
| 엔드포인트 | SSE + 별도 POST 2개 | 단일 MCP 엔드포인트(POST+GET) |
| 상태 | 세션 상태 유지 필요 | 무상태(stateless) 운영 가능 |
| 스트리밍 | 항상 SSE 연결 유지 | 필요 시에만 SSE 업그레이드 |
| 확장성 | 로드밸런서 sticky 필요 | 수평 확장 친화 |
Streamable HTTP는 2025-03-26에 도입되어 11월 리비전에서도 유지되는, 사실상의 프로덕션 표준 전송입니다. 서버는 단일 경로로 POST와 GET을 모두 받고, 다수의 서버 메시지를 보낼 필요가 있을 때만 선택적으로 SSE로 업그레이드합니다.
심층 분석 1: Streamable HTTP MCP 서버 구현
먼저 도구(tool)와 리소스(resource)를 노출하는 기본 서버를 TypeScript SDK로 작성합니다. 핵심은 stdio가 아니라 Streamable HTTP 전송을 붙이는 부분입니다.
// server.ts — MCP 서버 본체 (도구 + 리소스 정의)
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
export function buildServer(): McpServer {
const server = new McpServer({
name: 'intentcode-agent-tools',
version: '1.0.0',
});
// 도구: 사내 지식베이스 검색 (에이전트가 호출)
server.tool(
'search_kb',
'사내 지식베이스에서 관련 문서를 검색합니다',
{ query: z.string().min(2), topK: z.number().int().min(1).max(20).default(5) },
async ({ query, topK }) => {
const docs = await kbSearch(query, topK); // 내부 RAG 검색
return { content: [{ type: 'text', text: JSON.stringify(docs) }] };
},
);
// 리소스: 읽기 전용 컨텍스트 (URI 템플릿으로 노출)
server.resource('runbook', 'runbook://{service}', async (uri) => ({
contents: [{ uri: uri.href, text: await loadRunbook(uri) }],
}));
return server;
}
다음으로 Streamable HTTP 전송을 Express에 연결합니다. 단일 엔드포인트가 POST(클라이언트→서버)와 GET(서버 스트리밍)을 모두 처리하는 것이 핵심입니다.
// http.ts — 단일 MCP 엔드포인트에 Streamable HTTP 전송 연결
import express from 'express';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { buildServer } from './server.js';
const app = express();
app.use(express.json());
// 무상태 모드: 요청마다 transport를 생성 → 수평 확장 친화
app.post('/mcp', async (req, res) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless
});
res.on('close', () => transport.close());
const server = buildServer();
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
// 서버→클라이언트 알림 스트리밍이 필요 없으면 GET은 405로 차단
app.get('/mcp', (_req, res) => res.status(405).end());
app.listen(8080, () => console.log('MCP on :8080/mcp'));
심층 분석 2: OAuth 2.1 리소스 서버로 보호하기
2025-06-18 스펙부터 보호된 MCP 서버는 OAuth 2.1 리소스 서버(Resource Server) 로 동작합니다. 더 이상 MCP 서버가 직접 토큰을 발급하지 않고, 외부 인증 서버가 발급한 액세스 토큰을 검증해 소비할 뿐입니다.
세 가지 의무 사항을 반드시 지켜야 합니다. 첫째, OAuth 2.0 Protected Resource Metadata(RFC 9728)를 .well-known으로 노출합니다. 둘째, 인증이 필요하면 401과 함께 WWW-Authenticate 헤더로 메타데이터 URL을 알려줍니다. 셋째, 받은 토큰이 자신을 대상으로 발급되었는지(audience) 검증하고, 업스트림 API로 토큰을 그대로 흘려보내지 않습니다.
# /.well-known/oauth-protected-resource (RFC 9728 PRM 문서)
resource: "https://mcp.intentcode.co.kr"
authorization_servers:
- "https://auth.intentcode.co.kr"
scopes_supported: ["mcp:tools", "mcp:resources"]
bearer_methods_supported: ["header"]
// auth.ts — 토큰 검증 미들웨어 (audience·scope 강제)
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(
new URL('https://auth.intentcode.co.kr/.well-known/jwks.json'),
);
export async function requireToken(req, res, next) {
const token = (req.headers.authorization ?? '').replace(/^Bearer /, '');
if (!token) {
// PRM 위치를 알려주는 401 (RFC 9728)
res.set(
'WWW-Authenticate',
'Bearer resource_metadata="https://mcp.intentcode.co.kr/.well-known/oauth-protected-resource"',
);
return res.status(401).end();
}
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://auth.intentcode.co.kr',
audience: 'https://mcp.intentcode.co.kr', // ⚠️ audience 검증 필수
});
req.scopes = String(payload.scope ?? '').split(' ');
next();
} catch {
res.status(401).end();
}
}
클라이언트 측은 RFC 8707 Resource Indicators를 사용해, 토큰 요청 시 resource 파라미터로 "이 토큰은 어떤 MCP 서버용인지"를 명시해야 합니다. 이렇게 발급된 토큰만 위 미들웨어의 audience 검증을 통과합니다.
# 클라이언트: Resource Indicator(RFC 8707)를 붙여 토큰 요청
curl -X POST https://auth.intentcode.co.kr/oauth/token \
-d grant_type=authorization_code \
-d code="$AUTH_CODE" \
-d client_id="$CLIENT_ID" \
-d redirect_uri="https://app.intentcode.co.kr/cb" \
-d resource="https://mcp.intentcode.co.kr" # ← audience 고정
실전 가이드: 배포까지의 단계
프로덕션 배포는 다음 순서로 진행합니다.
# 1) 의존성 설치 (TypeScript SDK + 검증 라이브러리)
npm i @modelcontextprotocol/sdk zod jose express
# 2) 빌드 & 로컬 검증 — MCP Inspector로 핸드셰이크 확인
npx @modelcontextprotocol/inspector node dist/http.js
# 3) 컨테이너 빌드
docker build -t mcp-agent-tools:1.0.0 .
쿠버네티스에 올릴 때는 무상태 전송 덕분에 일반 Deployment + HPA로 충분합니다. 세션 어피니티(sticky session)가 필요 없다는 점이 구 HTTP+SSE 대비 가장 큰 운영 이점입니다.
# k8s: 무상태 MCP 서버는 일반 Deployment로 수평 확장
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-agent-tools
spec:
replicas: 3 # 무상태 → 자유로운 스케일아웃
selector: { matchLabels: { app: mcp-agent-tools } }
template:
metadata: { labels: { app: mcp-agent-tools } }
spec:
containers:
- name: server
image: mcp-agent-tools:1.0.0
ports: [{ containerPort: 8080 }]
readinessProbe: # /mcp는 POST 전용이므로 별도 /healthz 권장
httpGet: { path: /healthz, port: 8080 }
트러블슈팅: 프로덕션에서 자주 막히는 지점
1) 토큰 패스스루(passthrough)로 인한 권한 혼선. MCP 서버가 클라이언트에게 받은 토큰을 그대로 업스트림 API에 재사용하면, audience가 맞지 않아 거부되거나 과도한 권한이 전파됩니다. 스펙상 금지 사항입니다. 업스트림 호출은 서버 자신의 자격증명(또는 토큰 교환)으로 별도 수행해야 합니다.
2) WWW-Authenticate 누락으로 클라이언트가 인증 서버를 못 찾음. 401만 던지고 resource_metadata를 안 알려주면, 표준 클라이언트는 어디서 토큰을 받아야 할지 모릅니다. PRM 문서 노출과 401 헤더는 한 쌍입니다.
3) 세션 상태에 의존한 코드를 그대로 확장. 구 HTTP+SSE 시절 코드를 옮기면서 메모리에 세션을 들고 있으면, 레플리카가 늘 때 요청이 다른 파드로 가 깨집니다. 무상태 모드(sessionIdGenerator: undefined)로 두거나, 세션이 꼭 필요하면 Redis 등 외부 저장소로 분리합니다.
결론: IntentCode의 에이전트 연동 관점
2026년의 MCP 프로덕션 체크리스트는 명확합니다. Streamable HTTP로 무상태 확장을 확보하고, OAuth 2.1 리소스 서버 모델로 audience 검증과 PRM 노출을 강제하며, 토큰 패스스루를 금지하는 것입니다. IntentCode는 AI 에이전트 라인에서 사내 도구·지식베이스를 MCP 서버로 표준화하되, 위 인증 게이트를 기본 골격으로 삼아 "안전하게 확장 가능한 에이전트 연동"을 설계합니다.