zarz 0.3.1-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +2815 -0
- package/Cargo.toml +30 -0
- package/QUICKSTART.md +326 -0
- package/README.md +72 -0
- package/bin/zarz.js +80 -0
- package/package.json +53 -0
- package/scripts/postinstall.js +91 -0
- package/src/cli.rs +201 -0
- package/src/config.rs +249 -0
- package/src/conversation_store.rs +183 -0
- package/src/executor.rs +164 -0
- package/src/fs_ops.rs +117 -0
- package/src/intelligence/context.rs +143 -0
- package/src/intelligence/mod.rs +60 -0
- package/src/intelligence/rust_parser.rs +141 -0
- package/src/intelligence/symbol_search.rs +97 -0
- package/src/main.rs +867 -0
- package/src/mcp/client.rs +316 -0
- package/src/mcp/config.rs +133 -0
- package/src/mcp/manager.rs +186 -0
- package/src/mcp/mod.rs +12 -0
- package/src/mcp/types.rs +170 -0
- package/src/providers/anthropic.rs +214 -0
- package/src/providers/glm.rs +209 -0
- package/src/providers/mod.rs +90 -0
- package/src/providers/openai.rs +197 -0
- package/src/repl.rs +1910 -0
- package/src/session.rs +173 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
use anyhow::{anyhow, Context, Result};
|
|
2
|
+
use serde_json::{json, Value};
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
use std::process::Stdio;
|
|
5
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
6
|
+
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
|
7
|
+
use tokio::process::{Child, ChildStdin, ChildStdout, Command};
|
|
8
|
+
use tokio::sync::Mutex;
|
|
9
|
+
|
|
10
|
+
use super::config::McpServerConfig;
|
|
11
|
+
use super::types::*;
|
|
12
|
+
|
|
13
|
+
/// MCP Client for communicating with MCP servers
|
|
14
|
+
pub struct McpClient {
|
|
15
|
+
#[allow(dead_code)]
|
|
16
|
+
name: String,
|
|
17
|
+
config: McpServerConfig,
|
|
18
|
+
process: Option<Mutex<Child>>,
|
|
19
|
+
stdin: Option<Mutex<ChildStdin>>,
|
|
20
|
+
stdout: Option<Mutex<BufReader<ChildStdout>>>,
|
|
21
|
+
request_id: AtomicU64,
|
|
22
|
+
initialized: bool,
|
|
23
|
+
server_info: Option<ServerInfo>,
|
|
24
|
+
capabilities: Option<ServerCapabilities>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl McpClient {
|
|
28
|
+
/// Create a new MCP client
|
|
29
|
+
pub fn new(name: String, config: McpServerConfig) -> Self {
|
|
30
|
+
Self {
|
|
31
|
+
name,
|
|
32
|
+
config,
|
|
33
|
+
process: None,
|
|
34
|
+
stdin: None,
|
|
35
|
+
stdout: None,
|
|
36
|
+
request_id: AtomicU64::new(1),
|
|
37
|
+
initialized: false,
|
|
38
|
+
server_info: None,
|
|
39
|
+
capabilities: None,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Start the MCP server process (for STDIO servers)
|
|
44
|
+
pub async fn start(&mut self) -> Result<()> {
|
|
45
|
+
match &self.config {
|
|
46
|
+
McpServerConfig::Stdio { command, args, env } => {
|
|
47
|
+
// On Windows, wrap in cmd /c for proper PATH resolution
|
|
48
|
+
let mut cmd = if cfg!(target_os = "windows") {
|
|
49
|
+
let mut win_cmd = Command::new("cmd");
|
|
50
|
+
win_cmd.arg("/c");
|
|
51
|
+
win_cmd.arg(command);
|
|
52
|
+
if let Some(args) = args {
|
|
53
|
+
win_cmd.args(args);
|
|
54
|
+
}
|
|
55
|
+
win_cmd
|
|
56
|
+
} else {
|
|
57
|
+
let mut unix_cmd = Command::new(command);
|
|
58
|
+
if let Some(args) = args {
|
|
59
|
+
unix_cmd.args(args);
|
|
60
|
+
}
|
|
61
|
+
unix_cmd
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if let Some(env_vars) = env {
|
|
65
|
+
cmd.envs(env_vars);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
cmd.stdin(Stdio::piped())
|
|
69
|
+
.stdout(Stdio::piped())
|
|
70
|
+
.stderr(Stdio::inherit());
|
|
71
|
+
|
|
72
|
+
let mut child = cmd.spawn()
|
|
73
|
+
.with_context(|| format!("Failed to start MCP server: {}", command))?;
|
|
74
|
+
|
|
75
|
+
let stdin = child.stdin.take()
|
|
76
|
+
.context("Failed to open stdin")?;
|
|
77
|
+
let stdout = child.stdout.take()
|
|
78
|
+
.context("Failed to open stdout")?;
|
|
79
|
+
|
|
80
|
+
self.stdin = Some(Mutex::new(stdin));
|
|
81
|
+
self.stdout = Some(Mutex::new(BufReader::new(stdout)));
|
|
82
|
+
self.process = Some(Mutex::new(child));
|
|
83
|
+
|
|
84
|
+
self.initialize().await?;
|
|
85
|
+
|
|
86
|
+
Ok(())
|
|
87
|
+
}
|
|
88
|
+
_ => Err(anyhow!("Only STDIO servers are currently supported")),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Initialize MCP connection
|
|
93
|
+
async fn initialize(&mut self) -> Result<()> {
|
|
94
|
+
let params = json!({
|
|
95
|
+
"protocolVersion": "2024-11-05",
|
|
96
|
+
"capabilities": {
|
|
97
|
+
"tools": {},
|
|
98
|
+
"resources": {},
|
|
99
|
+
"prompts": {}
|
|
100
|
+
},
|
|
101
|
+
"clientInfo": {
|
|
102
|
+
"name": "ZarzCLI",
|
|
103
|
+
"version": "0.1.0"
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
let response = self.send_request("initialize", Some(params)).await?;
|
|
108
|
+
|
|
109
|
+
let result: InitializeResult = serde_json::from_value(response)
|
|
110
|
+
.context("Failed to parse initialize response")?;
|
|
111
|
+
|
|
112
|
+
self.server_info = Some(result.server_info);
|
|
113
|
+
self.capabilities = Some(result.capabilities);
|
|
114
|
+
self.initialized = true;
|
|
115
|
+
|
|
116
|
+
self.send_notification("notifications/initialized", None).await?;
|
|
117
|
+
|
|
118
|
+
Ok(())
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// List available tools from the MCP server
|
|
122
|
+
pub async fn list_tools(&self) -> Result<Vec<McpTool>> {
|
|
123
|
+
if !self.initialized {
|
|
124
|
+
return Err(anyhow!("MCP client not initialized"));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let response = self.send_request("tools/list", None).await?;
|
|
128
|
+
let result: ToolsListResult = serde_json::from_value(response)
|
|
129
|
+
.context("Failed to parse tools/list response")?;
|
|
130
|
+
|
|
131
|
+
Ok(result.tools)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Call a tool on the MCP server
|
|
135
|
+
#[allow(dead_code)]
|
|
136
|
+
pub async fn call_tool(&self, name: String, arguments: Option<HashMap<String, Value>>) -> Result<CallToolResult> {
|
|
137
|
+
if !self.initialized {
|
|
138
|
+
return Err(anyhow!("MCP client not initialized"));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let params = CallToolParams { name, arguments };
|
|
142
|
+
let params_value = serde_json::to_value(params)?;
|
|
143
|
+
|
|
144
|
+
let response = self.send_request("tools/call", Some(params_value)).await?;
|
|
145
|
+
let result: CallToolResult = serde_json::from_value(response)
|
|
146
|
+
.context("Failed to parse tools/call response")?;
|
|
147
|
+
|
|
148
|
+
Ok(result)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// List available resources from the MCP server
|
|
152
|
+
#[allow(dead_code)]
|
|
153
|
+
pub async fn list_resources(&self) -> Result<Vec<McpResource>> {
|
|
154
|
+
if !self.initialized {
|
|
155
|
+
return Err(anyhow!("MCP client not initialized"));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let response = self.send_request("resources/list", None).await?;
|
|
159
|
+
let result: ResourcesListResult = serde_json::from_value(response)
|
|
160
|
+
.context("Failed to parse resources/list response")?;
|
|
161
|
+
|
|
162
|
+
Ok(result.resources)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// List available prompts from the MCP server
|
|
166
|
+
#[allow(dead_code)]
|
|
167
|
+
pub async fn list_prompts(&self) -> Result<Vec<McpPrompt>> {
|
|
168
|
+
if !self.initialized {
|
|
169
|
+
return Err(anyhow!("MCP client not initialized"));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let response = self.send_request("prompts/list", None).await?;
|
|
173
|
+
let result: PromptsListResult = serde_json::from_value(response)
|
|
174
|
+
.context("Failed to parse prompts/list response")?;
|
|
175
|
+
|
|
176
|
+
Ok(result.prompts)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Send a JSON-RPC request and wait for response
|
|
180
|
+
async fn send_request(&self, method: &str, params: Option<Value>) -> Result<Value> {
|
|
181
|
+
let id = self.request_id.fetch_add(1, Ordering::SeqCst);
|
|
182
|
+
|
|
183
|
+
let request = JsonRpcRequest {
|
|
184
|
+
jsonrpc: "2.0".to_string(),
|
|
185
|
+
id,
|
|
186
|
+
method: method.to_string(),
|
|
187
|
+
params,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
let request_json = serde_json::to_string(&request)?;
|
|
191
|
+
|
|
192
|
+
if let Some(stdin) = &self.stdin {
|
|
193
|
+
let mut stdin = stdin.lock().await;
|
|
194
|
+
stdin.write_all(request_json.as_bytes()).await?;
|
|
195
|
+
stdin.write_all(b"\n").await?;
|
|
196
|
+
stdin.flush().await?;
|
|
197
|
+
} else {
|
|
198
|
+
return Err(anyhow!("STDIN not available"));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if let Some(stdout) = &self.stdout {
|
|
202
|
+
let mut stdout = stdout.lock().await;
|
|
203
|
+
|
|
204
|
+
loop {
|
|
205
|
+
let mut line = String::new();
|
|
206
|
+
let bytes_read = stdout.read_line(&mut line).await?;
|
|
207
|
+
|
|
208
|
+
if bytes_read == 0 {
|
|
209
|
+
return Err(anyhow!("MCP server closed the connection unexpectedly"));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if line.trim().is_empty() {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let value: Value = serde_json::from_str(&line)
|
|
217
|
+
.with_context(|| format!("Failed to parse JSON-RPC message: {}", line.trim()))?;
|
|
218
|
+
|
|
219
|
+
// Notifications do not include an `id`, so we skip them (surface useful info when present)
|
|
220
|
+
if value.get("id").is_none() {
|
|
221
|
+
if let Some(method) = value.get("method").and_then(|m| m.as_str()) {
|
|
222
|
+
if method == "notifications/message" {
|
|
223
|
+
if let Some(msg) = value
|
|
224
|
+
.get("params")
|
|
225
|
+
.and_then(|p| p.get("data"))
|
|
226
|
+
.and_then(|d| d.get("message"))
|
|
227
|
+
.and_then(|m| m.as_str())
|
|
228
|
+
{
|
|
229
|
+
eprintln!("MCP notification: {}", msg);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let response: JsonRpcResponse = serde_json::from_value(value)
|
|
237
|
+
.with_context(|| format!("Failed to parse JSON-RPC response: {}", line.trim()))?;
|
|
238
|
+
|
|
239
|
+
if let Some(error) = response.error {
|
|
240
|
+
return Err(anyhow!("MCP error: {} (code: {})", error.message, error.code));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if let Some(result) = response.result {
|
|
244
|
+
return Ok(result);
|
|
245
|
+
} else {
|
|
246
|
+
return Err(anyhow!("No result in response"));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
Err(anyhow!("STDOUT not available"))
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// Send a JSON-RPC notification (no response expected)
|
|
255
|
+
async fn send_notification(&self, method: &str, params: Option<Value>) -> Result<()> {
|
|
256
|
+
let notification = json!({
|
|
257
|
+
"jsonrpc": "2.0",
|
|
258
|
+
"method": method,
|
|
259
|
+
"params": params
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
let notification_json = serde_json::to_string(¬ification)?;
|
|
263
|
+
|
|
264
|
+
if let Some(stdin) = &self.stdin {
|
|
265
|
+
let mut stdin = stdin.lock().await;
|
|
266
|
+
stdin.write_all(notification_json.as_bytes()).await?;
|
|
267
|
+
stdin.write_all(b"\n").await?;
|
|
268
|
+
stdin.flush().await?;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
Ok(())
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// Get server info
|
|
275
|
+
pub fn server_info(&self) -> Option<&ServerInfo> {
|
|
276
|
+
self.server_info.as_ref()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/// Get server capabilities
|
|
280
|
+
#[allow(dead_code)]
|
|
281
|
+
pub fn capabilities(&self) -> Option<&ServerCapabilities> {
|
|
282
|
+
self.capabilities.as_ref()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/// Check if client is initialized
|
|
286
|
+
#[allow(dead_code)]
|
|
287
|
+
pub fn is_initialized(&self) -> bool {
|
|
288
|
+
self.initialized
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/// Get server name
|
|
292
|
+
#[allow(dead_code)]
|
|
293
|
+
pub fn name(&self) -> &str {
|
|
294
|
+
&self.name
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// Stop the MCP server process
|
|
298
|
+
pub async fn stop(&mut self) -> Result<()> {
|
|
299
|
+
if let Some(process) = &self.process {
|
|
300
|
+
let mut process = process.lock().await;
|
|
301
|
+
process.kill().await?;
|
|
302
|
+
}
|
|
303
|
+
Ok(())
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
impl Drop for McpClient {
|
|
308
|
+
fn drop(&mut self) {
|
|
309
|
+
// Note: We can't await in drop, so we just kill the process synchronously
|
|
310
|
+
if let Some(process) = &self.process {
|
|
311
|
+
if let Ok(mut process) = process.try_lock() {
|
|
312
|
+
let _ = process.start_kill();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
use anyhow::{Context, Result};
|
|
2
|
+
use serde::{Deserialize, Serialize};
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
use std::fs;
|
|
5
|
+
use std::path::PathBuf;
|
|
6
|
+
|
|
7
|
+
/// MCP server configuration
|
|
8
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
9
|
+
#[serde(untagged)]
|
|
10
|
+
pub enum McpServerConfig {
|
|
11
|
+
Stdio {
|
|
12
|
+
command: String,
|
|
13
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
14
|
+
args: Option<Vec<String>>,
|
|
15
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
16
|
+
env: Option<HashMap<String, String>>,
|
|
17
|
+
},
|
|
18
|
+
Http {
|
|
19
|
+
url: String,
|
|
20
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
21
|
+
headers: Option<HashMap<String, String>>,
|
|
22
|
+
},
|
|
23
|
+
Sse {
|
|
24
|
+
url: String,
|
|
25
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
26
|
+
headers: Option<HashMap<String, String>>,
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Root MCP configuration file structure
|
|
31
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
32
|
+
pub struct McpConfig {
|
|
33
|
+
#[serde(rename = "mcpServers")]
|
|
34
|
+
pub mcp_servers: HashMap<String, McpServerConfig>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
impl McpConfig {
|
|
38
|
+
/// Get the path to the MCP config file (~/.zarz/mcp.json)
|
|
39
|
+
pub fn config_path() -> Result<PathBuf> {
|
|
40
|
+
let home = dirs::home_dir()
|
|
41
|
+
.context("Could not determine home directory")?;
|
|
42
|
+
Ok(home.join(".zarz").join("mcp.json"))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Load MCP config from file, or return default if file doesn't exist
|
|
46
|
+
pub fn load() -> Result<Self> {
|
|
47
|
+
let path = Self::config_path()?;
|
|
48
|
+
|
|
49
|
+
if !path.exists() {
|
|
50
|
+
return Ok(Self::default());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let content = fs::read_to_string(&path)
|
|
54
|
+
.context("Failed to read MCP config file")?;
|
|
55
|
+
|
|
56
|
+
let config: McpConfig = serde_json::from_str(&content)
|
|
57
|
+
.context("Failed to parse MCP config file")?;
|
|
58
|
+
|
|
59
|
+
Ok(config)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Save MCP config to file
|
|
63
|
+
pub fn save(&self) -> Result<()> {
|
|
64
|
+
let path = Self::config_path()?;
|
|
65
|
+
|
|
66
|
+
// Create parent directory if it doesn't exist
|
|
67
|
+
if let Some(parent) = path.parent() {
|
|
68
|
+
fs::create_dir_all(parent)
|
|
69
|
+
.context("Failed to create MCP config directory")?;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let content = serde_json::to_string_pretty(self)
|
|
73
|
+
.context("Failed to serialize MCP config")?;
|
|
74
|
+
|
|
75
|
+
fs::write(&path, content)
|
|
76
|
+
.context("Failed to write MCP config file")?;
|
|
77
|
+
|
|
78
|
+
Ok(())
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Add a new MCP server
|
|
82
|
+
pub fn add_server(&mut self, name: String, config: McpServerConfig) {
|
|
83
|
+
self.mcp_servers.insert(name, config);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Remove an MCP server
|
|
87
|
+
pub fn remove_server(&mut self, name: &str) -> bool {
|
|
88
|
+
self.mcp_servers.remove(name).is_some()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Get an MCP server config by name
|
|
92
|
+
pub fn get_server(&self, name: &str) -> Option<&McpServerConfig> {
|
|
93
|
+
self.mcp_servers.get(name)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// List all configured servers
|
|
97
|
+
#[allow(dead_code)]
|
|
98
|
+
pub fn list_servers(&self) -> Vec<String> {
|
|
99
|
+
self.mcp_servers.keys().cloned().collect()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Check if config has any servers
|
|
103
|
+
#[allow(dead_code)]
|
|
104
|
+
pub fn has_servers(&self) -> bool {
|
|
105
|
+
!self.mcp_servers.is_empty()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
impl McpServerConfig {
|
|
110
|
+
/// Create a new STDIO server config
|
|
111
|
+
pub fn stdio(command: String, args: Option<Vec<String>>, env: Option<HashMap<String, String>>) -> Self {
|
|
112
|
+
McpServerConfig::Stdio { command, args, env }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Create a new HTTP server config
|
|
116
|
+
pub fn http(url: String, headers: Option<HashMap<String, String>>) -> Self {
|
|
117
|
+
McpServerConfig::Http { url, headers }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Create a new SSE server config
|
|
121
|
+
pub fn sse(url: String, headers: Option<HashMap<String, String>>) -> Self {
|
|
122
|
+
McpServerConfig::Sse { url, headers }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Get server type as string
|
|
126
|
+
pub fn server_type(&self) -> &'static str {
|
|
127
|
+
match self {
|
|
128
|
+
McpServerConfig::Stdio { .. } => "stdio",
|
|
129
|
+
McpServerConfig::Http { .. } => "http",
|
|
130
|
+
McpServerConfig::Sse { .. } => "sse",
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
use anyhow::{anyhow, Result};
|
|
2
|
+
use std::collections::HashMap;
|
|
3
|
+
use tokio::sync::RwLock;
|
|
4
|
+
|
|
5
|
+
use super::client::McpClient;
|
|
6
|
+
use super::config::{McpConfig, McpServerConfig};
|
|
7
|
+
use super::types::{McpTool, McpResource, McpPrompt};
|
|
8
|
+
|
|
9
|
+
/// Manages multiple MCP clients
|
|
10
|
+
pub struct McpManager {
|
|
11
|
+
clients: RwLock<HashMap<String, McpClient>>,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
impl McpManager {
|
|
15
|
+
/// Create a new MCP manager
|
|
16
|
+
pub fn new() -> Self {
|
|
17
|
+
Self {
|
|
18
|
+
clients: RwLock::new(HashMap::new()),
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Load and start all configured MCP servers
|
|
23
|
+
pub async fn load_from_config(&self) -> Result<()> {
|
|
24
|
+
let config = McpConfig::load()?;
|
|
25
|
+
|
|
26
|
+
for (name, server_config) in config.mcp_servers {
|
|
27
|
+
if let Err(e) = self.start_server(name.clone(), server_config).await {
|
|
28
|
+
eprintln!("Warning: Failed to start MCP server '{}': {}", name, e);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Ok(())
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Start a specific MCP server
|
|
36
|
+
pub async fn start_server(&self, name: String, config: McpServerConfig) -> Result<()> {
|
|
37
|
+
let mut client = McpClient::new(name.clone(), config);
|
|
38
|
+
client.start().await?;
|
|
39
|
+
|
|
40
|
+
let mut clients = self.clients.write().await;
|
|
41
|
+
clients.insert(name, client);
|
|
42
|
+
|
|
43
|
+
Ok(())
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Stop a specific MCP server
|
|
47
|
+
#[allow(dead_code)]
|
|
48
|
+
pub async fn stop_server(&self, name: &str) -> Result<()> {
|
|
49
|
+
let mut clients = self.clients.write().await;
|
|
50
|
+
|
|
51
|
+
if let Some(mut client) = clients.remove(name) {
|
|
52
|
+
client.stop().await?;
|
|
53
|
+
Ok(())
|
|
54
|
+
} else {
|
|
55
|
+
Err(anyhow!("Server '{}' not found", name))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// List all running servers
|
|
60
|
+
pub async fn list_servers(&self) -> Vec<String> {
|
|
61
|
+
let clients = self.clients.read().await;
|
|
62
|
+
clients.keys().cloned().collect()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Get all available tools from all servers
|
|
66
|
+
pub async fn get_all_tools(&self) -> Result<HashMap<String, Vec<McpTool>>> {
|
|
67
|
+
let clients = self.clients.read().await;
|
|
68
|
+
let mut all_tools = HashMap::new();
|
|
69
|
+
|
|
70
|
+
for (name, client) in clients.iter() {
|
|
71
|
+
match client.list_tools().await {
|
|
72
|
+
Ok(tools) => {
|
|
73
|
+
all_tools.insert(name.clone(), tools);
|
|
74
|
+
}
|
|
75
|
+
Err(e) => {
|
|
76
|
+
eprintln!("Warning: Failed to get tools from '{}': {}", name, e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Ok(all_tools)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Get all available resources from all servers
|
|
85
|
+
#[allow(dead_code)]
|
|
86
|
+
pub async fn get_all_resources(&self) -> Result<HashMap<String, Vec<McpResource>>> {
|
|
87
|
+
let clients = self.clients.read().await;
|
|
88
|
+
let mut all_resources = HashMap::new();
|
|
89
|
+
|
|
90
|
+
for (name, client) in clients.iter() {
|
|
91
|
+
match client.list_resources().await {
|
|
92
|
+
Ok(resources) => {
|
|
93
|
+
all_resources.insert(name.clone(), resources);
|
|
94
|
+
}
|
|
95
|
+
Err(e) => {
|
|
96
|
+
eprintln!("Warning: Failed to get resources from '{}': {}", name, e);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Ok(all_resources)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Get all available prompts from all servers
|
|
105
|
+
#[allow(dead_code)]
|
|
106
|
+
pub async fn get_all_prompts(&self) -> Result<HashMap<String, Vec<McpPrompt>>> {
|
|
107
|
+
let clients = self.clients.read().await;
|
|
108
|
+
let mut all_prompts = HashMap::new();
|
|
109
|
+
|
|
110
|
+
for (name, client) in clients.iter() {
|
|
111
|
+
match client.list_prompts().await {
|
|
112
|
+
Ok(prompts) => {
|
|
113
|
+
all_prompts.insert(name.clone(), prompts);
|
|
114
|
+
}
|
|
115
|
+
Err(e) => {
|
|
116
|
+
eprintln!("Warning: Failed to get prompts from '{}': {}", name, e);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
Ok(all_prompts)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Call a tool on a specific server
|
|
125
|
+
#[allow(dead_code)]
|
|
126
|
+
pub async fn call_tool(
|
|
127
|
+
&self,
|
|
128
|
+
server_name: &str,
|
|
129
|
+
tool_name: String,
|
|
130
|
+
mut arguments: Option<HashMap<String, serde_json::Value>>,
|
|
131
|
+
) -> Result<super::types::CallToolResult> {
|
|
132
|
+
if let Some(args) = arguments.as_mut() {
|
|
133
|
+
if let Some(value) = args.get_mut("sources") {
|
|
134
|
+
if let serde_json::Value::Array(items) = value {
|
|
135
|
+
let contains_strings = items.iter().all(|item| matches!(item, serde_json::Value::String(_)));
|
|
136
|
+
if contains_strings {
|
|
137
|
+
eprintln!("Warning: Removing incompatible 'sources' parameter from MCP tool call (expected object array). Using server defaults instead.");
|
|
138
|
+
args.remove("sources");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let clients = self.clients.read().await;
|
|
145
|
+
|
|
146
|
+
let client = clients.get(server_name)
|
|
147
|
+
.ok_or_else(|| anyhow!("Server '{}' not found", server_name))?;
|
|
148
|
+
|
|
149
|
+
client.call_tool(tool_name, arguments).await
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Get server info for a specific server
|
|
153
|
+
pub async fn get_server_info(&self, name: &str) -> Option<String> {
|
|
154
|
+
let clients = self.clients.read().await;
|
|
155
|
+
clients.get(name).and_then(|c| {
|
|
156
|
+
c.server_info().map(|info| {
|
|
157
|
+
format!("{} v{}", info.name, info.version)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Check if any servers are running
|
|
163
|
+
pub async fn has_servers(&self) -> bool {
|
|
164
|
+
let clients = self.clients.read().await;
|
|
165
|
+
!clients.is_empty()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// Stop all servers
|
|
169
|
+
pub async fn stop_all(&self) -> Result<()> {
|
|
170
|
+
let mut clients = self.clients.write().await;
|
|
171
|
+
|
|
172
|
+
for (name, mut client) in clients.drain() {
|
|
173
|
+
if let Err(e) = client.stop().await {
|
|
174
|
+
eprintln!("Warning: Failed to stop server '{}': {}", name, e);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
Ok(())
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
impl Default for McpManager {
|
|
183
|
+
fn default() -> Self {
|
|
184
|
+
Self::new()
|
|
185
|
+
}
|
|
186
|
+
}
|
package/src/mcp/mod.rs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// MCP (Model Context Protocol) support for ZarzCLI
|
|
2
|
+
pub mod config;
|
|
3
|
+
pub mod client;
|
|
4
|
+
pub mod types;
|
|
5
|
+
pub mod manager;
|
|
6
|
+
|
|
7
|
+
pub use config::{McpConfig, McpServerConfig};
|
|
8
|
+
#[allow(unused_imports)]
|
|
9
|
+
pub use client::McpClient;
|
|
10
|
+
#[allow(unused_imports)]
|
|
11
|
+
pub use types::{McpTool, McpResource, McpPrompt};
|
|
12
|
+
pub use manager::McpManager;
|