저는 사내 데이터 인프라와 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 / MTok15.00 USD / MTok19.00 ~ 30.00 USD / MTok
GPT-4.1 출력 단가8.00 USD / MTok8.00 USD / MTok10.00 ~ 13.00 USD / MTok
Gemini 2.5 Flash 출력 단가2.50 USD / MTok2.50 USD / MTok3.50 ~ 5.00 USD / MTok
평균 지연 (서울 → 도쿄 POP)280 ms320 ms560 ~ 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 서버를 만들면 다음 모든 클라이언트에서 그대로 동작합니다.

MCP 서버가 노출하는 세 가지 핵심 프리미티브는 다음과 같습니다.

3. 시스템 아키텍처 한눈에 보기

구간역할프로토콜
Claude Desktop (클라이언트 GUI)사용자 질의 → MCP 요청 직렬화stdio or HTTP+SSE
MCP Server (본문 구현 대상)JSON-RPC 디스패치, 권한 검사, 쿼리 실행JSON-RPC 2.0
Adapter LayerPostgreSQL Pool / AWS SDK v3 래퍼TCP 5432 / HTTPS 443
LLM 호출도구 결과 메시지를 모델에 합쳐 추론Anthropic 호환 REST (HolySheep)

4. 사전 준비물

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 측은 ListObjectsV2GetObject만 노출합니다. 큰 객체는 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 AI를 사용해 보세요

직접 AI API 게이트웨이. Claude, GPT-5, Gemini, DeepSeek 지원. VPN 불필요.

👉 무료 가입 →

지표HolySheep (gpt-4.1)공식 OpenAI (gpt-4.1)타 중계 (gpt-4.1)
평균 첫 토큰 지연 (TTFT)612 ms748 ms1,430 ms