Claude Code에 사내 API 연결하기: TypeScript로 MCP 서버 구현하는 법
"어떻게 LLM을 우리 사내 시스템과 연결할 것인가"는 AI 보조 개발 도구가 팀의 워크플로우에 자리 잡으면서 선택이 아닌 실무 과제가 되었습니다. 기존에는 사내 REST API를 Claude 같은 모델에 연결하려면 제각각의 커스텀 통합 코드를 직접 작성해야 했습니다. 클라이언트가 바뀔 때마다 처음부터 다시 만들어야 했고, 결과적으로 동일한 API에 Claude용, Copilot용, Cursor용 연동 코드가 제각각 쌓이는 상황이 반복되었습니다.
Anthropic이 2024년 공개한 **Model Context Protocol(MCP)**은 이 문제를 표준화된 방식으로 해결합니다. 현재 Claude Code, Cursor, VS Code Copilot, Zed, Replit 등 주요 AI 개발 도구가 MCP를 지원하고 있어, 서버를 한 번 구현하면 모든 클라이언트에서 동일하게 재사용할 수 있습니다. 이 글에서는 TypeScript SDK를 사용해 기존 MCP 서버를 확장하고 사내 API를 Claude Code에 연결하는 과정을 단계적으로 살펴봅니다. 에러 핸들링, 보안 주의사항, 팀 배포 설정까지 실무에서 바로 적용할 수 있는 내용을 담았습니다.
이 글은 TypeScript와 REST API에 익숙한 개발자를 대상으로 합니다. MCP를 처음 접하더라도 핵심 개념부터 차근차근 설명하므로 함께 따라가실 수 있습니다. 단, 이 글에서는 로컬 환경의 stdio 트랜스포트만 다룹니다. 멀티유저 환경을 위한 Streamable HTTP 트랜스포트와 OAuth 2.1 인증 연동은 다음 글에서 이어서 다룰 예정입니다.
핵심 개념
MCP란 무엇인가 — "AI를 위한 USB-C 포트"
Model Context Protocol은 AI 모델이 외부 도구·데이터 소스와 상호작용하는 방식을 통일한 오픈 표준입니다. 마치 USB-C 포트가 다양한 기기를 하나의 규격으로 연결하듯, MCP는 AI 클라이언트와 외부 시스템 사이의 표준 인터페이스 역할을 합니다.
MCP(Model Context Protocol): Anthropic이 2024년 공개한 오픈 표준. LLM이 외부 API·데이터베이스·도구와 상호작용하는 방식을 표준화해, 클라이언트별 커스텀 통합 코드를 없애는 것을 목표로 합니다.
아키텍처는 세 계층으로 구성됩니다.
| 역할 | 구성 요소 | 예시 |
|---|---|---|
| Host | AI 클라이언트 | Claude Code, Claude Desktop |
| Client | Host 내부의 MCP 커넥터 | 프로토콜 협상·메시지 라우팅 담당 |
| Server | 개발자가 직접 구현 | 사내 API를 래핑한 커스텀 서버 |
MCP 서버가 제공하는 세 가지 프리미티브
MCP 서버는 Claude에게 다음 세 종류의 기능을 노출할 수 있습니다.
| 프리미티브 | 역할 | 실무 예시 |
|---|---|---|
| Tools | Claude가 호출할 수 있는 함수 (액션·계산·외부 요청) | 사내 REST API 엔드포인트 호출 |
| Resources | 읽기 전용 데이터 소스 | 내부 위키, DB 스키마, 문서 |
| Prompts | 재사용 가능한 프롬프트 템플릿 | 코드 리뷰 지시 템플릿 |
트랜스포트 방식 — 로컬 vs. 원격
MCP 서버가 클라이언트와 통신하는 방식은 두 가지입니다. 선택하는 방식에 따라 배포 전략이 크게 달라집니다.
| 트랜스포트 | 적합한 환경 | 특징 |
|---|---|---|
| StdioServerTransport | 로컬 개발, 개인 머신 | 별도 서버 불필요, 간단한 설정 |
| StreamableHTTPServerTransport | 프로덕션, 멀티유저, 클라우드 | 2025년 3월 스펙 개정으로 SSE 대체 |
Streamable HTTP: 2025년 3월 26일 MCP 스펙 개정으로 기존 SSE(Server-Sent Events) 방식을 공식 대체한 전송 방식입니다. 업계 추정치에 따르면 현재 프로덕션 배포의 약 70%가 Streamable HTTP를 사용하고 있어, 새 프로젝트에서는 기본으로 사용하는 것을 권장합니다.
실전 적용
먼저 개발 환경을 구성합니다.
pnpm init
pnpm add @modelcontextprotocol/sdk zod
pnpm add -D typescript tsx @types/nodeTypeScript 프로젝트를 올바르게 빌드하려면 tsconfig.json도 필요합니다.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src"]
}그리고 package.json에 ESM 설정과 빌드 스크립트를 추가합니다.
{
"type": "module",
"scripts": {
"dev": "tsx src/server.ts",
"build": "tsc",
"start": "node dist/server.js"
}
}"type": "module" 설정은 .js 확장자를 사용하는 ESM 임포트(예: ...stdio.js)를 올바르게 해석하기 위해 필수입니다.
예시 1: 사내 직원 정보 API를 MCP Tool로 래핑하기
아래는 사내 직원 정보 REST API를 MCP Tool과 Resource로 노출하는 기본 서버 구현입니다.
// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "company-api-server",
version: "1.0.0",
});
// 사내 직원 정보 API를 Tool로 노출
server.tool(
"get-employee-info",
"직원 정보를 사번으로 조회합니다",
{
employeeId: z.string().describe("직원 사번 (예: EMP-1234)"),
},
async ({ employeeId }) => {
try {
const response = await fetch(
`https://internal-api.company.com/employees/${employeeId}`,
{
headers: {
Authorization: `Bearer ${process.env.INTERNAL_API_TOKEN}`,
},
}
);
if (!response.ok) {
return {
content: [
{
type: "text",
text: `오류: 직원 정보를 가져오지 못했습니다 (상태 코드: ${response.status})`,
},
],
isError: true,
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `네트워크 오류: ${error instanceof Error ? error.message : "알 수 없는 오류"}`,
},
],
isError: true,
};
}
}
);
// 사내 프로젝트 목록을 Resource로 노출
server.resource(
"project-list",
"internal://projects/active",
{ mimeType: "application/json" },
async () => {
const response = await fetch(
"https://internal-api.company.com/projects?status=active",
{
headers: {
Authorization: `Bearer ${process.env.INTERNAL_API_TOKEN}`,
},
}
);
const data = await response.json();
return {
contents: [
{
uri: "internal://projects/active",
mimeType: "application/json",
text: JSON.stringify(data, null, 2),
},
],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);몇 가지 핵심 포인트를 짚어봅니다.
server.tool()의 세 번째 인자로 Zod 스키마를 넘기면 입력값이 런타임에 자동 검증됩니다.z.string().describe()텍스트는 Claude가 파라미터 용도를 파악하는 힌트가 됩니다. 설명이 충분할수록 Claude가 Tool을 더 정확하게 활용합니다.isError: true를 반환하면 Claude가 Tool 실행 실패를 명시적으로 인식합니다. 오류 텍스트만 반환하면 Claude가 실패 상황을 제대로 인식하지 못할 수 있습니다.internal://projects/active는 이 MCP 서버 내에서만 사용하는 커스텀 URI 스킴입니다. 클라이언트가 외부에서 직접 해석하는 것이 아니라, 서버 내 Resource를 식별하는 네임스페이스 역할을 합니다. 표준 URI 스킴(https://, file://)과 달리 MCP 서버가 해당 URI를 어떻게 처리할지 직접 정의합니다.try/catch로 네트워크 장애나 JSON 파싱 실패 같은 예외 상황을 처리하는 것을 권장합니다.
Zod: TypeScript 생태계의 런타임 스키마 검증 라이브러리입니다. MCP SDK와 함께 사용하면 Tool 파라미터의 타입을 컴파일 타임과 런타임 양쪽에서 모두 보장할 수 있습니다.
예시 2: 기존 오픈소스 MCP 서버를 실제로 확장하기
공개된 MCP 서버(예: PostgreSQL MCP, GitHub MCP)를 그대로 쓰는 대신 사내 특화 Tool을 추가하고 싶을 때 사용하는 패턴입니다. 핵심은 기존 서버를 팩토리 함수로 분리한 뒤, 반환된 인스턴스에 추가 Tool을 등록하는 것입니다.
먼저 베이스가 되는 서버를 팩토리 함수로 노출합니다(오픈소스 서버를 포크했다고 가정).
// src/base-server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function createBaseServer(): McpServer {
const server = new McpServer({
name: "base-server",
version: "1.0.0",
});
// 오픈소스 서버가 제공하는 기본 Tool
server.tool(
"query-database",
"내부 데이터베이스를 조회합니다",
{ table: z.string().describe("조회할 테이블 이름") },
async ({ table }) => {
// ... DB 조회 로직
return { content: [{ type: "text", text: `${table} 조회 결과` }] };
}
);
return server; // 인스턴스를 반환해 외부에서 확장 가능하게 합니다
}이제 베이스 서버를 가져와서 사내 특화 Tool을 추가로 등록합니다.
// src/extended-server.ts
import { createBaseServer } from "./base-server.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// 기존 서버 인스턴스를 가져옵니다
const server = createBaseServer();
// 사내 기술 문서 검색 Tool을 추가 등록합니다
server.tool(
"search-internal-docs",
"사내 기술 문서를 키워드로 검색합니다",
{
query: z.string().describe("검색 키워드"),
category: z
.enum(["architecture", "runbook", "api-spec", "onboarding"])
.optional()
.describe("문서 카테고리 필터"),
},
async ({ query, category }) => {
const params = new URLSearchParams({ q: query });
if (category) params.set("category", category);
try {
const response = await fetch(
`https://docs.internal.company.com/api/search?${params}`,
{
headers: { Authorization: `Bearer ${process.env.DOCS_API_TOKEN}` },
}
);
if (!response.ok) {
return {
content: [
{
type: "text",
text: `오류: 문서 검색 실패 (상태 코드: ${response.status})`,
},
],
isError: true,
};
}
const data = await response.json() as {
items: Array<{ title: string; url: string; summary: string }>;
};
// 실제 프로덕션 코드에서는 Zod로 API 응답 타입을 검증하는 것을 권장합니다
return {
content: [
{
type: "text",
text: data.items
.map((item) => `## ${item.title}\n${item.url}\n${item.summary}`)
.join("\n\n"),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `네트워크 오류: ${error instanceof Error ? error.message : "알 수 없는 오류"}`,
},
],
isError: true,
};
}
}
);
const transport = new StdioServerTransport();
await server.connect(transport);이 패턴의 핵심은 createBaseServer()가 서버 인스턴스를 반환하고, 호출하는 쪽에서 server.tool()로 추가 등록한다는 점입니다. 오픈소스 서버를 직접 수정하지 않아도 되므로, 업스트림 업데이트를 병합하기가 훨씬 쉬워집니다.
예시 3: MCP Inspector로 서버 디버깅하기
구현한 서버가 올바르게 동작하는지 확인하려면 MCP Inspector를 활용하는 것을 권장합니다.
# 개발 중인 서버를 tsx로 직접 실행하면서 Inspector 연결
npx @modelcontextprotocol/inspector npx tsx src/extended-server.tsInspector를 실행하면 브라우저 기반 GUI가 열리며, Claude Code 없이도 다음 작업이 가능합니다.
- 등록된 Tool과 Resource 목록 확인
- 파라미터를 직접 입력해 Tool 호출 테스트
- 응답 결과와 오류 메시지 검증
빌드된 파일로 확인하고 싶다면 npx @modelcontextprotocol/inspector node dist/extended-server.js를 사용하면 됩니다. 개발 중에는 매번 빌드할 필요가 없는 tsx 버전이 편리합니다.
예시 4: .mcp.json으로 팀 전체에 배포하기
구현한 서버를 팀 전체와 공유하려면 프로젝트 루트에 .mcp.json 파일을 추가하고 버전 관리에 포함시킵니다.
{
"mcpServers": {
"company-api": {
"command": "node",
"args": ["dist/extended-server.js"],
"env": {
"INTERNAL_API_TOKEN": "${INTERNAL_API_TOKEN}",
"DOCS_API_TOKEN": "${DOCS_API_TOKEN}"
}
}
}
}개발 중에는 매번 빌드하지 않고 tsx로 직접 실행하는 설정이 편리합니다.
{
"mcpServers": {
"company-api-dev": {
"command": "npx",
"args": ["tsx", "src/extended-server.ts"],
"env": {
"INTERNAL_API_TOKEN": "${INTERNAL_API_TOKEN}",
"DOCS_API_TOKEN": "${DOCS_API_TOKEN}"
}
}
}
}설정 파일의 스코프 우선순위는 로컬 > 프로젝트 > 유저 순서로 적용됩니다. 팀 공용 설정은 프로젝트 스코프(.mcp.json)에, 개인 오버라이드는 로컬 스코프에 두는 방식으로 관리할 수 있습니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 표준화된 재사용성 | 한 번 구현한 MCP 서버를 Claude Code, Cursor, VS Code Copilot, Zed 등 모든 MCP 호환 클라이언트에서 동일하게 사용할 수 있습니다 |
| 느슨한 결합 | AI 클라이언트와 사내 API를 독립적으로 업데이트할 수 있어, 한쪽 변경이 다른 쪽에 영향을 미치지 않습니다 |
| Zod 기반 타입 안전성 | 입력 파라미터를 런타임에 자동 검증해, 잘못된 값이 사내 API에 도달하기 전에 차단합니다 |
| 점진적 도입 | 기존 REST API를 수정하지 않고 래핑만으로 Claude에 노출할 수 있어 도입 리스크가 낮습니다 |
| 팀 단위 공유 | .mcp.json을 저장소에 커밋하면 팀 전체가 동일한 MCP 설정을 공유할 수 있습니다 |
단점 및 주의사항
| 항목 | 주요 리스크 | 대응 방안 |
|---|---|---|
| 정적 시크릿 의존 | MCP 서버의 53%가 장수명 API 키에 의존해 탈취 위험이 높음 (Astrix Security, 2025) | 단기 토큰 + AWS Secrets Manager / Vault 사용 |
| 프롬프트 인젝션 | 외부 콘텐츠를 가져오는 Tool에 악의적 지시가 주입될 수 있음 | 신뢰할 수 없는 외부 소스 응답을 반드시 정제(sanitize) 후 반환 |
| stdio의 로컬 한계 | stdio 트랜스포트는 로컬 환경에서만 동작 | 멀티유저·클라우드 환경에서는 StreamableHTTPServerTransport로 전환 |
| 스펙 변화 속도 | SSE → Streamable HTTP처럼 스펙이 빠르게 진화 | SDK 버전을 정기적으로 확인하고 package.json에 버전 범위를 명확히 고정 |
| 낮은 OAuth 채택률 | MCP 서버의 OAuth 채택률은 8.5%에 불과 (Astrix Security, 2025) | 기업 환경에서는 Okta, Azure AD 연동을 통한 OAuth 2.1 도입을 장기 목표로 설정 |
OAuth 2.1: 기업 환경에서 사용하는 표준 인증 프로토콜입니다. 2025년 6월 MCP 스펙 개정으로 MCP 서버가 공식 OAuth Resource Server로 분류되었습니다. Okta나 Azure AD 같은 기업 ID 관리 서비스(IdP)가 인증을 담당하고, MCP 서버는 발급된 토큰을 검증하는 방식으로 연동합니다. 이 글에서는 다루지 않으며, 다음 글에서 상세히 설명할 예정입니다.
실무에서 가장 흔한 실수
.mcp.json에 시크릿을 직접 하드코딩하는 경우 —"INTERNAL_API_TOKEN": "sk-1234..."형태로 실제 키 값을 파일에 쓰면 저장소에 시크릿이 노출됩니다. 반드시"${INTERNAL_API_TOKEN}"형태의 환경변수 레퍼런스를 사용하고, 실제 값은.env파일이나 비밀 관리 서비스에서 주입하는 방식을 권장합니다.- JSON 설정 파일의 사소한 문법 오류 —
.mcp.json에서 trailing comma(마지막 항목 뒤의 쉼표)나 작은따옴표 사용 같은 문법 오류는 오류 메시지 없이 MCP 서버가 조용히 실패하는 원인이 됩니다. 에디터의 JSON 린팅을 활성화하거나 JSON 형식 검증 도구를 사용하는 것을 권장합니다. - Tool 설명(description)을 소홀히 작성하는 경우 — Claude는 Tool의
name과description, 그리고 파라미터의.describe()텍스트를 보고 언제 어떤 Tool을 사용할지 판단합니다. 설명이 부정확하거나 지나치게 짧으면 Claude가 Tool을 올바르게 선택하지 못하거나 전혀 사용하지 않는 상황이 발생할 수 있습니다.
마치며
MCP는 사내 API를 한 번 구현해 모든 AI 개발 도구에서 재사용할 수 있는 표준 레이어이며, TypeScript SDK를 사용하면 수십 줄의 코드로 첫 번째 연결을 만들 수 있습니다.
지금 바로 시작해볼 수 있는 3단계는 다음과 같습니다.
- 개발 환경 구성 및 첫 번째 Tool 등록 —
pnpm add @modelcontextprotocol/sdk zod로 의존성을 설치한 뒤, 예시 1의 코드를src/server.ts로 저장해 가장 자주 사용하는 사내 API 엔드포인트 하나를 Tool로 등록해 볼 수 있습니다. - MCP Inspector로 동작 검증 —
npx @modelcontextprotocol/inspector npx tsx src/server.ts를 실행해 브라우저에서 직접 Tool을 호출해 보고, Claude Code 없이도 서버가 올바르게 응답하는지 확인해 볼 수 있습니다. .mcp.json작성 후 팀과 공유 — 프로젝트 루트에.mcp.json을 생성하고 저장소에 커밋하면 팀원 모두가 같은 MCP 서버를 Claude Code에서 바로 사용할 수 있습니다. 시크릿은 반드시${ENV_VAR}형태로 분리하는 것을 권장합니다.
다음 글: Streamable HTTP 트랜스포트로 멀티유저 환경에 MCP 서버를 배포하고, OAuth 2.1로 엔터프라이즈 인증을 연동하는 방법을 다룹니다. 공개 시점이 정해지면 이 글에 링크를 추가할 예정입니다.
참고 자료
- modelcontextprotocol/typescript-sdk | GitHub
- MCP TypeScript SDK 공식 문서 | ts.sdk.modelcontextprotocol.io
- Connect Claude Code to tools via MCP | Claude Code 공식 문서
- Build MCP Server with TypeScript: Complete Tutorial (2026) | MCPize
- How to Build a Custom MCP Server with TypeScript | freeCodeCamp
- Getting Started with Claude Desktop and custom MCP servers | WorkOS Blog
- Building your first MCP server | GitHub Blog
- Elastic MCP server: How to create an MCP server with TypeScript | Elastic
- Internal API MCP Server | Enterprise MCP Documentation
- State of MCP Server Security 2025 | Astrix Security
- MCP Server Best Practices for 2026 | CData
- Example Servers | Model Context Protocol 공식 사이트