Files
agent-craft/m10_mcp_basics/mcp_client.py
T

73 lines
2.5 KiB
Python

from contextlib import AsyncExitStack
from typing import Optional
from mcp import ClientSession,StdioServerParameters
from mcp.client.stdio import stdio_client
class MCPClient:
def __init__(self,command:str,args:list[str],env:dict=None):
# MCP启动方式(npx/uvx/python -m xxx)
self.params = StdioServerParameters(command=command,args=args,env=env)
# 工程核心:资源栈
self.exit_stack = AsyncExitStack()
# 连接会话(长连接)
self.session:Optional[ClientSession]=None
async def connect(self):
"""建立MCP长连接(一次连接,多次调用)"""
if self.session:
return # 已连接无需重复
# 进入transport(读/写管道)
transport = await self.exit_stack.enter_async_context(
stdio_client(self.params)
)
# 创建JSON-RPC对话
self.session = await self.exit_stack.enter_async_context(
ClientSession(transport[0],transport[1])
)
# 等待MCP服务器返回工具清单
await self.session.initialize()
async def list_tools(self):
"""查询工具列表,为LLM建立上下文用"""
if not self.session:
raise RuntimeError("未连接,请先 connect()")
result = await self.session.list_tools()
# 🔍 调试:打印工具的完整信息,确认工具是否被正确封装
# if result.tools:
# import json
# # 使用 model_dump() (Pydantic v2) 或 dict() (v1) 查看原始数据
# first_tool = result.tools[0]
# print(f"\n🔍 [DEBUG] 原始工具数据: {first_tool}\n")
# 转为纯字典,LLM能读
return[
{
"name":tool.name,
"description":tool.description,
"input_schema":tool.inputSchema
}
for tool in result.tools
]
async def call_tool(self,name:str,args:dict):
"""调用工具(工程化:加上防御性处理)"""
if not self.session:
raise RuntimeError("未连接,请先connect()")
result = await self.session.call_tool(name,args)
# 有些工具可能执行成功但无文本返回
if hasattr(result,"content") and result.content:
return result.content[0].text
return "工具执行成功,但无文本返回"
async def cleanup(self):
"""关闭MCP服务、会话和transport"""
if self.session:
await self.exit_stack.aclose()
self.session = None