MCP 协议集成:扩展 Hermes 的能力边界

TL;DR: 本教程深入讲解如何通过 Model Context Protocol (MCP) 将 Hermes Agent 与外部工具无缝集成。你将学习构建自定义 MCP 服务器、连接第三方 API、实现实时数据获取,以及构建一个集成 GitHub、Notion 和自定义数据源的智能工作流。完成本部分后,你的 Hermes Agent 将具备与任何外部系统交互的能力。

🎯 学习目标

完成本教程后,你将能够:

  1. 理解 MCP 协议的核心架构与通信模型
  2. 构建自定义 MCP 服务器(Python/TypeScript)
  3. 实现与 GitHub API、Notion API 的深度集成
  4. 创建支持实时数据获取的 Hermes 技能
  5. 调试和优化 MCP 连接性能

📋 前置知识回顾

在开始之前,确保你已掌握:

如果你需要复习,建议先阅读 [Part 3: Skill 系统深度解析] 中关于 hermes skill createhermes skill test 的部分。

🔧 MCP 协议基础

什么是 MCP?

Model Context Protocol (MCP) 是 Anthropic 于 2024 年提出的开放协议,旨在标准化 AI 模型与外部工具的通信方式。与传统的 REST API 不同,MCP 专门为 LLM 交互设计,支持:

Hermes Agent 中的 MCP 架构

┌─────────────────────────────────────────┐
│           Hermes Agent                  │
│  ┌──────────────────────────────────┐   │
│  │        MCP Client Layer         │   │
│  │  (hermes mcp connect/discover)   │   │
│  └──────────┬───────────────────────┘   │
│             │                            │
│  ┌──────────▼───────────────────────┐   │
│  │      Skill Execution Engine      │   │
│  │  (hermes skill run --mcp)        │   │
│  └──────────┬───────────────────────┘   │
└─────────────┼───────────────────────────┘

    ┌─────────▼─────────┐
    │   MCP Server(s)   │
    │  (Python/TS/Go)   │
    └─────────┬─────────┘

    ┌─────────▼─────────┐
    │  External APIs    │
    │ (GitHub/Notion/DB)│
    └───────────────────┘

🚀 实战:构建你的第一个 MCP 服务器

环境准备

# 确保 Hermes Agent 版本 >= 2.5.0
hermes --version
# 应输出: Hermes Agent v2.5.0 (MCP support enabled)

# 安装 MCP SDK
pip install mcp-python-sdk==0.8.0

# 验证安装
python -c "from mcp import Server; print('MCP SDK ready')"

项目结构

mcp-servers/
├── github-server/
│   ├── server.py
│   ├── requirements.txt
│   └── config.yaml
├── notion-server/
│   ├── server.py
│   ├── requirements.txt
│   └── config.yaml
└── custom-server/
    ├── server.py
    ├── data_processor.py
    └── config.yaml

案例 1:GitHub 数据获取服务器

这是最实用的集成场景之一。我们将构建一个 MCP 服务器,让 Hermes Agent 能实时获取 GitHub 仓库的统计数据和 Issue 信息。

server.py:

# github-server/server.py
from mcp import Server, Tool, ToolContext
from typing import Dict, List, Optional
import httpx
import os
from datetime import datetime, timedelta

class GitHubMCPServer(Server):
    def __init__(self):
        super().__init__(
            name="github-data-server",
            version="1.0.0",
            description="Real-time GitHub repository data access"
        )
        self.github_token = os.getenv("GITHUB_TOKEN")
        self.base_url = "https://api.github.com"
        
    async def register_tools(self):
        """注册所有可用工具"""
        
        # 工具 1: 获取仓库统计
        self.register_tool(Tool(
            name="get_repo_stats",
            description="获取指定仓库的详细统计信息,包括星标、Fork、Issue 等",
            parameters={
                "owner": {"type": "string", "description": "仓库所有者"},
                "repo": {"type": "string", "description": "仓库名称"},
                "include_contributors": {
                    "type": "boolean", 
                    "description": "是否包含贡献者统计",
                    "default": False
                }
            },
            handler=self.handle_get_repo_stats
        ))
        
        # 工具 2: 搜索 Issue
        self.register_tool(Tool(
            name="search_issues",
            description="搜索仓库中的 Issue 和 Pull Request",
            parameters={
                "owner": {"type": "string"},
                "repo": {"type": "string"},
                "state": {
                    "type": "string",
                    "enum": ["open", "closed", "all"],
                    "default": "open"
                },
                "labels": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "按标签过滤"
                },
                "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 50,
                    "default": 10
                }
            },
            handler=self.handle_search_issues
        ))
        
        # 工具 3: 获取活跃开发者
        self.register_tool(Tool(
            name="get_active_developers",
            description="获取最近 30 天最活跃的贡献者",
            parameters={
                "owner": {"type": "string"},
                "repo": {"type": "string"},
                "days": {
                    "type": "integer",
                    "default": 30
                }
            },
            handler=self.handle_get_active_developers
        ))
    
    async def handle_get_repo_stats(self, params: Dict, context: ToolContext) -> Dict:
        """处理仓库统计请求"""
        owner = params["owner"]
        repo = params["repo"]
        
        headers = {"Authorization": f"token {self.github_token}"}
        
        async with httpx.AsyncClient() as client:
            # 并行获取多个数据源
            repo_resp, issues_resp, prs_resp = await asyncio.gather(
                client.get(f"{self.base_url}/repos/{owner}/{repo}", headers=headers),
                client.get(f"{self.base_url}/repos/{owner}/{repo}/issues?state=open", headers=headers),
                client.get(f"{self.base_url}/repos/{owner}/{repo}/pulls?state=open", headers=headers)
            )
            
            if repo_resp.status_code != 200:
                return {"error": f"Repository not found: {repo_resp.status_code}"}
            
            repo_data = repo_resp.json()
            
            result = {
                "repository": f"{owner}/{repo}",
                "stars": repo_data["stargazers_count"],
                "forks": repo_data["forks_count"],
                "open_issues": repo_data["open_issues_count"],
                "open_pull_requests": len(prs_resp.json()),
                "last_updated": repo_data["updated_at"],
                "primary_language": repo_data.get("language"),
                "topics": repo_data.get("topics", []),
                "license": repo_data.get("license", {}).get("spdx_id", "N/A")
            }
            
            # 可选:获取贡献者统计
            if params.get("include_contributors"):
                contrib_resp = await client.get(
                    f"{self.base_url}/repos/{owner}/{repo}/contributors?per_page=5",
                    headers=headers
                )
                if contrib_resp.status_code == 200:
                    result["top_contributors"] = [
                        {"login": c["login"], "contributions": c["contributions"]}
                        for c in contrib_resp.json()
                    ]
            
            return result
    
    async def handle_search_issues(self, params: Dict, context: ToolContext) -> Dict:
        """搜索 Issue 和 PR"""
        owner = params["owner"]
        repo = params["repo"]
        state = params.get("state", "open")
        labels = params.get("labels", [])
        limit = params.get("limit", 10)
        
        headers = {"Authorization": f"token {self.github_token}"}
        
        # 构建查询参数
        query_parts = [f"repo:{owner}/{repo}", f"state:{state}"]
        if labels:
            for label in labels:
                query_parts.append(f"label:{label}")
        
        query = " ".join(query_parts)
        
        async with httpx.AsyncClient() as client:
            resp = await client.get(
                f"{self.base_url}/search/issues",
                headers=headers,
                params={"q": query, "per_page": limit, "sort": "updated"}
            )
            
            if resp.status_code != 200:
                return {"error": f"Search failed: {resp.status_code}"}
            
            data = resp.json()
            
            issues = []
            for item in data["items"][:limit]:
                issues.append({
                    "number": item["number"],
                    "title": item["title"],
                    "state": item["state"],
                    "type": "pull_request" if "pull_request" in item else "issue",
                    "labels": [l["name"] for l in item["labels"]],
                    "created_at": item["created_at"],
                    "updated_at": item["updated_at"],
                    "author": item["user"]["login"],
                    "url": item["html_url"]
                })
            
            return {
                "total_count": data["total_count"],
                "issues": issues,
                "query": query
            }
    
    async def handle_get_active_developers(self, params: Dict, context: ToolContext) -> Dict:
        """获取活跃开发者"""
        owner = params["owner"]
        repo = params["repo"]
        days = params.get("days", 30)
        
        since_date = (datetime.now() - timedelta(days=days)).isoformat()
        
        headers = {"Authorization": f"token {self.github_token}"}
        
        async with httpx.AsyncClient() as client:
            # 获取最近的 commits
            resp = await client.get(
                f"{self.base_url}/repos/{owner}/{repo}/commits",
                headers=headers,
                params={"since": since_date, "per_page": 100}
            )
            
            if resp.status_code != 200:
                return {"error": f"Failed to get commits: {resp.status_code}"}
            
            commits = resp.json()
            
            # 统计每个开发者的提交数
            developer_stats = {}
            for commit in commits:
                author = commit.get("author")
                if author and author.get("login"):
                    login = author["login"]
                    if login not in developer_stats:
                        developer_stats[login] = {
                            "commits": 0,
                            "avatar": author.get("avatar_url", ""),
                            "last_commit": commit["commit"]["author"]["date"]
                        }
                    developer_stats[login]["commits"] += 1
                    developer_stats[login]["last_commit"] = commit["commit"]["author"]["date"]
            
            # 按提交数排序
            sorted_developers = sorted(
                developer_stats.items(),
                key=lambda x: x[1]["commits"],
                reverse=True
            )
            
            return {
                "repository": f"{owner}/{repo}",
                "period_days": days,
                "total_commits": len(commits),
                "active_developers": [
                    {
                        "login": login,
                        "commits": stats["commits"],
                        "last_commit": stats["last_commit"]
                    }
                    for login, stats in sorted_developers[:10]
                ]
            }

# 启动服务器
if __name__ == "__main__":
    server = GitHubMCPServer()
    server.run(host="0.0.0.0", port=8000)

config.yaml:

# github-server/config.yaml
server:
  name: github-data-server
  version: "1.0.0"
  port: 8000
  rate_limit: 30  # 每分钟请求数

authentication:
  type: token
  env_var: GITHUB_TOKEN

logging:
  level: INFO
  format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"

案例 2:Notion 数据库集成

这个服务器允许 Hermes Agent 读写 Notion 数据库,实现内容管理和知识库集成。

server.py:

# notion-server/server.py
from mcp import Server, Tool, ToolContext
from typing import Dict, List, Optional
import httpx
import os

class NotionMCPServer(Server):
    def __init__(self):
        super().__init__(
            name="notion-database-server",
            version="1.0.0",
            description="Notion database read/write operations"
        )
        self.notion_token = os.getenv("NOTION_TOKEN")
        self.base_url = "https://api.notion.com/v1"
        self.headers = {
            "Authorization": f"Bearer {self.notion_token}",
            "Notion-Version": "2022-06-28",
            "Content-Type": "application/json"
        }
        
    async def register_tools(self):
        # 工具 1: 查询数据库
        self.register_tool(Tool(
            name="query_database",
            description="查询 Notion 数据库中的条目",
            parameters={
                "database_id": {"type": "string", "description": "数据库 ID"},
                "filter": {
                    "type": "object",
                    "description": "过滤条件(Notion 过滤语法)",
                    "optional": True
                },
                "sort": {
                    "type": "array",
                    "description": "排序规则",
                    "optional": True
                },
                "page_size": {
                    "type": "integer",
                    "default": 10,
                    "maximum": 100
                }
            },
            handler=self.handle_query_database
        ))
        
        # 工具 2: 创建页面
        self.register_tool(Tool(
            name="create_database_page",
            description="在 Notion 数据库中创建新条目",
            parameters={
                "database_id": {"type": "string"},
                "properties": {
                    "type": "object",
                    "description": "页面属性(字段名: 值)"
                },
                "content": {
                    "type": "object",
                    "description": "页面内容(富文本块)",
                    "optional": True
                }
            },
            handler=self.handle_create_page
        ))
        
        # 工具 3: 更新页面
        self.register_tool(Tool(
            name="update_page",
            description="更新 Notion 页面属性",
            parameters={
                "page_id": {"type": "string"},
                "properties": {"type": "object"},
                "archived": {
                    "type": "boolean",
                    "description": "是否归档页面",
                    "optional": True
                }
            },
            handler=self.handle_update_page
        ))
    
    async def handle_query_database(self, params: Dict, context: ToolContext) -> Dict:
        """查询 Notion 数据库"""
        database_id = params["database_id"]
        page_size = params.get("page_size", 10)
        
        request_body = {"page_size": page_size}
        if "filter" in params:
            request_body["filter"] = params["filter"]
        if "sort" in params:
            request_body["sorts"] = params["sort"]
        
        async with httpx.AsyncClient() as client:
            resp = await client.post(
                f"{self.base_url}/databases/{database_id}/query",
                headers=self.headers,
                json=request_body
            )
            
            if resp.status_code != 200:
                return {"error": f"Query failed: {resp.status_code}", "details": resp.text}
            
            data = resp.json()
            
            # 转换结果为可读格式
            results = []
            for page in data["results"]:
                properties = {}
                for prop_name, prop_data in page["properties"].items():
                    properties[prop_name] = self._extract_property_value(prop_data)
                
                results.append({
                    "id": page["id"],
                    "url": page["url"],
                    "created_time": page["created_time"],
                    "last_edited_time": page["last_edited_time"],
                    "properties": properties
                })
            
            return {
                "total_results": len(results),
                "has_more": data.get("has_more", False),
                "results": results
            }
    
    def _extract_property_value(self, prop_data: Dict) -> any:
        """提取 Notion 属性值"""
        prop_type = prop_data.get("type", "")
        
        if prop_type == "title":
            return prop_data["title"][0]["plain_text"] if prop_data["title"] else ""
        elif prop_type == "rich_text":
            return prop_data["rich_text"][0]["plain_text"] if prop_data["rich_text"] else ""
        elif prop_type == "number":
            return prop_data.get("number")
        elif prop_type == "select":
            return prop_data["select"]["name"] if prop_data.get("select") else None
        elif prop_type == "multi_select":
            return [s["name"] for s in prop_data.get("multi_select", [])]
        elif prop_type == "date":
            return prop_data.get("date")
        elif prop_type == "checkbox":
            return prop_data.get("checkbox", False)
        elif prop_type == "url":
            return prop_data.get("url", "")
        elif prop_type == "email":
            return prop_data.get("email", "")
        elif prop_type == "phone_number":
            return prop_data.get("phone_number", "")
        else:
            return str(prop_data)

# 启动
if __name__ == "__main__":
    server = NotionMCPServer()
    server.run(host="0.0.0.0", port=8001)

🔗 集成到 Hermes Agent

配置 MCP 连接

# ~/.hermes/config.yaml
mcp:
  servers:
    github:
      url: "http://localhost:8000"
      timeout: 30
      retry_count: 3
      auth_token: "${GITHUB_MCP_TOKEN}"  # 可选,用于 MCP 服务器间认证
    
    notion:
      url: "http://localhost:8001"
      timeout: 15
      retry_count: 2
    
    custom:
      url: "http://localhost:8002"
      timeout: 60
      health_check_interval: 30  # 秒
  
  # 全局设置
  global:
    max_parallel_requests: 5
    cache_ttl: 300  # 缓存工具结果 5 分钟
    enable_discovery: true  # 自动发现新工具

创建集成技能

现在我们将创建一个 Hermes Skill,利用上述 MCP 服务器实现智能代码审查助手。

hermes skill create code-review-assistant

skill.yaml:

# skills/code-review-assistant/skill.yaml
name: code-review-assistant
version: "1.0.0"
description: |
  智能代码审查助手,结合 GitHub 数据和 Notion 知识库
  自动分析 PR,检查代码规范,并记录审查结果到 Notion

mcp_servers:
  - github
  - notion

triggers:
  - type: webhook
    event: github.pull_request.opened
    condition: "repository.topics contains 'review-required'"
  
  - type: command
    pattern: "review pr {owner}/{repo}#{pr_number}"
  
  - type: cron
    schedule: "0 9 * * 1-5"  # 工作日早上 9 点
    task: "check_pending_reviews"

workflow:
  steps:
    - name: fetch_pr_details
      tool: github.get_repo_stats
      params:
        owner: "{owner}"
        repo: "{repo}"
      output: repo_stats
    
    - name: analyze_pr
      tool: github.search_issues
      params:
        owner: "{owner}"
        repo: "{repo}"
        state: open
        labels: ["needs-review"]
      output: pending_prs
    
    - name: check_code_standards
      tool: notion.query_database
      params:
        database_id: "${NOTION_CODE_STANDARDS_DB}"
        filter:
          property: "Language"
          select:
            equals: "{repo.primary_language}"
      output: standards
    
    - name: generate_review
      prompt: |
        基于以下信息进行代码审查:
        
        仓库统计:{repo_stats}
        待审查 PR:{pending_prs}
        代码规范:{standards}
        
        请生成一个详细的审查报告,包括:
        1. 代码质量评估
        2. 潜在问题
        3. 改进建议
        4. 是否通过审查
        
        输出格式:JSON
      output: review_result
    
    - name: save_to_notion
      tool: notion.create_database_page
      params:
        database_id: "${NOTION_REVIEWS_DB}"
        properties:
          Title:
            title: "Review: {owner}/{repo}#{pr_number}"
          Repository:
            rich_text: "{owner}/{repo}"
          PR Number:
            number: "{pr_number}"
          Status:
            select: "{review_result.passed}"
          Review Date:
            date: "{current_date}"
        content:
          - type: heading_2
            heading_2:
              rich_text: [{text: {content: "审查报告"}}]
          - type: code
            code:
              language: json
              rich_text: [{text: {content: "{review_result.json}"}}]

dependencies:
  - name: httpx
    version: ">=0.27.0"
  - name: mcp-python-sdk
    version: ">=0.8.0"

测试集成

# 1. 启动 MCP 服务器
cd mcp-servers/github-server
GITHUB_TOKEN=ghp_xxx python server.py &
# 输出: [INFO] Starting GitHub MCP server on port 8000

cd ../notion-server
NOTION_TOKEN=secret_xxx python server.py &
# 输出: [INFO] Starting Notion MCP server on port 8001

# 2. 验证连接
hermes mcp connect --server github --url http://localhost:8000
# 输出: ✓ Connected to github-data-server v1.0.0
# 发现工具: get_repo_stats, search_issues, get_active_developers

hermes mcp connect --server notion --url http://localhost:8001
# 输出: ✓ Connected to notion-database-server v1.0.0
# 发现工具: query_database, create_database_page, update_page

# 3. 测试工具调用
hermes mcp call \
  --server github \
  --tool get_repo_stats \
  --params '{"owner": "facebook", "repo": "react", "include_contributors": true}'

# 输出示例:
# {
#   "repository": "facebook/react",
#   "stars": 220000,
#   "forks": 45000,
#   "open_issues": 1200,
#   "open_pull_requests": 85,
#   "primary_language": "JavaScript",
#   "top_contributors": [
#     {"login": "gaearon", "contributions": 1500},
#     {"login": "sophiebits", "contributions": 800}
#   ]
# }

# 4. 运行集成技能
hermes skill run code-review-assistant \
  --params '{"owner": "my-org", "repo": "my-project", "pr_number": 42}'

⚡ 高级用法:构建自定义数据处理服务器

对于需要复杂数据处理或实时信息获取的场景,我们可以构建一个更强大的自定义 MCP 服务器。

custom-server/server.py:

# custom-server/server.py
from mcp import Server, Tool, ToolContext
from typing import Dict, List, Optional
import httpx
import json
from datetime import datetime
import aiohttp
from bs4 import BeautifulSoup
import re

class DataProcessingServer(Server):
    def __init__(self):
        super().__init__(
            name="data-processing-server",
            version="2.0.0",
            description="高级数据处理和实时信息获取"
        )
        
    async def register_tools(self):
        # 工具 1: 实时网站数据提取
        self.register_tool(Tool(
            name="extract_website_data",
            description="从任意网站提取结构化数据(支持 JavaScript 渲染)",
            parameters={
                "url": {"type": "string", "description": "目标 URL"},
                "selectors": {
                    "type": "object",
                    "description": "CSS 选择器映射,如 {title: 'h1', price: '.price'}",
                    "optional": True
                },
                "use_headless": {
                    "type": "boolean",
                    "description": "是否使用无头浏览器渲染 JS",
                    "default": False
                }
            },
            handler=self.handle_extract_website
        ))
        
        # 工具 2: 技术栈分析
        self.register_tool(Tool(
            name="analyze_tech_stack",
            description="分析网站的技术栈(框架、库、CDN 等)",
            parameters={
                "url": {"type": "string"},
                "deep_scan": {
                    "type": "boolean",
                    "description": "是否深度扫描子页面",
                    "default": False
                }
            },
            handler=self.handle_tech_analysis
        ))
        
        # 工具 3: 代码质量分析
        self.register_tool(Tool(
            name="analyze_code_quality",
            description="分析 GitHub 仓库的代码质量指标",
            parameters={
                "owner": {"type": "string"},
                "repo": {"type": "string"},
                "metrics": {
                    "

---

*Have questions? Join our [Discord community](https://discord.gg/smartotics) or follow us on [X](https://x.com/smartotics).*