zarz 0.3.4-alpha → 0.3.5-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/src/mcp/client.rs DELETED
@@ -1,316 +0,0 @@
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(&notification)?;
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
- }
package/src/mcp/config.rs DELETED
@@ -1,133 +0,0 @@
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
- }
@@ -1,186 +0,0 @@
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 DELETED
@@ -1,12 +0,0 @@
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;