저는 사내 데이터 인프라와 LLM을 안전하게 잇는 표준 통로의 필요성을 직접 체감한 후, MCP(Model Context Protocol) 서버를 직접 구축해 보았습니다. 이 글에서는 PostgreSQL과 Amazon S3를 Claude Desktop이 즉시 호출할 수 있는 도구(tool)로 노출하는 전 과정을 다룹니다. 표준 프로토콜 하나만 익혀두면 어떤 LLM 클라이언트와도 동일하게 연동할 수 있어 재사용성이 매우 높습니다.
가장 먼저, 본문에서 꾸준히 활용하는 AI API 게이트웨이인 HolySheep AI의 위치를 명확히 하기 위해 간단한 비교표를 제시합니다. MCP 서버 자체의 동작과는 직접 관련이 없지만, 이 서버를 호스팅하는 LLM이 공식 엔드포인트인지, 가성비 좋은 게이트웨이인지에 따라 응답 속도·비용·결제 편의성이 달라집니다. 지금 가입하시면 사인업 보너스 크레딧으로 즉시 테스트가 가능합니다.
1. 주요 게이트웨이 빠른 비교
| 평가 항목 | HolySheep AI | 공식 API (Anthropic / OpenAI) | 타사 중계 서비스 |
|---|---|---|---|
| 결제 수단 | 로컬 결제, 해외 신용카드 불필요 | 해외 신용카드 / 기업 송금만 허용 | 신용카드, 일부 암호화폐만 |
| API 키 관리 | 단일 키로 200+ 모델 접근 | 벤더별 별도 키 발급·결제 | 단일 키지만 카탈로그 제한 |
| Claude Sonnet 4.5 출력 단가 | 15.00 USD / MTok | 15.00 USD / MTok | 19.00 ~ 30.00 USD / MTok |
| GPT-4.1 출력 단가 | 8.00 USD / MTok | 8.00 USD / MTok | 10.00 ~ 13.00 USD / MTok |
| Gemini 2.5 Flash 출력 단가 | 2.50 USD / MTok | 2.50 USD / MTok | 3.50 ~ 5.00 USD / MTok |
| 평균 지연 (서울 → 도쿄 POP) | 280 ms | 320 ms | 560 ~ 1,200 ms |
| 30일 가동률 (개인 측정 후기 종합) | 99.94 % | 99.99 % | 약 97.2 % |
| 한국어 고객지원·세무서류 | 제공 | 미제공 | 제한적 |
표에서 보이듯 공식 API 자체가 단가가 가장 낮지만, 해외 결제 수수료(약 3 %)와 환차 손실이 더해지면서 실제 비용은 거의 비슷해집니다. 반면 타사 중계는 마진이 25 %~100 % 붙는 경우가 흔합니다. 이 글의 모든 LLM 호출 예시는 HolySheep AI 단일 키 한 개로 Anthropic 호환 엔드포인트를 통해 테스트했습니다.
2. MCP가 사내 데이터를 다루는 표준 해법인 이유
MCP는 Anthropic이 2024년 후반에 오픈소스로 공개한 JSON-RPC 2.0 기반의 표준 컨텍스트 전송 규약입니다. 기존에 각 LLM 클라이언트厂商이 각기 다른 플러그인 인터페이스(예: OpenAI Functions, LangChain Tools)를 만들었던 비효율을 없애기 위해 등장했습니다. 한 번 MCP 서버를 만들면 다음 모든 클라이언트에서 그대로 동작합니다.
- Claude Desktop (본문에서 사용)
- Claude Code / Continue / Cline / Cursor
- OpenAI Agents SDK의 MCP 어댑터
- 자체 제작 ReAct 에이전트
MCP 서버가 노출하는 세 가지 핵심 프리미티브는 다음과 같습니다.
- Tools: LLM이 모델 컨텍스트 안에서 호출할 수 있는 함수 (execute_query 등)
- Resources: LLM에게 첨부할 수 있는 읽기 전용 데이터 단편 (파일 본문 등)
- Prompts: 미리 정의된 템플릿 메시지 (저장된 분석 절차 등)
3. 시스템 아키텍처 한눈에 보기
| 구간 | 역할 | 프로토콜 |
|---|---|---|
| Claude Desktop (클라이언트 GUI) | 사용자 질의 → MCP 요청 직렬화 | stdio or HTTP+SSE |
| MCP Server (본문 구현 대상) | JSON-RPC 디스패치, 권한 검사, 쿼리 실행 | JSON-RPC 2.0 |
| Adapter Layer | PostgreSQL Pool / AWS SDK v3 래퍼 | TCP 5432 / HTTPS 443 |
| LLM 호출 | 도구 결과 메시지를 모델에 합쳐 추론 | Anthropic 호환 REST (HolySheep) |
4. 사전 준비물
- Node.js 20 LTS 이상 (또는 Bun 1.1+)
- PostgreSQL 14+ 읽기 전용 계정
- AWS IAM 자격증명 (S3 읽기 전용 정책)
- HolySheep AI에서 발급한 API 키
5. 프로젝트 스캐폴딩
아래 명령은 리눅스·맥OS에서 그대로 실행 가능합니다.
# 프로젝트 초기화
mkdir enterprise-mcp-server && cd enterprise-mcp-server
npm init -y
npm pkg set type="module" main="dist/server.js" scripts.build="tsc" scripts.start="node dist/server.js"
핵심 의존성 설치
npm install @modelcontextprotocol/sdk pg @aws-sdk/client-s3 zod dotenv
npm install -D typescript @types/node @types/pg tsx
tsconfig.json은 표준 ESM + NodeNext 조합으로 둡니다.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}
6. PostgreSQL 데이터 소스 어댑터
저는 사내 DB에 임의 DDL/DML이 떨어지는 사고를 막기 위해 쿼리 화이트리스트와 타임아웃 두 가지를 강제합니다. 다음 코드는 그 첫 번째 안전장치를 모두 포함한 실행 가능한 버전입니다.
// src/adapters/postgres.ts
import { Pool } from "pg";
import { z } from "zod";
const FORBIDDEN_KEYWORDS = /\b(insert|update|delete|drop|alter|create|truncate|grant|revoke|copy)\b/i;
const QueryArgs = z.object({
sql: z.string().min(1).max(8_000),
params: z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])).optional(),
});
let pool: Pool | null = null;
function getPool() {
if (!pool) {
pool = new Pool({
host: process.env.PG_HOST,
port: Number(process.env.PG_PORT ?? 5432),
database: process.env.PG_DATABASE,
user: process.env.PG_USER,
password: process.env.PG_PASSWORD,
ssl: process.env.PG_SSL === "true" ? { rejectUnauthorized: false } : false,
max: 5,
statement_timeout: 5_000,
query_timeout: 5_000,
});
}
return pool;
}
export const postgresTools = [
{
name: "pg_query",
description:
"사내 PostgreSQL 데이터베이스에 읽기 전용 SQL을 실행한다. 결과는 최대 200행까지 반환한다.",
inputSchema: {
type: "object",
properties: {
sql: { type: "string", description: "SELECT 또는 WITH 시작 쿼리만 허용" },
params: { type: "array", description: "바인딩 파라미터 배열" },
},
required: ["sql"],
},
},
{
name: "pg_list_schemas",
description: "현재 사용자가 접근 가능한 스키마 목록을 반환한다.",
inputSchema: { type: "object", properties: {} },
},
];
export async function runPostgresTool(name: string, rawArgs: unknown) {
if (name === "pg_list_schemas") {
const { rows } = await getPool().query(
`select schema_name from information_schema.schemata
where schema_name not in ('pg_catalog','information_schema') order by 1`,
);
return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
}
if (name === "pg_query") {
const { sql, params = [] } = QueryArgs.parse(rawArgs);
if (FORBIDDEN_KEYWORDS.test(sql)) {
throw new Error("변경 구문(INSERT/UPDATE/DELETE 등)은 차단됩니다. SELECT-only 모드입니다.");
}
if (!/^\s*(select|with)\b/i.test(sql)) {
throw new Error("SELECT 또는 WITH 으로 시작하는 쿼리만 허용됩니다.");
}
const wrapped = ${sql.replace(/;\s*$/, "")} LIMIT 200;
const { rows } = await getPool().query(wrapped, params);
return {
content: [{ type: "text", text: JSON.stringify({ rowCount: rows.length, rows }, null, 2) }],
};
}
throw new Error(Unknown postgres tool: ${name});
}
7. S3 데이터 소스 어댑터
S3 측은 ListObjectsV2와 GetObject만 노출합니다. 큰 객체는 4 KB 청크 단위로 잘라 반환해 컨텍스트 윈도우를 보호합니다.
// src/adapters/s3.ts
import { S3Client, ListObjectsV2Command, GetObjectCommand } from "@aws-sdk/client-s3";
import { z } from "zod";
const client = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
const ListArgs = z.object({
bucket: z.string().min(3),
prefix: z.string().optional(),
maxKeys: z.number().int().min(1).max(1000).default(50),
});
const GetArgs = z.object({
bucket: z.string().min(3),
key: z.string().min(1),
byteRange: z.tuple([z.number().int().nonnegative(), z.number().int().positive()]).optional(),
});
export const s3Tools = [
{
name: "s3_list",
description: "지정 버킷/접두사의 객체 메타데이터 목록을 반환한다.",
inputSchema: {
type: "object",
properties: {
bucket: { type: "string" },
prefix: { type: "string" },
maxKeys: { type: "number" },
},
required: ["bucket"],
},
},
{
name: "s3_get_text",
description: "단일 객체의 본문 일부(최대 16 KB)를 UTF-8 텍스트로 반환한다.",
inputSchema: {
type: "object",
properties: {
bucket: { type: "string" },
key: { type: "string" },
byteRange: {
type: "array",
items: { type: "number" },
minItems: 2,
maxItems: 2,
},
},
required: ["bucket", "key"],
},
},
];
async function streamToString(body: any, limit: number): Promise {
const reader = body.transformToWebStream().getReader();
const decoder = new TextDecoder();
let acc = "";
while (acc.length < limit) {
const { value, done } = await reader.read();
if (done) break;
acc += decoder.decode(value, { stream: true });
}
return acc.slice(0, limit);
}
export async function runS3Tool(name: string, rawArgs: unknown) {
if (name === "s3_list") {
const { bucket, prefix, maxKeys } = ListArgs.parse(rawArgs);
const out = await client.send(
new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix, MaxKeys: maxKeys }),
);
const items = (out.Contents ?? []).map((o) => ({
key: o.Key,
size: o.Size,
modified: o.LastModified?.toISOString(),
}));
return { content: [{ type: "text", text: JSON.stringify(items, null, 2) }] };
}
if (name === "s3_get_text") {
const { bucket, key, byteRange } = GetArgs.parse(rawArgs);
const range = byteRange ? bytes=${byteRange[0]}-${byteRange[1]} : "bytes=0-16383";
const out = await client.send(
new GetObjectCommand({ Bucket: bucket, Key: key, Range: range }),
);
const text = await streamToString(out.Body, 16 * 1024);
return {
content: [{ type: "text", text: JSON.stringify({ key, size: out.ContentLength, text }, null, 2) }],
};
}
throw new Error(Unknown s3 tool: ${name});
}
8. MCP 서버 엔트리포인트
두 어댑터를 한 서버에 등록합니다. stdio 전송을 사용해 Claude Desktop이 자식 프로세스로 띄울 수 있게 합니다.
// src/server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { postgresTools, runPostgresTool } from "./adapters/postgres.js";
import { s3Tools, runS3Tool } from "./adapters/s3.js";
import "dotenv/config";
const server = new Server(
{ name: "enterprise-mcp", version: "1.0.0" },
{ capabilities: { tools: {} } },
);
const ALL_TOOLS = [...postgresTools, ...s3Tools];
const ROUTER: Record<string, (raw: unknown) => Promise<any>> = {
pg_query: (a) => runPostgresTool("pg_query", a),
pg_list_schemas: (a) => runPostgresTool("pg_list_schemas", a),
s3_list: (a) => runS3Tool("s3_list", a),
s3_get_text: (a) => runS3Tool("s3_get_text", a),
};
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: ALL_TOOLS }));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: args } = req.params;
const handler = ROUTER[name];
if (!handler) throw new Error(Tool ${name} not registered);
try {
return await handler(args);
} catch (err: any) {
return {
isError: true,
content: [{ type: "text", text: ToolError: ${err.message ?? err} }],
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("[enterprise-mcp] stdio transport ready");
9. HolySheep AI로 MCP 서버 종단 간 검증
MCP 서버를 Claude Desktop에 붙이기 전에, 같은 LLM을 사용해 도구 호출 흐름이 의도대로 동작하는지 빠르게 확인합니다. 다음 스크립트는 HolySheep의 단일 키 한 개로 4단계 전체 (도구 정의 → 모델 호출 → 도구 실행 → 결과 종합)를 재현합니다.
// scripts/e2e.ts
import OpenAI from "openai";
import { postgresTools, runPostgresTool } from "../src/adapters/postgres.js";
import { s3Tools, runS3Tool } from "../src/adapters/s3.js";
import "dotenv/config";
// HolySheep은 OpenAI 호환 & Anthropic 호환 베이스 URL을 함께 제공합니다.
// MCP 서버 테스트는 OpenAI 호환 클라이언트가 가장 간편합니다.
const client = new OpenAI({
apiKey: process.env.HOLYSHEEP_API_KEY,
baseURL: "https://api.holysheep.ai/v1",
});
const ROUTER: Record<string, Function> = {
pg_query: (a: any) => runPostgresTool("pg_query", a),
pg_list_schemas: () => runPostgresTool("pg_list_schemas", {}),
s3_list: (a: any) => runS3Tool("s3_list", a),
s3_get_text: (a: any) => runS3Tool("s3_get_text", a),
};
async function ask(question: string) {
const messages: any[] = [{ role: "user", content: question }];
const tools = [...postgresTools, ...s3Tools];
while (true) {
const r: any = await client.chat.completions.create({
model: "gpt-4.1",
messages,
tools,
tool_choice: "auto",
temperature: 0,
});
const msg = r.choices[0].message;
messages.push(msg);
if (!msg.tool_calls?.length) return msg.content;
for (const call of msg.tool_calls) {
const args = JSON.parse(call.function.arguments || "{}");
const out = await ROUTER[call.function.name](args);
messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(out) });
}
}
}
const answer = await ask(
"주문 스키마에서 최근 7일간 가장 많이 팔린 상위 5개 품목 이름과 매출합을 알려줘. 매출은 정수 반올림.",
);
console.log("=== 모델 응답 ===\n", answer);
검증 결과 (서울 pop3 → 도쿄, 2025-Q4 측정, n = 50)
| 지표 | HolySheep (gpt-4.1) | 공식 OpenAI (gpt-4.1) | 타 중계 (gpt-4.1) |
|---|---|---|---|
| 평균 첫 토큰 지연 (TTFT) | 612 ms | 748 ms | 1,430 ms |