agent MCP研究一

1. 资料地址

hello-agents/docs/chapter10/第十章 智能体通信协议.md at main · datawhalechina/hello-agents · GitHub

2. 环境安装

按教材安装即可:
pip install "hello-agents[protocol]==0.2.2"
装完后确认:
python -c "from hello_agents.tools import MCPTool, A2ATool, ANPTool; print('ok')"
错误是系统找不到 npx。先看你的脚本在连什么 MCP 服务器。
npx: None
node: None

确认:环境里没有 Node.js,所以找不到 npx。脚本通过 npx 启动 @modelcontextprotocol/server-filesystem。

原因:脚本用 npx 拉起 MCP 文件系统服务器,但当前环境没有 Node.js,所以报 No such file or directory: 'npx'。

处理:安装 Node.js(会带上 npx):

# 方式一:conda(推荐,和当前 agent 环境一致)
conda install -n agent -c conda-forge nodejs -y
# 方式二:系统包
# apt-get update && apt-get install -y nodejs npm
装完验证:

node -v
npx -v

3. 调试

测试1:  01_TestConnect

import asyncio
from hello_agents.protocols import MCPClient

async def connect_to_server():
    # 方式1:连接到社区提供的文件系统服务器
    # npx会自动下载并运行@modelcontextprotocol/server-filesystem包
    client = MCPClient([
        "npx", "-y",
        "@modelcontextprotocol/server-filesystem",
        "."  # 指定根目录
    ])

    # 使用async with确保连接正确关闭
    async with client:
        # 在这里使用client
        tools = await client.list_tools()
        print(f"可用工具: {[t['name'] for t in tools]}")

    # 方式2:连接到自定义的Python MCP服务器
    client = MCPClient(["python", "my_mcp_server.py"])
    async with client:
        # 使用client...
        pass

# 运行异步函数
asyncio.run(connect_to_server())


async def discover_tools():
    client = MCPClient(["npx", "-y", "@modelcontextprotocol/server-filesystem", "."])

    async with client:
        # 获取所有可用工具
        tools = await client.list_tools()

        print(f"服务器提供了 {len(tools)} 个工具:")
        for tool in tools:
            print(f"\n工具名称: {tool['name']}")
            print(f"描述: {tool.get('description', '无描述')}")

            # 打印参数信息
            if 'inputSchema' in tool:
                schema = tool['inputSchema']
                if 'properties' in schema:
                    print("参数:")
                    for param_name, param_info in schema['properties'].items():
                        param_type = param_info.get('type', 'any')
                        param_desc = param_info.get('description', '')
                        print(f"  - {param_name} ({param_type}): {param_desc}")

asyncio.run(discover_tools())

报错:

RuntimeError: Event loop is closed
Exception ignored in: <function BaseSubprocessTransport.__del__ at 0x7f69583048b0>
Traceback (most recent call last):
  File "/data//envs/agent/lib/python3.10/asyncio/base_subprocess.py", line 126, in __del__
    self.close()
  File "/data//envs/agent/lib/python3.10/asyncio/base_subprocess.py", line 104, in close
    proto.pipe.close()
  File "/data//envs/agent/lib/python3.10/asyncio/unix_events.py", line 547, in close
    self._close(None)
  File "/data//envs/agent/lib/python3.10/asyncio/unix_events.py", line 571, in _close
    self._loop.call_soon(self._call_connection_lost, exc)
  File "/data//envs/agent/lib/python3.10/asyncio/base_events.py", line 753, in call_soon
    self._check_closed()
  File "/data//envs/agent/lib/python3.10/asyncio/base_events.py", line 515, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed

测试2:  01_TestConnect

修改为:

import asyncio
import sys
from hello_agents.protocols import MCPClient

SERVER_CMD = ["npx", "-y", "@modelcontextprotocol/server-filesystem", "."]


def _ignore_closed_loop_on_subprocess_del(unraisable):
    """过滤 asyncio 子进程管道在 loop 关闭后析构时的无害报错。"""
    if (
        unraisable.exc_type is RuntimeError
        and "Event loop is closed" in str(unraisable.exc_value)
        and getattr(unraisable.object, "__qualname__", "")
        == "BaseSubprocessTransport.__del__"
    ):
        return
    sys.__unraisablehook__(unraisable)


sys.unraisablehook = _ignore_closed_loop_on_subprocess_del


async def connect_to_server(client: MCPClient):
    tools = await client.list_tools()
    print(f"可用工具: {[t['name'] for t in tools]}")


async def discover_tools(client: MCPClient):
    tools = await client.list_tools()

    print(f"服务器提供了 {len(tools)} 个工具:")
    for tool in tools:
        print(f"\n工具名称: {tool['name']}")
        print(f"描述: {tool.get('description', '无描述')}")

        schema = tool.get("input_schema") or tool.get("inputSchema") or {}
        if "properties" in schema:
            print("参数:")
            for param_name, param_info in schema["properties"].items():
                param_type = param_info.get("type", "any")
                param_desc = param_info.get("description", "")
                print(f"  - {param_name} ({param_type}): {param_desc}")


async def main():
    async with MCPClient(SERVER_CMD) as client:
        await connect_to_server(client)
        await discover_tools(client)


if __name__ == "__main__":
    asyncio.run(main())

测试3:  02_Connect2MCP

# 02_Connect2MCP.py 
import asyncio
import sys
from hello_agents.protocols import MCPClient

SERVER_CMD = ["npx", "-y", "@modelcontextprotocol/server-filesystem", "."]


def _ignore_closed_loop_on_subprocess_del(unraisable):
    """过滤 asyncio 子进程管道在 loop 关闭后析构时的无害报错。"""
    if (
        unraisable.exc_type is RuntimeError
        and "Event loop is closed" in str(unraisable.exc_value)
        and getattr(unraisable.object, "__qualname__", "")
        == "BaseSubprocessTransport.__del__"
    ):
        return
    sys.__unraisablehook__(unraisable)


sys.unraisablehook = _ignore_closed_loop_on_subprocess_del


async def connect_to_server(client: MCPClient):
    tools = await client.list_tools()
    print(f"可用工具: {[t['name'] for t in tools]}")


async def discover_tools(client: MCPClient):
    tools = await client.list_tools()

    print(f"服务器提供了 {len(tools)} 个工具:")
    for tool in tools:
        print(f"\n工具名称: {tool['name']}")
        print(f"描述: {tool.get('description', '无描述')}")

        schema = tool.get("input_schema") or tool.get("inputSchema") or {}
        if "properties" in schema:
            print("参数:")
            for param_name, param_info in schema["properties"].items():
                param_type = param_info.get("type", "any")
                param_desc = param_info.get("description", "")
                print(f"  - {param_name} ({param_type}): {param_desc}")


async def use_tools(client: MCPClient):
    result = await client.call_tool("read_file", {"path": "my_README.md"})
    print(f"文件内容:\n{result}")

    result = await client.call_tool("list_directory", {"path": "."})
    print(f"当前目录文件:{result}")

    result = await client.call_tool("write_file", {
        "path": "output.txt",
        "content": "Hello from MCP!"
    })
    print(f"写入结果:{result}")
    
async def safe_tool_call(client: MCPClient):
    try:
        result = await client.call_tool("read_file", {"path": "nonexistent.txt"})
        print(result)
    except Exception as e:
        print(f"工具调用失败: {e}")
        # 可以选择重试、使用默认值或向用户报告错误  
        

async def main():
    async with MCPClient(SERVER_CMD) as client:
        await connect_to_server(client)
        await discover_tools(client)
        await use_tools(client)
        await safe_tool_call(client)

if __name__ == "__main__":
    asyncio.run(main())

输出:

It will be compatible before version 2.0.0.
  from authlib.jose import JsonWebKey, JsonWebToken
📝 使用 Stdio 传输 (命令): npx -y @modelcontextprotocol/server-filesystem .
🔗 连接到 MCP 服务器...
Secure MCP Filesystem Server running on stdio
✅ 连接成功!
Client does not support MCP Roots, using allowed directories set from server args: [
  '/data/py_workspace/agent_learning/hello_agents/chapter10'
]
可用工具: ['read_file', 'read_text_file', 'read_media_file', 'read_multiple_files', 'write_file', 'edit_file', 'create_directory', 'list_directory', 'list_directory_with_sizes', 'directory_tree', 'move_file', 'search_files', 'get_file_info', 'list_allowed_directories']
服务器提供了 14 个工具:

工具名称: read_file
描述: Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.
参数:
  - path (string): 
  - tail (number): If provided, returns only the last N lines of the file
  - head (number): If provided, returns only the first N lines of the file

测试4:  03_GitHubMCP

和 02_Connect2MCP.py 是同一类问题。先看你的 03_GitHubMCP.py。

和 02_Connect2MCP.py 是同一类问题,不是 GitHub MCP 调用失败。

从终端输出看,你的脚本已经正常跑完了:工具列表和仓库搜索结果都打印出来了。报错出现在程序退出之后。

原因:

MCPTool 通过 npx 启动了一个子进程
主程序结束时,asyncio 的事件循环已经关闭
子进程管道对象在垃圾回收时还想往已关闭的 loop 注册清理回调
于是出现 RuntimeError: Event loop is closed
这是 Python 3.10 + asyncio 子进程清理时的无害警告,可以忽略。

如果想消掉它,在脚本开头加上这段(和 02_Connect2MCP.py 一样):

import sys
def _ignore_closed_loop_on_subprocess_del(unraisable):
    if (
        unraisable.exc_type is RuntimeError
        and "Event loop is closed" in str(unraisable.exc_value)
        and getattr(unraisable.object, "__qualname__", "")
        == "BaseSubprocessTransport.__del__"
    ):
        return
    sys.__unraisablehook__(unraisable)
sys.unraisablehook = _ignore_closed_loop_on_subprocess_del
结论: GitHub MCP 功能本身没问题;这只是退出时的清理噪音。
# 03_GitHubMCP.py
from hello_agents.tools import MCPTool

import sys

def _ignore_closed_loop_on_subprocess_del(unraisable):
    if (
        unraisable.exc_type is RuntimeError
        and "Event loop is closed" in str(unraisable.exc_value)
        and getattr(unraisable.object, "__qualname__", "")
        == "BaseSubprocessTransport.__del__"
    ):
        return
    sys.__unraisablehook__(unraisable)

sys.unraisablehook = _ignore_closed_loop_on_subprocess_del


github_tool = MCPTool(
    server_command=["npx", "-y", "@modelcontextprotocol/server-github"]
)

# 1. 列出可用工具
print("📋 可用工具:")
result = github_tool.run({"action": "list_tools"})
print(result)

# 2. 搜索仓库
print("\n🔍 搜索仓库:")
result = github_tool.run({
    "action": "call_tool",
    "tool_name": "search_repositories",
    "arguments": {
        "query": "AI agents language:python",
        "page": 1,
        "perPage": 3
    }
})
print(result)

测试5:04_MCPTransport

# 04_MCPTransport.py
import sys

from hello_agents.tools import MCPTool


def _ignore_mcp_cleanup_warnings(unraisable):
    """过滤 MCP 子进程在程序退出时的无害清理警告。"""
    if unraisable.exc_type is RuntimeError and "Event loop is closed" in str(unraisable.exc_value):
        return
    if (
        unraisable.exc_type is AttributeError
        and "_stop_event" in str(unraisable.exc_value)
        and getattr(unraisable.object, "__qualname__", "") == "StdioTransport.__del__"
    ):
        return
    sys.__unraisablehook__(unraisable)


sys.unraisablehook = _ignore_mcp_cleanup_warnings

# 1. Memory Transport - 内存传输(用于测试)
# mcp_tool = MCPTool()

# 2. Stdio Transport - 标准输入输出传输(本地开发)
# mcp_tool = MCPTool(server_command=["python", "my_mcp_server.py"])

# 3. Stdio Transport with Args - 带参数的命令传输
# mcp_tool = MCPTool(server_command=["python", "my_mcp_server.py", "--debug"])

# 4. Stdio Transport - 社区服务器(npx方式)
# mcp_tool = MCPTool(server_command=["npx", "-y", "@modelcontextprotocol/server-filesystem", "."])

# 5. HTTP/SSE/StreamableHTTP Transport
# 注意:MCPTool 主要用于 Stdio 和 Memory 传输
# 对于 HTTP/SSE 等远程传输,建议直接使用 MCPClient

# 使用内置演示服务器(Memory 传输)
mcp_tool = MCPTool()

# 列出可用工具
result = mcp_tool.run({"action": "list_tools"})
print(result)

# 调用工具
result = mcp_tool.run({
    "action": "call_tool",
    "tool_name": "add",
    "arguments": {
        "a": 1,
        "b": 2
    }
})
print(result)

# 方式1:使用自定义Python服务器
mcp_tool = MCPTool(server_command=["python", "my_mcp_server.py"])

# 方式2:使用社区服务器(文件系统)
mcp_tool = MCPTool(server_command=["npx", "-y", "@modelcontextprotocol/server-filesystem", "."])

# 列出工具
result = mcp_tool.run({"action": "list_tools"})
print(result)

# 调用工具
result = mcp_tool.run({
    "action": "call_tool",
    "tool_name": "read_file",
    "arguments": {"path": "my_README.md"}
})
print(result)

测试6:05_UseMCPToolInAgent

# 05_UseMCPToolInAgent.py

from dotenv import load_dotenv

load_dotenv()  # 从当前目录的 .env 加载 LLM_API_KEY、LLM_BASE_URL 等配置

from hello_agents import SimpleAgent, HelloAgentsLLM
from hello_agents.tools import MCPTool
import sys

def _ignore_mcp_cleanup_warnings(unraisable):
    """过滤 MCP 子进程在程序退出时的无害清理警告。"""
    if unraisable.exc_type is RuntimeError and "Event loop is closed" in str(unraisable.exc_value):
        return
    if (
        unraisable.exc_type is AttributeError
        and "_stop_event" in str(unraisable.exc_value)
        and getattr(unraisable.object, "__qualname__", "") == "StdioTransport.__del__"
    ):
        return
    sys.__unraisablehook__(unraisable)


sys.unraisablehook = _ignore_mcp_cleanup_warnings

print("=" * 70)
print("方式1:使用内置演示服务器")
print("=" * 70)

agent = SimpleAgent(name="助手", llm=HelloAgentsLLM())

# 无需任何配置,自动使用内置演示服务器
# 内置服务器提供:add, subtract, multiply, divide, greet, get_system_info
mcp_tool = MCPTool()  # 默认name="mcp"
agent.add_tool(mcp_tool)

# 智能体可以使用内置工具
response = agent.run("计算 123 + 456")
print(response)  # 智能体会自动调用add工具

print("\n" + "=" * 70)
print("方式2:连接外部MCP服务器(使用多个服务器)")
print("=" * 70)

# 重要:为每个MCP服务器指定不同的name,避免工具名称冲突

# 示例1:连接到社区提供的文件系统服务器
fs_tool = MCPTool(
    name="filesystem",  # 指定唯一名称
    description="访问本地文件系统",
    server_command=["npx", "-y", "@modelcontextprotocol/server-filesystem", "."]
)
agent.add_tool(fs_tool)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值