anveesa 0.7.2 → 0.7.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 +2250 -0
- package/Cargo.toml +30 -0
- package/LICENSE +21 -0
- package/bin/anveesa.js +50 -0
- package/package.json +35 -22
- package/scripts/install.js +203 -0
- package/src/cli.rs +126 -0
- package/src/config.rs +743 -0
- package/src/display.rs +794 -0
- package/src/image.rs +344 -0
- package/src/lib.rs +777 -0
- package/src/main.rs +4 -0
- package/src/mcp.rs +271 -0
- package/src/prompt.rs +616 -0
- package/src/provider/command.rs +310 -0
- package/src/provider/mod.rs +210 -0
- package/src/provider/openai_compatible.rs +1635 -0
- package/src/provider/openai_compatible_tests.rs +533 -0
- package/src/session.rs +565 -0
- package/src/tools.rs +2729 -0
- package/src/tools_scenarios.rs +2026 -0
- package/src/tui/commands.rs +515 -0
- package/src/tui/format.rs +439 -0
- package/src/tui/input.rs +198 -0
- package/src/tui/render.rs +735 -0
- package/src/tui/stream.rs +439 -0
- package/src/tui.rs +709 -0
- package/src/web.rs +185 -0
- package/src/web_ui.html +213 -0
- package/src/workspace.rs +216 -0
- package/bin/anveesa +0 -12
- package/install.js +0 -92
package/src/main.rs
ADDED
package/src/mcp.rs
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
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
|
|
92
|
+
.read_line(&mut line)
|
|
93
|
+
.await
|
|
94
|
+
.context("MCP server closed")?;
|
|
95
|
+
if line.is_empty() {
|
|
96
|
+
bail!("MCP server closed connection");
|
|
97
|
+
}
|
|
98
|
+
Ok(serde_json::from_str(line.trim())?)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async fn request(&self, method: &str, params: Value) -> Result<Value> {
|
|
102
|
+
let id = {
|
|
103
|
+
let mut n = self.next_id.lock().await;
|
|
104
|
+
let v = *n;
|
|
105
|
+
*n += 1;
|
|
106
|
+
v
|
|
107
|
+
};
|
|
108
|
+
self.send_msg(json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params }))
|
|
109
|
+
.await?;
|
|
110
|
+
|
|
111
|
+
// Wait for our response with a timeout
|
|
112
|
+
let timeout = tokio::time::Duration::from_secs(30);
|
|
113
|
+
let result = tokio::time::timeout(timeout, async {
|
|
114
|
+
loop {
|
|
115
|
+
let resp = self.recv_msg().await?;
|
|
116
|
+
if resp.get("id").and_then(|v| v.as_u64()) == Some(id) {
|
|
117
|
+
if let Some(err) = resp.get("error") {
|
|
118
|
+
anyhow::bail!("MCP error from '{}': {}", self.name, err);
|
|
119
|
+
}
|
|
120
|
+
return Ok(resp["result"].clone());
|
|
121
|
+
}
|
|
122
|
+
// Drop unmatched messages (notifications, other ids)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
.await
|
|
126
|
+
.context(format!(
|
|
127
|
+
"MCP request to '{}' timed out after 30s",
|
|
128
|
+
self.name
|
|
129
|
+
))??;
|
|
130
|
+
Ok(result)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async fn notify(&self, method: &str, params: Value) -> Result<()> {
|
|
134
|
+
self.send_msg(json!({ "jsonrpc": "2.0", "method": method, "params": params }))
|
|
135
|
+
.await
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async fn initialize(&self) -> Result<()> {
|
|
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?;
|
|
148
|
+
self.notify("notifications/initialized", json!({})).await?;
|
|
149
|
+
Ok(())
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async fn list_tools(&self) -> Result<Vec<McpTool>> {
|
|
153
|
+
let result = self.request("tools/list", json!({})).await?;
|
|
154
|
+
let raw = result["tools"].as_array().cloned().unwrap_or_default();
|
|
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
|
+
})
|
|
172
|
+
})
|
|
173
|
+
.collect())
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async fn call_tool(&self, original_name: &str, arguments: Value) -> Result<String> {
|
|
177
|
+
let result = self
|
|
178
|
+
.request(
|
|
179
|
+
"tools/call",
|
|
180
|
+
json!({
|
|
181
|
+
"name": original_name,
|
|
182
|
+
"arguments": arguments,
|
|
183
|
+
}),
|
|
184
|
+
)
|
|
185
|
+
.await?;
|
|
186
|
+
|
|
187
|
+
// MCP returns content as an array of typed blocks
|
|
188
|
+
let content = result["content"].as_array().cloned().unwrap_or_default();
|
|
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,
|
|
194
|
+
})
|
|
195
|
+
.collect::<Vec<_>>()
|
|
196
|
+
.join("\n");
|
|
197
|
+
|
|
198
|
+
let is_error = result["isError"].as_bool().unwrap_or(false);
|
|
199
|
+
Ok(json!({ "ok": !is_error, "result": text }).to_string())
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── McpManager — holds all connected servers ──────────────────────────────────
|
|
204
|
+
|
|
205
|
+
pub struct McpManager {
|
|
206
|
+
servers: Vec<(McpServer, Vec<McpTool>)>,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
impl std::fmt::Debug for McpManager {
|
|
210
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
211
|
+
let names: Vec<&str> = self.servers.iter().map(|(s, _)| s.name.as_str()).collect();
|
|
212
|
+
write!(f, "McpManager {{ servers: {:?} }}", names)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
impl McpManager {
|
|
217
|
+
/// Connect to all configured MCP servers. Errors for individual servers are
|
|
218
|
+
/// logged and skipped so one broken server doesn't block startup.
|
|
219
|
+
pub async fn connect(configs: &BTreeMap<String, McpServerConfig>) -> Self {
|
|
220
|
+
let mut servers = Vec::new();
|
|
221
|
+
for (name, cfg) in configs {
|
|
222
|
+
match McpServer::connect(name, cfg).await {
|
|
223
|
+
Ok(pair) => {
|
|
224
|
+
eprintln!(
|
|
225
|
+
"\x1b[2m MCP: connected to '{name}' ({} tools)\x1b[0m",
|
|
226
|
+
pair.1.len()
|
|
227
|
+
);
|
|
228
|
+
servers.push(pair);
|
|
229
|
+
}
|
|
230
|
+
Err(e) => {
|
|
231
|
+
eprintln!("\x1b[33m MCP: failed to connect to '{name}': {e:#}\x1b[0m");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
Self { servers }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// All tool definitions from all connected servers.
|
|
239
|
+
pub fn tool_definitions(&self) -> Vec<Value> {
|
|
240
|
+
self.servers
|
|
241
|
+
.iter()
|
|
242
|
+
.flat_map(|(_, tools)| tools.iter().map(|t| t.to_definition()))
|
|
243
|
+
.collect()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// All tool names from all connected servers.
|
|
247
|
+
pub fn tool_names(&self) -> Vec<String> {
|
|
248
|
+
self.servers
|
|
249
|
+
.iter()
|
|
250
|
+
.flat_map(|(_, tools)| tools.iter().map(|t| t.name.clone()))
|
|
251
|
+
.collect()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// Dispatch a call to the appropriate MCP server.
|
|
255
|
+
pub async fn call(&self, tool_name: &str, arguments: &str) -> Option<String> {
|
|
256
|
+
let args: Value = serde_json::from_str(arguments).unwrap_or(json!({}));
|
|
257
|
+
for (server, tools) in &self.servers {
|
|
258
|
+
if let Some(tool) = tools.iter().find(|t| t.name == tool_name) {
|
|
259
|
+
return Some(match server.call_tool(&tool.original_name, args).await {
|
|
260
|
+
Ok(r) => r,
|
|
261
|
+
Err(e) => json!({ "ok": false, "error": e.to_string() }).to_string(),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
None
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
pub fn is_empty(&self) -> bool {
|
|
269
|
+
self.servers.is_empty()
|
|
270
|
+
}
|
|
271
|
+
}
|