
AI 에이전트에게 사내 도구를 '안전하게' 전달하기 ― 30줄로 시작하는 MCP 서버 제작과 경계 설계 입문
요약
AI 에이전트에게 사내 도구를 안전하게 연결하기 위한 MCP(Model Context Protocol)의 설계 사상과 구현 방법을 다룹니다. MCP를 통해 도구 제공 측을 독립된 서버로 분리함으로써 보안 경계를 설정하고 재사용성을 높이는 방법을 설명합니다.
핵심 포인트
- MCP를 활용해 도구 제공과 에이전트 사용 간의 보안 경계 설계 가능
- N×M 연결 문제를 N+M 구조로 단순화하여 확장성 확보
- Tools, Resources, Prompts 세 가지 프리미티브를 통한 역할 구분
- TypeScript SDK를 이용한 최소한의 MCP 서버 구현 가이드
이 기사의 목표
"Claude나 Cursor에게 우리 회사의 사내 DB나 사내 API를 만지게 하고 싶다. 하지만 함수를 그대로 호출하게 하거나 API 키를 통째로 넘겨주는 것은 솔직히 무섭다."
저(아키라파파) 자신도 AI 에이전트에게 실무 도구를 연결할 때마다 이 "두려움"과 마주해 왔습니다. 결론부터 말씀드리면, 그 불안의 상당 부분은 MCP (Model Context Protocol) 서버를 "공급 측"으로서 직접 설계하는 것으로 상당히 해소할 수 있습니다.
이 기사에서는 다음 세 가지를 하나의 흐름으로 다룹니다.
설계 사상: 왜 함수의 직접 호출이 아니라 MCP라는 "경계"를 사이에 두는가 -
작동하는 최소 구현: TypeScript SDK로 30줄 정도의 MCP 서버 만들기 -
안전하게 전달하는 설계: 권한, 입력 검증, 감사 로그라는 세 가지 경계를 긋는 법
왜 "함수를 직접 전달"하는 것이 아니라 MCP인가
AI 에이전트에게 도구를 전달하는 방법으로서 가장 간편한 것은 "자작 함수를 그대로 도구 정의에 올리는" 방식입니다. 작은 검증이라면 이것으로 충분합니다. 하지만 실무에 도입하면 세 가지 벽에 부딪힙니다.
N×M 문제: 에이전트(Claude / Cursor / 자작 앱)가 M개, 사내 도구가 N개 있다면, 단순하게는 N×M개의 연결 코드를 작성해야 하는 상황에 놓입니다. -
경계가 사라지는 문제: 함수를 직접 전달하면 "AI가 호출할 수 있는 조작"과 "앱 내부의 구현"이 하나로 이어져, 권한이나 로그를 삽입할 지점이 없어집니다. -
재사용할 수 없는 문제: 특정 에이전트용으로 작성한 도구 연결을 다른 에이전트에서 재사용할 수 없습니다.
MCP는 도구 제공 측을 "서버"라는 독립된 프로세스로 분리합니다. 이를 통해 하나의 MCP 서버를 작성하면, MCP를 지원하는 어떤 호스트(Claude Desktop, Cursor, VS Code 등)에서도 동일한 도구를 사용할 수 있게 됩니다. N×M이 N+M으로 압축된다는 점이 가장 큰 설계상의 이점입니다.
MCP의 구조를 최단 시간에 이해하기
MCP의 등장인물은 세 가지뿐입니다.
Host: LLM을 구동하는 앱 본체(IDE, 채팅 UI 등) -
Client: Host 내부에 있으며, 서버 하나당 하나의 연결 역할을 수행하는 존재 -
Server: 실제로 능력(도구 등)을 공개하는 프로세스. 로컬이라면 stdio, 원격이라면 Streamable HTTP와 같은 트랜스포트(Transport)로 연결합니다
그리고 서버가 공개할 수 있는 "프리미티브 (Primitive)"는 세 종류입니다. 이 용도 구분(使い分け)이 설계의 핵심입니다.
| 프리미티브 | 역할 | 누가 주도하여 사용하는가 |
|---|---|---|
| Tools | LLM이 호출하는 함수 (API 실행 · DB 조회 · 계산 등) | 모델 (AI)이 주도 |
| Resources | 문맥(Context)으로 읽히게 할 데이터 (파일 내용 · 레코드 등) | 앱 / 사용자가 주도 |
| Prompts | 인수가 포함된 재사용 템플릿 (정형 워크플로우) | 사용자가 주도 |
30줄로 작동하는 최소 MCP 서버 만들기
먼저 직접 움직여 봅시다. 공식 TypeScript SDK와 입력 검증을 위한 zod를 설치합니다.
npm install @modelcontextprotocol/sdk zod
다음은 stdio 트랜스포트로 작동하는 최소한의 MCP 서버입니다. 여기서는 "사내 재고 수를 반환하는" 더미 도구를 하나만 공개하고 있습니다.
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
...
Claude Desktop 등의 호스트 설정에 이 서버를 등록하면, AI 측에서 get_stock을 볼 수 있게 됩니다. 동작 확인은 다음과 같은 자연어 프롬프트로 수행할 수 있습니다.
프롬프트 예시 1 (정상계):
"상품 코드 A-001의 재고를 확인하고, 발주가 필요한지 알려줘"
프롬프트 예시 2 (이상계 확인):
"존재하지 않는 코드 Z-999의 재고를 조회하면 어떻게 동작해? 그대로 시도해봐"
프롬프트 예시 3 (복수 도구 전제 설계 확인):
「get_stock만 사용해서 재고가 0인 상품만 목록으로 만들어줘. DB를 직접 수정하는 작업은 절대로 하지 마」
이처럼 "AI에게 직접 도구를 실행하게 하여 동작을 관찰하는 것"까지가 MCP 서버 개발의 최소 루프입니다.
Tool · Resource · Prompt의 구분 설계
최소 구현이 완료되었다면, 다음은 "무엇을 Tool로 하고, 무엇을 Resource / Prompt로 할 것인가"의 경계 설정입니다. 제가 항상 사용하는 판단 기준은 심플합니다.
- 부작용 (Side Effect, 쓰기·외부 실행)이 있는가? → 있다면 Tool, 그것도 최소 권한으로
- 단순히 읽게 하고 싶은 컨텍스트인가? → Resource
- 매번 같은 지시를 반복하고 있는가? → Prompt 템플릿화
Resource는 URI 템플릿을 통해 동적으로 공개할 수 있습니다.
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
server.registerResource(
"product",
...
"재고를 조회한다"는 부작용은 없지만, 내부적으로 외부 DB를 호출하기 때문에 저는 Tool로 설정합니다. 반면, "상품 마스터의 설명문을 단순히 읽게 하는 것"이라면 Resource가 적절합니다. 이 경계 설정을 소홀히 하면, AI가 "읽기만 해도 되는 것"까지 능동적으로 호출하게 되어, 불필요한 API 호출과 로그 오염이 증가합니다.
"안전하게 전달하기" 위한 3가지 경계 설계
이 부분이 핵심입니다. MCP 서버는 "AI에게 실무 도구를 전달하는 마지막 검문소"이므로, 다음 3가지 경계를 반드시 직접 설정해야 합니다.
1. 권한의 경계 (최소 권한 래퍼 (Wrapper))
도구의 실제 처리는 반드시 "허가된 작업인가"를 확인하는 얇은 래퍼를 통과해야 합니다.
type Action = "read" | "write";
function guard(action: Action, allowed: Action[]) {
if (!allowed.includes(action)) {
...
"위험한 작업은 서버 기동 시 플래그로 통째로 무효화할 수 있는" 상태로 만들어 두면, 검증 환경에서는 읽기 전용, 운영 환경에서만 쓰기 허용과 같은 운용을 안전하게 수행할 수 있습니다.
2. 입력의 경계 (스키마 검증 (Schema Validation))
inputSchema에 zod를 사용하는 것은 단지 "타입을 위해서"만이 아닙니다. AI는 아무렇지 않게 예상치 못한 값을 전달하기 때문에, 경계에서 이를 차단하는 것이 목적입니다.
inputSchema: {
sku: z.string().regex(/^[A-Z]-\d{3}$/, "형식은 X-000"),
limit: z.number().int().min(1).max(100).default(20),
...
3. 감사의 경계 (누가 무엇을 호출했는지 기록)
function audit(tool: string, args: unknown) {
// 개인정보·비밀정보는 로그에 남기지 않는다. SKU 등 민감하지 않은 식별자만 기록한다.
console.error(JSON.stringify({ ts: new Date().toISOString(), tool, args }));
...
함정과 운영 체크리스트
마지막으로, 실제로 막히기 쉬운 포인트들을 정리해 두겠습니다.
자주 빠지는 함정 (클릭하여 확장)
- stdout을 오염시킴:
console.log를 통한 디버그 출력이 JSON-RPC를 망가뜨림 → 로그는console.error로 - Tools에 너무 치중함: 읽기 전용 데이터까지 Tool화 → Resource로 분리
- 권한 플래그가 없음: 파괴적인 작업을 상시 활성화 → 기동 시 플래그로 무효화할 수 있는 설계로
- 입력을 신뢰함: AI가 전달하는 인자를 검증하지 않고 실행 → zod로 경계 검증
- 사양 버전 혼동: MCP 사양은 날짜로 버저닝됨 (예: 2025-06-18 등) → 의존하는 기능은 버전을 명시
운영에 투입하기 전의 체크리스트는 이 5가지입니다.
- 파괴적인 도구는 기동 플래그로 무효화할 수 있는가
- 모든 도구 입력에 스키마 검증이 있는가
- 감사 로그에 민감 정보가 유출되지 않았는가
- Tool / Resource / Prompt의 역할 분담을 설명할 수 있는가
- 의존하는 MCP 사양의 버전을 고정하거나 명시했는가
마치며
MCP 서버를 직접 제작하는 가치는 단순히 "편리하기 때문"이 아닙니다. AI에게 실무 도구를 전달할 때의 "경계선"을 자신의 언어로 그을 수 있게 되는 것입니다. 함수를 직접 전달하던 시절의 막연한 불안감이 「권한(Permission)・입력(Input)・감사(Audit)」라는 세 가지 구체적인 설계 판단으로 변하는 것, 이것이 가장 큰 리턴(Return)이라고 느낍니다.
우선 오늘 만든 30줄의 서버를 구동하여 AI가 호출하게 해보세요. "AI가 예상치 못한 값을 전달했을" 때, 경계 설계(Boundary Design)의 필요성을 피부로 느끼게 될 것입니다.
📌 관련 과거 기사: 「AI를 한 대만 사용하는 것을 그만두기 ― 멀티 에이전트 오케스트레이션 (Multi-agent Orchestration) 입문」도 함께 읽어보세요 (도구를 전달하는 설계와 에이전트를 늘리는 설계는 세트로 작용합니다).
🐦 AI 활용과 구현에 대한 학습 내용은 X에서 매일 발신 중입니다. 괜찮으시다면 구경하러 와주세요.
참고 (1차 정보)
- MCP 공식 사양: https://modelcontextprotocol.io/specification/2025-06-18
- 공식 TypeScript SDK: https://github.com/modelcontextprotocol/typescript-sdk
- Anthropic 발표: https://www.anthropic.com/news/model-context-protocol
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기