anveesa 0.7.0 → 0.7.1

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 CHANGED
@@ -60,7 +60,7 @@ dependencies = [
60
60
 
61
61
  [[package]]
62
62
  name = "anveesa"
63
- version = "0.7.0"
63
+ version = "0.7.1"
64
64
  dependencies = [
65
65
  "anyhow",
66
66
  "axum",
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "anveesa"
3
- version = "0.7.0"
3
+ version = "0.7.1"
4
4
  edition = "2024"
5
5
  default-run = "anveesa"
6
6
  description = "Multi-provider terminal AI assistant — TUI, web UI, and one-shot mode backed by any OpenAI-compatible API"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anveesa",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Multi-provider terminal AI assistant — TUI, web UI, and one-shot mode backed by any OpenAI-compatible API",
5
5
  "main": "bin/anveesa.js",
6
6
  "bin": {
package/src/display.rs CHANGED
@@ -240,7 +240,13 @@ pub fn print_status(message: &str, is_tty: bool) {
240
240
  }
241
241
  }
242
242
 
243
- pub fn print_tool_result(summary: &str, ok: bool, elapsed_ms: u128, error: Option<&str>, is_tty: bool) {
243
+ pub fn print_tool_result(
244
+ summary: &str,
245
+ ok: bool,
246
+ elapsed_ms: u128,
247
+ error: Option<&str>,
248
+ is_tty: bool,
249
+ ) {
244
250
  let elapsed = format_duration_ms(elapsed_ms);
245
251
  if is_tty {
246
252
  if ok {
@@ -287,7 +293,7 @@ pub fn print_file_op(
287
293
 
288
294
  // Summary: └ Added N lines, removed M lines
289
295
  let summary = match (added, removed) {
290
- (a, 0) if a == 0 => String::new(),
296
+ (0, 0) => String::new(),
291
297
  (a, 0) => format!("Added {} {}", a, if a == 1 { "line" } else { "lines" }),
292
298
  (0, r) => format!("Removed {} {}", r, if r == 1 { "line" } else { "lines" }),
293
299
  (a, r) => format!(
@@ -565,11 +571,15 @@ pub fn print_help_inline(is_tty: bool) {
565
571
  println!("\x1b[90m ──────────────────────────────────────\x1b[0m");
566
572
  println!(" \x1b[1;32m/status\x1b[0m provider, model, turns, token usage");
567
573
  println!(" \x1b[1;32m/session\x1b[0m show session file, age, and turn count");
568
- println!(" \x1b[1;32m/export\x1b[0m \x1b[2m[path]\x1b[0m save conversation to a markdown file");
574
+ println!(
575
+ " \x1b[1;32m/export\x1b[0m \x1b[2m[path]\x1b[0m save conversation to a markdown file"
576
+ );
569
577
  println!(" \x1b[1;32m/model\x1b[0m \x1b[2m[name]\x1b[0m switch or show current model");
570
578
  println!(" \x1b[1;32m/provider\x1b[0m \x1b[2m[name]\x1b[0m switch or show current provider");
571
579
  println!(" \x1b[1;32m/clear\x1b[0m reset conversation and delete saved session");
572
- println!(" \x1b[1;32m/attach\x1b[0m \x1b[2m[path]\x1b[0m attach image from file or clipboard");
580
+ println!(
581
+ " \x1b[1;32m/attach\x1b[0m \x1b[2m[path]\x1b[0m attach image from file or clipboard"
582
+ );
573
583
  println!(" \x1b[1;32m/exit\x1b[0m, \x1b[1;32m/quit\x1b[0m leave the session");
574
584
  println!(" \x1b[1;32m/help\x1b[0m show this message");
575
585
  println!();
@@ -584,9 +594,13 @@ pub fn print_help_inline(is_tty: bool) {
584
594
  println!();
585
595
  println!("\x1b[2m Images\x1b[0m");
586
596
  println!("\x1b[90m ──────────────────────────────────────\x1b[0m");
587
- println!(" \x1b[2mCtrl+V\x1b[0m to paste a clipboard image inline (shows \x1b[2m[📎]\x1b[0m indicator).");
597
+ println!(
598
+ " \x1b[2mCtrl+V\x1b[0m to paste a clipboard image inline (shows \x1b[2m[📎]\x1b[0m indicator)."
599
+ );
588
600
  println!(" Or Cmd+C an image and send any message — it attaches automatically.");
589
- println!(" Or use \x1b[1;32m/attach\x1b[0m \x1b[2mpath/to/file.png\x1b[0m for a specific file.");
601
+ println!(
602
+ " Or use \x1b[1;32m/attach\x1b[0m \x1b[2mpath/to/file.png\x1b[0m for a specific file."
603
+ );
590
604
  println!(" For broadest clipboard support: \x1b[2mbrew install pngpaste\x1b[0m");
591
605
  println!();
592
606
  }
@@ -639,7 +653,10 @@ pub fn print_session_header(
639
653
 
640
654
  if !is_tty {
641
655
  let tag = if resumed {
642
- format!(" (resumed · {turns} turns · {})", format_session_age(saved_at))
656
+ format!(
657
+ " (resumed · {turns} turns · {})",
658
+ format_session_age(saved_at)
659
+ )
643
660
  } else {
644
661
  String::new()
645
662
  };
@@ -670,7 +687,10 @@ pub fn print_session_header(
670
687
  }
671
688
 
672
689
  let greeting = if resumed {
673
- format!(" · Resumed ({turns} turns · {})", format_session_age(saved_at))
690
+ format!(
691
+ " · Resumed ({turns} turns · {})",
692
+ format_session_age(saved_at)
693
+ )
674
694
  } else {
675
695
  String::new()
676
696
  };
package/src/image.rs CHANGED
@@ -289,19 +289,23 @@ pub fn read_clipboard_text() -> Option<String> {
289
289
  let out = std::process::Command::new("pbpaste").output().ok()?;
290
290
  if out.status.success() {
291
291
  let text = String::from_utf8_lossy(&out.stdout).into_owned();
292
- if !text.is_empty() { return Some(text); }
292
+ if !text.is_empty() {
293
+ return Some(text);
294
+ }
293
295
  }
294
296
  }
295
297
  #[cfg(not(target_os = "macos"))]
296
298
  for (cmd, args) in &[
297
299
  ("wl-paste", vec!["--no-newline"]),
298
- ("xclip", vec!["-o", "-selection", "clipboard"]),
299
- ("xsel", vec!["--clipboard", "--output"]),
300
+ ("xclip", vec!["-o", "-selection", "clipboard"]),
301
+ ("xsel", vec!["--clipboard", "--output"]),
300
302
  ] {
301
- if let Ok(out) = std::process::Command::new(cmd).args(args).output() {
302
- if out.status.success() {
303
- let text = String::from_utf8_lossy(&out.stdout).into_owned();
304
- if !text.is_empty() { return Some(text); }
303
+ if let Ok(out) = std::process::Command::new(cmd).args(args).output()
304
+ && out.status.success()
305
+ {
306
+ let text = String::from_utf8_lossy(&out.stdout).into_owned();
307
+ if !text.is_empty() {
308
+ return Some(text);
305
309
  }
306
310
  }
307
311
  }
package/src/lib.rs CHANGED
@@ -28,16 +28,13 @@ use crate::{
28
28
  set_default_provider,
29
29
  },
30
30
  display::{
31
- render_stream, print_session_header, print_help_inline, print_session_info,
32
- print_status_inline, prompt_label, print_input_separator, term_width,
33
- },
34
- image::{
35
- attach_image, grab_clipboard_image, image_fingerprint, parse_attach_command,
31
+ print_help_inline, print_input_separator, print_session_header, print_session_info,
32
+ print_status_inline, prompt_label, render_stream, term_width,
36
33
  },
34
+ image::{attach_image, grab_clipboard_image, image_fingerprint, parse_attach_command},
37
35
  prompt::{PromptRead, read_prompt_line},
38
36
  provider::{
39
- ApprovalPolicy, ChatMessage, ChatRole, ImageAttachment,
40
- PromptRequest, TurnResult, Usage,
37
+ ApprovalPolicy, ChatMessage, ChatRole, ImageAttachment, PromptRequest, TurnResult, Usage,
41
38
  },
42
39
  session::{
43
40
  append_repl_history, legacy_session_path, load_interactive_session, purge_stale_sessions,
@@ -124,25 +121,31 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
124
121
  let _ = fs::remove_file(&legacy);
125
122
  Some(session)
126
123
  });
127
- let mut history = loaded_session.as_ref().map(|s| s.messages.clone()).unwrap_or_default();
124
+ let mut history = loaded_session
125
+ .as_ref()
126
+ .map(|s| s.messages.clone())
127
+ .unwrap_or_default();
128
128
  // saved_at at load time — used only for the startup header so it shows when the previous
129
129
  // run ended, not the current run's save time.
130
- let session_saved_at = loaded_session.as_ref().filter(|s| s.saved_at > 0).map(|s| s.saved_at);
130
+ let session_saved_at = loaded_session
131
+ .as_ref()
132
+ .filter(|s| s.saved_at > 0)
133
+ .map(|s| s.saved_at);
131
134
  // tracks the most recent successful save this run — kept fresh for /session display
132
135
  let mut last_saved_at: u64 = session_saved_at.unwrap_or(0);
133
136
  // Per-project config: .anveesa.toml (extended) or .anveesa (plain system prompt)
134
137
  if let Ok(raw) = fs::read_to_string(cwd.join(".anveesa.toml")) {
135
138
  if let Ok(cfg) = toml::from_str::<toml::Value>(&raw) {
136
- if session_options.system.is_none() {
137
- if let Some(sp) = cfg.get("system_prompt").and_then(|v| v.as_str()) {
138
- session_options.system = Some(sp.trim().to_string());
139
- }
139
+ if session_options.system.is_none()
140
+ && let Some(sp) = cfg.get("system_prompt").and_then(|v| v.as_str())
141
+ {
142
+ session_options.system = Some(sp.trim().to_string());
140
143
  }
141
144
  // Override model if not set by CLI
142
- if session_options.model.is_none() {
143
- if let Some(m) = cfg.get("model").and_then(|v| v.as_str()) {
144
- session_options.model = Some(m.to_string());
145
- }
145
+ if session_options.model.is_none()
146
+ && let Some(m) = cfg.get("model").and_then(|v| v.as_str())
147
+ {
148
+ session_options.model = Some(m.to_string());
146
149
  }
147
150
  // auto_approve
148
151
  if let Some(true) = cfg.get("auto_approve").and_then(|v| v.as_bool()) {
@@ -150,12 +153,12 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
150
153
  images_available = true; // keep as-is; just document capability
151
154
  }
152
155
  }
153
- } else if session_options.system.is_none() {
154
- if let Ok(text) = fs::read_to_string(cwd.join(".anveesa")) {
155
- let trimmed = text.trim().to_string();
156
- if !trimmed.is_empty() {
157
- session_options.system = Some(trimmed);
158
- }
156
+ } else if session_options.system.is_none()
157
+ && let Ok(text) = fs::read_to_string(cwd.join(".anveesa"))
158
+ {
159
+ let trimmed = text.trim().to_string();
160
+ if !trimmed.is_empty() {
161
+ session_options.system = Some(trimmed);
159
162
  }
160
163
  }
161
164
 
@@ -164,7 +167,12 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
164
167
  let input_history: Vec<String> = history_path
165
168
  .as_deref()
166
169
  .and_then(|p| fs::read_to_string(p).ok())
167
- .map(|c| c.lines().filter(|l| !l.is_empty()).map(String::from).collect())
170
+ .map(|c| {
171
+ c.lines()
172
+ .filter(|l| !l.is_empty())
173
+ .map(String::from)
174
+ .collect()
175
+ })
168
176
  .unwrap_or_default();
169
177
 
170
178
  print_session_header(
@@ -190,10 +198,9 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
190
198
  // Spawn a background task to read keyboard events (crossterm::event::read is blocking).
191
199
  let (key_tx, key_rx) = tokio::sync::mpsc::unbounded_channel();
192
200
  tokio::task::spawn_blocking(move || {
193
- loop {
194
- match crossterm::event::read() {
195
- Ok(ev) => { if key_tx.send(ev).is_err() { break; } }
196
- Err(_) => break,
201
+ while let Ok(ev) = crossterm::event::read() {
202
+ if key_tx.send(ev).is_err() {
203
+ break;
197
204
  }
198
205
  }
199
206
  });
@@ -204,7 +211,10 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
204
211
 
205
212
  let app = tui::App::new(
206
213
  provider_name.clone(),
207
- session_options.model.clone().unwrap_or_else(|| "-".to_string()),
214
+ session_options
215
+ .model
216
+ .clone()
217
+ .unwrap_or_else(|| "-".to_string()),
208
218
  short_cwd,
209
219
  history,
210
220
  images_available,
@@ -234,16 +244,21 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
234
244
 
235
245
  loop {
236
246
  print_input_separator(is_tty, width);
237
- let (line, ctrl_v_image) =
238
- match read_prompt_line(&label, width, &mut paste_count, images_available, &input_history) {
239
- Ok(PromptRead::Line(line, img)) => (line, img),
240
- Ok(PromptRead::Interrupted) => continue,
241
- Ok(PromptRead::Eof) => {
242
- println!();
243
- break;
244
- }
245
- Err(error) => return Err(error).context("failed to read interactive prompt"),
246
- };
247
+ let (line, ctrl_v_image) = match read_prompt_line(
248
+ &label,
249
+ width,
250
+ &mut paste_count,
251
+ images_available,
252
+ &input_history,
253
+ ) {
254
+ Ok(PromptRead::Line(line, img)) => (line, img),
255
+ Ok(PromptRead::Interrupted) => continue,
256
+ Ok(PromptRead::Eof) => {
257
+ println!();
258
+ break;
259
+ }
260
+ Err(error) => return Err(error).context("failed to read interactive prompt"),
261
+ };
247
262
 
248
263
  // Ctrl+V image takes precedence over a previously pending image.
249
264
  if let Some(img) = ctrl_v_image {
@@ -321,7 +336,10 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
321
336
  s if s.starts_with("/model") => {
322
337
  let arg = s.strip_prefix("/model").unwrap().trim();
323
338
  if arg.is_empty() {
324
- let current = session_options.model.as_deref().unwrap_or("(provider default)");
339
+ let current = session_options
340
+ .model
341
+ .as_deref()
342
+ .unwrap_or("(provider default)");
325
343
  if is_tty {
326
344
  println!("\x1b[2m model: {current}\x1b[0m");
327
345
  } else {
@@ -341,14 +359,18 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
341
359
  let arg = s.strip_prefix("/provider").unwrap().trim();
342
360
  if arg.is_empty() {
343
361
  if is_tty {
344
- println!("\x1b[2m provider: {provider_name} model: {}\x1b[0m",
345
- session_options.model.as_deref().unwrap_or("(default)"));
362
+ println!(
363
+ "\x1b[2m provider: {provider_name} model: {}\x1b[0m",
364
+ session_options.model.as_deref().unwrap_or("(default)")
365
+ );
346
366
  } else {
347
367
  println!("provider: {provider_name}");
348
368
  }
349
369
  } else if !config.providers.contains_key(arg) {
350
370
  if is_tty {
351
- eprintln!("\x1b[1;31m✗\x1b[0m unknown provider '{arg}' — run: anveesa providers");
371
+ eprintln!(
372
+ "\x1b[1;31m✗\x1b[0m unknown provider '{arg}' — run: anveesa providers"
373
+ );
352
374
  } else {
353
375
  eprintln!("error: unknown provider '{arg}'");
354
376
  }
@@ -361,7 +383,9 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
361
383
  session_options.provider = Some(arg.to_string());
362
384
  let model_display = session_options.model.as_deref().unwrap_or("(default)");
363
385
  if is_tty {
364
- println!("\x1b[2m Switched to provider: {arg} model: {model_display}\x1b[0m");
386
+ println!(
387
+ "\x1b[2m Switched to provider: {arg} model: {model_display}\x1b[0m"
388
+ );
365
389
  } else {
366
390
  println!("switched provider: {arg} model: {model_display}");
367
391
  }
@@ -373,7 +397,9 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
373
397
  if let Some(path) = parse_attach_command(&prompt) {
374
398
  if !images_available {
375
399
  if is_tty {
376
- eprintln!("\x1b[1;31m✗\x1b[0m image attachments require an openai-compatible provider");
400
+ eprintln!(
401
+ "\x1b[1;31m✗\x1b[0m image attachments require an openai-compatible provider"
402
+ );
377
403
  } else {
378
404
  eprintln!("error: image attachments require an openai-compatible provider");
379
405
  }
@@ -451,10 +477,17 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
451
477
  }
452
478
  history.push(ChatMessage::user(prompt));
453
479
  history.push(ChatMessage::assistant(result.text));
454
- if let Some(path) = &session_path {
455
- if save_interactive_session(path, &cwd, &provider_name, &session_options, &history).is_ok() {
456
- last_saved_at = unix_now();
457
- }
480
+ if let Some(path) = &session_path
481
+ && save_interactive_session(
482
+ path,
483
+ &cwd,
484
+ &provider_name,
485
+ &session_options,
486
+ &history,
487
+ )
488
+ .is_ok()
489
+ {
490
+ last_saved_at = unix_now();
458
491
  }
459
492
  }
460
493
  Some(Err(error)) => {
@@ -468,10 +501,17 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
468
501
  history.push(ChatMessage::assistant(format!(
469
502
  "The previous turn failed inside Anveesa before a final answer was produced: {error:#}"
470
503
  )));
471
- if let Some(path) = &session_path {
472
- if save_interactive_session(path, &cwd, &provider_name, &session_options, &history).is_ok() {
473
- last_saved_at = unix_now();
474
- }
504
+ if let Some(path) = &session_path
505
+ && save_interactive_session(
506
+ path,
507
+ &cwd,
508
+ &provider_name,
509
+ &session_options,
510
+ &history,
511
+ )
512
+ .is_ok()
513
+ {
514
+ last_saved_at = unix_now();
475
515
  }
476
516
  }
477
517
  None => {
@@ -483,7 +523,13 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
483
523
  eprintln!("interrupted");
484
524
  }
485
525
  if let Some(path) = &session_path {
486
- let _ = save_interactive_session(path, &cwd, &provider_name, &session_options, &history);
526
+ let _ = save_interactive_session(
527
+ path,
528
+ &cwd,
529
+ &provider_name,
530
+ &session_options,
531
+ &history,
532
+ );
487
533
  }
488
534
  break;
489
535
  }
@@ -530,6 +576,7 @@ async fn run_ask(options: AskOptions, prompt_parts: Vec<String>) -> Result<()> {
530
576
  Ok(())
531
577
  }
532
578
 
579
+ #[allow(clippy::too_many_arguments)]
533
580
  async fn ask_streaming(
534
581
  config: &AppConfig,
535
582
  options: &AskOptions,
@@ -579,8 +626,7 @@ pub fn export_conversation(path: &std::path::Path, history: &[ChatMessage]) -> R
579
626
  }
580
627
  }
581
628
  }
582
- fs::write(path, out.trim_end())
583
- .with_context(|| format!("failed to write {}", path.display()))
629
+ fs::write(path, out.trim_end()).with_context(|| format!("failed to write {}", path.display()))
584
630
  }
585
631
 
586
632
  fn list_providers() -> Result<()> {
@@ -591,7 +637,11 @@ fn list_providers() -> Result<()> {
591
637
  for (name, provider) in &config.providers {
592
638
  let is_default = config.default_provider.as_deref() == Some(name.as_str());
593
639
  let model = provider.default_model().unwrap_or("-");
594
- println!("{} {name} {model} {}", if is_default { "*" } else { " " }, provider.kind());
640
+ println!(
641
+ "{} {name} {model} {}",
642
+ if is_default { "*" } else { " " },
643
+ provider.kind()
644
+ );
595
645
  }
596
646
  return Ok(());
597
647
  }
package/src/mcp.rs CHANGED
@@ -88,8 +88,13 @@ impl McpServer {
88
88
  async fn recv_msg(&self) -> Result<Value> {
89
89
  let mut stdout = self.stdout.lock().await;
90
90
  let mut line = String::new();
91
- stdout.read_line(&mut line).await.context("MCP server closed")?;
92
- if line.is_empty() { bail!("MCP server closed connection"); }
91
+ stdout
92
+ .read_line(&mut line)
93
+ .await
94
+ .context("MCP server closed")?;
95
+ if line.is_empty() {
96
+ bail!("MCP server closed connection");
97
+ }
93
98
  Ok(serde_json::from_str(line.trim())?)
94
99
  }
95
100
 
@@ -100,7 +105,8 @@ impl McpServer {
100
105
  *n += 1;
101
106
  v
102
107
  };
103
- self.send_msg(json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params })).await?;
108
+ self.send_msg(json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params }))
109
+ .await?;
104
110
 
105
111
  // Wait for our response with a timeout
106
112
  let timeout = tokio::time::Duration::from_secs(30);
@@ -117,20 +123,28 @@ impl McpServer {
117
123
  }
118
124
  })
119
125
  .await
120
- .context(format!("MCP request to '{}' timed out after 30s", self.name))??;
126
+ .context(format!(
127
+ "MCP request to '{}' timed out after 30s",
128
+ self.name
129
+ ))??;
121
130
  Ok(result)
122
131
  }
123
132
 
124
133
  async fn notify(&self, method: &str, params: Value) -> Result<()> {
125
- self.send_msg(json!({ "jsonrpc": "2.0", "method": method, "params": params })).await
134
+ self.send_msg(json!({ "jsonrpc": "2.0", "method": method, "params": params }))
135
+ .await
126
136
  }
127
137
 
128
138
  async fn initialize(&self) -> Result<()> {
129
- self.request("initialize", json!({
130
- "protocolVersion": "2024-11-05",
131
- "capabilities": {},
132
- "clientInfo": { "name": "anveesa", "version": env!("CARGO_PKG_VERSION") }
133
- })).await?;
139
+ self.request(
140
+ "initialize",
141
+ json!({
142
+ "protocolVersion": "2024-11-05",
143
+ "capabilities": {},
144
+ "clientInfo": { "name": "anveesa", "version": env!("CARGO_PKG_VERSION") }
145
+ }),
146
+ )
147
+ .await?;
134
148
  self.notify("notifications/initialized", json!({})).await?;
135
149
  Ok(())
136
150
  }
@@ -138,35 +152,45 @@ impl McpServer {
138
152
  async fn list_tools(&self) -> Result<Vec<McpTool>> {
139
153
  let result = self.request("tools/list", json!({})).await?;
140
154
  let raw = result["tools"].as_array().cloned().unwrap_or_default();
141
- Ok(raw.into_iter().filter_map(|t| {
142
- let original_name = t["name"].as_str()?.to_string();
143
- let description = t["description"].as_str().unwrap_or("").to_string();
144
- let input_schema = t.get("inputSchema").cloned().unwrap_or(json!({"type":"object","properties":{}}));
145
- let safe_server = self.name.replace('-', "_").replace('.', "_");
146
- Some(McpTool {
147
- name: format!("mcp__{safe_server}__{original_name}"),
148
- description,
149
- input_schema,
150
- server: self.name.clone(),
151
- original_name,
155
+ Ok(raw
156
+ .into_iter()
157
+ .filter_map(|t| {
158
+ let original_name = t["name"].as_str()?.to_string();
159
+ let description = t["description"].as_str().unwrap_or("").to_string();
160
+ let input_schema = t
161
+ .get("inputSchema")
162
+ .cloned()
163
+ .unwrap_or(json!({"type":"object","properties":{}}));
164
+ let safe_server = self.name.replace(['-', '.'], "_");
165
+ Some(McpTool {
166
+ name: format!("mcp__{safe_server}__{original_name}"),
167
+ description,
168
+ input_schema,
169
+ server: self.name.clone(),
170
+ original_name,
171
+ })
152
172
  })
153
- }).collect())
173
+ .collect())
154
174
  }
155
175
 
156
176
  async fn call_tool(&self, original_name: &str, arguments: Value) -> Result<String> {
157
- let result = self.request("tools/call", json!({
158
- "name": original_name,
159
- "arguments": arguments,
160
- })).await?;
177
+ let result = self
178
+ .request(
179
+ "tools/call",
180
+ json!({
181
+ "name": original_name,
182
+ "arguments": arguments,
183
+ }),
184
+ )
185
+ .await?;
161
186
 
162
187
  // MCP returns content as an array of typed blocks
163
188
  let content = result["content"].as_array().cloned().unwrap_or_default();
164
- let text = content.iter()
165
- .filter_map(|c| {
166
- match c["type"].as_str() {
167
- Some("text") => c["text"].as_str().map(str::to_string),
168
- _ => None,
169
- }
189
+ let text = content
190
+ .iter()
191
+ .filter_map(|c| match c["type"].as_str() {
192
+ Some("text") => c["text"].as_str().map(str::to_string),
193
+ _ => None,
170
194
  })
171
195
  .collect::<Vec<_>>()
172
196
  .join("\n");
@@ -197,8 +221,10 @@ impl McpManager {
197
221
  for (name, cfg) in configs {
198
222
  match McpServer::connect(name, cfg).await {
199
223
  Ok(pair) => {
200
- eprintln!("\x1b[2m MCP: connected to '{name}' ({} tools)\x1b[0m",
201
- pair.1.len());
224
+ eprintln!(
225
+ "\x1b[2m MCP: connected to '{name}' ({} tools)\x1b[0m",
226
+ pair.1.len()
227
+ );
202
228
  servers.push(pair);
203
229
  }
204
230
  Err(e) => {
@@ -211,14 +237,16 @@ impl McpManager {
211
237
 
212
238
  /// All tool definitions from all connected servers.
213
239
  pub fn tool_definitions(&self) -> Vec<Value> {
214
- self.servers.iter()
240
+ self.servers
241
+ .iter()
215
242
  .flat_map(|(_, tools)| tools.iter().map(|t| t.to_definition()))
216
243
  .collect()
217
244
  }
218
245
 
219
246
  /// All tool names from all connected servers.
220
247
  pub fn tool_names(&self) -> Vec<String> {
221
- self.servers.iter()
248
+ self.servers
249
+ .iter()
222
250
  .flat_map(|(_, tools)| tools.iter().map(|t| t.name.clone()))
223
251
  .collect()
224
252
  }