MCP tools
Here you give an agent a few tools from a Model Context Protocol (MCP) server that runs as a child process and hide the rest from the model. Swiftide speaks MCP through the rmcp crate. Turn on the mcp feature flag, which also enables swiftide-agents, and add rmcp with a client and the transport you use:
cargo add swiftide@0.34 --features openai,mcpcargo add rmcp@2.1 --features client,transport-child-processcargo add tokio --features macros,rt-multi-thread,process,timeChild process toolbox
Section titled “Child process toolbox”The release-notes-agent showcase reads pull requests through the GitHub MCP server when you pass --mcp:
/// Starts `@modelcontextprotocol/server-github` with `npx` and exposes a few of its tools.////// Errors explain what is missing, so the caller can carry on without GitHub.pub async fn github_toolbox() -> Result<impl ToolBox> { let token = env::var("GITHUB_PERSONAL_ACCESS_TOKEN") .or_else(|_| env::var("GITHUB_TOKEN")) .context("set GITHUB_PERSONAL_ACCESS_TOKEN (or GITHUB_TOKEN)")?;
let server = Command::new("npx").configure(|cmd| { cmd.args(["-y", "@modelcontextprotocol/server-github"]) .env("GITHUB_PERSONAL_ACCESS_TOKEN", token); }); let (transport, _stderr) = TokioChildProcess::builder(server) .stderr(Stdio::null()) .spawn() .context("could not run `npx`; is Node.js installed?")?;
let mut toolbox = timeout( Duration::from_mins(1), McpToolbox::try_from_transport(transport), ) .await .context("the GitHub MCP server did not start within a minute")? .context("the GitHub MCP server failed to start")?; toolbox.with_whitelist(GITHUB_TOOLS); // Swiftide names MCP tools `<server>:<tool>`; OpenAI rejects ':', see `OpenAiToolNames` below. let toolbox = OpenAiToolNames(toolbox);
// Fail early, rather than mid-run, if the server does not offer the tools we expect. let available = toolbox.available_tools().await?; if available.len() != GITHUB_TOOLS.len() { bail!("the GitHub MCP server does not offer {GITHUB_TOOLS:?}"); } Ok(toolbox)}Source: examples/release-notes-agent/src/mcp.rs#L18-L52
McpToolbox::try_from_transport takes any rmcp client transport, such as a child process or a Transmission Control Protocol (TCP) stream. It performs the MCP handshake with a default client info named swiftide and returns the toolbox. The handshake waits for npx to download the package on a first run, so the showcase puts a one-minute timeout around it.
Calling available_tools up front turns a missing tool into a startup error instead of a failure in the middle of a run. src/agent.rs adds the toolbox with builder.add_toolbox(github_tools).
Run it from the swiftide repository. You need OPENAI_API_KEY, Node.js, a token in GITHUB_PERSONAL_ACCESS_TOKEN or GITHUB_TOKEN, and an origin remote on GitHub. Without the remote, the showcase skips the GitHub tools:
cargo run -p release-notes-agent -- --mcpRunning service toolbox
Section titled “Running service toolbox”When you need your own client info or capabilities, start the rmcp service yourself and hand it over:
// First set up our client info to identify ourselves to the serverlet client_info = ClientInfo::new( ClientCapabilities::default(), Implementation::new("swiftide-example", env!("CARGO_PKG_VERSION")),);
// Use `rmcp` to start the serverlet running_service = client_info .serve(TokioChildProcess::new( tokio::process::Command::new("npx").configure(|cmd| { cmd.args(["-y", "@modelcontextprotocol/server-everything"]); }), )?) .await?;
// Create a toolbox from the running server, and only use the `add` tool//// A toolbox reveals it's tools to the swiftide agent the first time it starts (if the state of// the agent was pending). You can add as many toolboxes as you want. MCP services are an// implmenentation of a toolbox. A list of tools is another.let everything_toolbox = McpToolbox::from_running_service(running_service) .with_whitelist(["add"]) .to_owned();Source: examples/agents_mcp_tools.rs#L34-L56
ClientInfo::serve comes from rmcp’s ServiceExt and returns a RunningService. McpToolbox::from_running_service wraps it without contacting the server. Serve from a ClientInfo, not from ().
The filter methods return &mut Self, so finish a chain on a temporary with .to_owned().
agents::Agent::builder() .llm(&openai) // Add the toolbox to the agent .add_toolbox(everything_toolbox) // Every message added by the agent will be printed to stdout .on_new_message(move |_, msg| { let msg = msg.to_string(); let tx = tx.clone(); Box::pin(async move { tx.send(msg).unwrap(); Ok(()) }) }) .build()? .query("Use the add tool to add 1 and 2") .await?;Source: examples/agents_mcp_tools.rs#L60-L75
add_toolbox accepts any owned ToolBox + 'static. You can add as many toolboxes as you like. A Vec<Box<dyn Tool>> is a toolbox too.
The agent lists a toolbox’s tools when it first starts, after the before_all hooks and the system prompt, not when you call build. If listing fails, query returns AgentError::ToolBoxFailedToLoad. After a successful listing the agent keeps the list, so tools the server adds later do not reach it. A failed listing is retried on the next query, and the before_all hooks run again.
The example needs OPENAI_API_KEY and Node.js:
cargo run -p swiftide-examples --example agents-mcpIt sends the prefixed name <server name>:add to OpenAI unchanged. If OpenAI rejects it, wrap the toolbox as described under Tool names.
Filters
Section titled “Filters”A toolbox holds one filter, and each call replaces it:
with_whitelist(names)keeps only the named tools.with_blacklist(names)keeps every tool except the named ones.with_filter(ToolFilter::Whitelist(..))orwith_filter(ToolFilter::Blacklist(..))sets a filter built elsewhere, such as from configuration.ToolFilterimplementsSerializeandDeserialize.
Names match the tool names the server reports, exactly and without the server prefix. A whitelisted name that the server does not offer is skipped without an error.
A blacklist suits a server where most tools are safe to expose. This toolbox keeps the GitHub server’s read tools and hides three that write:
// `github` is an `McpToolbox` from `try_from_transport` or `from_running_service`.github .with_name("github") .with_blacklist(["create_or_update_file", "push_files", "merge_pull_request"]);
let agent = Agent::builder() .llm(openai) .add_toolbox(github) .build()?;Source: snippets/src/agents/mcp.rs#L11-L19
with_name and with_blacklist both return &mut Self, so they chain on a let mut binding, and the toolbox itself then moves into add_toolbox. A tool that a new version of the server adds passes a blacklist, but not a whitelist.
Tool names
Section titled “Tool names”Since 0.32, Swiftide registers each MCP tool as <server name>:<tool>. The server name comes from the handshake, and falls back to mcp when it is missing or empty. The model sees the prefixed name, and Swiftide calls the server with the original one.
with_name only names the toolbox in logs and does not change the prefix.
OpenAI accepts tool names made of letters, digits, _ and -, so it rejects the :. Swiftide does not rewrite the names. The showcase wraps its toolbox in OpenAiToolNames, a ToolBox in src/mcp.rs that replaces every other character with _. The model then sees github-mcp-server_get_pull_request.
Tool names are unique per agent. When a toolbox tool has the same name as a tool the agent already has, the agent keeps the existing one.
Results
Section titled “Results”Since 0.33, Swiftide reads the content blocks of rmcp 2 tool results:
- Text blocks are joined with newlines into a text tool output. Other block kinds, such as images, are dropped.
- Only a result with no content blocks at all uses its
structured_content, serialized as JSON (JavaScript Object Notation). - A successful result with no text left becomes
Tool executed successfully. That includes a result with only image blocks, even when it carriesstructured_content. - A result with
is_errorset becomesToolError::Unknownwith the server’s text, orUnknown errorwhen there is none. The model reads the error as the tool output, and the failure counts toward the tool retry limit (tool_retry_limit). See Tool errors and retries. - Arguments that are not valid JSON return
ToolError::WrongArgumentsbefore the server is called.
Shutdown
Section titled “Shutdown”McpToolbox::cancel disconnects from the server and stops the rmcp service. A toolbox is Clone, and clones share one connection with the tools they listed. Bind a clone with let mut before you pass the toolbox to add_toolbox, and call cancel().await on it once the agent is done.
After cancel, calls to the server’s tools return a tool error that says the server is not running, and listing the tools fails. A second cancel logs a warning and returns Ok.
Gotchas
Section titled “Gotchas”- Swiftide supports MCP tools only. Prompts, resources and sampling from the server are not used.
- Use
rmcp2. Another major version brings its ownRunningServicetype, whichfrom_running_servicedoes not accept. - Every tool that passes the filter needs a description, and its input schema must parse as a JSON Schema that Swiftide accepts. Otherwise
queryreturnsAgentError::ToolBoxFailedToLoadfor the whole toolbox, so filter out the tools you do not need. - A whitelist limits what the model can call, not what the server can do. Pick read-only tools when the model’s arguments are untrusted, as the showcase does.