> EulerAgent > 튜토리얼 > 기본 > 08. MCP 프로바이더 설정과 도구 관리

08. MCP 프로바이더 설정과 도구 관리


학습 목표

이 튜토리얼을 마치면 다음을 할 수 있습니다:


사전 준비

euleragent new research-agent --template personal-assistant
ollama serve

Step 1: MCP란 무엇인가?

MCP(Model Context Protocol) 개요

MCP는 JSON-RPC 2.0 기반 프로토콜로, 외부 도구와 데이터 소스를 표준화된 인터페이스로 연결합니다. euleragent에서 MCP는 다음과 같은 역할을 합니다:

euleragent에서 MCP의 위치

MCP는 LLM에게 직접 노출되지 않습니다. 에이전트는 단일 web.search 도구만 호출하고, 내부의 SearchRouter가 설정된 소스 중 적절한 곳으로 라우팅합니다:

┌─────────────────────────────────────────────────────┐
│  에이전트 (LLM)                                       │
│    │                                                 │
│    ▼                                                 │
│  web.search("API gateway 비교")                       │
│    │                                                 │
│    ▼                                                 │
│  ┌─────────────┐                                     │
│  │ SearchRouter │ ← 소스 라우팅 결정                   │
│  └──────┬──────┘                                     │
│         │                                            │
│    ┌────┴────┬──────────┐                            │
│    ▼         ▼          ▼                            │
│  [Native]  [MCP:       [MCP:                         │
│  Brave     Tavily]     내부 KB]                       │
│                                                      │
│  각 소스는 독립적으로 승인/차단/stale 처리              │
└─────────────────────────────────────────────────────┘

핵심: LLM은 개별 소스의 존재를 모릅니다. SearchRouter가 enabled/approved 소스 중에서 자동으로 선택하고, 결과를 표준 형식(SearchResult)으로 통합하여 반환합니다.

MCP 프로토콜 기본 동작

MCP 서버는 두 가지 핵심 메서드를 제공합니다:

메서드 설명 용도
tools/list 서버가 제공하는 도구 목록 반환 카탈로그 동기화 시
tools/call 특정 도구 실행 실제 검색 실행 시
// tools/list 응답 예시
{
  "tools": [
    {
      "name": "web_search",
      "description": "Search the web using Tavily API",
      "inputSchema": {
        "type": "object",
        "properties": {
          "query": {"type": "string"}
        },
        "required": ["query"]
      }
    }
  ]
}

Step 1.5: 데모 MCP 서버로 빠르게 시작하기

euleragent 저장소에 포함된 데모 MCP 서버를 사용하면 외부 서비스 없이 MCP 통합을 체험할 수 있습니다. 이 서버는 stdlib만 사용하며 두 가지 도구를 제공합니다:

1. 샘플 데이터베이스 생성 및 서버 시작

# 샘플 DB 생성
python -c "
import sqlite3, pathlib
pathlib.Path('.euleragent/state').mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect('.euleragent/state/demo.db')
conn.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)')
conn.execute(\"INSERT OR IGNORE INTO users VALUES (1, 'Alice', 'alice@example.com')\")
conn.execute(\"INSERT OR IGNORE INTO users VALUES (2, 'Bob', 'bob@example.com')\")
conn.commit(); conn.close()
print('Demo database created.')
"

# 데모 MCP 서버 시작 (포트 9020)
python examples/mcp/sqlite_tools_server.py

2. workspace.yaml에서 MCP 서버 주석 해제

euleragent init으로 생성된 workspace.yaml에는 데모 서버 설정이 주석으로 포함되어 있습니다. 주석을 해제하고 enabled: true로 변경하세요:

mcp:
  enabled: true
  servers:
    - id: demo_sqlite
      url: http://localhost:9020/mcp
      allow_tools: [sqlite.query, demo.echo]
      cost_tier: free
      risk_default: low
      require_approval_enable: false
      timeout_seconds: 30

3. 카탈로그 동기화 및 확인

euleragent mcp sync
euleragent mcp show

4. curl로 직접 테스트

# 도구 목록 확인
curl -s -X POST http://localhost:9020/mcp \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' | python -m json.tool

# 쿼리 실행
curl -s -X POST http://localhost:9020/mcp \
  -d '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"sqlite.query","arguments":{"sql":"SELECT * FROM users"}}}' | python -m json.tool

보안: 데모 서버는 SELECTPRAGMA만 허용하며, INSERT/UPDATE/DELETE/DROP 등 쓰기 작업은 거부합니다. SQLite 연결도 ?mode=ro로 읽기 전용으로 열립니다.


Step 2: workspace.yaml에 MCP 설정 추가

기본 MCP 설정

workspace.yamlmcp: 섹션에서 MCP 서버를 설정합니다:

# .euleragent/config/workspace.yaml

mcp:
  enabled: true
  catalog_path: .euleragent/state/mcp_catalog.json
  servers:
    - id: tavily_search
      url: http://localhost:3001/mcp
      allow_tools:
        - web_search
      cost_tier: paid
      risk_default: high
      require_approval_enable: true   # source_enable 승인 필요

    - id: internal_kb
      url: http://localhost:3002/mcp
      allow_tools:
        - document_search
      cost_tier: free
      risk_default: medium
      require_approval_enable: false  # 내부 소스, 승인 불필요

설정 필드 설명

필드 필수 설명
id 서버 고유 식별자 (카탈로그에서 참조용)
url MCP 서버의 JSON-RPC endpoint URL
allow_tools 아니오 허용할 도구 이름 목록 (비어있으면 전체 허용)
cost_tier 아니오 비용 등급: free, paid, unknown
risk_default 아니오 기본 위험 수준: low, medium, high
require_approval_enable 아니오 true: 사용 전 source_enable 승인 필요 (기본값: true)

환경변수로 API 키 보호

API 키가 필요한 MCP 서버는 환경변수 보간을 사용합니다:

mcp:
  enabled: true
  servers:
    - id: tavily_search
      url: ${TAVILY_MCP_URL}          # 환경변수에서 URL 로드
      allow_tools:
        - web_search
      cost_tier: paid
# 환경변수 설정
export TAVILY_MCP_URL="http://localhost:3001/mcp"
export TAVILY_API_KEY="tvly-YOUR_API_KEY"

보안: API 키를 workspace.yaml에 직접 기록하지 마세요. ${ENV_VAR} 구문을 사용하면 런타임에 환경변수에서 해석됩니다. workspace.yaml은 버전 관리에 포함될 수 있으므로 이 패턴이 중요합니다.


Step 3: mcp sync — 카탈로그 동기화

MCP 설정을 추가한 후 카탈로그를 동기화합니다. 이 과정에서 각 서버에 tools/list를 호출하여 제공 가능한 도구 목록을 가져옵니다.

실행

euleragent mcp sync

예상 출력

MCP catalog sync completed.
  Servers: 2
    [OK]   tavily_search  (web_search, connected)
    [OK]   internal_kb    (document_search, connected)
  Snapshot: .euleragent/state/mcp_catalog.json

동기화 중 오류 발생 시

MCP catalog sync completed.
  Servers: 2
    [OK]   internal_kb    (document_search, connected)
    [FAIL] tavily_search  (connection refused: http://localhost:3001/mcp)
  Snapshot: .euleragent/state/mcp_catalog.json
  Warnings: 1 server(s) unreachable

오류가 발생한 서버의 소스는 카탈로그에 포함되지 않습니다. 연결 문제를 해결한 후 mcp sync를 다시 실행하세요.

일반적인 오류와 해결 방법

오류 원인 해결
connection refused MCP 서버가 실행 중이 아님 서버 프로세스 시작 확인
timeout after 30s 서버 응답 지연 timeout_seconds 설정 증가
MCP error -32601 지원하지 않는 메서드 MCP 서버 버전 확인
tool X not in allow_tools 허용 목록에 없는 도구 allow_tools 수정

Step 4: mcp show — 카탈로그 상태 확인

동기화된 카탈로그의 현재 상태를 확인합니다:

euleragent mcp show

예상 출력

MCP Catalog
  Servers:
    NAME             URL                           STATUS    TOOLS
    tavily_search    http://localhost:3001/mcp      OK        1 (web_search)
    internal_kb      http://localhost:3002/mcp      OK        1 (document_search)

  Source Sets:
    default: tavily_search, internal_kb
    offline: internal_kb

  Snapshot Hash: sha256:a3f8d21...
  Last Sync: 2026-02-24 14:30:15

출력 해석


Step 5: 첫 번째 MCP 검색 실행

MCP 소스가 설정된 상태에서 검색 태스크를 실행합니다.

태스크 실행

euleragent run research-agent \
  --task "2025년 API 게이트웨이 솔루션 비교 리포트 작성" \
  --mode plan

실행 중 발생하는 일

  1. Runner가 시작되면 SearchCatalogSnapshot을 생성합니다
  2. 에이전트가 web.search를 호출합니다
  3. SearchRouter가 enabled 소스를 확인합니다
  4. 외부 MCP 소스(tavily_search)는 승인이 필요합니다
  5. kind: source_enable 승인 레코드가 생성됩니다

예상 출력

[run] Starting plan mode for research-agent
[run] SearchCatalog: 2 sources (1 native, 1 mcp)
[run] MCP source 'tavily_search' requires approval (source_enable)
[llm] Generating plan...
[approval] Created: kind=source_enable, tool=web.search, source=mcp_web_search
[run] Plan generated: 1 file(s)
  └─ plan.md (645 tokens)
[run] Run finalized: a1b2c3d4

승인 확인

euleragent approve show
Pending Approvals:
  ID        KIND             TOOL          SOURCE           RISK    STATUS
  ap_001    source_enable    web.search    mcp_web_search   high    pending

Step 6: source_enable 승인 흐름 상세

승인 레코드 구조

source_enable 승인 레코드는 다음과 같은 구조를 가집니다:

euleragent approve show ap_001
{
  "id": "ap_001",
  "kind": "source_enable",
  "tool_name": "web.search",
  "tool_params": {
    "query": "API gateway comparison 2025"
  },
  "risk_level": "high",
  "side_effects": ["external_network"],
  "status": "pending",
  "resolved_source_id": "mcp_web_search",
  "candidate_sources": ["mcp_web_search", "mcp_document_search"],
  "routing_reason": "mcp_source_selected"
}

필드 설명

필드 설명
kind source_enable — MCP 소스 활성화 승인
resolved_source_id SearchRouter가 선택한 소스 ID
candidate_sources 라우팅 후보였던 모든 소스 목록
routing_reason 라우팅 결정 사유 (예: mcp_source_selected)

승인 수락

euleragent approve accept ap_001 --actor "user:you"
Accepted: ap_001
  Kind: source_enable
  Source: mcp_web_search (tavily via MCP)

승인 거부

소스를 신뢰할 수 없다면 거부합니다. 해당 소스는 사용되지 않고 다른 eligible 소스로 폴백합니다:

euleragent approve deny ap_001 --reason "Tavily 비용 초과 우려"

--edit-params로 소스 변경

승인 시 라우팅 대상 소스를 변경할 수 있습니다. SearchRouter가 mcp_web_search를 선택했지만, 다른 소스를 사용하고 싶은 경우:

euleragent approve accept ap_001 \
  --actor "user:you" \
  --edit-params '{"resolved_source_id": "mcp_document_search"}'

이렇게 하면 resolved_source_idmcp_document_search로 변경되어, 다음 실행에서 해당 소스가 우선 사용됩니다.

참고: --edit-params로 변경한 값은 승인 레코드의 final_params에 기록되어 감사 추적이 가능합니다.


Step 7: source_sets로 검색 소스 그룹 관리

다양한 용도에 맞게 검색 소스를 그룹으로 묶어 관리할 수 있습니다.

workspace.yaml에 source_sets 설정

# .euleragent/config/workspace.yaml

mcp:
  enabled: true
  servers:
    - id: tavily_search
      url: http://localhost:3001/mcp
      allow_tools: [web_search]
      cost_tier: paid

    - id: internal_kb
      url: http://localhost:3002/mcp
      allow_tools: [document_search]
      cost_tier: free

    - id: arxiv_search
      url: http://localhost:3003/mcp
      allow_tools: [paper_search]
      cost_tier: free

  source_sets:
    default:
      - internal_kb
      - tavily_search
    academic:
      - internal_kb
      - arxiv_search
    offline:
      - internal_kb
    news:
      - tavily_search

--source-set으로 실행별 소스 집합 지정

# 학술 리서치 → 내부 KB + arXiv만 사용
euleragent run research-agent \
  --task "트랜스포머 아키텍처 최신 논문 분석" \
  --mode plan \
  --source-set academic

# 뉴스 수집 → Tavily만 사용
euleragent run research-agent \
  --task "오늘의 AI 뉴스 요약" \
  --mode plan \
  --source-set news

# 오프라인 모드 → 내부 KB만 사용
euleragent run research-agent \
  --task "내부 문서 기반 FAQ 생성" \
  --mode plan \
  --source-set offline

--source-set을 지정하지 않으면 workspace.yamlsource_sets.default가 사용됩니다.


Step 8: 카탈로그 스냅샷과 재현성

스냅샷의 목적

모든 실행은 시작 시점의 검색 소스 구성을 스냅샷으로 고정합니다. 이를 통해:

스냅샷 파일 확인

실행 완료 후 아티팩트 디렉토리에서 스냅샷을 확인합니다:

# 검색 소스 스냅샷
cat .euleragent/runs/a1b2c3d4/artifacts/search_sources_snapshot.json
{
  "sources": [
    {
      "id": "brave",
      "type": "native",
      "provider_name": "brave",
      "enabled": true,
      "require_approval": true,
      "risk_level": "high",
      "cost_tier": "paid"
    },
    {
      "id": "mcp_web_search",
      "type": "mcp",
      "provider_name": "mcp",
      "enabled": false,
      "require_approval": true,
      "risk_level": "high",
      "cost_tier": "paid",
      "mcp_server_id": "tavily_search",
      "mcp_tool_name": "web_search"
    }
  ],
  "created_at": 1740400000.0,
  "hash": "sha256:a3f8d21..."
}
# MCP 카탈로그 스냅샷
cat .euleragent/runs/a1b2c3d4/artifacts/mcp_catalog_snapshot.json
{
  "catalogs": [
    {
      "server_id": "tavily_search",
      "tools": [
        {
          "name": "web_search",
          "description": "Search the web using Tavily API",
          "input_schema": {"type": "object", "properties": {"query": {"type": "string"}}},
          "server_id": "tavily_search"
        }
      ],
      "fetched_at": 1740400000.0,
      "hash": "sha256:b4c9e32...",
      "stale": false
    }
  ],
  "snapshot_hash": "sha256:a3f8d21..."
}

두 실행의 스냅샷 비교

# 해시로 빠르게 비교
diff <(jq '.hash' .euleragent/runs/RUN1/artifacts/search_sources_snapshot.json) \
     <(jq '.hash' .euleragent/runs/RUN2/artifacts/search_sources_snapshot.json)

해시가 동일하면 두 실행에서 동일한 검색 소스 구성을 사용한 것입니다.


Step 9: fail-closed 동작

list_changed란?

MCP 서버는 도구 목록이 변경되면 list_changed 알림을 보냅니다. euleragent는 이 알림을 받으면 해당 카탈로그를 stale(유효기간 만료)로 표시하고, 해당 소스의 검색을 차단합니다.

fail-closed 정책

euleragent는 fail-closed(폐쇄 실패) 정책을 따릅니다. 알 수 없는 변경이 감지되면 차단하고 사람의 개입을 요구합니다:

[run] MCP catalog stale for server 'tavily_search' (list_changed=true)
[run] Source 'mcp_web_search' blocked: mcp_catalog_stale
[run] Falling back to native sources only

stale 상태의 의미

상태 SearchRouter 동작 필요한 조치
stale: false 정상적으로 소스 사용 없음
stale: true 해당 소스 자동 skip mcp sync 실행 필요

복구 방법

# 카탈로그 재동기화
euleragent mcp sync

동기화 후 새로운 도구 목록이 반영되고, stale 상태가 해제됩니다. 만약 도구가 추가되거나 변경되었다면 새로운 카탈로그 해시가 생성됩니다.

보안적 의미

fail-closed 정책은 다음을 방지합니다:

원칙: "모르는 것은 차단한다." euleragent의 deny-all 정책은 MCP 소스에도 동일하게 적용됩니다.


Step 10: 다중 MCP 서버와 신뢰 수준

신뢰 수준별 서버 구성

실전 환경에서는 내부 서버와 외부 서버를 구분하여 관리합니다:

# .euleragent/config/workspace.yaml

mcp:
  enabled: true
  servers:
    # 내부 서버 — 승인 불필요, 항상 사용 가능
    - id: company_kb
      url: http://internal-mcp.company.local:3000/mcp
      allow_tools:
        - document_search
        - code_search
      cost_tier: free
      risk_default: low
      require_approval_enable: false  # 내부 서버이므로 자동 활성화

    # 외부 유료 서버 — 승인 필요
    - id: tavily_search
      url: http://localhost:3001/mcp
      allow_tools:
        - web_search
      cost_tier: paid
      risk_default: high
      require_approval_enable: true   # 외부 서비스, 승인 필수

    # 외부 무료 서버 — 승인 필요 (데이터 유출 위험)
    - id: duckduckgo_search
      url: http://localhost:3004/mcp
      allow_tools:
        - web_search
      cost_tier: free
      risk_default: medium
      require_approval_enable: true   # 외부 전송이므로 승인 필요

  source_sets:
    default:
      - company_kb
      - tavily_search
    internal_only:
      - company_kb
    full:
      - company_kb
      - tavily_search
      - duckduckgo_search

서버별 승인 동작 차이

서버 require_approval_enable 동작
company_kb false 즉시 사용 가능, 승인 불필요
tavily_search true 첫 사용 시 source_enable 승인 필요
duckduckgo_search true 첫 사용 시 source_enable 승인 필요

allow_tools로 도구 범위 제한

MCP 서버가 많은 도구를 제공하더라도, allow_tools로 euleragent에서 사용할 도구를 제한할 수 있습니다:

- id: multi_tool_server
  url: http://localhost:3005/mcp
  allow_tools:
    - web_search       # 허용
    - news_search      # 허용
    # image_generate 등 나머지 도구는 차단됨

allow_tools에 포함되지 않은 도구는 카탈로그에 등록되지 않으며, SearchRouter가 라우팅하지 않습니다. 이는 euleragent의 deny-all 원칙과 일치합니다.

실전 예제: 비용 인식 라우팅

SearchRouter는 소스 선택 시 cost_tier를 고려합니다. 기본적으로 free 소스를 우선 시도하고, 결과가 부족하면 paid 소스로 확장합니다:

# 비용 민감 태스크 — 내부 KB 우선
euleragent run research-agent \
  --task "사내 API 문서에서 인증 방법 찾기" \
  --mode plan \
  --source-set internal_only

# 종합 리서치 — 모든 소스 활용
euleragent run research-agent \
  --task "경쟁사 API 게이트웨이 비교 분석" \
  --mode plan \
  --source-set full

검색 라우팅 결정 추적

모든 검색 라우팅 결정은 search_routing.jsonl에 기록됩니다:

cat .euleragent/runs/a1b2c3d4/artifacts/search_routing.jsonl
{
  "query": "API gateway comparison 2025",
  "selected_sources": ["mcp_web_search"],
  "candidate_sources": ["mcp_web_search", "mcp_document_search"],
  "routing_reason": "mcp_source_selected",
  "timestamp": 1740400123.456
}

이 로그를 통해 각 검색이 어떤 소스로 라우팅되었는지, 다른 후보는 무엇이었는지 사후에 확인할 수 있습니다.


전체 흐름 요약

1. workspace.yaml에 mcp 설정 추가
                 │
                 ▼
2. euleragent mcp sync
   (서버별 tools/list 호출 → 카탈로그 생성)
                 │
                 ▼
3. euleragent mcp show
   (소스 상태 확인)
                 │
                 ▼
4. euleragent run --task "..." --source-set default
   (SearchRouter가 소스 선택 → 승인 필요 시 source_enable 생성)
                 │
                 ▼
5. euleragent approve accept <id> --actor "user:you"
   (외부 소스 승인)
                 │
                 ▼
6. 재실행 → 승인된 소스로 검색 수행
                 │
                 ▼
7. 결과 확인:
   - search_routing.jsonl (라우팅 결정)
   - search_sources_snapshot.json (소스 구성)
   - mcp_catalog_snapshot.json (MCP 카탈로그)

자주 묻는 질문

Q: MCP 서버를 직접 구축할 수 있나요?

네. MCP는 JSON-RPC 2.0 표준을 따르므로, tools/listtools/call을 구현하는 HTTP 서버를 만들면 됩니다. Python으로 간단한 MCP 서버를 구축하는 예시:

# 간단한 MCP 서버 (Flask 기반)
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/mcp", methods=["POST"])
def handle_rpc():
    data = request.json
    method = data.get("method")

    if method == "tools/list":
        return jsonify({
            "jsonrpc": "2.0",
            "id": data["id"],
            "result": {
                "tools": [
                    {
                        "name": "custom_search",
                        "description": "Search internal database",
                        "inputSchema": {
                            "type": "object",
                            "properties": {"query": {"type": "string"}},
                            "required": ["query"],
                        },
                    }
                ]
            },
        })
    elif method == "tools/call":
        # 실제 검색 로직 구현
        query = data["params"]["arguments"]["query"]
        return jsonify({
            "jsonrpc": "2.0",
            "id": data["id"],
            "result": {
                "content": [{"type": "text", "text": f"Results for: {query}"}]
            },
        })

    return jsonify({"jsonrpc": "2.0", "id": data["id"], "error": {"code": -32601, "message": "Method not found"}})

Q: MCP 없이도 웹 검색을 사용할 수 있나요?

네. MCP는 선택 사항입니다. mcp.enabled: false(기본값)이면 기존의 네이티브 검색 프로바이더(Brave, Tavily 직접 연결)를 그대로 사용할 수 있습니다. MCP는 여러 검색 소스를 통합 관리하고 싶을 때 사용합니다. 네이티브 검색 설정은 07_web_rag.md를 참조하세요.


Q: source_enable 승인을 일괄 처리할 수 있나요?

네. accept-all을 사용하면 됩니다:

# web.search 관련 소스 승인만 일괄 수락
euleragent approve accept-all --tool web.search --actor "user:you"

다른 도구(file.write, shell.exec 등)의 승인은 영향받지 않습니다.


Q: 카탈로그가 stale 상태인데 무시하고 실행하고 싶습니다.

보안상 stale 소스는 강제로 사용할 수 없습니다. 반드시 euleragent mcp sync로 재동기화한 후 실행하세요. 이것은 euleragent의 fail-closed 정책입니다.


자주 하는 실수 (순서 어긋남)

증상 원인 복구
MCP is not enabled. Set mcp.enabled: true in: <path> MCP 비활성화 workspace.yaml에서 mcp.enabled: true 설정
No MCP servers configured. 서버 미설정 workspace.yamlmcp.servers 섹션 추가
MCP 카탈로그 stale 동기화 미실행 euleragent mcp sync

다음 단계: 09_scoped_llm_profile.md — 스코프별 LLM 프로필 설정과 외부 LLM 승인 워크플로우를 학습합니다.

← 이전 목록으로 다음 →