anveesa 0.4.1 → 0.4.3
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/config.rs +20 -0
- package/src/lib.rs +47 -7
- package/src/mcp.rs +237 -0
- package/src/provider/command.rs +1 -0
- package/src/provider/mod.rs +3 -1
- package/src/provider/openai_compatible.rs +31 -2
- package/src/tools.rs +241 -5
- package/src/tui.rs +302 -57
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/package.json
CHANGED
package/src/config.rs
CHANGED
|
@@ -174,6 +174,23 @@ pub struct AppConfig {
|
|
|
174
174
|
|
|
175
175
|
#[serde(default)]
|
|
176
176
|
pub providers: BTreeMap<String, ProviderConfig>,
|
|
177
|
+
|
|
178
|
+
/// MCP servers to connect to on startup.
|
|
179
|
+
/// Example config:
|
|
180
|
+
/// [mcp.filesystem]
|
|
181
|
+
/// command = "npx"
|
|
182
|
+
/// args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
|
|
183
|
+
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
|
184
|
+
pub mcp: BTreeMap<String, McpServerConfig>,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
188
|
+
pub struct McpServerConfig {
|
|
189
|
+
pub command: String,
|
|
190
|
+
#[serde(default)]
|
|
191
|
+
pub args: Vec<String>,
|
|
192
|
+
#[serde(default)]
|
|
193
|
+
pub env: BTreeMap<String, String>,
|
|
177
194
|
}
|
|
178
195
|
|
|
179
196
|
impl AppConfig {
|
|
@@ -351,6 +368,7 @@ impl AppConfig {
|
|
|
351
368
|
Self {
|
|
352
369
|
default_provider: Some("sumopod".to_string()),
|
|
353
370
|
providers,
|
|
371
|
+
mcp: BTreeMap::new(),
|
|
354
372
|
}
|
|
355
373
|
}
|
|
356
374
|
|
|
@@ -375,6 +393,7 @@ impl AppConfig {
|
|
|
375
393
|
self.default_provider = user_config.default_provider;
|
|
376
394
|
}
|
|
377
395
|
self.providers.extend(user_config.providers);
|
|
396
|
+
self.mcp.extend(user_config.mcp);
|
|
378
397
|
}
|
|
379
398
|
|
|
380
399
|
pub fn provider_name<'a>(&'a self, requested: Option<&'a str>) -> Result<&'a str> {
|
|
@@ -620,6 +639,7 @@ fn load_user_config_for_write(path: &Path) -> Result<AppConfig> {
|
|
|
620
639
|
return Ok(AppConfig {
|
|
621
640
|
default_provider: Some("sumopod".to_string()),
|
|
622
641
|
providers: BTreeMap::new(),
|
|
642
|
+
mcp: BTreeMap::new(),
|
|
623
643
|
});
|
|
624
644
|
}
|
|
625
645
|
|
package/src/lib.rs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
pub mod cli;
|
|
2
2
|
pub mod config;
|
|
3
|
+
pub mod mcp;
|
|
3
4
|
pub mod provider;
|
|
4
5
|
pub mod tools;
|
|
5
6
|
pub mod tui;
|
|
@@ -154,6 +155,14 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
|
|
|
154
155
|
|
|
155
156
|
// ── TUI mode ──────────────────────────────────────────────────────────────
|
|
156
157
|
if is_tty {
|
|
158
|
+
// Connect to any configured MCP servers.
|
|
159
|
+
let mcp_manager = if !config.mcp.is_empty() {
|
|
160
|
+
let m = mcp::McpManager::connect(&config.mcp).await;
|
|
161
|
+
Some(std::sync::Arc::new(m))
|
|
162
|
+
} else {
|
|
163
|
+
None
|
|
164
|
+
};
|
|
165
|
+
|
|
157
166
|
// Spawn a background task to read keyboard events (crossterm::event::read is blocking).
|
|
158
167
|
let (key_tx, key_rx) = tokio::sync::mpsc::unbounded_channel();
|
|
159
168
|
tokio::task::spawn_blocking(move || {
|
|
@@ -183,6 +192,7 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
|
|
|
183
192
|
workspace_context,
|
|
184
193
|
policy,
|
|
185
194
|
key_rx,
|
|
195
|
+
mcp_manager,
|
|
186
196
|
);
|
|
187
197
|
|
|
188
198
|
tui::run(app).await?;
|
|
@@ -624,6 +634,7 @@ async fn ask_streaming(
|
|
|
624
634
|
workspace_context: workspace_context.map(str::to_string),
|
|
625
635
|
history: history.to_vec(),
|
|
626
636
|
image,
|
|
637
|
+
mcp: None, // REPL path: MCP not yet wired here
|
|
627
638
|
};
|
|
628
639
|
|
|
629
640
|
let result = provider::ask(config, &provider_name, request, policy, &tx).await;
|
|
@@ -1764,16 +1775,20 @@ fn read_prompt_line(
|
|
|
1764
1775
|
buffer.clear_all();
|
|
1765
1776
|
display_rows = redraw!();
|
|
1766
1777
|
}
|
|
1767
|
-
22
|
|
1768
|
-
// Ctrl+V — paste image
|
|
1769
|
-
|
|
1770
|
-
Some(img)
|
|
1778
|
+
22 => {
|
|
1779
|
+
// Ctrl+V — universal paste: image first, then clipboard text
|
|
1780
|
+
if images_available {
|
|
1781
|
+
if let Some(img) = grab_clipboard_image() {
|
|
1771
1782
|
ctrl_v_image = Some(img);
|
|
1772
1783
|
display_rows = redraw!();
|
|
1784
|
+
continue;
|
|
1773
1785
|
}
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1786
|
+
}
|
|
1787
|
+
// Fall back to clipboard text via pbpaste / xclip
|
|
1788
|
+
if let Some(text) = read_clipboard_text() {
|
|
1789
|
+
if !text.is_empty() {
|
|
1790
|
+
buffer.push_text(&text.replace('\r', "\n"));
|
|
1791
|
+
display_rows = redraw!();
|
|
1777
1792
|
}
|
|
1778
1793
|
}
|
|
1779
1794
|
}
|
|
@@ -2313,6 +2328,31 @@ fn repl_history_path() -> Option<PathBuf> {
|
|
|
2313
2328
|
Some(dir.join("history"))
|
|
2314
2329
|
}
|
|
2315
2330
|
|
|
2331
|
+
pub fn read_clipboard_text() -> Option<String> {
|
|
2332
|
+
#[cfg(target_os = "macos")]
|
|
2333
|
+
{
|
|
2334
|
+
let out = std::process::Command::new("pbpaste").output().ok()?;
|
|
2335
|
+
if out.status.success() {
|
|
2336
|
+
let text = String::from_utf8_lossy(&out.stdout).into_owned();
|
|
2337
|
+
if !text.is_empty() { return Some(text); }
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
#[cfg(not(target_os = "macos"))]
|
|
2341
|
+
for (cmd, args) in &[
|
|
2342
|
+
("wl-paste", vec!["--no-newline"]),
|
|
2343
|
+
("xclip", vec!["-o", "-selection", "clipboard"]),
|
|
2344
|
+
("xsel", vec!["--clipboard", "--output"]),
|
|
2345
|
+
] {
|
|
2346
|
+
if let Ok(out) = std::process::Command::new(cmd).args(args).output() {
|
|
2347
|
+
if out.status.success() {
|
|
2348
|
+
let text = String::from_utf8_lossy(&out.stdout).into_owned();
|
|
2349
|
+
if !text.is_empty() { return Some(text); }
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
None
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2316
2356
|
pub fn unix_now() -> u64 {
|
|
2317
2357
|
std::time::SystemTime::now()
|
|
2318
2358
|
.duration_since(std::time::UNIX_EPOCH)
|
package/src/mcp.rs
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
//! MCP (Model Context Protocol) client — connects to external tool servers
|
|
2
|
+
//! over JSON-RPC / stdio, discovers their tools, and routes calls to them.
|
|
3
|
+
|
|
4
|
+
use std::collections::BTreeMap;
|
|
5
|
+
|
|
6
|
+
use anyhow::{Context, Result, bail};
|
|
7
|
+
use serde_json::{Value, json};
|
|
8
|
+
use tokio::{
|
|
9
|
+
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
|
10
|
+
process::Child,
|
|
11
|
+
sync::Mutex,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
use crate::config::McpServerConfig;
|
|
15
|
+
|
|
16
|
+
// ── Tool definition exposed to the rest of anveesa ───────────────────────────
|
|
17
|
+
|
|
18
|
+
#[derive(Debug, Clone)]
|
|
19
|
+
pub struct McpTool {
|
|
20
|
+
/// Namespaced as mcp__{server}__{original_name}
|
|
21
|
+
pub name: String,
|
|
22
|
+
pub description: String,
|
|
23
|
+
pub input_schema: Value,
|
|
24
|
+
pub server: String,
|
|
25
|
+
pub original_name: String,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
impl McpTool {
|
|
29
|
+
/// Convert to an OpenAI-compatible function definition.
|
|
30
|
+
pub fn to_definition(&self) -> Value {
|
|
31
|
+
json!({
|
|
32
|
+
"type": "function",
|
|
33
|
+
"function": {
|
|
34
|
+
"name": self.name,
|
|
35
|
+
"description": format!("[MCP:{}] {}", self.server, self.description),
|
|
36
|
+
"parameters": self.input_schema,
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Single MCP server connection ──────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
struct McpServer {
|
|
45
|
+
name: String,
|
|
46
|
+
stdin: Mutex<tokio::process::ChildStdin>,
|
|
47
|
+
stdout: Mutex<BufReader<tokio::process::ChildStdout>>,
|
|
48
|
+
next_id: Mutex<u64>,
|
|
49
|
+
_child: Child,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
impl McpServer {
|
|
53
|
+
async fn connect(name: &str, cfg: &McpServerConfig) -> Result<(Self, Vec<McpTool>)> {
|
|
54
|
+
let mut child = tokio::process::Command::new(&cfg.command)
|
|
55
|
+
.args(&cfg.args)
|
|
56
|
+
.envs(&cfg.env)
|
|
57
|
+
.stdin(std::process::Stdio::piped())
|
|
58
|
+
.stdout(std::process::Stdio::piped())
|
|
59
|
+
.stderr(std::process::Stdio::null())
|
|
60
|
+
.kill_on_drop(true)
|
|
61
|
+
.spawn()
|
|
62
|
+
.with_context(|| format!("failed to start MCP server '{name}' ({})", cfg.command))?;
|
|
63
|
+
|
|
64
|
+
let stdin = child.stdin.take().context("MCP server has no stdin")?;
|
|
65
|
+
let stdout = BufReader::new(child.stdout.take().context("MCP server has no stdout")?);
|
|
66
|
+
|
|
67
|
+
let server = Self {
|
|
68
|
+
name: name.to_string(),
|
|
69
|
+
stdin: Mutex::new(stdin),
|
|
70
|
+
stdout: Mutex::new(stdout),
|
|
71
|
+
next_id: Mutex::new(1),
|
|
72
|
+
_child: child,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
server.initialize().await?;
|
|
76
|
+
let tools = server.list_tools().await?;
|
|
77
|
+
Ok((server, tools))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async fn send_msg(&self, msg: Value) -> Result<()> {
|
|
81
|
+
let line = serde_json::to_string(&msg)? + "\n";
|
|
82
|
+
let mut stdin = self.stdin.lock().await;
|
|
83
|
+
stdin.write_all(line.as_bytes()).await?;
|
|
84
|
+
stdin.flush().await?;
|
|
85
|
+
Ok(())
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async fn recv_msg(&self) -> Result<Value> {
|
|
89
|
+
let mut stdout = self.stdout.lock().await;
|
|
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"); }
|
|
93
|
+
Ok(serde_json::from_str(line.trim())?)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async fn request(&self, method: &str, params: Value) -> Result<Value> {
|
|
97
|
+
let id = {
|
|
98
|
+
let mut n = self.next_id.lock().await;
|
|
99
|
+
let v = *n;
|
|
100
|
+
*n += 1;
|
|
101
|
+
v
|
|
102
|
+
};
|
|
103
|
+
self.send_msg(json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params })).await?;
|
|
104
|
+
|
|
105
|
+
// Drain notifications until we get our response id
|
|
106
|
+
loop {
|
|
107
|
+
let resp = self.recv_msg().await?;
|
|
108
|
+
if resp.get("id").and_then(|v| v.as_u64()) == Some(id) {
|
|
109
|
+
if let Some(err) = resp.get("error") {
|
|
110
|
+
bail!("MCP error from '{}': {}", self.name, err);
|
|
111
|
+
}
|
|
112
|
+
return Ok(resp["result"].clone());
|
|
113
|
+
}
|
|
114
|
+
// Drop unmatched messages (notifications, other ids)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async fn notify(&self, method: &str, params: Value) -> Result<()> {
|
|
119
|
+
self.send_msg(json!({ "jsonrpc": "2.0", "method": method, "params": params })).await
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async fn initialize(&self) -> Result<()> {
|
|
123
|
+
self.request("initialize", json!({
|
|
124
|
+
"protocolVersion": "2024-11-05",
|
|
125
|
+
"capabilities": {},
|
|
126
|
+
"clientInfo": { "name": "anveesa", "version": env!("CARGO_PKG_VERSION") }
|
|
127
|
+
})).await?;
|
|
128
|
+
self.notify("notifications/initialized", json!({})).await?;
|
|
129
|
+
Ok(())
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async fn list_tools(&self) -> Result<Vec<McpTool>> {
|
|
133
|
+
let result = self.request("tools/list", json!({})).await?;
|
|
134
|
+
let raw = result["tools"].as_array().cloned().unwrap_or_default();
|
|
135
|
+
Ok(raw.into_iter().filter_map(|t| {
|
|
136
|
+
let original_name = t["name"].as_str()?.to_string();
|
|
137
|
+
let description = t["description"].as_str().unwrap_or("").to_string();
|
|
138
|
+
let input_schema = t.get("inputSchema").cloned().unwrap_or(json!({"type":"object","properties":{}}));
|
|
139
|
+
let safe_server = self.name.replace('-', "_").replace('.', "_");
|
|
140
|
+
Some(McpTool {
|
|
141
|
+
name: format!("mcp__{safe_server}__{original_name}"),
|
|
142
|
+
description,
|
|
143
|
+
input_schema,
|
|
144
|
+
server: self.name.clone(),
|
|
145
|
+
original_name,
|
|
146
|
+
})
|
|
147
|
+
}).collect())
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async fn call_tool(&self, original_name: &str, arguments: Value) -> Result<String> {
|
|
151
|
+
let result = self.request("tools/call", json!({
|
|
152
|
+
"name": original_name,
|
|
153
|
+
"arguments": arguments,
|
|
154
|
+
})).await?;
|
|
155
|
+
|
|
156
|
+
// MCP returns content as an array of typed blocks
|
|
157
|
+
let content = result["content"].as_array().cloned().unwrap_or_default();
|
|
158
|
+
let text = content.iter()
|
|
159
|
+
.filter_map(|c| {
|
|
160
|
+
match c["type"].as_str() {
|
|
161
|
+
Some("text") => c["text"].as_str().map(str::to_string),
|
|
162
|
+
_ => None,
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
.collect::<Vec<_>>()
|
|
166
|
+
.join("\n");
|
|
167
|
+
|
|
168
|
+
let is_error = result["isError"].as_bool().unwrap_or(false);
|
|
169
|
+
Ok(json!({ "ok": !is_error, "result": text }).to_string())
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── McpManager — holds all connected servers ──────────────────────────────────
|
|
174
|
+
|
|
175
|
+
pub struct McpManager {
|
|
176
|
+
servers: Vec<(McpServer, Vec<McpTool>)>,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
impl std::fmt::Debug for McpManager {
|
|
180
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
181
|
+
let names: Vec<&str> = self.servers.iter().map(|(s, _)| s.name.as_str()).collect();
|
|
182
|
+
write!(f, "McpManager {{ servers: {:?} }}", names)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
impl McpManager {
|
|
187
|
+
/// Connect to all configured MCP servers. Errors for individual servers are
|
|
188
|
+
/// logged and skipped so one broken server doesn't block startup.
|
|
189
|
+
pub async fn connect(configs: &BTreeMap<String, McpServerConfig>) -> Self {
|
|
190
|
+
let mut servers = Vec::new();
|
|
191
|
+
for (name, cfg) in configs {
|
|
192
|
+
match McpServer::connect(name, cfg).await {
|
|
193
|
+
Ok(pair) => {
|
|
194
|
+
eprintln!("\x1b[2m MCP: connected to '{name}' ({} tools)\x1b[0m",
|
|
195
|
+
pair.1.len());
|
|
196
|
+
servers.push(pair);
|
|
197
|
+
}
|
|
198
|
+
Err(e) => {
|
|
199
|
+
eprintln!("\x1b[33m MCP: failed to connect to '{name}': {e:#}\x1b[0m");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
Self { servers }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// All tool definitions from all connected servers.
|
|
207
|
+
pub fn tool_definitions(&self) -> Vec<Value> {
|
|
208
|
+
self.servers.iter()
|
|
209
|
+
.flat_map(|(_, tools)| tools.iter().map(|t| t.to_definition()))
|
|
210
|
+
.collect()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// All tool names from all connected servers.
|
|
214
|
+
pub fn tool_names(&self) -> Vec<String> {
|
|
215
|
+
self.servers.iter()
|
|
216
|
+
.flat_map(|(_, tools)| tools.iter().map(|t| t.name.clone()))
|
|
217
|
+
.collect()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// Dispatch a call to the appropriate MCP server.
|
|
221
|
+
pub async fn call(&self, tool_name: &str, arguments: &str) -> Option<String> {
|
|
222
|
+
let args: Value = serde_json::from_str(arguments).unwrap_or(json!({}));
|
|
223
|
+
for (server, tools) in &self.servers {
|
|
224
|
+
if let Some(tool) = tools.iter().find(|t| t.name == tool_name) {
|
|
225
|
+
return Some(match server.call_tool(&tool.original_name, args).await {
|
|
226
|
+
Ok(r) => r,
|
|
227
|
+
Err(e) => json!({ "ok": false, "error": e.to_string() }).to_string(),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
None
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
pub fn is_empty(&self) -> bool {
|
|
235
|
+
self.servers.is_empty()
|
|
236
|
+
}
|
|
237
|
+
}
|
package/src/provider/command.rs
CHANGED
package/src/provider/mod.rs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
mod command;
|
|
2
|
-
mod openai_compatible;
|
|
2
|
+
pub mod openai_compatible;
|
|
3
3
|
|
|
4
4
|
use anyhow::{Result, anyhow};
|
|
5
5
|
use serde::{Deserialize, Serialize};
|
|
@@ -51,6 +51,8 @@ pub struct PromptRequest {
|
|
|
51
51
|
pub history: Vec<ChatMessage>,
|
|
52
52
|
/// Optional image grabbed from the clipboard for the current turn only.
|
|
53
53
|
pub image: Option<ImageAttachment>,
|
|
54
|
+
/// Connected MCP servers (runtime only, not part of session history).
|
|
55
|
+
pub mcp: Option<std::sync::Arc<crate::mcp::McpManager>>,
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/// How tool calls that modify the system (write/edit/run) should be handled.
|
|
@@ -86,7 +86,11 @@ pub async fn ask(
|
|
|
86
86
|
body["max_tokens"] = json!(max_tokens);
|
|
87
87
|
}
|
|
88
88
|
if tools_enabled {
|
|
89
|
-
|
|
89
|
+
let mut defs = tools::definitions(policy.allows_write_tools());
|
|
90
|
+
if let Some(m) = &request.mcp {
|
|
91
|
+
defs.extend(m.tool_definitions());
|
|
92
|
+
}
|
|
93
|
+
body["tools"] = json!(defs);
|
|
90
94
|
body["tool_choice"] = json!("auto");
|
|
91
95
|
}
|
|
92
96
|
|
|
@@ -168,7 +172,7 @@ pub async fn ask(
|
|
|
168
172
|
|
|
169
173
|
messages.push(assistant_tool_message(&state));
|
|
170
174
|
for call in &state.tool_calls {
|
|
171
|
-
let content = dispatch_tool(call, policy, &mut approval_state, events).await;
|
|
175
|
+
let content = dispatch_tool(call, policy, &mut approval_state, events, request.mcp.as_deref()).await;
|
|
172
176
|
messages.push(json!({
|
|
173
177
|
"role": "tool",
|
|
174
178
|
"tool_call_id": call.id,
|
|
@@ -209,7 +213,28 @@ async fn dispatch_tool(
|
|
|
209
213
|
policy: ApprovalPolicy,
|
|
210
214
|
approval_state: &mut ToolApprovalState,
|
|
211
215
|
events: &UnboundedSender<StreamEvent>,
|
|
216
|
+
mcp: Option<&crate::mcp::McpManager>,
|
|
212
217
|
) -> String {
|
|
218
|
+
// Route MCP tools directly — no approval policy, no filesystem restrictions.
|
|
219
|
+
if tools::is_mcp_tool(&call.name) {
|
|
220
|
+
let summary = format!("mcp {}", &call.name[5..]); // strip "mcp__"
|
|
221
|
+
let _ = events.send(StreamEvent::ToolCall { summary: summary.clone() });
|
|
222
|
+
let result = if let Some(m) = mcp {
|
|
223
|
+
m.call(&call.name, &call.arguments).await
|
|
224
|
+
.unwrap_or_else(|| serde_json::json!({ "ok": false, "error": "server not found" }).to_string())
|
|
225
|
+
} else {
|
|
226
|
+
serde_json::json!({ "ok": false, "error": "MCP not configured" }).to_string()
|
|
227
|
+
};
|
|
228
|
+
let (ok, err) = parse_tool_result_status(&result);
|
|
229
|
+
let _ = events.send(StreamEvent::ToolResult {
|
|
230
|
+
summary,
|
|
231
|
+
ok,
|
|
232
|
+
elapsed_ms: 0,
|
|
233
|
+
error: err,
|
|
234
|
+
});
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
|
|
213
238
|
let summary = tools::describe_call(&call.name, &call.arguments);
|
|
214
239
|
|
|
215
240
|
// Plan tools — display only, no approval or filesystem access needed.
|
|
@@ -316,6 +341,10 @@ async fn dispatch_tool(
|
|
|
316
341
|
result
|
|
317
342
|
}
|
|
318
343
|
|
|
344
|
+
pub fn parse_tool_result_status_pub(result: &str) -> (bool, Option<String>) {
|
|
345
|
+
parse_tool_result_status(result)
|
|
346
|
+
}
|
|
347
|
+
|
|
319
348
|
fn parse_tool_result_status(result: &str) -> (bool, Option<String>) {
|
|
320
349
|
let Ok(json) = serde_json::from_str::<Value>(result) else {
|
|
321
350
|
return (true, None);
|