사내 REST API를 LLM이 직접 호출하게 만드는 법: TypeScript MCP 서버 구현과 Gateway 패턴
팀에서 AI 에이전트를 도입하려다 "그래서 우리 내부 API는 어떻게 연결해?"라는 질문에 막혀본 적 있으신가요? 저도 처음엔 LLM에게 사내 API 문서를 통째로 붙여넣거나, 직접 fetch 코드를 프롬프트에 넣어주는 방식을 시도했습니다. 잘 됩니다—딱 한 번만요. 그 다음 요청부터는 LLM이 엔드포인트 URL을 창작하거나, 인증 헤더를 잊어버리거나, 존재하지 않는 파라미터를 보내기 시작합니다.
MCP(Model Context Protocol) 서버 래핑은 기존 API 로직을 건드리지 않고 LLM이 구조화된 방식으로 사내 시스템을 호출할 수 있게 해주는 아키텍처 패턴입니다. 프롬프트에 API 명세를 욱여넣는 방식 대신, LLM이 표준화된 Tool 인터페이스를 통해 직접 함수를 호출하는 구조입니다. 이 글을 끝까지 읽으면 기존 REST API를 MCP Tool로 노출하는 TypeScript 코드를 작성할 수 있게 됩니다. Node.js + TypeScript 기본 환경이 설치되어 있다는 가정 하에 설명합니다.
Claude Code, Cursor, GitHub Copilot이 모두 MCP 클라이언트로 동작하는 지금, 사내 시스템을 MCP로 노출하는 것은 팀 전체의 AI 생산성 인프라를 구축하는 일이기도 합니다.
핵심 개념
MCP 래핑이란 무엇인가
아이디어 자체는 단순합니다. 기존 REST API 앞에 JSON-RPC 기반의 얇은 인터페이스 레이어를 추가하는 겁니다. LLM은 이 레이어를 통해 "어떤 함수를 호출할 수 있는지", "각 파라미터가 무엇인지"를 명확하게 파악하고, 타입 안전한 방식으로 호출합니다.
용어 정리: JSON-RPC는 JSON을 이용한 원격 프로시저 호출 프로토콜입니다. MCP는 이를 기반으로 LLM ↔ 외부 도구 간 표준 통신 방식을 정의합니다.
MCP는 세 가지 기본 추상화를 제공합니다:
| 추상화 | 역할 | 사내 API 매핑 예시 |
|---|---|---|
| Tool | LLM이 부수효과를 일으키거나 계산을 요청하는 함수 | createTicket, triggerDeploy |
| Resource | 읽기 전용 데이터 노출 | 사내 위키, API 문서, 코드베이스 |
| Prompt | 재사용 가능한 프롬프트 템플릿 | 특정 도메인 전용 지시문 |
사내 API 래핑에서 실질적으로 핵심이 되는 건 Tool입니다. 내부 API 엔드포인트를 Tool로 일대일 매핑하거나, 여러 엔드포인트를 의미 있는 단위로 묶어 하나의 Tool로 노출하는 작업이 대부분입니다.
Transport: stdio vs Streamable HTTP
Transport 선택은 서버의 배포 방식을 결정합니다. 2025년 3월 기준으로 기존 HTTP+SSE 방식은 공식 deprecated됐고, 두 가지 선택지가 남아있습니다:
| Transport | 사용 시나리오 | 특징 |
|---|---|---|
stdio |
로컬 개발, 단일 사용자 CLI 도구 | 프로세스 간 표준 입출력, 설정 간단 |
| Streamable HTTP | 팀·조직 규모의 원격 서버 | HTTP 기반, OAuth 2.1 필수, 스케일 아웃 가능 |
사내 API를 팀 전체에 노출하려면 Streamable HTTP + OAuth 2.1 조합이 권장 방식입니다. 로컬에서 빠르게 테스트할 때는 stdio로 시작하고, 팀 공용 서버로 올릴 시점에 Streamable HTTP로 전환하는 순서가 현실적입니다.
스키마 설계의 핵심
저는 처음에 내부 API의 중첩 객체 구조를 그대로 Tool 파라미터로 노출했다가, LLM이 metadata.labels[0].value를 labelsValue라는 존재하지 않는 필드로 보내는 상황을 겪었습니다. 그 뒤로 파라미터 평탄화를 원칙으로 삼게 됐습니다. 스키마 모호성 = LLM의 Tool 오호출이라는 등식이 실무에서 꽤 자주 성립합니다.
Before — 중첩 객체 그대로 노출 (LLM이 자주 오호출):
{
metadata: z.object({
labels: z.array(z.object({
key: z.string(),
value: z.string()
}))
})
}After — 평탄화 (LLM이 정확하게 채움):
{
label_key: z.string().describe("레이블 키 (예: env, team)"),
label_value: z.string().describe("레이블 값 (예: production, backend)")
}좋은 Tool 스키마의 원칙을 요약하면:
- 파라미터는 가능한 한 평탄화(flatten) — 중첩 객체보다 단순 필드
describe()로 각 파라미터의 의미와 허용 값을 명시- enum 타입을 적극 활용해 LLM의 선택지를 제한
장단점 분석
MCP 래핑 도입을 결정하기 전에 트레이드오프를 먼저 살펴보는 게 좋습니다.
장점
| 항목 | 내용 |
|---|---|
| 빠른 통합 | 기존 API 로직 재작성 없이 MCP 인터페이스 레이어만 추가 |
| 멀티 클라이언트 지원 | Claude, GPT, Copilot, Cursor 등 MCP를 지원하는 모든 클라이언트에서 동일하게 호출 가능 |
| OpenAPI 자동화 | 스펙이 있으면 수작업 Tool 작성 최소화 |
| 표준 보안 레이어 | OAuth 2.1 기반 인증 체계를 프로토콜 레벨에서 강제 |
| 에이전트 생산성 | 자연어 지시로 복잡한 사내 작업 자동화 가능 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 프롬프트 인젝션 | Tool 설명이나 반환값에 악의적 지시가 포함되면 LLM이 의도치 않은 동작 수행 가능 | 반환 데이터를 신뢰하지 않는 원칙으로 설계, 출력 sanitization |
| 컨텍스트 오염 | Tool 수가 많으면 LLM 컨텍스트 윈도우가 Tool 설명으로 가득 차 성능 저하 | 도메인별 서버 분리, 동적 Tool 로딩 고려 |
| 스키마 복잡성 | 내부 API의 복잡한 객체를 단순화하는 재설계 필요 | 평탄화된 파라미터 구조, enum 적극 활용 |
| 감사 로그 부재 | AI 에이전트의 API 호출 기록이 없으면 컴플라이언스 감사 불가 | MCP 서버 또는 게이트웨이 레벨에서 모든 호출 로깅 |
| 권한 과잉 | 한 MCP 서버가 필요 이상으로 넓은 API 접근권을 가지는 경우 | Tool별 최소 권한 원칙 적용 |
용어 정리: Tool Poisoning Attack은 MCP Tool의 description 또는 반환값에 악의적인 LLM 지시를 심어 에이전트가 의도치 않은 동작을 수행하도록 유도하는 공격 방식입니다. 외부에서 유입된 데이터(사용자 입력, 외부 API 응답)가 Tool 반환값에 포함될 수 있기 때문에 내부 시스템에서도 무시할 수 없는 위협입니다.
실무에서 가장 흔한 실수
-
Tool 설명(description)을 대충 작성하는 것 —
"티켓을 만든다"같은 한 줄짜리 설명은 LLM이 언제, 어떻게 이 Tool을 써야 할지 파악하기 어렵게 만듭니다. 파라미터 각각에 구체적인 예시와 허용 값을describe()로 명시하는 것이 Tool 호출 정확도에 직결됩니다. -
인증 토큰을 Tool 파라미터로 받는 것 — LLM이 토큰을 직접 다루게 되면 로그에 토큰이 노출되거나 프롬프트 인젝션을 통해 탈취될 위험이 있습니다. 인증 정보는 서버 환경변수에서 주입하고 Tool 인터페이스에는 절대 노출하지 않는 것이 권장됩니다.
-
stdio서버를 팀 전체에 그대로 배포하는 것 — 로컬 개발용stdio서버를 팀 공용으로 그대로 쓰면 인증, 레이트 리밋, 감사 로그가 모두 없는 상태로 사내 API가 노출됩니다. 팀 규모가 되면 Streamable HTTP + OAuth 2.1로 전환하는 것이 권장됩니다.
실전 적용
TypeScript SDK 직접 구현: 사내 이슈 트래커를 Tool로 래핑하기
Jira, Linear, 혹은 자체 티켓 시스템의 REST API를 MCP Tool로 래핑하면, 에디터 AI에게 "이 버그 P1 티켓으로 만들어줘"라고 말하는 것만으로 실제 이슈가 생성됩니다.
먼저 의존성을 설치합니다:
pnpm add @modelcontextprotocol/sdk zod axios그다음 서버 코드입니다. internalClient가 어디서 왔는지 처음 보면 헷갈릴 수 있는데, axios 인스턴스를 환경변수에서 초기화하는 방식입니다:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import axios from "axios";
// 인증 토큰은 환경변수에서 주입 — Tool 파라미터로 받지 않습니다
const internalClient = axios.create({
baseURL: process.env.INTERNAL_API_URL,
headers: {
Authorization: `Bearer ${process.env.API_TOKEN}`
}
});
const server = new McpServer({ name: "internal-issue-tracker", version: "1.0.0" });
server.tool(
"create_ticket",
"사내 이슈 트래커에 새 티켓을 생성합니다",
{
title: z.string().describe("티켓 제목 (명확하고 간결하게)"),
priority: z.enum(["P1", "P2", "P3"]).describe("우선순위: P1=긴급, P2=높음, P3=일반"),
assignee: z.string().email().optional().describe("담당자 이메일 주소")
},
async ({ title, priority, assignee }) => {
const res = await internalClient.post("/issues", { title, priority, assignee });
return {
content: [{ type: "text", text: `티켓 생성 완료: ${res.data.id} — ${res.data.url}` }]
};
}
);
// 서버 시작 — 로컬 개발 시 stdio transport 사용
const transport = new StdioServerTransport();
await server.connect(transport);| 포인트 | 설명 |
|---|---|
z.enum(["P1", "P2", "P3"]) |
LLM이 임의 값을 넣지 못하도록 선택지를 고정 |
.describe() |
각 파라미터의 의미를 LLM에게 직접 전달 |
axios.create(...) |
인증 헤더를 클라이언트 레벨에서 처리 — Tool 인터페이스에는 노출되지 않음 |
server.connect(transport) |
이 줄이 있어야 서버가 실제로 실행됩니다 |
OpenAPI 스펙 자동 변환: 수작업 없이 MCP 서버 만들기
사내 API에 OpenAPI(Swagger) 스펙이 이미 있다면 Tool을 수작업으로 작성하지 않아도 됩니다. TypeScript 환경이라면 CLI 도구를 활용할 수 있습니다:
npx openapi-mcp-generator --input ./api-spec.yaml --output ./mcp-serverPython 생태계라면 FastMCP의 OpenAPI 통합이 가장 빠릅니다:
from fastmcp import FastMCP
from your_internal_api import app # 기존 FastAPI 앱
# FastAPI 앱을 MCP 서버로 변환
mcp = FastMCP.from_fastapi(app=app)
if __name__ == "__main__":
mcp.run()한 가지 솔직하게 짚어둘 점이 있습니다. from your_internal_api import app처럼 한 줄로 쓰면 간단해 보이지만, 실제로는 해당 FastAPI 앱의 의존성 설치, 환경변수 설정, DB 연결 등이 모두 함께 따라옵니다. 기존 앱과 같은 Python 환경에서 실행할 수 있다면 가장 빠른 방법이고, 환경이 분리되어 있다면 OpenAPI 스펙 파일을 export해서 openapi-mcp-generator로 처리하는 편이 현실적입니다.
OpenAPI 스펙의 operationId, summary, description이 그대로 Tool 이름과 설명으로 매핑됩니다. 스펙 품질이 곧 MCP Tool 품질이 된다는 점에서, 이미 잘 관리된 OpenAPI 스펙을 보유한 팀에게 특히 효과적입니다.
팀 규모 확장: MCP Gateway 패턴
팀이 여럿이 되고 도메인마다 MCP 서버를 각자 운영하기 시작하면 인증과 감사 로그가 제각각이 되는 문제가 생깁니다. 솔직히 이 시점에서 "그냥 각 서버에 토큰 하드코딩하면 안 되나"라는 생각이 드는데—그 선택이 나중에 보안 감사 때 발목을 잡습니다.
MCP Gateway는 이 문제를 해결하는 패턴입니다. Gateway는 MCP 프로토콜 레벨의 역방향 프록시로 동작합니다. AI 에이전트가 단일 엔드포인트(Gateway)에 연결하면, Gateway는 OAuth 2.1 인증을 처리한 뒤 내부적으로 각 도메인 MCP 서버로 Tool 호출을 라우팅합니다. 도메인 서버들은 인터넷에 노출되지 않고 Gateway 뒤에만 있으면 됩니다.
[Claude / AI Agent]
|
↓ (단일 연결점)
[MCP Gateway]
- OAuth 2.1 인증 처리
- 레이트 리밋
- 감사 로그 (모든 Tool 호출 기록)
- 도메인별 서버로 라우팅
|
_____|_____
| | |
↓ ↓ ↓
[HR [CI/CD [Analytics
API] API] API]
MCP MCP MCP
Srv Srv Srv
(내부망에만 존재)실사용 사례로 검증된 도구들:
| 도구 | 특징 |
|---|---|
| Kong AI MCP Proxy | 기존 HTTP API를 MCP로 브릿징, 레이트 리밋·인증 통합 |
| Azure API Management + Entra ID | Microsoft 스택 환경에서 MCP + AD 페더레이션 |
| mcp-gateway-registry | Keycloak/Entra 연동 오픈소스 게이트웨이 레지스트리 |
MCP SDK 월간 다운로드가 18개월 만에 약 970배 성장한 배경에는 이런 Gateway 패턴의 엔터프라이즈 확산이 있습니다. 2026년 기준으로 이 패턴은 팀 규모 이상의 사내 AI 인프라에서 사실상 표준이 되고 있습니다.
마치며
사내 API를 MCP로 래핑하면 기존 코드는 그대로 두면서 팀 전체가 AI 에이전트를 통해 사내 시스템을 자연어로 다룰 수 있는 인프라가 만들어집니다.
지금 바로 시작해볼 수 있는 3단계입니다. 상황에 따라 선택하시면 됩니다:
-
사내 API에 OpenAPI 스펙이 있다면
npx openapi-mcp-generator --input ./your-api-spec.yaml --output ./mcp-server로 골격 코드를 먼저 생성해볼 수 있습니다. 이 경우 2단계(수작업 Tool 작성)는 건너뛰어도 됩니다. -
스펙이 없다면
pnpm add @modelcontextprotocol/sdk zod axios를 설치하고, 팀이 매일 쓰는 API 하나(티켓 생성, 배포 상태 조회 등)를 위 TypeScript 예시를 참고해 Tool 하나로 먼저 변환해보시면 됩니다. Tool 하나만으로도 에이전트가 자연어 지시를 실제 호출로 바꾸는 경험을 바로 할 수 있습니다. -
Claude Code나 Cursor의 MCP 설정에 로컬 서버를 연결해 직접 사용해보시면 됩니다. 에이전트가 Tool을 호출할 때 어떤 파라미터를 채우는지 보면, 스키마에서 무엇을 개선해야 할지 바로 드러납니다. 이 단계를 거치고 나면
describe()와 enum이 왜 중요한지 실감하게 됩니다.
참고 자료
시작점으로 추천:
- Wrapping an Existing API with MCP: How to Expose Your Current APIs to LLMs | Gun.io
- How to build MCP servers with TypeScript SDK | DEV Community
- OpenAPI 🤝 FastMCP | FastMCP 공식 문서
심화 학습:
- Should you wrap MCP around your existing API? | Scalekit
- MCP Best Practices: Architecture & Implementation Guide | modelcontextprotocol.info
- From OpenAPI Spec to MCP Server: A Practical Guide | Xata
- API MCP Server Architecture Guide | Stainless
- What Is an MCP Gateway and Why Your Enterprise Needs One in 2026 | Composio
- Advanced authentication and authorization for MCP Gateway | Red Hat Developer
- Understanding Authorization in MCP | MCP 공식 문서
- MCP Server Security Best Practices: 2026 Engineering Guide | Digital Applied
- Model Context Protocol has prompt injection security problems | Simon Willison
- From REST to MCP: An Empirical Study of API Wrapping | arXiv