MCP로 서비스를 AI 에이전트와 연결하기 - Embedded vs Bridge 패턴 실전 구현

이 글에서는 FastAPI 기반 RAG 서버에 MCP를 도입한 경험을 바탕으로, 앱에 MCP를 직접 임베딩하는 Embedded 패턴과 별도 프로세스로 분리하는 Bridge 패턴의 차이, Transport 선택 기준, 그리고 LLM이 Tool을 올바르게 사용하도록 유도하는 description 작성법까지 단계별로 살펴봅니다.

MCP로 서비스를 AI 에이전트와 연결하기 - Embedded vs Bridge 패턴 실전 구현

목차

  1. 들어가며
  2. MCP란 무엇인가
  3. 기존 서비스에 MCP를 연결하는 두 가지 방법
  4. Transport 선택: Streamable HTTP vs stdio
  5. 구현 A: FastAPI 앱에 MCP 임베딩하기
  6. 구현 B: Bridge MCP 패턴
  7. Tool Description — LLM에게 보내는 지시문
  8. 마무리

1. 들어가며

서비스는 완성됐습니다. API 엔드포인트도 있고, 문서도 있습니다. 그런데 Claude나 openclaw 같은 AI 에이전트가 이 서비스를 직접 쓰게 하려면 어떻게 해야 할까요?

MCP(Model Context Protocol)가 등장하기 전에는 뾰족한 방법이 없었습니다. AI 에이전트에게 API 스펙 문서를 직접 읽히거나, OpenAI function calling 형식에 맞게 스키마를 손으로 작성해야 했습니다. 서비스가 바뀌면 스키마도 같이 고쳐야 했고, 클라이언트가 늘어날수록 연동 코드도 따로 관리해야 했습니다.

MCP는 이 문제를 표준 프로토콜로 해결합니다. AI 에이전트가 서비스를 스스로 탐색하고 호출할 수 있게 됩니다.

이 글은 실제 RAG 서버에 MCP를 붙인 경험을 바탕으로, 다음을 순서대로 살펴봅니다.

  • MCP 개념과 작동 방식
  • 기존 서비스에 MCP를 연결하는 두 가지 패턴 (Embedded vs Bridge)
  • Transport 선택 기준 (Streamable HTTP vs stdio)
  • 각 패턴의 실제 구현
  • LLM을 위한 Tool description 작성법

2. MCP란 무엇인가

MCP(Model Context Protocol)는 Anthropic이 2024년 11월에 발표한 오픈 표준 프로토콜입니다. AI 모델과 외부 세계를 연결하기 위한 USB-C 같은 규격이라고 생각하면 이해하기 쉽습니다. USB-C 이전에는 기기마다 다른 케이블이 필요했듯, MCP 이전에는 서비스마다 별도의 AI 연동 코드가 필요했습니다.

모든 메시지는 JSON-RPC 2.0 형식으로 교환됩니다. 클라이언트가 요청을 보내고, 서버가 결과를 돌려주는 방식입니다.

MCP 서버가 외부에 노출하는 기능은 세 가지입니다.

  • Tools: AI가 호출할 수 있는 실행 가능한 함수 (예: 파일 읽기, 검색, API 호출)
  • Resources: AI가 읽을 수 있는 데이터나 문서 (예: 파일, DB 레코드)
  • Prompts: 재사용 가능한 프롬프트 템플릿

이 글에서는 Tools만 다룹니다. AI 에이전트가 서비스 기능을 직접 실행하는 것이 목적이기 때문입니다.

작동 흐름

┌──────────────────────────────────────┐
│  AI 클라이언트 (Claude Code, openclaw)  │
└────────────────────┬─────────────────┘
                     │  MCP 프로토콜 (JSON-RPC 2.0)
                     │
                     │  1. initialize  ← 프로토콜 버전 협상
                     │  2. tools/list  ← Tool 목록 수신
                     │  3. tools/call  ← Tool 실행 + 결과 반환
                     ▼
              ┌─────────────┐
              │   MCP 서버   │
              └─────────────┘

클라이언트가 연결되면 먼저 initialize로 핸드셰이크를 수행하고, tools/list로 사용 가능한 Tool 목록을 받아옵니다. 이후 LLM이 사용자 의도를 판단해 필요한 Tool을 tools/call로 호출합니다. 반환된 결과는 LLM의 컨텍스트에 포함되어 최종 응답 생성에 쓰입니다.


3. 기존 서비스에 MCP를 연결하는 두 가지 방법

기존 서비스를 MCP로 연결하는 방법은 크게 두 가지입니다.

방식 A: Embedded MCP

기존 앱 안에 MCP를 직접 심는 방식입니다. REST API와 MCP가 같은 프로세스에서 실행되고, MCP Tool은 HTTP를 거치지 않고 내부 서비스 함수를 바로 호출합니다. 기존 코드를 수정해야 하지만, 추가 네트워크 홉이 없고 DB 커넥션 같은 리소스를 REST와 MCP가 함께 씁니다.

          ┌─────────────┐
          │  AI Client  │
          └──────┬──────┘
                 │ Streamable HTTP (POST /mcp)
                 ▼
┌────────────────────────────────────────┐
│           기존 앱 (FastAPI 등)           │
│                                        │
│  ┌─────────────────────────────────┐   │
│  │  REST Adapter  (/api/...)       │   │
│  └─────────────────────────────────┘   │
│  ┌─────────────────────────────────┐   │
│  │  MCP Adapter   (/mcp)           │   │
│  └────────────────┬────────────────┘   │
│                   │ 직접 함수 호출        │
│                   ▼                    │
│  ┌─────────────────────────────────┐   │
│  │    내부 서비스 / 비즈니스 로직        │   │
│  └─────────────────────────────────┘   │
└────────────────────────────────────────┘

방식 B: Bridge MCP (별도 프로세스)

기존 서버와 완전히 분리된 MCP 서버를 따로 띄우는 방식입니다. AI 에이전트는 이 Bridge 서버와 대화하고, Bridge는 내부적으로 기존 REST API를 HTTP로 호출합니다. 기존 서버는 전혀 건드리지 않고, Bridge 프로세스를 하나 더 띄워서 AI 에이전트와 기존 서버 사이를 중계합니다. GitHub MCP(mcp-server-github), Postgres MCP(mcp-server-postgres) 등 대부분의 오픈소스 MCP 서버가 이 패턴을 따릅니다.

     ┌─────────────┐
     │  AI Client  │
     └──────┬──────┘
            │ stdio 또는 HTTP
            ▼
┌───────────────────────────┐
│   MCP Bridge Server       │
│   (별도 프로세스)            │
└─────────────┬─────────────┘
              │ HTTP REST (httpx 등)
              ▼
┌───────────────────────────┐
│   기존 서버 (REST API)      │
│   수정 없음                 │
└───────────────────────────┘

비교

항목 Embedded MCP (방식 A) Bridge MCP (방식 B)
배포 복잡도 낮음 (단일 프로세스) 높음 (2개 프로세스)
성능 높음 (직접 함수 호출) 낮음 (HTTP 오버헤드)
기존 서버 수정 필요 불필요
적합한 상황 서비스 소유자가 MCP 추가 외부에서 기존 서버에 MCP를 씌울 때
주로 쓰는 Transport Streamable HTTP stdio
주요 예시 이 글의 구현 예시 mcp-server-github, mcp-server-postgres

4. Transport 선택: Streamable HTTP vs stdio

Transport는 AI 클라이언트와 MCP 서버가 메시지를 주고받는 통신 방식입니다. 쉽게 말해 "어떤 채널로 연결하느냐"입니다.

SSE(Server-Sent Events)는 이전 버전의 MCP에서 사용하던 방식입니다. 현재는 Streamable HTTP로 대체되었으며, 신규 프로젝트에서는 사용하지 않습니다.

Streamable HTTP — 원격 서버 배포용

MCP 서버가 HTTP 서버로 동작합니다. 포트를 열고 클라이언트의 연결을 기다리며, 여러 클라이언트가 동시에 접속할 수 있습니다. 네트워크에 노출되는 만큼 인증 레이어가 필요합니다.

Claude Code 설정 예시입니다.

// .claude/settings.json
{
  "mcpServers": {
    "my-service": {
      "type": "http",
      "url": "http://localhost:8001/mcp/"
    }
  }
}

stdio — 로컬 클라이언트용

MCP 서버가 HTTP 서버로 동작하지 않습니다. 클라이언트가 설정에 적힌 명령어로 MCP 서버 프로세스를 직접 실행하고, 그 프로세스와 통신합니다. 서버를 미리 띄워둘 필요 없고 인증도 불필요합니다. Bridge MCP 패턴에서 주로 쓰는 방식입니다.

Claude Code 설정 예시입니다. commandargs에 적힌 명령어로 클라이언트가 MCP 서버를 실행합니다.

// .claude/settings.json
{
  "mcpServers": {
    "my-service-bridge": {
      "command": "python",
      "args": ["bridge_server.py"]
    }
  }
}

선택 기준

원격 배포 / 다중 클라이언트 / Embedded 패턴  →  Streamable HTTP
로컬 전용 / Bridge 패턴                     →  stdio

5. 구현 A: FastAPI 앱에 MCP 임베딩하기

예시로 사용할 서비스는 문서 검색 RAG 서버입니다. PDF 문서를 인제스트해 벡터 DB에 저장하고, 검색을 제공하는 FastAPI 앱입니다. 기존 REST 라우터는 건드리지 않고 MCP를 추가한 경우입니다.

5-1. FastMCP 인스턴스 생성

Python mcp 라이브러리의 FastMCP 클래스를 사용합니다.

# backend/api/mcp/server.py
from mcp.server.fastmcp import FastMCP

mcp_server = FastMCP("rag-mcp-server", streamable_http_path="/")

# Tool 등록 — import 시 @mcp_server.tool() 데코레이터가 실행됨
import backend.api.mcp.tools  # noqa: F401, E402

streamable_http_path="/" 는 FastMCP 앱 내부의 엔드포인트 경로입니다. 이후 FastAPI에서 /mcp에 마운트하면 최종 MCP 엔드포인트는 /mcp/가 됩니다.

마지막 줄의 import가 핵심입니다. 같은 디렉터리의 tools.py(Tool 정의 파일)를 import하는 순간 @mcp_server.tool() 데코레이터들이 모두 실행되어 Tool 등록이 완료됩니다. 함수를 일일이 등록하는 코드를 따로 작성할 필요가 없습니다.

5-2. FastAPI에 마운트 + lifespan 연동

FastAPI의 sub-application 마운트 기능으로 기존 앱에 MCP를 붙입니다.

# backend/api/server.py
from contextlib import asynccontextmanager
from fastapi import FastAPI

@asynccontextmanager
async def lifespan(app: FastAPI):
    await init_services()                          # 서비스 싱글톤 초기화
    async with mcp_server.session_manager.run():   # MCP 세션 관리자 시작
        yield
    await shutdown_services()

def create_app() -> FastAPI:
    app = FastAPI(title=settings.APP_NAME, lifespan=lifespan)

    # 기존 REST 라우터 (변경 없음)
    app.include_router(search.router)
    app.include_router(ingest.router)
    app.include_router(collections.router, prefix="/collections")
    # ...

    # MCP Adapter — Streamable HTTP transport (POST + GET /mcp)
    app.mount("/mcp", mcp_server.streamable_http_app())

    return app

REST API와 MCP Tool이 같은 서비스 인스턴스를 씁니다. MCP를 추가해도 기존 비즈니스 로직을 그대로 재사용할 수 있습니다.

5-3. Tool 정의

검색, 인제스트, 컬렉션 관리 등 모든 Tool이 같은 패턴으로 구현됩니다. rag_search를 보면 구조가 명확합니다.

# backend/api/mcp/tools.py
@mcp_server.tool()
async def rag_search(
    collection: str,
    query: str,
    top_k: int = 10,
    mode: str = "dense",
    filters: Optional[dict] = None,
) -> dict:
    """문서 컬렉션에서 관련 문서 청크를 검색합니다. ..."""
    svc = get_search_service()          # deps.py 싱글톤 getter
    req = SearchRequest(
        collection=collection,
        query=query,
        top_k=top_k,
        mode=SearchMode(mode),
        filters=filters,
    )
    resp = await svc.search(req)
    return resp.model_dump()

REST API와 동일한 서비스를 직접 호출합니다.

5-4. 비동기 작업 — "fire and poll" 패턴

PDF 문서 인제스트처럼 수십 초가 걸리는 작업이 문제입니다. MCP 연결에는 타임아웃이 있어, Tool에서 즉시 응답을 반환해야 합니다.

해결책은 간단합니다. Job Queue에 작업을 제출하고 job_id만 즉시 반환합니다. AI 에이전트는 rag_get_job_status Tool로 주기적으로 상태를 확인하면 됩니다.

@mcp_server.tool()
async def rag_ingest(collection: str, source_type: str, ...) -> dict:
    """PDF 문서를 RAG 시스템에 등록합니다. ..."""
    # ... 요청 객체 생성 ...
    job_id = generate_job_id()
    await jm.submit(
        job_id,
        svc.execute_ingest(request, job_id),
        request.collection,
    )
    return {"job_id": job_id, "status": "pending", "message": "Job submitted"}

작업은 백그라운드에서 처리되고, Tool은 즉시 job_id를 반환합니다. AI 에이전트는 rag_get_job_status(job_id)로 완료 여부를 주기적으로 확인합니다.


6. 구현 B: Bridge MCP 패턴

기존 서버를 전혀 수정하지 않고 MCP를 씌우는 방식입니다.

# bridge_server.py — 별도 프로세스로 실행
import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("my-service-bridge")
SERVICE_BASE = "http://localhost:8001"

@mcp.tool()
async def search(query: str, collection: str = "default", top_k: int = 10) -> dict:
    """서비스에서 관련 문서를 검색합니다."""
    async with httpx.AsyncClient() as client:
        resp = await client.post(
            f"{SERVICE_BASE}/search",
            json={"query": query, "collection": collection, "top_k": top_k},
        )
        resp.raise_for_status()
        return resp.json()

if __name__ == "__main__":
    mcp.run(transport="stdio")

mcp.run(transport="stdio") 한 줄로 이 스크립트가 MCP 서버로 동작합니다. Claude Code, Claude Desktop, Cursor 등이 이 방식으로 MCP 서버를 실행합니다.

장점: 기존 서버 코드를 한 줄도 건드리지 않습니다. REST API가 있는 서비스라면 어디든 이 패턴으로 MCP를 씌울 수 있습니다.

단점: HTTP 왕복 비용이 발생하고, 기존 서버와 Bridge 두 개의 프로세스를 관리해야 합니다. 기존 서버의 에러가 Bridge를 통해 전파될 때 처리가 복잡해지기도 합니다.


7. Tool Description — LLM에게 보내는 지시문

Tool의 docstring은 LLM이 읽는 유일한 API 문서입니다. 이 품질이 AI 에이전트의 Tool 선택 정확도에 직결됩니다.

나쁜 예

@mcp_server.tool()
async def rag_search(collection: str, query: str, top_k: int = 10) -> dict:
    """문서를 검색합니다."""

LLM 입장에서는 이 Tool로 무엇을 할 수 있는지, 언제 써야 하는지, 파라미터에 어떤 값을 넣어야 하는지 전혀 알 수 없습니다. 결과적으로 Tool을 잘못 선택하거나 엉뚱한 값을 넣는 일이 생깁니다.

좋은 예

@mcp_server.tool()
async def rag_search(collection: str, query: str, top_k: int = 10, mode: str = "dense") -> dict:
    """문서 컬렉션에서 관련 문서 청크를 검색합니다.

    사용 시점:
    - 사용자가 법률, 계약, 규정 등에 대해 질문할 때
    - 문서 내용을 근거로 답변해야 할 때

    파라미터:
    - collection: 검색 대상 컬렉션 이름. 모르면 먼저 rag_list_collections 호출.
    - query: 자연어 검색 질문. 한국어 그대로 전달 가능.
    - top_k: 반환할 최대 결과 수. 간단한 질문은 5, 포괄적 조사는 20 권장.
    - mode: "dense"(의미 기반) 또는 "hybrid_parallel_rrf"(의미+키워드, 정확도 높음).

    주의: 존재하지 않는 컬렉션을 지정하면 에러가 발생합니다.
    """

핵심 포인트 세 가지입니다.

① "사용 시점" 항목이 가장 중요합니다. LLM이 수십 개의 Tool 중에서 이 Tool을 선택할지 판단하는 기준이 됩니다.

② 파라미터 허용값과 추천값을 명시합니다. 정의만 있으면 LLM이 스스로 판단해야 하지만, 추천값이 있으면 일관된 동작을 유도할 수 있습니다.

③ 결과 처리 힌트를 포함합니다. LLM의 후속 행동을 안내하면 사용자 경험이 일관적으로 유지됩니다.

Tool description은 코드가 아니라 LLM에게 보내는 지시문입니다. 작성에 시간을 들일 가치가 있습니다.


보안 참고: Embedded MCP는 검색뿐 아니라 삭제·관리 Tool까지 모두 노출합니다. 프로덕션 환경에서는 AI 에이전트에게 필요한 Tool만 선택적으로 노출하고, /mcp 엔드포인트는 내부망으로 격리하도록 구성해야 합니다.


8. 마무리

MCP를 도입하면 기존 서비스를 크게 건드리지 않고도 AI 에이전트에게 새로운 능력을 부여할 수 있습니다.

서비스 소유자가 직접 MCP를 붙이는 상황이라면 Embedded 패턴 + Streamable HTTP가 자연스러운 선택입니다. 기존 서버를 수정할 수 없거나 로컬 클라이언트를 위한 구성이라면 Bridge 패턴 + stdio가 더 적합합니다.

MCP는 아직 빠르게 발전 중인 생태계입니다. 클라이언트마다 지원하는 Transport나 기능 범위가 다를 수 있으므로, 연결하려는 클라이언트의 문서를 먼저 확인하는 것이 좋습니다.


구현 A의 코드는 FastAPI + FastMCP 기반 RAG 서버에서 발췌했습니다. 구현 B는 패턴 설명을 위한 예시 코드입니다.

Subscribe to PAASUP IDEAS

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe