본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 8장: 기존 시스템과 MCP 연동하기
2026년 2월 4일·AI / ML·

8장: 기존 시스템과 MCP 연동하기

데이터베이스, REST API, 레거시 시스템을 MCP 서버로 래핑하여 AI 모델이 접근할 수 있도록 만드는 실전 패턴을 다룹니다.

18분1,115자8개 섹션
mcptypescriptpython
공유
mcp-guide8 / 10
12345678910
이전7장: MCP 클라이언트 구현하기다음9장: 보안, 인증, 권한 관리

연동의 핵심 원칙

기존 시스템을 MCP로 연동한다는 것은, 해당 시스템의 기능을 도구, 리소스, 프롬프트 형태로 노출하는 MCP 서버를 구축하는 것입니다. 이때 중요한 원칙이 있습니다.

최소 권한 원칙(Principle of Least Privilege)을 적용합니다. 시스템의 모든 기능을 노출하지 않고, AI가 사용해야 하는 최소한의 기능만 도구로 제공합니다.

읽기와 쓰기를 분리합니다. 읽기 전용 작업은 리소스로, 상태를 변경하는 작업은 도구로 구현합니다. 이를 통해 리소스 접근은 안전하게, 도구 사용은 신중하게 관리할 수 있습니다.

기존 인터페이스를 존중합니다. MCP 서버는 기존 시스템의 인터페이스(API, SDK, 드라이버) 위에 구축합니다. 기존 시스템의 코드를 수정할 필요가 없어야 합니다.

데이터베이스 연동

데이터베이스는 MCP 연동의 가장 일반적인 대상입니다. 스키마 정보를 리소스로, 쿼리 실행을 도구로 제공합니다.

PostgreSQL 연동 서버

postgres-server.ts
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import pg from "pg";
 
const pool = new pg.Pool({
  host: process.env.DB_HOST || "localhost",
  port: parseInt(process.env.DB_PORT || "5432"),
  database: process.env.DB_NAME || "mydb",
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
});
 
const server = new McpServer({
  name: "postgres-server",
  version: "1.0.0",
});
 
// 리소스: 데이터베이스 스키마 정보
server.resource(
  "db-schema",
  "postgres://schema",
  { description: "데이터베이스 전체 테이블 스키마" },
  async () => {
    const result = await pool.query(
      "SELECT table_name, column_name, data_type, is_nullable " +
      "FROM information_schema.columns " +
      "WHERE table_schema = 'public' " +
      "ORDER BY table_name, ordinal_position"
    );
 
    const schema: Record<string, Array<{
      column: string;
      type: string;
      nullable: string;
    }>> = {};
 
    for (const row of result.rows) {
      if (!schema[row.table_name]) {
        schema[row.table_name] = [];
      }
      schema[row.table_name].push({
        column: row.column_name,
        type: row.data_type,
        nullable: row.is_nullable,
      });
    }
 
    return {
      contents: [{
        uri: "postgres://schema",
        text: JSON.stringify(schema, null, 2),
        mimeType: "application/json",
      }],
    };
  }
);
 
// 리소스: 테이블 목록과 행 수
server.resource(
  "db-tables",
  "postgres://tables",
  { description: "테이블 목록과 각 테이블의 행 수" },
  async () => {
    const result = await pool.query(
      "SELECT schemaname, tablename, n_live_tup as row_count " +
      "FROM pg_stat_user_tables " +
      "ORDER BY n_live_tup DESC"
    );
 
    return {
      contents: [{
        uri: "postgres://tables",
        text: JSON.stringify(result.rows, null, 2),
        mimeType: "application/json",
      }],
    };
  }
);
 
// 도구: SELECT 쿼리 실행 (읽기 전용)
server.tool(
  "query",
  "데이터베이스에 SELECT 쿼리를 실행합니다. SELECT 문만 허용됩니다.",
  {
    sql: z.string().describe("실행할 SQL SELECT 쿼리"),
    limit: z.number().default(100).describe("최대 반환 행 수"),
  },
  async (params) => {
    // SELECT 문만 허용
    const normalized = params.sql.trim().toUpperCase();
    if (!normalized.startsWith("SELECT")) {
      return {
        content: [{ type: "text" as const, text: "SELECT 쿼리만 실행할 수 있습니다." }],
        isError: true,
      };
    }
 
    // LIMIT 강제 적용
    const limitedSql = params.sql.replace(/;?\s*$/, "") +
      " LIMIT " + params.limit;
 
    try {
      const result = await pool.query(limitedSql);
      return {
        content: [{
          type: "text" as const,
          text: JSON.stringify({
            rows: result.rows,
            rowCount: result.rowCount,
            fields: result.fields.map((f) => f.name),
          }, null, 2),
        }],
      };
    } catch (error) {
      return {
        content: [{
          type: "text" as const,
          text: "쿼리 실행 실패: " + (error as Error).message,
        }],
        isError: true,
      };
    }
  }
);
 
// 도구: 데이터 삽입/수정 (쓰기)
server.tool(
  "execute",
  "데이터베이스에 INSERT, UPDATE, DELETE 쿼리를 실행합니다. 주의: 데이터가 변경됩니다.",
  {
    sql: z.string().describe("실행할 SQL 문"),
    params: z.array(z.unknown()).default([]).describe("매개변수화된 쿼리의 값 배열"),
  },
  async (params) => {
    // DDL 차단
    const normalized = params.sql.trim().toUpperCase();
    const blocked = ["DROP", "TRUNCATE", "ALTER", "CREATE"];
    for (const keyword of blocked) {
      if (normalized.startsWith(keyword)) {
        return {
          content: [{
            type: "text" as const,
            text: keyword + " 문은 허용되지 않습니다.",
          }],
          isError: true,
        };
      }
    }
 
    try {
      const result = await pool.query(params.sql, params.params);
      return {
        content: [{
          type: "text" as const,
          text: "실행 완료. 영향받은 행: " + result.rowCount,
        }],
      };
    } catch (error) {
      return {
        content: [{
          type: "text" as const,
          text: "실행 실패: " + (error as Error).message,
        }],
        isError: true,
      };
    }
  }
);
 
const transport = new StdioServerTransport();
await server.connect(transport);
Warning

데이터베이스 MCP 서버에서 가장 중요한 것은 보안입니다. 위 예제에서는 SELECT 전용 도구와 쓰기 도구를 분리하고, DDL 문(DROP, ALTER 등)을 차단하며, LIMIT을 강제 적용하고 있습니다. 프로덕션 환경에서는 읽기 전용 데이터베이스 사용자를 사용하거나, 쿼리 화이트리스트를 적용하는 등 더 엄격한 제어가 필요합니다.

REST API 래핑

기존 REST API를 MCP 서버로 래핑하면, AI 모델이 해당 API의 기능을 자연어로 활용할 수 있습니다.

일반적인 REST API 래핑 패턴

api_wrapper_server.py
python
import json
import httpx
from fastmcp import FastMCP
 
mcp = FastMCP("api-wrapper")
 
BASE_URL = "https://api.example.com/v1"
API_KEY = None  # 환경 변수에서 로드
 
import os
API_KEY = os.environ.get("API_KEY", "")
 
 
def get_headers() -> dict:
    return {
        "Authorization": "Bearer " + API_KEY,
        "Content-Type": "application/json",
    }
 
 
@mcp.tool()
async def list_users(page: int = 1, per_page: int = 20) -> str:
    """사용자 목록을 조회합니다."""
    async with httpx.AsyncClient() as client:
        response = await client.get(
            BASE_URL + "/users",
            headers=get_headers(),
            params={"page": page, "per_page": per_page},
        )
        response.raise_for_status()
        return json.dumps(response.json(), ensure_ascii=False, indent=2)
 
 
@mcp.tool()
async def get_user(user_id: str) -> str:
    """특정 사용자의 상세 정보를 조회합니다."""
    async with httpx.AsyncClient() as client:
        response = await client.get(
            BASE_URL + "/users/" + user_id,
            headers=get_headers(),
        )
        response.raise_for_status()
        return json.dumps(response.json(), ensure_ascii=False, indent=2)
 
 
@mcp.tool()
async def create_user(name: str, email: str, role: str = "member") -> str:
    """새 사용자를 생성합니다."""
    async with httpx.AsyncClient() as client:
        response = await client.post(
            BASE_URL + "/users",
            headers=get_headers(),
            json={"name": name, "email": email, "role": role},
        )
        response.raise_for_status()
        return json.dumps(response.json(), ensure_ascii=False, indent=2)
 
 
# API 문서를 리소스로 제공
@mcp.resource("api://docs")
def get_api_docs() -> str:
    """API 엔드포인트 목록과 사용 가이드"""
    docs = {
        "base_url": BASE_URL,
        "endpoints": [
            {"method": "GET", "path": "/users", "description": "사용자 목록 조회"},
            {"method": "GET", "path": "/users/:id", "description": "사용자 상세 조회"},
            {"method": "POST", "path": "/users", "description": "사용자 생성"},
            {"method": "PUT", "path": "/users/:id", "description": "사용자 수정"},
            {"method": "DELETE", "path": "/users/:id", "description": "사용자 삭제"},
        ],
        "rate_limit": "60 requests/minute",
    }
    return json.dumps(docs, ensure_ascii=False, indent=2)
 
 
if __name__ == "__main__":
    mcp.run()

API 래핑 설계 원칙

REST API를 MCP 도구로 변환할 때 다음 원칙을 따릅니다.

엔드포인트별 도구 분리: 하나의 API 엔드포인트를 하나의 도구로 매핑합니다. CRUD 작업을 각각 별도의 도구로 분리합니다.

에러 메시지의 한국어화: API의 영문 에러 메시지를 LLM이 사용자에게 전달하기 좋은 형태로 변환합니다.

응답 형식의 단순화: API 응답에서 LLM이 활용할 핵심 정보만 추출하여 반환합니다. 전체 응답을 그대로 반환하면 토큰을 낭비할 수 있습니다.

응답 단순화 예시
python
@mcp.tool()
async def get_order_summary(order_id: str) -> str:
    """주문 요약 정보를 조회합니다."""
    async with httpx.AsyncClient() as client:
        response = await client.get(
            BASE_URL + "/orders/" + order_id,
            headers=get_headers(),
        )
        response.raise_for_status()
        data = response.json()
 
        # 전체 API 응답 대신 핵심 정보만 추출
        summary = {
            "order_id": data["id"],
            "status": data["status"],
            "total": data["total_amount"],
            "items_count": len(data["line_items"]),
            "created_at": data["created_at"],
            "customer": data["customer"]["name"],
        }
        return json.dumps(summary, ensure_ascii=False, indent=2)

레거시 시스템 연동

레거시 시스템은 현대적인 API를 제공하지 않는 경우가 많습니다. CLI 도구, SOAP 서비스, FTP 서버, 메인프레임 터미널 등이 이에 해당합니다. MCP 서버는 이러한 시스템과 AI를 연결하는 어댑터 역할을 합니다.

CLI 도구 래핑

cli-wrapper-server.ts
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { exec } from "child_process";
import { promisify } from "util";
 
const execAsync = promisify(exec);
 
const server = new McpServer({
  name: "cli-tools-server",
  version: "1.0.0",
});
 
// Docker 컨테이너 관리
server.tool(
  "docker_ps",
  "실행 중인 Docker 컨테이너 목록을 조회합니다",
  {
    all: z.boolean().default(false).describe("중지된 컨테이너도 포함할지 여부"),
  },
  async (params) => {
    const flag = params.all ? " -a" : "";
    const { stdout } = await execAsync(
      "docker ps" + flag + " --format '{{json .}}'"
    );
 
    const containers = stdout
      .trim()
      .split("\n")
      .filter(Boolean)
      .map((line) => JSON.parse(line));
 
    return {
      content: [{
        type: "text" as const,
        text: JSON.stringify(containers, null, 2),
      }],
    };
  }
);
 
// Git 저장소 정보
server.tool(
  "git_log",
  "Git 커밋 히스토리를 조회합니다",
  {
    repo_path: z.string().describe("Git 저장소 경로"),
    count: z.number().default(10).describe("조회할 커밋 수"),
  },
  async (params) => {
    const { stdout } = await execAsync(
      "git -C " + params.repo_path +
      " log --oneline -" + params.count +
      " --format='%H|%an|%ad|%s' --date=short"
    );
 
    const commits = stdout
      .trim()
      .split("\n")
      .filter(Boolean)
      .map((line) => {
        const [hash, author, date, ...msgParts] = line.split("|");
        return { hash, author, date, message: msgParts.join("|") };
      });
 
    return {
      content: [{
        type: "text" as const,
        text: JSON.stringify(commits, null, 2),
      }],
    };
  }
);
 
const transport = new StdioServerTransport();
await server.connect(transport);
Warning

CLI 명령을 실행하는 MCP 서버는 명령 주입(Command Injection) 공격에 취약합니다. 사용자 입력을 셸 명령에 직접 삽입하지 마십시오. 매개변수 유효성 검사, 화이트리스트 기반 명령 제한, 셸 이스케이프 처리 등의 보안 조치가 필수입니다. 9장에서 보안 모범 사례를 상세히 다룹니다.

연동 아키텍처 패턴

게이트웨이 패턴

하나의 MCP 서버가 여러 백엔드 시스템에 대한 진입점 역할을 합니다.

이 패턴은 관련된 여러 시스템을 하나의 도메인으로 묶어 제공할 때 유용합니다. 예를 들어 "주문 관리 MCP 서버"가 주문 데이터베이스, 결제 API, 재고 시스템을 통합하여 제공할 수 있습니다.

사이드카 패턴

기존 애플리케이션 옆에 MCP 서버를 별도 프로세스로 배치합니다.

기존 애플리케이션의 코드를 수정하지 않고, MCP 서버가 애플리케이션의 데이터나 API에 접근하여 기능을 노출합니다. 마이크로서비스 아키텍처에서 각 서비스에 MCP 사이드카를 붙이는 방식으로 활용할 수 있습니다.

어댑터 패턴

기존 시스템의 인터페이스를 MCP 프리미티브로 변환하는 어댑터를 구현합니다.

어댑터 패턴 구조
typescript
// 기존 시스템의 인터페이스
interface LegacyOrderSystem {
  getOrder(id: string): Promise<LegacyOrder>;
  createOrder(data: LegacyOrderInput): Promise<string>;
  cancelOrder(id: string, reason: string): Promise<boolean>;
}
 
// MCP 어댑터
function registerOrderTools(
  server: McpServer,
  orderSystem: LegacyOrderSystem
) {
  server.tool(
    "get_order",
    "주문 정보를 조회합니다",
    { order_id: z.string() },
    async (params) => {
      const order = await orderSystem.getOrder(params.order_id);
      return {
        content: [{
          type: "text" as const,
          text: JSON.stringify(adaptOrderForAI(order), null, 2),
        }],
      };
    }
  );
 
  server.tool(
    "cancel_order",
    "주문을 취소합니다. 취소 사유를 반드시 포함해야 합니다.",
    {
      order_id: z.string(),
      reason: z.string().describe("취소 사유"),
    },
    async (params) => {
      const success = await orderSystem.cancelOrder(
        params.order_id,
        params.reason
      );
      return {
        content: [{
          type: "text" as const,
          text: success
            ? "주문이 취소되었습니다."
            : "주문 취소에 실패했습니다.",
        }],
      };
    }
  );
}

커뮤니티 MCP 서버 활용

직접 구축하지 않고도 이미 만들어진 MCP 서버를 활용할 수 있습니다. 주요 커뮤니티 서버를 소개합니다.

서버설명설치
@modelcontextprotocol/server-githubGitHub 리포지토리 관리npx 실행
@modelcontextprotocol/server-postgresPostgreSQL 조회npx 실행
@modelcontextprotocol/server-filesystem파일 시스템 접근npx 실행
@modelcontextprotocol/server-slackSlack 메시지 관리npx 실행
@modelcontextprotocol/server-puppeteer웹 브라우저 자동화npx 실행

커뮤니티 서버를 사용할 때는 해당 서버의 코드를 검토하여 보안 위험이 없는지 확인하는 것이 중요합니다.

정리

이 장에서는 기존 시스템을 MCP로 연동하는 실전 패턴을 다루었습니다.

  • 데이터베이스 연동: 스키마를 리소스로, 쿼리 실행을 도구로 제공하며, 보안을 위해 SELECT 전용 도구와 쓰기 도구를 분리합니다.
  • REST API 래핑: API 엔드포인트를 MCP 도구로 변환하고, 응답을 단순화하여 토큰 효율성을 높입니다.
  • 레거시 시스템: CLI 도구, SOAP 서비스 등을 MCP 어댑터로 래핑합니다.
  • 아키텍처 패턴: 게이트웨이, 사이드카, 어댑터 패턴으로 다양한 연동 시나리오에 대응합니다.

다음 장 미리보기

9장에서는 MCP의 보안, 인증, 권한 관리를 다룹니다. OAuth 2.1 기반 인증 흐름, 입력 유효성 검사, 명령 주입 방지, 데이터 유출 방지 등 프로덕션 환경에서 반드시 고려해야 할 보안 모범 사례를 상세히 살펴보겠습니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#mcp#typescript#python

관련 글

AI / ML

9장: 보안, 인증, 권한 관리

MCP 서버의 OAuth 2.1 인증, 입력 유효성 검사, 명령 주입 방지, 데이터 유출 방지 등 프로덕션 보안 모범 사례를 다룹니다.

2026년 2월 6일·20분
AI / ML

7장: MCP 클라이언트 구현하기

MCP 클라이언트를 직접 구현하여 서버에 연결하고, LLM과 통합하여 도구 호출 파이프라인을 완성하는 방법을 다룹니다.

2026년 2월 2일·15분
AI / ML

10장: 실전 프로젝트 - 풀스택 MCP 시스템 구축

여러 MCP 서버를 조합하고 클라이언트 애플리케이션을 구축하여, 프로덕션 수준의 풀스택 MCP 시스템을 완성하는 실전 프로젝트입니다.

2026년 2월 8일·22분
이전 글7장: MCP 클라이언트 구현하기
다음 글9장: 보안, 인증, 권한 관리

댓글

목차

약 18분 남음
  • 연동의 핵심 원칙
  • 데이터베이스 연동
    • PostgreSQL 연동 서버
  • REST API 래핑
    • 일반적인 REST API 래핑 패턴
    • API 래핑 설계 원칙
  • 레거시 시스템 연동
    • CLI 도구 래핑
  • 연동 아키텍처 패턴
    • 게이트웨이 패턴
    • 사이드카 패턴
    • 어댑터 패턴
  • 커뮤니티 MCP 서버 활용
  • 정리
  • 다음 장 미리보기