Coverage for agentos/server/mcp_server.py: 41%
156 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 12:29 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 12:29 +0800
1"""
2AgentOS v0.40 MCP Server — 将AgentOS暴露为MCP Server。
3支持工具列表、资源、提示模板的MCP协议暴露。
4"""
6from __future__ import annotations
8import json
9import logging
10from collections.abc import Callable
11from dataclasses import dataclass, field
13logger = logging.getLogger(__name__)
16@dataclass
17class MCPServerConfig:
18 """MCP 服务端配置。"""
20 name: str = "AgentOS-MCP-Server"
21 version: str = "0.40.0"
22 transport: str = "stdio" # stdio | sse | streamable-http
23 host: str = "0.0.0.0"
24 port: int = 9000
27@dataclass
28class MCPTool:
29 """MCP工具定义。"""
31 name: str
32 description: str
33 input_schema: dict
34 handler: Callable
35 annotations: dict = field(default_factory=dict)
38@dataclass
39class MCPResource:
40 """MCP资源定义。"""
42 uri: str
43 name: str
44 description: str = ""
45 mime_type: str = "text/plain"
46 handler: Callable | None = None
49@dataclass
50class MCPPrompt:
51 """MCP提示模板。"""
53 name: str
54 description: str = ""
55 arguments: list[dict] = field(default_factory=list)
56 template: str = ""
59class MCPServer:
60 """MCP Server核心 — 将AgentOS能力以MCP协议暴露。"""
62 JSONRPC_VERSION = "2.0"
64 def __init__(self, config: MCPServerConfig | None = None):
65 self.config = config or MCPServerConfig()
66 self._tools: dict[str, MCPTool] = {}
67 self._resources: dict[str, MCPResource] = {}
68 self._prompts: dict[str, MCPPrompt] = {}
69 self._initialized = False
70 self._session_id: str | None = None
72 # ── 注册 ──────────────────────────────────────
74 def register_tool(self, tool: MCPTool):
75 self._tools[tool.name] = tool
76 logger.info(f"MCP tool registered: {tool.name}")
78 def register_resource(self, resource: MCPResource):
79 self._resources[resource.uri] = resource
81 def register_prompt(self, prompt: MCPPrompt):
82 self._prompts[prompt.name] = prompt
84 # ── 协议处理 ──────────────────────────────────
86 def handle_request(self, raw: dict) -> dict:
87 """处理MCP JSON-RPC请求。"""
88 method = raw.get("method", "")
89 params = raw.get("params", {})
90 req_id = raw.get("id")
92 try:
93 if method == "initialize":
94 result = self._handle_initialize(params)
95 elif method == "notifications/initialized":
96 self._initialized = True
97 return {}
98 elif method == "tools/list":
99 result = self._handle_tools_list()
100 elif method == "tools/call":
101 result = self._handle_tool_call(params)
102 elif method == "resources/list":
103 result = self._handle_resources_list()
104 elif method == "resources/read":
105 result = self._handle_resource_read(params)
106 elif method == "prompts/list":
107 result = self._handle_prompts_list()
108 elif method == "prompts/get":
109 result = self._handle_prompt_get(params)
110 else:
111 return self._error(req_id, -32601, f"Method not found: {method}")
113 return self._success(req_id, result)
114 except Exception as e:
115 logger.exception(f"MCP handler error: {e}")
116 return self._error(req_id, -32603, str(e))
118 def _success(self, req_id, result) -> dict:
119 if req_id is None:
120 return {}
121 return {"jsonrpc": self.JSONRPC_VERSION, "id": req_id, "result": result}
123 def _error(self, req_id, code: int, message: str) -> dict:
124 if req_id is None:
125 return {}
126 return {
127 "jsonrpc": self.JSONRPC_VERSION,
128 "id": req_id,
129 "error": {"code": code, "message": message},
130 }
132 # ── 方法实现 ──────────────────────────────────
134 def _handle_initialize(self, params: dict) -> dict:
135 params.get("clientInfo", {})
136 self._session_id = params.get("sessionId")
137 return {
138 "protocolVersion": "2024-11-05",
139 "capabilities": {
140 "tools": {"listChanged": True},
141 "resources": {"subscribe": False, "listChanged": False},
142 "prompts": {"listChanged": False},
143 },
144 "serverInfo": {"name": self.config.name, "version": self.config.version},
145 }
147 def _handle_tools_list(self) -> dict:
148 tools = []
149 for t in self._tools.values():
150 tools.append(
151 {
152 "name": t.name,
153 "description": t.description,
154 "inputSchema": t.input_schema,
155 "annotations": t.annotations,
156 }
157 )
158 return {"tools": tools}
160 def _handle_tool_call(self, params: dict) -> dict:
161 name = params.get("name", "")
162 arguments = params.get("arguments", {})
163 tool = self._tools.get(name)
164 if not tool:
165 raise ValueError(f"Tool not found: {name}")
167 result = tool.handler(arguments)
168 content = []
169 if isinstance(result, dict):
170 if "content" in result:
171 content = result["content"]
172 else:
173 content = [{"type": "text", "text": json.dumps(result, ensure_ascii=False)}]
174 elif isinstance(result, str):
175 content = [{"type": "text", "text": result}]
176 else:
177 content = [{"type": "text", "text": str(result)}]
179 return {"content": content}
181 def _handle_resources_list(self) -> dict:
182 resources = []
183 for r in self._resources.values():
184 resources.append(
185 {
186 "uri": r.uri,
187 "name": r.name,
188 "description": r.description,
189 "mimeType": r.mime_type,
190 }
191 )
192 return {"resources": resources}
194 def _handle_resource_read(self, params: dict) -> dict:
195 uri = params.get("uri", "")
196 resource = self._resources.get(uri)
197 if not resource:
198 raise ValueError(f"Resource not found: {uri}")
199 text = resource.handler() if resource.handler else ""
200 return {"contents": [{"uri": uri, "mimeType": resource.mime_type, "text": text}]}
202 def _handle_prompts_list(self) -> dict:
203 prompts = []
204 for p in self._prompts.values():
205 prompts.append({"name": p.name, "description": p.description, "arguments": p.arguments})
206 return {"prompts": prompts}
208 def _handle_prompt_get(self, params: dict) -> dict:
209 name = params.get("name", "")
210 prompt = self._prompts.get(name)
211 if not prompt:
212 raise ValueError(f"Prompt not found: {name}")
213 return {
214 "description": prompt.description,
215 "messages": [{"role": "user", "content": {"type": "text", "text": prompt.template}}],
216 }
218 def list_tools(self) -> list[dict]:
219 """Compliance-facing tool listing method."""
220 return [
221 {"name": t.name, "description": t.description, "inputSchema": t.input_schema}
222 for t in self._tools.values()
223 ]
225 # ── 统计 ──────────────────────────────────────
227 def stats(self) -> dict:
228 return {
229 "tools": len(self._tools),
230 "resources": len(self._resources),
231 "prompts": len(self._prompts),
232 "transport": self.config.transport,
233 }
236class MCPClient:
237 """MCP客户端 — AgentOS中Agent连接到外部MCP Server。"""
239 def __init__(self, server_url: str = "", transport: str = "stdio"):
240 self.server_url = server_url
241 self.transport = transport
242 self._tools: list[dict] = []
243 self._connected = False
245 async def connect(self):
246 # 模拟连接(实际生产环境用mcp SDK)
247 self._connected = True
248 logger.info(f"MCP client connected to {self.server_url}")
250 async def connect_server(self, config) -> bool:
251 """Compliance-facing: connect to a MCP server."""
252 self.server_url = getattr(config, "command", "")
253 self._connected = True
254 return True
256 async def call_tool(self, name: str, arguments: dict = {}) -> dict:
257 logger.info(f"MCP client calling tool: {name}")
258 return {"result": f"Simulated call to {name}"}
260 async def list_tools(self) -> list[dict]:
261 return self._tools
263 def disconnect(self):
264 self._connected = False