refactor(module11): restructure MCP client implementation and migrate to chapter 11
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import json
|
||||
import uuid
|
||||
from typing import Optional,Literal
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
class HttpMCPTransport:
|
||||
def __init__(self,url:str=None):
|
||||
"""
|
||||
MCP 客户端 - 支持 stdio 与 HTTP 两种传输方式
|
||||
|
||||
:param url: http模式的端点 URL
|
||||
"""
|
||||
if not url:
|
||||
raise ValueError("HTTP模式必须提供url参数")
|
||||
self.url = url
|
||||
self.session_id:Optional[str] = None
|
||||
self.http_client:Optional[httpx.AsyncClient] = None
|
||||
|
||||
|
||||
|
||||
async def connect(self):
|
||||
"""建立MCP长连接(一次连接,多次调用)"""
|
||||
if self.http_client:
|
||||
return
|
||||
self.http_client = httpx.AsyncClient(timeout=60.0)
|
||||
|
||||
# 发送 initialize 请求
|
||||
response = await self._http_request("initialize",{
|
||||
"protocolVersion":"2024-11-05",
|
||||
"capabilities":{},
|
||||
"clientInfo":{"name":"mcp-client","version":"1.0"}
|
||||
})
|
||||
|
||||
# 保存session ID
|
||||
if response and "result" in response:
|
||||
# Session ID在响应头内
|
||||
pass # 已在_http_request中处理
|
||||
|
||||
|
||||
async def list_tools(self):
|
||||
"""查询工具列表,为LLM建立上下文用"""
|
||||
if not self.http_client:
|
||||
raise RuntimeError("未连接,请先 connect()")
|
||||
|
||||
result = await self._http_request("tools/list")
|
||||
if result and "result" in result:
|
||||
tools = result["result"].get("tools",[])
|
||||
return [
|
||||
{
|
||||
"name":tool["name"],
|
||||
"description":tool["description"],
|
||||
"input_schema":tool.get("inputSchema",{})
|
||||
}
|
||||
for tool in tools
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
async def call_tool(self,name:str,args:dict):
|
||||
"""调用工具(工程化:加上防御性处理)"""
|
||||
if not self.http_client:
|
||||
raise RuntimeError("未连接,请先 connect()")
|
||||
|
||||
result = await self._http_request("tools/call",{
|
||||
"name":name,
|
||||
"arguments":args
|
||||
})
|
||||
if result and "result" in result:
|
||||
content = result["result"].get("content",{})
|
||||
if content and len(content):
|
||||
return content[0].get("text",str(content[0]))
|
||||
|
||||
return "工具执行成功,但无文本返回"
|
||||
|
||||
|
||||
async def cleanup(self):
|
||||
"""关闭MCP服务、会话和transport"""
|
||||
if self.http_client:
|
||||
await self.http_client.aclose()
|
||||
self.http_client = None
|
||||
self.session_id = None
|
||||
|
||||
|
||||
async def _http_request(self, method: str, params: dict = None):
|
||||
""" 发送 HTTP JSON-RPC 请求 """
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": str(uuid.uuid4()),
|
||||
"method": method,
|
||||
}
|
||||
if params:
|
||||
payload["params"] = params
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json, text/event-stream"
|
||||
}
|
||||
if self.session_id:
|
||||
headers["Mcp-Session-Id"] = self.session_id
|
||||
|
||||
response = await self.http_client.post(
|
||||
self.url,
|
||||
json=payload,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if "Mcp-Session-Id" in response.headers:
|
||||
self.session_id = response.headers["Mcp-Session-Id"]
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
# 判断响应类型
|
||||
content_type = response.headers.get("Content-Type", "")
|
||||
|
||||
if "text/event-stream" in content_type:
|
||||
# SSE 流式响应(本地服务器)
|
||||
for line in response.text.split('\n'):
|
||||
line = line.strip()
|
||||
if line.startswith("data:"):
|
||||
data_str = line[5:].strip()
|
||||
try:
|
||||
return json.loads(data_str)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return None
|
||||
else:
|
||||
# 普通 JSON 响应(云端服务)
|
||||
return response.json()
|
||||
Reference in New Issue
Block a user