Coverage for agentos/server/mcp_server.py: 37%

156 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-08 12:20 +0800

1""" 

2AgentOS v0.40 MCP Server — 将AgentOS暴露为MCP Server。 

3支持工具列表、资源、提示模板的MCP协议暴露。 

4""" 

5 

6from __future__ import annotations 

7 

8import json 

9import logging 

10from collections.abc import Callable 

11from dataclasses import dataclass, field 

12 

13logger = logging.getLogger(__name__) 

14 

15 

16@dataclass 

17class MCPServerConfig: 

18 """MCP 服务端配置。""" 

19 

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 

25 

26 

27@dataclass 

28class MCPTool: 

29 """MCP工具定义。""" 

30 

31 name: str 

32 description: str 

33 input_schema: dict 

34 handler: Callable 

35 annotations: dict = field(default_factory=dict) 

36 

37 

38@dataclass 

39class MCPResource: 

40 """MCP资源定义。""" 

41 

42 uri: str 

43 name: str 

44 description: str = "" 

45 mime_type: str = "text/plain" 

46 handler: Callable | None = None 

47 

48 

49@dataclass 

50class MCPPrompt: 

51 """MCP提示模板。""" 

52 

53 name: str 

54 description: str = "" 

55 arguments: list[dict] = field(default_factory=list) 

56 template: str = "" 

57 

58 

59class MCPServer: 

60 """MCP Server核心 — 将AgentOS能力以MCP协议暴露。""" 

61 

62 JSONRPC_VERSION = "2.0" 

63 

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 

71 

72 # ── 注册 ────────────────────────────────────── 

73 

74 def register_tool(self, tool: MCPTool): 

75 self._tools[tool.name] = tool 

76 logger.info(f"MCP tool registered: {tool.name}") 

77 

78 def register_resource(self, resource: MCPResource): 

79 self._resources[resource.uri] = resource 

80 

81 def register_prompt(self, prompt: MCPPrompt): 

82 self._prompts[prompt.name] = prompt 

83 

84 # ── 协议处理 ────────────────────────────────── 

85 

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") 

91 

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}") 

112 

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)) 

117 

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} 

122 

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 } 

131 

132 # ── 方法实现 ────────────────────────────────── 

133 

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 } 

146 

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} 

159 

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}") 

166 

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)}] 

178 

179 return {"content": content} 

180 

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} 

193 

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}]} 

201 

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} 

207 

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 } 

217 

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 ] 

224 

225 # ── 统计 ────────────────────────────────────── 

226 

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 } 

234 

235 

236class MCPClient: 

237 """MCP客户端 — AgentOS中Agent连接到外部MCP Server。""" 

238 

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 

244 

245 async def connect(self): 

246 # 模拟连接(实际生产环境用mcp SDK) 

247 self._connected = True 

248 logger.info(f"MCP client connected to {self.server_url}") 

249 

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 

255 

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}"} 

259 

260 async def list_tools(self) -> list[dict]: 

261 return self._tools 

262 

263 def disconnect(self): 

264 self._connected = False