mofa_kernel/agent/components/mcp.rs
1//! MCP (Model Context Protocol) 组件
2//!
3//! 定义 MCP 客户端接口,用于连接外部 MCP 服务器并使用其工具。
4//!
5//! # 概述
6//!
7//! MCP 是 Anthropic 提出的标准化协议,允许 AI Agent 与外部工具服务器通信。
8//! 本模块定义了 MoFA 框架中的 MCP 客户端抽象。
9//!
10//! # 架构
11//!
12//! ```text
13//! ┌─────────────────────────────────────────────┐
14//! │ MoFA Agent │
15//! │ ┌─────────────────────────────────────────┐│
16//! │ │ ToolRegistry ││
17//! │ │ ┌───────────┐ ┌───────────────────┐ ││
18//! │ │ │ Built-in │ │ McpToolAdapter │ ││
19//! │ │ │ Tools │ │ (per MCP tool) │ ││
20//! │ │ └───────────┘ └────────┬──────────┘ ││
21//! │ └──────────────────────────┼──────────────┘│
22//! └─────────────────────────────┼───────────────┘
23//! │
24//! ┌──────────▼──────────┐
25//! │ McpClient │
26//! │ (manages MCP │
27//! │ connections) │
28//! └──────────┬──────────┘
29//! │
30//! ┌────────────────┼────────────────┐
31//! ▼ ▼ ▼
32//! ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
33//! │ MCP Server A │ │ MCP Server B │ │ MCP Server C │
34//! │ (stdio) │ │ (HTTP) │ │ (stdio) │
35//! └──────────────┘ └──────────────┘ └──────────────┘
36//! ```
37
38use async_trait::async_trait;
39use serde::{Deserialize, Serialize};
40use std::collections::HashMap;
41
42// ============================================================================
43// MCP Transport Configuration
44// ============================================================================
45
46/// MCP 服务器传输配置
47///
48/// 定义如何连接到 MCP 服务器
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub enum McpTransportConfig {
51 /// 通过标准输入输出连接 (启动子进程)
52 ///
53 /// # 示例
54 ///
55 /// ```rust,ignore
56 /// let config = McpTransportConfig::Stdio {
57 /// command: "npx".to_string(),
58 /// args: vec!["-y".to_string(), "@modelcontextprotocol/server-github".to_string()],
59 /// env: HashMap::from([("GITHUB_TOKEN".to_string(), "ghp_xxx".to_string())]),
60 /// };
61 /// ```
62 Stdio {
63 /// 可执行命令
64 command: String,
65 /// 命令参数
66 args: Vec<String>,
67 /// 环境变量
68 env: HashMap<String, String>,
69 },
70
71 /// 通过 HTTP/SSE 连接
72 ///
73 /// # 示例
74 ///
75 /// ```rust,ignore
76 /// let config = McpTransportConfig::Http {
77 /// url: "http://localhost:8080/mcp".to_string(),
78 /// };
79 /// ```
80 Http {
81 /// 服务器 URL
82 url: String,
83 },
84}
85
86// ============================================================================
87// MCP Server Configuration
88// ============================================================================
89
90/// MCP 服务器配置
91///
92/// 定义单个 MCP 服务器的完整配置
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct McpServerConfig {
95 /// 服务器名称 (用于标识)
96 pub name: String,
97 /// 传输配置
98 pub transport: McpTransportConfig,
99 /// 是否自动连接
100 pub auto_connect: bool,
101}
102
103impl McpServerConfig {
104 /// 创建 Stdio 类型的 MCP 服务器配置
105 pub fn stdio(
106 name: impl Into<String>,
107 command: impl Into<String>,
108 args: Vec<String>,
109 ) -> Self {
110 Self {
111 name: name.into(),
112 transport: McpTransportConfig::Stdio {
113 command: command.into(),
114 args,
115 env: HashMap::new(),
116 },
117 auto_connect: true,
118 }
119 }
120
121 /// 创建 HTTP 类型的 MCP 服务器配置
122 pub fn http(name: impl Into<String>, url: impl Into<String>) -> Self {
123 Self {
124 name: name.into(),
125 transport: McpTransportConfig::Http { url: url.into() },
126 auto_connect: true,
127 }
128 }
129
130 /// 设置环境变量 (仅对 Stdio 有效)
131 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
132 if let McpTransportConfig::Stdio { ref mut env, .. } = self.transport {
133 env.insert(key.into(), value.into());
134 }
135 self
136 }
137
138 /// 设置是否自动连接
139 pub fn with_auto_connect(mut self, auto_connect: bool) -> Self {
140 self.auto_connect = auto_connect;
141 self
142 }
143}
144
145// ============================================================================
146// MCP Tool Information
147// ============================================================================
148
149/// MCP 工具信息
150///
151/// 从 MCP 服务器发现的工具元信息
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct McpToolInfo {
154 /// 工具名称
155 pub name: String,
156 /// 工具描述
157 pub description: String,
158 /// 输入参数 JSON Schema
159 pub input_schema: serde_json::Value,
160}
161
162// ============================================================================
163// MCP Server Information
164// ============================================================================
165
166/// MCP 服务器信息
167///
168/// MCP 服务器在初始化握手时返回的元信息
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct McpServerInfo {
171 /// 服务器名称
172 pub name: String,
173 /// 服务器版本
174 pub version: String,
175 /// 服务器说明
176 pub instructions: Option<String>,
177}
178
179// ============================================================================
180// MCP Client Trait
181// ============================================================================
182
183/// MCP 客户端 Trait
184///
185/// 定义与 MCP 服务器通信的接口。
186/// 具体实现在 `mofa-foundation` 层。
187///
188/// # 示例
189///
190/// ```rust,ignore
191/// use mofa_kernel::agent::components::mcp::*;
192///
193/// let config = McpServerConfig::stdio(
194/// "github",
195/// "npx",
196/// vec!["-y".into(), "@modelcontextprotocol/server-github".into()],
197/// );
198///
199/// let mut client = MyMcpClient::new();
200/// client.connect(config).await?;
201///
202/// let tools = client.list_tools("github").await?;
203/// for tool in &tools {
204/// println!("{}: {}", tool.name, tool.description);
205/// }
206///
207/// let result = client.call_tool("github", "list_repos", json!({"owner": "mofa-org"})).await?;
208/// ```
209#[async_trait]
210pub trait McpClient: Send + Sync {
211 /// 连接到 MCP 服务器
212 ///
213 /// 使用给定配置建立与 MCP 服务器的连接。
214 /// 连接建立后,可以通过 `server_name` 引用该服务器。
215 async fn connect(&mut self, config: McpServerConfig) -> crate::agent::error::AgentResult<()>;
216
217 /// 断开与 MCP 服务器的连接
218 async fn disconnect(&mut self, server_name: &str) -> crate::agent::error::AgentResult<()>;
219
220 /// 列出 MCP 服务器提供的工具
221 async fn list_tools(
222 &self,
223 server_name: &str,
224 ) -> crate::agent::error::AgentResult<Vec<McpToolInfo>>;
225
226 /// 调用 MCP 服务器上的工具
227 async fn call_tool(
228 &self,
229 server_name: &str,
230 tool_name: &str,
231 arguments: serde_json::Value,
232 ) -> crate::agent::error::AgentResult<serde_json::Value>;
233
234 /// 获取 MCP 服务器信息
235 async fn server_info(
236 &self,
237 server_name: &str,
238 ) -> crate::agent::error::AgentResult<McpServerInfo>;
239
240 /// 获取所有已连接的服务器名称
241 fn connected_servers(&self) -> Vec<String>;
242
243 /// 检查服务器是否已连接
244 fn is_connected(&self, server_name: &str) -> bool;
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_mcp_server_config_stdio() {
253 let config = McpServerConfig::stdio(
254 "test-server",
255 "node",
256 vec!["server.js".to_string()],
257 )
258 .with_env("API_KEY", "test-key")
259 .with_auto_connect(false);
260
261 assert_eq!(config.name, "test-server");
262 assert!(!config.auto_connect);
263
264 if let McpTransportConfig::Stdio { command, args, env } = &config.transport {
265 assert_eq!(command, "node");
266 assert_eq!(args, &["server.js"]);
267 assert_eq!(env.get("API_KEY"), Some(&"test-key".to_string()));
268 } else {
269 panic!("Expected Stdio transport");
270 }
271 }
272
273 #[test]
274 fn test_mcp_server_config_http() {
275 let config = McpServerConfig::http("api-server", "http://localhost:8080/mcp");
276
277 assert_eq!(config.name, "api-server");
278 assert!(config.auto_connect);
279
280 if let McpTransportConfig::Http { url } = &config.transport {
281 assert_eq!(url, "http://localhost:8080/mcp");
282 } else {
283 panic!("Expected Http transport");
284 }
285 }
286
287 #[test]
288 fn test_mcp_tool_info_serialization() {
289 let tool = McpToolInfo {
290 name: "list_repos".to_string(),
291 description: "List GitHub repositories".to_string(),
292 input_schema: serde_json::json!({
293 "type": "object",
294 "properties": {
295 "owner": { "type": "string" }
296 },
297 "required": ["owner"]
298 }),
299 };
300
301 let json = serde_json::to_string(&tool).unwrap();
302 let deserialized: McpToolInfo = serde_json::from_str(&json).unwrap();
303 assert_eq!(deserialized.name, "list_repos");
304 }
305
306 #[test]
307 fn test_mcp_server_info() {
308 let info = McpServerInfo {
309 name: "github".to_string(),
310 version: "1.0.0".to_string(),
311 instructions: Some("GitHub MCP Server".to_string()),
312 };
313
314 assert_eq!(info.name, "github");
315 assert_eq!(info.version, "1.0.0");
316 assert_eq!(info.instructions, Some("GitHub MCP Server".to_string()));
317 }
318}