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 +1 -1
- package/Cargo.toml +1 -1
- package/package.json +1 -1
- package/src/display.rs +28 -8
- package/src/image.rs +11 -7
- package/src/lib.rs +107 -57
- package/src/mcp.rs +64 -36
- package/src/prompt.rs +21 -25
- package/src/provider/command.rs +5 -1
- package/src/provider/openai_compatible.rs +152 -74
- package/src/session.rs +71 -29
- package/src/tools.rs +519 -222
- package/src/tools_scenarios.rs +490 -157
- package/src/tui/commands.rs +172 -61
- package/src/tui/format.rs +190 -51
- package/src/tui/input.rs +68 -23
- package/src/tui/render.rs +254 -82
- package/src/tui/stream.rs +151 -52
- package/src/tui.rs +163 -105
- package/src/web.rs +15 -9
- package/src/workspace.rs +29 -23
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/package.json
CHANGED
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(
|
|
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
|
-
(
|
|
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!(
|
|
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!(
|
|
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!(
|
|
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!(
|
|
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!(
|
|
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!(
|
|
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() {
|
|
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",
|
|
299
|
-
("xsel",
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
32
|
-
print_status_inline, prompt_label,
|
|
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
|
|
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
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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|
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
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!(
|
|
345
|
-
|
|
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!(
|
|
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!(
|
|
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!(
|
|
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
|
-
|
|
456
|
-
|
|
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
|
-
|
|
473
|
-
|
|
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(
|
|
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!(
|
|
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
|
|
92
|
-
|
|
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 }))
|
|
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!(
|
|
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 }))
|
|
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(
|
|
130
|
-
"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
173
|
+
.collect())
|
|
154
174
|
}
|
|
155
175
|
|
|
156
176
|
async fn call_tool(&self, original_name: &str, arguments: Value) -> Result<String> {
|
|
157
|
-
let result = self
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
165
|
-
.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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!(
|
|
201
|
-
|
|
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
|
|
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
|
|
248
|
+
self.servers
|
|
249
|
+
.iter()
|
|
222
250
|
.flat_map(|(_, tools)| tools.iter().map(|t| t.name.clone()))
|
|
223
251
|
.collect()
|
|
224
252
|
}
|