loadMcpWithContext — cancellable load
Rust · crate toolnexus · SPEC §2
pub async fn load_mcp_with(config: &McpConfig, opts: McpLoadOptions) -> McpSourcepub struct McpLoadOptions { pub on_server: Option<OnServer>, pub wait_for: Option<WaitForFn> }pub type OnServer = Arc<dyn Fn(McpServerResult) + Send + Sync>;The ctx-aware load: bound connection time and cancel a slow or hung server without leaking a child process.
When to use it
Section titled “When to use it”When one slow server must not hold up the others: on_server fires the moment each server is connected, failed or disabled, so you can register its tools into a live toolkit.
Why this and not the alternative
Section titled “Why this and not the alternative”Use this over load_mcp when start-up latency matters; the returned source is the same once every server has finished.
Examples
Section titled “Examples”Rust has no context parameter: each server is bounded by its own timeout (milliseconds in the
config, 30 s by default), and the whole load can be bounded with tokio::time::timeout. The
examples use the repo’s dependency-free stdio fixture examples/mcp-progress-server.mjs (plain
node, no network).
1. A generous budget for the shared fixture, with readiness reported
Section titled “1. A generous budget for the shared fixture, with readiness reported”use std::path::Path;use std::sync::{Arc, Mutex};use std::time::Duration;use serde_json::json;use toolnexus::mcp::{McpLoadOptions, McpStatus, load_mcp_with, 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": { "progress": {"command": ["node", server.to_string_lossy()]}, "example-remote": {"url": "https://example.com/mcp", "enabled": false}, }}))?;
let seen = Arc::new(Mutex::new(Vec::new())); let log = seen.clone(); let opts = McpLoadOptions { on_server: Some(Arc::new(move |r| { log.lock().unwrap().push(format!("{}={}", r.name, r.status.as_str())) })), ..Default::default() }; let src = tokio::time::timeout(Duration::from_secs(30), load_mcp_with(&cfg, opts)).await?;
assert_eq!(src.status["example-remote"], McpStatus::Disabled); assert_eq!(src.status["progress"], McpStatus::Connected, "{:?}", src.errors); let mut seen = seen.lock().unwrap().clone(); seen.sort(); assert_eq!(seen, ["example-remote=disabled", "progress=connected"]);
src.close().await; println!("ok: loaded under a bounded budget: {seen:?}"); Ok(())}2. A hung server times out alone, not the whole load
Section titled “2. A hung server times out alone, not the whole load”A server that starts but never answers initialize is marked failed once its own timeout
elapses; the healthy server beside it still connects.
use std::path::Path;use serde_json::json;use toolnexus::mcp::{McpLoadOptions, McpStatus, load_mcp_with, 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": { "hung": {"command": ["node", "-e", "setInterval(() => {}, 1000)"], "timeout": 500}, "progress": {"command": ["node", server.to_string_lossy()]}, }}))?;
let src = load_mcp_with(&cfg, McpLoadOptions::default()).await;
assert_eq!(src.status["hung"], McpStatus::Failed); assert!(src.errors["hung"].contains("timed out after 500ms"), "{:?}", src.errors); assert_eq!(src.status["progress"], McpStatus::Connected);
src.close().await; println!("ok: hung -> {}", src.errors["hung"]); Ok(())}3. A disabled server is reported without ever needing the budget
Section titled “3. A disabled server is reported without ever needing the budget”Disabled servers are skipped before any connection attempt — a small deadline is plenty, because nothing races against it.
use std::sync::{Arc, Mutex};use std::time::Duration;use toolnexus::mcp::{McpLoadOptions, McpStatus, load_mcp_with, parse_mcp_config_str};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let cfg = parse_mcp_config_str( r#"{"mcpServers":{"off":{"type":"local","command":["unused"],"disabled":true}}}"#, )?;
let calls = Arc::new(Mutex::new(Vec::new())); let log = calls.clone(); let opts = McpLoadOptions { on_server: Some(Arc::new(move |r| log.lock().unwrap().push((r.name, r.status)))), ..Default::default() }; let src = tokio::time::timeout(Duration::from_millis(500), load_mcp_with(&cfg, opts)).await?;
assert_eq!(src.status["off"], McpStatus::Disabled); assert_eq!(*calls.lock().unwrap(), [("off".to_string(), McpStatus::Disabled)]);
println!("ok: disabled server reported as {}", src.status["off"].as_str()); Ok(())}See also
Section titled “See also”load_mcp— Read an mcp.json, connect every local stdio and remote streamable-HTTP server, expose each server tool as a Tool.parse_mcp_config— Parse and validate config without connecting — the fast fail for a malformed or misspelled server block.