ed-mcp: Connection::tools and Connection::call_tool
A host that manages its own servers (siGit Code reads them from mcp.toml and names tools its own way) needs the tools a connection listed and a call that says why it failed. call_raw keeps its Option and now goes through call_tool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seto Elkahfi committed
Oct 9, 2026 at 13:44 UTC
31f893d936295419d6b74dcc490c259fdf16b8f6
2 files changed
+43
-5
crates/ed-mcp/src/lib.rs
+19
-5
@@ -312,18 +312,32 @@ impl Connection {
312
&self.name
313
}
314
315
+ /// The tools the server listed when it connected, under their own names.
316
+ pub fn tools(&self) -> &[Tool] {
317
+ &self.tools
318
+ }
319
+
320
/// Call a tool on this server directly, outside any turn (for example to build a
321
/// permission prompt). Answers `None` on any failure or after `timeout`.
322
pub async fn call_raw(&self, tool: &str, args: Value, timeout: Duration) -> Option<Value> {
323
+ self.call_tool(tool, args, timeout).await.ok()
324
+ }
325
+
326
+ /// Call `tool` by its name on this server and return the `CallToolResult` as JSON. A
327
+ /// result with `isError: true` is `Ok`; a transport or protocol failure, or no answer
328
+ /// within `timeout`, is an `Err` that says which.
329
+ pub async fn call_tool(&self, tool: &str, args: Value, timeout: Duration) -> Result<Value> {
330
let mut params = CallToolRequestParams::new(tool.to_string());
319
- if let Value::Object(map) = args {
320
- params = params.with_arguments(map);
331
+ match args {
332
+ Value::Object(map) => params = params.with_arguments(map),
333
+ Value::Null => {}
334
+ _ => bail!("tool arguments must be a JSON object"),
335
}
336
let result = tokio::time::timeout(timeout, self.service.peer().call_tool(params))
337
.await
324
- .ok()?
325
- .ok()?;
326
- serde_json::to_value(result).ok()
338
+ .map_err(|_| anyhow!("timed out after {}s", timeout.as_secs()))?
339
+ .map_err(|e| anyhow!("{e}"))?;
340
+ Ok(serde_json::to_value(result)?)
341
}
342
343
async fn close(mut self) {
crates/ed-mcp/tests/http.rs
+24
@@ -130,6 +130,30 @@ async fn connects_lists_and_calls_with_a_bearer_token() {
130
set.shutdown().await;
131
}
132
133
+#[tokio::test]
134
+async fn a_connection_lists_and_calls_tools_by_their_own_names() {
135
+ let url = serve().await;
136
+ let spec = ServerSpec::http("mail", url).with_bearer(TOKEN);
137
+ let conn = Connection::connect(&spec, &client(), Duration::from_secs(5))
138
+ .await
139
+ .unwrap();
140
+
141
+ let mut names: Vec<_> = conn.tools().iter().map(|t| t.name.to_string()).collect();
142
+ names.sort();
143
+ assert_eq!(names, ["list_messages", "send_message", "whoami"]);
144
+
145
+ let result = conn
146
+ .call_tool("whoami", json!({}), Duration::from_secs(5))
147
+ .await
148
+ .unwrap();
149
+ assert_eq!(result["content"][0]["text"], "ran whoami");
150
+ let err = conn
151
+ .call_tool("whoami", json!([1]), Duration::from_secs(5))
152
+ .await
153
+ .unwrap_err();
154
+ assert!(err.to_string().contains("JSON object"), "{err:#}");
155
+}
156
+
157
#[tokio::test]
158
async fn a_wrong_bearer_token_is_rejected() {
159
let url = serve().await;