Harness Academy
Tài liệu gốc 19 12 phútTiếng Việt + English
docs/en/s16-mcp.mdXem bản gốc
Ngôn ngữ bài đọc
Bản dịch tiếng Việt

Bản dịch đầy đủ theo đúng cấu trúc tài liệu; code, liên kết và định danh kỹ thuật được giữ nguyên.

s16: Tích hợp MCP

s00 > s01 > s02 > s03 > s04 > s05 > s06 > s07 > s08 > s09 > s10 > s11 > s12 > s13 > s14 > s15 > [ s16 ] > s17 > s18 > s19 > s20 > s21 > s22 > s23 > s24 > s25 > s26 > s27

Các công cụ tích hợp sẵn có trong cơ sở mã của bạn; Các công cụ MCP đến từ các quy trình bên ngoài. Nhưng đối với agent, cả hai kiểu công cụ đều được sử dụng theo cách giống hệt nhau.

Giao thức máy khách-máy chủ MCP

Chương này giải quyết vấn đề gì

Hệ thống công cụ từ s02 có một điều kiện tiên quyết: các công cụ được mã hóa cứng trong cơ sở mã. Để thêm công cụ GitHub, bạn viết một github_tool.py gọi API GitHub, sau đó registry.register().

Nhưng nếu cộng đồng đã có sẵn máy chủ GitHub MCP có thể liệt kê các vấn đề, tạo PR và tìm kiếm mã -- bạn chỉ cần khởi động nó và agent có thể sử dụng tất cả các khả năng đó. Không cần mã công cụ.

MCP (Giao thức bối cảnh mô hình) là yếu tố giúp điều này trở nên khả thi: một giao thức tiêu chuẩn cho phép các quy trình bên ngoài bộc lộ khả năng của chúng với một agent.

Đề xuất đọc

  • s02-tool-system.md -- đăng ký và gửi công cụ; Các công cụ MCP tái sử dụng cùng một hệ thống
  • s08-skill-system.md -- công cụ so với kỹ năng so với MCP: hiểu sự khác biệt

Thuật ngữ chính

MCP là gì

Giao thức bối cảnh mô hình - một giao thức mở xác định cách "nhà cung cấp công cụ" và "người tiêu dùng công cụ" giao tiếp. agent Hermes là người tiêu dùng (khách hàng MCP); các quy trình bên ngoài là nhà cung cấp (máy chủ MCP).

Giao thức chỉ định ba điều:

  • Cách khám phá các công cụ (máy chủ nói với khách hàng "đây là những công cụ tôi có")
  • Cách gọi tool (client gửi request, server trả về kết quả)
  • Cách truyền tải tin nhắn (hai lựa chọn: ống stdio hoặc HTTP)

Máy chủ MCP là gì

Một quy trình độc lập hiển thị một bộ công cụ thông qua giao thức MCP. Ví dụ:

  • @modelcontextprotocol/server-github -- cung cấp các công cụ API GitHub
  • @modelcontextprotocol/server-filesystem -- cung cấp các công cụ hệ thống tập tin
  • Tập lệnh Python do bạn tự viết -- miễn là nó triển khai giao thức MCP

Vận chuyển stdio và vận chuyển HTTP là gì

Máy chủ MCP có thể giao tiếp với máy khách theo hai cách:

  • stdio: Hermes bắt đầu một quy trình con (ví dụ: npx @modelcontextprotocol/server-github) và giao tiếp qua ống stdin/stdout. Đơn giản, chạy cục bộ.
  • HTTP: Máy chủ MCP chạy từ xa; Hermes giao tiếp thông qua các yêu cầu HTTP. Thích hợp cho các dịch vụ đám mây và máy chủ dùng chung.

Bắt đầu với việc thực hiện đơn giản nhất

Bạn muốn agent sử dụng API GitHub. Cách tiếp cận trực tiếp nhất: viết một công cụ tích hợp sẵn.

def handle_github_list_issues(args, **kwargs):
    import requests
    resp = requests.get(
        f"https://api.github.com/repos/{args['repo']}/issues",
        headers={"Authorization": f"token {GITHUB_TOKEN}"},
    )
    return json.dumps(resp.json()[:10])

registry.register(
    name="github_list_issues",
    toolset="github",
    schema={...},
    handler=handle_github_list_issues,
)

Điều này hoạt động. Nhưng có ba vấn đề.

Vấn đề 1: Bạn phải xây dựng mọi API từ đầu

API GitHub có hàng tá điểm cuối: liệt kê các vấn đề, tạo PR, mã tìm kiếm, quản lý bản phát hành... Mỗi điểm cuối đều yêu cầu viết trình xử lý, xác định lược đồ, xử lý lỗi, xử lý phân trang. Trong khi đó, cộng đồng đã thực hiện tất cả những điều này, được đóng gói dưới dạng máy chủ MCP.

Vấn đề 2: Công cụ được liên kết chặt chẽ với agent

Các công cụ tích hợp chạy bên trong quy trình agent. Nếu một công cụ phụ thuộc vào Node.js (máy chủ GitHub MCP được viết bằng Node), agent Python của bạn cũng cần cài đặt runtime Node. Nếu công cụ gặp sự cố, agent sẽ gặp sự cố với nó.

Một máy chủ MCP chạy trong tiến trình riêng của nó. Nếu nó gặp sự cố, agent sẽ không bị ảnh hưởng -- chỉ cần khởi động lại nó.

Vấn đề 3: Chưa có cơ chế khám phá thống nhất

Hôm nay bạn tích hợp GitHub, ngày mai Jira, ngày hôm sau Slack. Mỗi loại có một API khác nhau, xác thực khác nhau, định dạng lược đồ khác nhau. Không có cách thống nhất nào để agent "khám phá" những công cụ này.

MCP thống nhất cả ba mối quan tâm: khám phá (list_tools), gọi (call_tool) và truyền tải (stdio / HTTP).

Mô hình tinh thần tối thiểu

At agent startup:

config.yaml has two MCP servers configured
    |
    v
Hermes starts two external processes (or connects to two HTTP endpoints)
    |
    v
Asks each server: "What tools do you have?"  <- list_tools
    |
    v
Registers them in s02's registry <- names prefixed: mcp_github_list_issues
    |
    v
The tool list the agent sees = built-in tools + MCP tools (mixed together, no distinction)

At runtime:

The agent calls mcp_github_list_issues(repo="...")
    |
    v
The registry finds the handler -> handler forwards the request to the GitHub MCP server
    |
    v
The MCP server calls the GitHub API -> returns the result
    |
    v
The handler returns the result to the agent (same format as built-in tools)

Đối với agent, không có sự khác biệt giữa công cụ MCP và công cụ tích hợp. Nó không biết -- và không cần biết -- rằng mcp_github_list_issues được hỗ trợ bởi một quy trình bên ngoài gọi API GitHub.

Cấu trúc dữ liệu chính

Cấu hình máy chủ MCP

# ~/.hermes/config.yaml
mcp_servers:
  github:
    command: "npx"                              # stdio transport
    args: ["@modelcontextprotocol/server-github"]
    env:
      GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_TOKEN}"
    tools:
      include: [list_issues, create_issue, search_code]   # Only enable these three

  analytics:
    url: "https://mcp.example.com/analytics"    # HTTP transport
    headers:
      Authorization: "Bearer ${ANALYTICS_KEY}"

Quy ước đặt tên công cụ

Khi các công cụ MCP được đăng ký trong sổ đăng ký, tên của chúng có tiền tố là mcp_<server_name>_:

MCP server "github" provides a tool "list_issues"
  -> registered as "mcp_github_list_issues"
  -> toolset is "mcp-github"

MCP server "analytics" provides a tool "query"
  -> registered as "mcp_analytics_query"
  -> toolset is "mcp-analytics"

Tiền tố tồn tại để tránh xung đột tên với các công cụ tích hợp. Nếu có tích hợp sẵn read_file và máy chủ MCP cũng cung cấp read_file, nó được đăng ký là mcp_filesystem_read_file - cả hai cùng tồn tại mà không có sự can thiệp. Nếu có xung đột thực sự (không có tiền tố), công cụ tích hợp sẽ được ưu tiên và công cụ MCP sẽ bị bỏ qua.

Thực hiện tối thiểu

Bước 1: Khởi động máy chủ MCP và khám phá các công cụ

import subprocess, json

def discover_mcp_tools(server_name: str, config: dict) -> list[dict]:
    """Start an MCP server and ask it what tools it has."""

    # Start subprocess (stdio transport)
    proc = subprocess.Popen(
        [config["command"]] + config.get("args", []),
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        env=config.get("env", {}),
    )

    # MCP protocol: send initialize request
    send_jsonrpc(proc.stdin, "initialize", {
        "protocolVersion": "2025-03-26",
        "capabilities": {},
        "clientInfo": {"name": "hermes-agent"},
    })
    read_jsonrpc(proc.stdout)  # Read initialize response

    # Send tools/list request
    send_jsonrpc(proc.stdin, "tools/list", {})
    result = read_jsonrpc(proc.stdout)

    return result["tools"]
    # -> [{"name": "list_issues", "description": "...", "inputSchema": {...}}, ...]

agent Hermes thực tế sử dụng SDK Python MCP (from mcp import ClientSession) thay vì gửi JSON-RPC theo cách thủ công, nhưng công việc cơ bản là giống nhau.

Bước 2: Đăng ký công cụ MCP trong sổ đăng ký

def register_mcp_tools(server_name: str, tools: list[dict], config: dict):
    """Register an MCP server's tools in s02's registry."""

    # Tool filtering
    include = config.get("tools", {}).get("include")
    exclude = config.get("tools", {}).get("exclude", [])

    for tool in tools:
        name = tool["name"]

        # Allowlist / blocklist filtering
        if include and name not in include:
            continue
        if name in exclude:
            continue

        # Add prefix
        prefixed_name = f"mcp_{server_name}_{name}"

        # Check for collision with built-in tools
        if registry.has_tool(prefixed_name):
            print(f"  [mcp] {prefixed_name} collides with built-in, skipped")
            continue

        # Create handler: forward calls to the MCP server
        handler = make_mcp_handler(server_name, name)

        registry.register(
            name=prefixed_name,
            toolset=f"mcp-{server_name}",
            schema={"name": prefixed_name, "description": tool["description"],
                    "parameters": tool["inputSchema"]},
            handler=handler,
        )

Bước 3: Trình xử lý công cụ MCP -- chuyển tiếp cuộc gọi

Đây là bước quan trọng nhất. Trình xử lý trong sổ đăng ký là một hàm đồng bộ, nhưng các lệnh gọi MCP không đồng bộ. Cần có một cây cầu:

# Global: MCP connection pool
_servers: dict[str, MCPServerTask] = {}  # server_name -> connection object

def make_mcp_handler(server_name: str, tool_name: str):
    """Create a handler that forwards calls to the MCP server."""

    def handler(args: dict, **kwargs) -> str:
        server = _servers.get(server_name)
        if not server or not server.session:
            return json.dumps({"error": f"MCP server '{server_name}' not connected"})

        # Async call, wait synchronously for the result
        async def _call():
            result = await server.session.call_tool(tool_name, arguments=args)
            if result.isError:
                return json.dumps({"error": str(result.content)})
            text = "\n".join(item.text for item in result.content if hasattr(item, "text"))
            return json.dumps({"result": text})

        return run_on_mcp_loop(_call())

    return handler

run_on_mcp_loop() lên lịch coroutine không đồng bộ vào vòng lặp sự kiện nền và chặn cho đến khi kết quả sẵn sàng:

_mcp_loop: asyncio.AbstractEventLoop = None  # Background event loop

def run_on_mcp_loop(coro, timeout=30) -> str:
    """Execute an async call on the background event loop, waiting synchronously for the result."""
    future = asyncio.run_coroutine_threadsafe(coro, _mcp_loop)
    return future.result(timeout=timeout)

Tại sao một vòng lặp sự kiện nền? Bởi vì kết nối MCP là kết nối không đồng bộ tồn tại lâu dài (ống stdio hoặc kết nối HTTP liên tục). Chúng chạy trên một luồng nền chuyên dụng, độc lập với vòng lặp chính đồng bộ của agent.

Hướng dẫn đầy đủ

1. Agent starts up
   -> Reads config.yaml, finds a github MCP server configured
   -> Starts npx @modelcontextprotocol/server-github subprocess
   -> Sends tools/list -> gets [list_issues, create_issue, search_code]
   -> Registers as mcp_github_list_issues, mcp_github_create_issue, mcp_github_search_code

2. Agent is running
   -> The model decides to call mcp_github_list_issues(repo="hermes-agent")
   -> The registry finds the handler -> handler forwards via MCP protocol to the GitHub server
   -> The GitHub server calls the GitHub API -> returns the issue list
   -> The handler returns the result to the agent
   -> The agent presents the issue list to the user

3. Agent shuts down
   -> Sends a shutdown signal to each MCP server
   -> Waits for subprocesses to exit (force kill after 10-second timeout)

Cách nó cắm vào vòng lặp chính

Các công cụ MCP và các công cụ tích hợp có chung sổ đăng ký. Vòng lặp cốt lõi không yêu cầu thay đổi.

Core loop
  |  tool_call: mcp_github_list_issues(repo="hermes-agent")
  v
registry.dispatch("mcp_github_list_issues", args)
  |  <- same dispatch as calling a built-in tool
  v
mcp handler -> run_on_mcp_loop -> server.session.call_tool
  |
  v
GitHub MCP server (external process)
  |  Calls GitHub API -> returns result
  v
handler returns json -> registry -> core loop

Vòng lặp cốt lõi không biết công cụ này đến từ một quy trình bên ngoài. Tất cả những gì nó thấy là register.dispatch trả về một chuỗi.

Những lỗi thường gặp của người mới bắt đầu

1. Truyền khóa API trong các biến môi trường máy chủ MCP

Bạn đã định cấu hình env: { OPENAI_API_KEY: "..." } trong config.yaml -- điều này làm rò rỉ khóa LLM của bạn sang một quy trình bên ngoài.

Khắc phục: Chỉ truyền các biến môi trường mà máy chủ MCP thực sự cần (ví dụ: GITHUB_TOKEN). Theo mặc định, Hermes chỉ chuyển các biến cơ sở an toàn (PATH, HOME, v.v.); chỉ các biến do người dùng định cấu hình mới được chuyển tiếp.

2. Đăng ký tất cả các công cụ mà không cần lọc

Một máy chủ MCP có thể hiển thị 50 công cụ. Việc đăng ký tất cả chúng sẽ mang lại cho mô hình một danh sách công cụ rất dài, khiến việc lựa chọn trở nên khó khăn hơn và tăng mức tiêu thụ token.

Khắc phục: Sử dụng tools.include dưới dạng danh sách cho phép để chỉ bật những danh sách bạn cần.

3. Không nhận thấy khi máy chủ MCP gặp sự cố

Nếu quy trình con stdio gặp sự cố, agent chỉ phát hiện ra kết nối bị hỏng khi nó cố gắng thực hiện cuộc gọi và trả về lỗi.

Khắc phục: MCPServerTask của Hermes có logic kết nối lại tự động -- 3 lần thử lại khi kết nối ban đầu không thành công, 5 lần thử lại khi ngắt kết nối giữa chừng, với thời gian chờ theo cấp số nhân.

4. Xung đột tên giữa công cụ MCP và công cụ tích hợp

Một máy chủ MCP cung cấp một read_file va chạm với phần tích hợp sẵn read_file.

Khắc phục: Các công cụ MCP được tự động thêm tiền tố vào mcp_<server>_. Nếu xung đột vẫn xảy ra, công cụ tích hợp sẽ được ưu tiên và công cụ MCP sẽ bị bỏ qua.

Phạm vi của chương này

Chương này chỉ đề cập đến agent Hermes với tư cách là khách hàng MCP (sử dụng các công cụ bên ngoài).

Nó bao gồm ba điều:

  1. Cách đăng ký các công cụ MCP trong sổ đăng ký -- khám phá -> bộ lọc -> tiền tố -> trình xử lý đăng ký
  2. Chuỗi lệnh gọi đầy đủ để gọi công cụ MCP -- trình xử lý -> vòng lặp sự kiện nền -> máy chủ MCP -> kết quả
  3. Cầu nối đồng bộ/không đồng bộ -- sổ đăng ký là đồng bộ, MCP không đồng bộ và vòng lặp sự kiện nền kết nối cả hai

Không được bảo hiểm:

  • Hermes với tư cách là máy chủ MCP (hiển thị khả năng nhắn tin cho khách hàng bên ngoài) -> hướng tích hợp ngược
  • Chi tiết JSON-RPC của giao thức MCP -> SDK xử lý nó; không cần phải xây dựng JSON theo cách thủ công
  • Luồng xác thực OAuth 2.1 PKCE -> cải tiến cho việc truyền tải HTTP
  • Lấy mẫu (máy chủ MCP yêu cầu tạo LLM ngược) -> tính năng nâng cao
  • Tài nguyên và lời nhắc MCP -> các khả năng phụ trợ không ảnh hưởng đến luồng gọi công cụ cốt lõi

Chương này liên quan đến những chương khác như thế nào

  • s02 xác định đăng ký và gửi công cụ -> Công cụ MCP sử dụng lại cùng một hệ thống
  • Hệ thống kỹ năng của s08 cho phép agent tạo/chỉnh sửa khả năng -> MCP là nguồn khả năng thứ ba: công cụ (mã), kỹ năng (đánh dấu), MCP (quy trình bên ngoài)
  • Phần phụ trợ đầu cuối của s14 tóm tắt "nơi lệnh chạy" -> MCP tóm tắt "nơi công cụ chạy" -- cách tiếp cận tách tương tự

Sau khi kết thúc chương này, bạn sẽ có thể trả lời

  • Có sự khác biệt nào giữa công cụ tích hợp và công cụ MCP nhìn từ góc độ của agent không?
  • Sau khi công cụ của máy chủ MCP được đăng ký vào sổ đăng ký, tên của nó trông như thế nào? Tại sao tiền tố?
  • Nếu một máy chủ MCP cung cấp 50 công cụ nhưng bạn chỉ muốn có 3 công cụ thì bạn cấu hình nó như thế nào?
  • Trình xử lý đăng ký đồng bộ nhưng các lệnh gọi MCP không đồng bộ. Hermes giải quyết sự không phù hợp này như thế nào?
  • Nếu máy chủ MCP gặp sự cố thì agent cũng bị sự cố? Tại sao hoặc tại sao không?

Một câu cần nhớ: MCP cho phép các công cụ từ các quy trình bên ngoài đăng ký trong cùng một sổ đăng ký và agent gọi chúng mà không có sự khác biệt nào so với các công cụ tích hợp sẵn.

Mục lục nguồn