121 lines
3.9 KiB
Python
121 lines
3.9 KiB
Python
"""MCP Client - 调用外部 MCP 工具服务器"""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from agentkit.mcp.transport import HTTPTransport, Transport
|
|
from agentkit.tools.base import Tool
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MCPClient:
|
|
"""MCP Client - 连接外部 MCP Server 并调用工具
|
|
|
|
支持两种模式:
|
|
1. 通过 Transport 层发送 JSON-RPC 请求(推荐)
|
|
2. 直接 HTTP 调用(向后兼容)
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
server_url: str,
|
|
timeout: int = 30,
|
|
transport: Transport | None = None,
|
|
):
|
|
self._server_url = server_url.rstrip("/")
|
|
self._timeout = timeout
|
|
self._tools_cache: list[dict] | None = None
|
|
self._transport = transport
|
|
|
|
@classmethod
|
|
def from_transport(cls, transport: Transport) -> "MCPClient":
|
|
"""从 Transport 实例创建 MCPClient"""
|
|
if isinstance(transport, HTTPTransport):
|
|
server_url = transport._endpoint
|
|
else:
|
|
server_url = ""
|
|
return cls(server_url=server_url, transport=transport)
|
|
|
|
async def list_tools(self) -> list[dict]:
|
|
"""列出远程 MCP Server 上的工具"""
|
|
if self._transport is not None:
|
|
if not self._transport.is_connected:
|
|
await self._transport.connect()
|
|
result = await self._transport.send_request("tools/list")
|
|
tools = result.get("tools", []) if isinstance(result, dict) else []
|
|
self._tools_cache = tools
|
|
return self._tools_cache
|
|
|
|
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
|
response = await client.get(f"{self._server_url}/tools/list")
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
self._tools_cache = data.get("tools", [])
|
|
return self._tools_cache
|
|
|
|
async def call_tool(self, tool_name: str, arguments: dict) -> dict:
|
|
"""调用远程 MCP 工具"""
|
|
if self._transport is not None:
|
|
if not self._transport.is_connected:
|
|
await self._transport.connect()
|
|
return await self._transport.send_request(
|
|
"tools/call",
|
|
params={"name": tool_name, "arguments": arguments},
|
|
)
|
|
|
|
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
|
response = await client.post(
|
|
f"{self._server_url}/tools/call",
|
|
json={"name": tool_name, "arguments": arguments},
|
|
)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
def as_tool(self, tool_name: str, description: str = "") -> "MCPTool":
|
|
"""将远程 MCP 工具包装为本地 Tool 对象"""
|
|
return MCPTool(
|
|
name=tool_name,
|
|
description=description,
|
|
client=self,
|
|
)
|
|
|
|
|
|
class MCPTool(Tool):
|
|
"""MCP 工具 - 通过 MCP Client 调用远程工具"""
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
description: str,
|
|
client: MCPClient,
|
|
input_schema: dict[str, Any] | None = None,
|
|
output_schema: dict[str, Any] | None = None,
|
|
version: str = "1.0.0",
|
|
tags: list[str] | None = None,
|
|
):
|
|
super().__init__(
|
|
name=name,
|
|
description=description,
|
|
input_schema=input_schema,
|
|
output_schema=output_schema,
|
|
version=version,
|
|
tags=tags or ["mcp"],
|
|
)
|
|
self._client = client
|
|
|
|
async def execute(self, **kwargs) -> dict:
|
|
result = await self._client.call_tool(self.name, kwargs)
|
|
# 解析 MCP 响应格式
|
|
if "content" in result:
|
|
for item in result["content"]:
|
|
if item.get("type") == "text":
|
|
import json
|
|
try:
|
|
return json.loads(item["text"])
|
|
except json.JSONDecodeError:
|
|
return {"result": item["text"]}
|
|
return result
|