Skip to content

McpSource::resource_tools

Rust · crate toolnexus · SPEC §2 · rust/src/mcp/resources.rs

impl McpSource {
pub fn resource_tools(&self) -> Vec<Tool> // [list_mcp_resources, read_mcp_resource]
}

Two tools bound to the servers an McpSource connected. Both are ToolSource::Mcp, carry hints: Some(ToolHints { read_only: true, .. }), and stop working once the source is closed. Nothing is added unless the host registers them.

  • list_mcp_resources {server?} — one line per resource, server uri — name (mimeType): description; metadata["resources"] holds the same as structured items and metadata["count"] the number. No resources ⇒ No resources.
  • read_mcp_resource {server, uri} — text contents as text; binary contents are described ([binary <mime>, N base64 chars]), never inlined. An unknown server or a server error comes back as an error result the model can read.

Your MCP servers publish resources (files, schemas, docs) as well as tools, and you want the model to browse and read them on its own. MCP tools are loaded automatically; resources are not, because they can be large and many servers do not offer them.

Each example connects the repo’s dependency-free stdio fixture examples/mcp-progress-server.mjs (plain node, no network), which offers one text resource, fixture://hello.

1. The smallest useful call — list and read

Section titled “1. The smallest useful call — list and read”
use std::path::Path;
use serde_json::json;
use toolnexus::ToolContext;
use toolnexus::mcp::{load_mcp, parse_mcp_config};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let repo = std::env::var("TOOLNEXUS_REPO")?;
let server = Path::new(&repo).join("examples/mcp-progress-server.mjs");
let cfg = parse_mcp_config(&json!({"mcpServers": {"docs": {"command": ["node", server.to_string_lossy()]}}}))?;
let src = load_mcp(&cfg).await;
let tools = src.resource_tools();
let (list, read) = (&tools[0], &tools[1]);
let out = list.call(json!({}), ToolContext::default()).await;
assert_eq!(out.output, "docs fixture://hello — hello (text/plain): A fixed text resource.");
let got = read.call(json!({"server": "docs", "uri": "fixture://hello"}), ToolContext::default()).await;
assert!(!got.is_error, "{}", got.output);
assert_eq!(got.output, "hello resource");
src.close().await;
println!("ok: {} {} -> {}", list.name, read.name, got.output);
Ok(())
}

2. The realistic case — hand them to the model through the toolkit

Section titled “2. The realistic case — hand them to the model through the toolkit”
use std::path::Path;
use serde_json::json;
use toolnexus::mcp::{load_mcp, parse_mcp_config};
use toolnexus::{ToolContext, ToolkitOptions, create_toolkit};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let repo = std::env::var("TOOLNEXUS_REPO")?;
let server = Path::new(&repo).join("examples/mcp-progress-server.mjs");
let cfg = parse_mcp_config(&json!({"mcpServers": {"docs": {"command": ["node", server.to_string_lossy()]}}}))?;
let src = load_mcp(&cfg).await;
let tk = create_toolkit(ToolkitOptions {
extra_tools: src.resource_tools(),
..Default::default()
})
.await?;
let res = tk
.execute("read_mcp_resource", json!({"server": "docs", "uri": "fixture://hello"}), ToolContext::default())
.await;
assert_eq!(res.output, "hello resource");
tk.close().await;
src.close().await;
println!("ok: toolkit read {}", res.output);
Ok(())
}

3. The full surface — read-only hints, metadata, and errors as results

Section titled “3. The full surface — read-only hints, metadata, and errors as results”
use std::path::Path;
use serde_json::json;
use toolnexus::ToolContext;
use toolnexus::mcp::{load_mcp, parse_mcp_config};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let repo = std::env::var("TOOLNEXUS_REPO")?;
let server = Path::new(&repo).join("examples/mcp-progress-server.mjs");
let cfg = parse_mcp_config(&json!({"mcpServers": {"docs": {"command": ["node", server.to_string_lossy()]}}}))?;
let src = load_mcp(&cfg).await;
let tools = src.resource_tools();
for t in &tools {
assert!(t.hints.is_some_and(|h| h.read_only), "{} should be read-only", t.name);
}
let list = tools[0].call(json!({"server": "docs"}), ToolContext::default()).await;
let meta = list.metadata.unwrap_or_default();
assert_eq!(meta["count"], 1);
let bad = tools[1].call(json!({"server": "nope", "uri": "fixture://hello"}), ToolContext::default()).await;
assert!(bad.is_error);
assert_eq!(bad.output, r#"read_mcp_resource: no connected MCP server "nope" (connected: docs)"#);
src.close().await;
println!("ok: read-only, count = {} | unknown server -> {}", meta["count"], bad.is_error);
Ok(())
}
  • load_mcp — connect the servers whose resources these tools read.
  • load_mcp_with — per-server readiness while servers connect.
  • list_mcp_tools — the MCP tools themselves (a gap in Rust; McpSource.tools holds them).