MCP로 사내 시스템을 Claude Code에 연결하는 방법을 다룬다. 범용 fetch부터 전용 Server 구축까지, 배포 플랫폼·모니터링·티켓 시스템 등의 연결 패턴과 도구 설계 원칙을 소개한다.
지난 글에서는 데이터베이스 연결을 다뤘다. 데이터베이스는 표준 프로토콜이 있어서 연결이 비교적 간단하다. 하지만 대부분의 팀은 일상 업무에서 다양한 사내 시스템에 의존한다: 배포 플랫폼, 모니터링 대시보드, 티켓 시스템, 내부 API, 설정 관리 등.
이런 시스템에는 보통 기성 MCP Server가 없지만, 거의 대부분 HTTP API를 제공한다. 이 글에서는 MCP를 사용해 이런 내부 도구를 Claude Code에 연결하여, 모니터링 확인, 배포 상태 조회, 티켓 조작을 직접 수행하는 방법을 설명한다.
내부 도구 연결에는 두 가지 방법이 있다:
방법 1: 범용 HTTP MCP Server 사용
커뮤니티에는 범용 MCP Server가 몇 가지 있어서, 임의의 REST API를 MCP 도구로 래핑할 수 있다. API 정의 파일을 작성하면 Claude가 호출할 수 있는 도구로 변환해준다. API 구조가 단순하고 복잡한 로직이 필요 없는 경우에 적합하다.
방법 2: 전용 MCP Server 직접 작성
TypeScript 또는 Python의 MCP SDK를 사용해 전용 Server를 작성하여 도구 정의, 파라미터 검증, 에러 처리를 완전히 제어한다. 여러 API를 조합하거나, 데이터 변환, 비즈니스 로직을 추가해야 하는 경우에 적합하다.
이 글에서는 두 방법을 모두 다룬다. 간단한 쪽부터 시작하자.
가장 가벼운 방법은 공식 @anthropic-ai/mcp-server-fetch를 사용하는 것이다. Claude가 직접 HTTP 요청을 보낼 수 있게 해준다. 설정은 매우 간단하다:
{
"mcpServers": {
"fetch": {
"command": "npx",
"args": ["-y", "@anthropic-ai/mcp-server-fetch"]
}
}
}
설정이 끝나면 Claude에서 바로 내부 API를 호출할 수 있다:
배포 플랫폼 https://deploy.internal.com/api/v1/services/user-service 의 현재 상태를 확인해줘
Claude가 GET 요청을 보내고, 응답을 받아서 파싱해준다.
하지만 이 방식에는 분명한 한계가 있다:
임시 용도로는 괜찮지만, 장기적인 솔루션으로는 적합하지 않다.
특정 사내 시스템을 반복적으로 사용해야 한다면, 전용 MCP Server를 작성하는 것이 더 나은 선택이다. 아래에서 실제 시나리오로 시연한다: 사내 배포 플랫폼 연결.
배포 플랫폼이 다음 API를 제공한다고 가정하자:
GET /api/v1/services — 모든 서비스 목록 조회GET /api/v1/services/:name/status — 서비스 상태 확인POST /api/v1/services/:name/deploy — 배포 트리거GET /api/v1/services/:name/logs — 최근 배포 로그 조회먼저 프로젝트를 초기화한다:
mkdir mcp-deploy && cd mcp-deploy
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init
핵심 코드 src/index.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const API_BASE = process.env.DEPLOY_API_URL!;
const API_TOKEN = process.env.DEPLOY_API_TOKEN!;
async function api(path: string, method = "GET", body?: unknown) {
const res = await fetch(`${API_BASE}${path}`, {
method,
headers: {
Authorization: `Bearer ${API_TOKEN}`,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
throw new Error(`API error: ${res.status} ${await res.text()}`);
}
return res.json();
}
const server = new McpServer({
name: "deploy-platform",
version: "1.0.0",
});
// 모든 서비스 목록 조회
server.tool("list_services", "배포 플랫폼의 모든 서비스와 상태를 목록으로 표시한다", {}, async () => {
const data = await api("/api/v1/services");
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
});
// 개별 서비스 상태 확인
server.tool(
"service_status",
"지정된 서비스의 현재 배포 상태, 버전, 헬스체크 결과를 확인한다",
{ name: z.string().describe("서비스 이름 (예: user-service)") },
async ({ name }) => {
const data = await api(`/api/v1/services/${name}/status`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
// 배포 로그 확인
server.tool(
"deploy_logs",
"지정된 서비스의 최근 배포 로그를 조회한다",
{
name: z.string().describe("서비스 이름"),
limit: z.number().optional().default(10).describe("반환할 건수 (기본값: 10)"),
},
async ({ name, limit }) => {
const data = await api(`/api/v1/services/${name}/logs?limit=${limit}`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
// 배포 트리거
server.tool(
"trigger_deploy",
"지정된 서비스의 배포를 트리거한다. 이것은 쓰기 작업이며 프로덕션 환경에 영향을 미친다",
{
name: z.string().describe("서비스 이름"),
version: z.string().describe("배포할 버전 번호 또는 git ref"),
},
async ({ name, version }) => {
const data = await api(`/api/v1/services/${name}/deploy`, "POST", {
version,
});
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
const transport = new StdioServerTransport();
server.connect(transport);
컴파일 후 실행:
npx tsc
{
"mcpServers": {
"deploy": {
"command": "node",
"args": ["/path/to/mcp-deploy/dist/index.js"],
"env": {
"DEPLOY_API_URL": "https://deploy.internal.com",
"DEPLOY_API_TOKEN": "your-api-token"
}
}
}
}
token은 .claude/settings.local.json(git에 커밋하지 않음)에, URL은 .claude/settings.json(git에 커밋하여 팀과 공유)에 배치한다.
설정이 완료되면 대화가 자연스러워진다:
user-service 지금 상태가 어때?
→ Claude가 service_status("user-service")를 호출
→ 결과: 실행 중, 버전 v2.3.1, 마지막 배포 2시간 전, 헬스체크 전부 통과
최근 배포 중에 실패한 게 있어?
→ Claude가 deploy_logs("user-service", 20)를 호출
→ 로그를 분석하여 3번째 배포가 롤백되었고, 원인은 헬스체크 타임아웃이라고 보고
user-service를 v2.3.2로 배포해줘
→ Claude가 trigger_deploy("user-service", "v2.3.2")를 호출
→ 도구 설명에 "쓰기 작업"이라고 표시되어 있으므로 Claude가 먼저 확인을 요청
이것은 신중하게 고려해야 할 문제다.
읽기 작업은 안심하고 연결해도 된다. 상태 확인, 로그 조회, 티켓 검색 — 이런 작업은 부수 효과가 없으므로, Claude가 실수해도 손해가 없다.
쓰기 작업은 두 가지 경우로 나뉜다:
저위험 쓰기 작업은 연결해도 되지만, 도구 설명에 명확히 표시해야 한다. Claude는 부수 효과가 명시된 작업에 대해 자동으로 사용자 확인을 요청한다. 예를 들어 티켓 생성, 메시지 발송, 설정 업데이트 등이다.
고위험 쓰기 작업은 연결하지 않는 것을 권장한다. 리소스 삭제, 롤백 트리거, 권한 변경 — 이런 작업은 결과가 심각하고 되돌릴 수 없으므로, 수동 조작으로 남겨두는 것이 더 안전하다.
반드시 쓰기 작업을 연결해야 한다면, 최소한 두 가지를 해야 한다:
| 시스템 | 노출하는 도구 | 주의사항 |
|---|---|---|
| 배포 플랫폼 (K8s / Kamal) | 서비스 상태 조회, 로그 조회, 배포 트리거 | 쓰기 작업에 확인 추가 |
| 모니터링 시스템 (Grafana / Datadog) | 메트릭 조회, 알림 이력 확인 | 쿼리 시간 범위를 제한하여 과도한 데이터 조회 방지 |
| 티켓 시스템 (Jira / Linear) | 티켓 검색, 생성, 상태 업데이트 | 티켓 생성은 쓰기 작업이지만 저위험 |
| 사내 문서 (Notion / Confluence) | 문서 검색, 페이지 내용 읽기 | 페이지네이션에 유의, 한 번에 너무 많이 가져오지 않기 |
| 설정 관리 (Consul / etcd) | 설정 읽기, 환경 간 차이 비교 | 읽기만 하고 쓰기 작업은 연결하지 않기 |
| CI/CD (GitHub Actions / Jenkins) | 빌드 상태 확인, 빌드 트리거 | 빌드 트리거는 중위험 쓰기 작업 |
MCP 도구 설계는 API 설계와 다르다. API는 엔지니어가 사용하지만, 도구는 AI가 사용한다. 몇 가지 원칙을 기억해두자:
도구 이름은 직관적으로
✗ get_svc_stat — Claude가 약어의 의미를 정확히 추측하지 못할 수 있다
✓ service_status — 보자마자 무엇을 하는지 알 수 있다
설명은 AI를 위해 작성한다
도구 설명은 사람을 위한 문서가 아니라, Claude가 "언제 이 도구를 호출해야 하는가"를 판단하기 위한 근거다. 설명에는 이 도구가 무엇을 하는지, 무엇을 반환하는지, 언제 사용해야 하는지를 명확히 써야 한다.
✗ "서비스 상태를 가져온다"
✓ "지정된 서비스의 현재 배포 상태, 버전, 헬스체크 결과를 확인한다. 사용자가 특정 서비스가 정상 실행 중인지 물어볼 때 사용한다"
파라미터는 zod로 명확하게 정의한다
.describe()가 붙은 파라미터만 Claude에게 무엇을 입력해야 하는지 알려줄 수 있다. 설명이 없는 파라미터는 이름으로 추측할 수밖에 없다.
구조화된 데이터를 반환한다
MCP 도구가 반환하는 것은 텍스트이지만, 가급적 포맷된 JSON을 반환하자. Claude는 구조화된 데이터를 일반 텍스트보다 훨씬 정확하게 처리한다.
적절한 단위로 나눈다
복잡한 플로우를 하나의 도구에 몰아넣지 말자. 반대로, 단순한 쿼리를 세 개의 도구로 쪼개는 것도 좋지 않다. 원칙은 하나의 도구가 하나의 독립적이고 의미 있는 작업을 완수하는 것이다.
MCP Server 코드는 몇 가지 위치에 배치할 수 있다:
프로젝트 저장소 내 (권장하는 시작 방법)
your-project/
├── .claude/settings.json
├── mcp-servers/
│ └── deploy/
│ ├── src/index.ts
│ ├── package.json
│ └── tsconfig.json
└── ...
코드와 설정이 한곳에 모여 있어, 팀원이 clone 후 의존성을 설치하면 바로 사용할 수 있다.
독립 저장소
MCP Server를 여러 프로젝트에서 사용할 경우, 독립 저장소에 배치하고 npm 패키지나 Docker 이미지로 배포한다.
{
"mcpServers": {
"deploy": {
"command": "npx",
"args": ["-y", "@yourcompany/mcp-deploy-server"]
}
}
}
전역 설치
회사 전체에서 공통으로 사용하는 MCP Server(예: 통합 인증, 통합 로깅 플랫폼 연결)는 전역으로 설치하고 ~/.claude/settings.json에 설정한다.
MCP Server 개발 시 가장 흔한 문제는 "Claude가 내 도구를 호출하지 않는다"거나 "호출했는데 에러가 난다"이다.
Server가 기동되었는지 확인한다
Claude Code를 재시작한 후 /mcp를 입력하여 연결된 MCP Server 목록을 확인한다. 자신의 Server가 목록에 없다면 command와 args가 올바른지 점검한다.
Server를 단독으로 테스트한다
MCP Server는 stdio로 통신하므로, 터미널에서 직접 테스트할 수 있다:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/index.js
도구 목록이 반환되면 Server 자체는 정상이다.
Claude의 도구 호출 로그를 확인한다
Claude Code는 매번 도구 호출의 입력과 출력을 표시한다. 파라미터가 잘못되었다면, 대부분 도구 설명이나 파라미터 정의가 충분하지 않아 Claude가 오해한 것이 원인이다.
실제 사례로 전체를 정리해보자. Sentry를 Claude Code에 연결하여 프로덕션 에러를 직접 확인할 수 있도록 한다.
server.tool(
"search_errors",
"在 Sentry 中搜索最近的錯誤。用於排查線上問題、查看錯誤趨勢",
{
query: z.string().describe("搜索關鍵詞,如錯誤訊息、函式名"),
hours: z.number().optional().default(24).describe("查看最近多少小時的錯誤"),
},
async ({ query, hours }) => {
const since = new Date(Date.now() - hours * 3600000).toISOString();
const data = await api(
`/api/0/projects/${ORG}/${PROJECT}/issues/?query=${encodeURIComponent(query)}&start=${since}&sort=date`
);
const summary = data.map((issue: any) => ({
title: issue.title,
count: issue.count,
firstSeen: issue.firstSeen,
lastSeen: issue.lastSeen,
link: issue.permalink,
}));
return {
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
};
}
);
server.tool(
"error_details",
"查看 Sentry 中某個錯誤的詳細資訊,包括堆疊和最近一次事件",
{ issueId: z.string().describe("Sentry issue ID") },
async ({ issueId }) => {
const [issue, latest] = await Promise.all([
api(`/api/0/issues/${issueId}/`),
api(`/api/0/issues/${issueId}/events/latest/`),
]);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
title: issue.title,
count: issue.count,
users: issue.userCount,
stacktrace: latest.entries?.find(
(e: any) => e.type === "exception"
),
},
null,
2
),
},
],
};
}
);
연결이 완료되면 프로덕션 문제 조사가 이런 대화로 바뀐다:
최근 4시간 동안 새로운 500 에러가 있어?
→ Claude가 Sentry를 검색
→ 3개의 새로운 issue를 발견, 가장 심각한 것은 120명의 사용자에게 영향
→ 자동으로 스택 트레이스를 가져와서 특정 Null Pointer Exception이 원인임을 파악
→ 코드에서 해당 위치를 찾아 수정 방안을 제시
문제 발견부터 코드 위치 파악까지 전체 과정이 하나의 대화에서 완료된다.
이 글에서는 MCP를 사용해 내부 도구를 연결하는 방법을 다뤘다. 핵심 아이디어는 이렇다: 사내 시스템에 HTTP API가 있다 → MCP Server로 래핑한다 → Claude가 바로 사용할 수 있게 된다.
이 글의 모든 예제는 기존 API를 래핑하는 것이었다. 배포 플랫폼이든 Sentry든 원래 인터페이스가 있고, MCP Server는 전달과 변환 레이어에 불과하다. 다음 글에서는 다른 시나리오를 다룬다: 필요한 기능에 기성 API가 없을 때, MCP Server를 처음부터 구축하여 직접 로직을 구현하고, 상태를 관리하고, 복잡한 다단계 인터랙션을 처리하는 방법이다.