Counting visitors…
Browse all docs
Start · 9Find your path through RunifoldLearn Runifold in 45 minutesUnderstand the complete Runifold platformYour first trustworthy runChoose the right execution APIChoose crates and Cargo featuresBuild common Runifold applicationsRunifold frequently asked questionsTroubleshoot Runifold applications
Execution kernel · 7Understand RunContextCoordinate external effects safelyBound work with budgets and cancellationHandle errors and retries safelyEvents, journals, and execution evidenceDesign capability-safe executionRecover safely from checkpoints
Models & providers · 7Route across models without duplicate outputChoose and configure a providerUse the provider-neutral model protocolBuild on the Provider Runtime contractUse OpenAI control-plane and Realtime APIsTest and benchmark provider adaptersSet up OpenAI, Anthropic, Gemini, and Ollama
Agents · 7Build and configure an AgentGive an Agent typed toolsAdd conversations and semantic memoryDelegate to child Agents safelyReturn structured Rust valuesStream without losing semanticsGround an Agent with retrieval
Durable workflows · 7Compose deterministic workflowsMake workflows durableOperate durable workflow workersCoordinate timers, signals, and durable waitsRun multi-tenant workflow infrastructureRun parallel branches and safe racesVersion and evolve durable workflows
Integrations · 7Connect through MCPChoose stores and persistence boundariesExpose durable work through MCP TasksBuild and evaluate retrieval pipelinesUse MCP Resources, Prompts, and SamplingCache MCP responses without crossing authorityDeploy Runifold in a Rust web service
Quality & operations · 10Test without the networkEvaluate quality and prevent regressionsObserve the complete run treeRun safely in browsers and at the edgeRead reliability claims preciselyRun reproducible evaluations in CIOperate Runifold with SLOsGovern Task retention and deletionArchive audit evidence to S3-compatible WORM storageManage compatibility and trusted releases
Docs/Integrations
NEW TO RUNIFOLD?Build the complete mental model in 45 minutes
Integrations

Connect through MCP

Connect to MCP servers, call and import remote Tools, or expose Runifold Tools over stdio and Streamable HTTP.

Practical guide·24 min

Choose your direction

Runifold supports MCP in both directions. Decide which path you need before writing code:

GoalCore typesStart here
Call a remote MCP Tool directlyMcpClient, CallToolParamsConnect over HTTP
Let an Agent select remote ToolsMcpRemoteTool, RemoteToolPolicyImport into an Agent
Supervise a local MCP processStdioTransportConnect over stdio
Expose Runifold Tools to MCP clientsMcpServer, serve_stdioBuild a server
Host an MCP HTTP endpointMcpHttpServerServe HTTP
Use Resources, Prompts, or SamplingRegistry and client APIsOther capabilities
Coordinate a long-running MCP TaskTask client APIsMCP Tasks

MCP is the protocol and integration boundary. Runifold still owns run identity, budgets, deadlines, cancellation, capabilities, effects, and observability.

Install

Enable mcp for both client and server APIs:

cargo add runifold@=0.9.0 --features mcp
cargo add tokio --features macros,rt-multi-thread,process,net
cargo add serde_json

Typed server Tools also need:

cargo add serde --features derive
cargo add schemars

The authenticated Streamable HTTP example uses:

cargo add secrecy
cargo add axum

Keep all Runifold workspace crates on 0.9.0. If a private registry mirror temporarily reports that runifold-mcp 0.9.0 is missing, refresh or wait for that mirror to synchronize; do not mix workspace crate versions.

All MCP APIs live under runifold::mcp:

use runifold::mcp::{
    CallToolParams, Implementation, McpClient, McpClientConfig,
    StreamableHttpTransport,
};

Connect to an HTTP server

This client uses bearer authentication, a 15-second timeout, and bounded automatic pagination:

use std::{sync::Arc, time::Duration};
 
use runifold::mcp::{
    Implementation, McpClient, McpClientConfig, StaticBearerAuth,
    StreamableHttpTransport,
};
use secrecy::SecretString;
 
let token = SecretString::from(std::env::var("MCP_TOKEN")?);
let auth = Arc::new(StaticBearerAuth::new(token));
let transport = Arc::new(
    StreamableHttpTransport::new("https://mcp.example.com/mcp")?
        .with_auth(auth),
);
let client = McpClient::new(
    transport,
    McpClientConfig::new(Implementation::new("my-app", "0.1.0"))
        .with_request_timeout(Duration::from_secs(15))
        .with_max_pagination_pages(16),
);
 
let mode = client.connect().await?;
println!("MCP protocol mode: {mode:?}");

Prefer connect(). It discovers the server and selects the newest mutually supported mode. A modern server uses stateless requests; a server that only supports 2025-11-25 falls back to legacy initialization. Call initialize() directly only when your application intentionally supports the legacy flow alone.

Keep McpClient as long-lived infrastructure. Do not reconnect for every Agent turn. Its clones share protocol state, cache, and transport.

Inspect the negotiated server information after connecting:

let server = client
    .server_info()
    .await
    .ok_or("MCP client is not active")?;
 
println!("server: {} {}", server.server_info.name, server.server_info.version);
println!("protocol: {}", server.protocol_version);
println!("tools: {}", server.capabilities.tools.is_some());

Server identity is self-reported. Use it for display and diagnostics, never as an authorization identity.

Discover and inspect Tools

list_tools() follows cursors automatically and honors the configured page limit:

let tools = client.list_tools().await?;
 
for tool in &tools {
    println!("name: {}", tool.name);
    println!("description: {}", tool.description.as_deref().unwrap_or(""));
    println!("input schema: {}", tool.input_schema);
    println!("output schema: {:?}", tool.output_schema);
}

Do not hand the complete remote list to an Agent. At minimum, verify:

  • the name is on a local allowlist;
  • input and output schemas match the reviewed contract;
  • descriptions or annotations have not changed unexpectedly;
  • the current user and tenant may invoke the Tool;
  • whether it reads, writes, pays, messages, or deletes;
  • response size and pagination remain inside local limits.

Use list_tools_page() or list_tools_page_with_cache() when you need manual cursor or cache control.

Call a Tool directly

MCP Tool arguments are a JSON object:

use runifold::mcp::CallToolParams;
use serde_json::json;
 
let arguments = serde_json::Map::from_iter([
    ("city".to_owned(), json!("Shanghai")),
]);
 
let result = client
    .call_tool(CallToolParams {
        name: "current_weather".to_owned(),
        arguments: Some(arguments),
    })
    .await?;

Handle all three result channels:

if result.is_error {
    return Err("remote Tool returned an application error".into());
}
 
if let Some(value) = &result.structured_content {
    println!("structured: {value}");
}
 
for block in &result.content {
    if let Some(text) = block.as_text() {
        println!("text: {text}");
    } else {
        println!("rich block type: {}", block.kind);
    }
}

content is ordered model-visible content and may contain text, images, audio, documents, or resource links. structured_content is an independent structured result. is_error is a completed application-level failure. Transport, protocol, deadline, and session failures return McpError instead.

call_tool() uses the client timeout. Streamable HTTP calls are not retried implicitly, avoiding accidental duplicate side effects after an ambiguous response.

Import a remote Tool into an Agent

McpRemoteTool adapts a remote MCP descriptor to Runifold's canonical Tool. Effect and risk must come from the local host; remote annotations never grant authority:

use std::sync::Arc;
 
use runifold::{AgentBuilder, core::{EffectClass, RiskLevel}};
use runifold::mcp::{McpRemoteTool, RemoteToolPolicy};
 
let remote = client
    .list_tools()
    .await?
    .into_iter()
    .find(|tool| tool.name == "current_weather")
    .ok_or("MCP server did not advertise current_weather")?;
 
let weather = Arc::new(McpRemoteTool::new(
    client.clone(),
    remote,
    RemoteToolPolicy::new(EffectClass::ReadOnly, RiskLevel::Low),
)?);
 
let agent = agent_builder
    .shared_tool(weather)
    .max_turns(6)
    .build()?;

The imported Tool now follows the normal Runifold path: schema validation, capability policy, budget, deadline, cancellation, and result-size limits still apply. The Agent's RunContext shortens the remote timeout and cancellation is propagated to the pending MCP request.

Import reviewed Tools one by one:

for descriptor in client.list_tools().await? {
    let Some(policy) = approved_policy_for(&descriptor.name) else {
        continue;
    };
    let remote = McpRemoteTool::new(client.clone(), descriptor, policy)?;
    agent_builder = agent_builder.shared_tool(Arc::new(remote));
}

approved_policy_for should use static host configuration or your policy service, not remote metadata.

Connect over stdio

Use stdio for a local process supervised by your application:

use std::sync::Arc;
 
use runifold::mcp::{
    Implementation, McpClient, McpClientConfig, StdioTransport,
};
use tokio::process::Command;
 
let command = Command::new("weather-mcp-server");
let transport = Arc::new(StdioTransport::spawn(command)?);
let client = McpClient::new(
    transport.clone(),
    McpClientConfig::new(Implementation::new("local-app", "0.1.0")),
);
 
client.connect().await?;
let tools = client.list_tools().await?;
println!("{} tools discovered", tools.len());
 
drop(client);
transport.shutdown().await?;

The server's stdout is reserved for newline-delimited JSON-RPC frames. Send diagnostics to stderr. StdioTransport multiplexes concurrent requests; shutdown() closes stdin and then terminates a child that does not exit within the shutdown timeout. Avoid command-line secrets.

Build an MCP server

Start with an ordinary typed Runifold Tool:

use runifold::{JsonSchema, ToolContext, ToolError, tool};
use serde::{Deserialize, Serialize};
 
#[derive(Debug, Deserialize, JsonSchema)]
struct WeatherInput {
    city: String,
}
 
#[derive(Debug, Serialize, JsonSchema)]
struct WeatherOutput {
    city: String,
    celsius: i32,
}
 
#[tool(description = "Read the current temperature for one city")]
async fn current_weather(
    input: WeatherInput,
    _context: ToolContext,
) -> Result<WeatherOutput, ToolError> {
    Ok(WeatherOutput { city: input.city, celsius: 21 })
}

Register the Tool and grant the server authority explicitly:

use std::sync::Arc;
 
use runifold::{
    Budget, BudgetTracker, CapabilitySet, RunContext, Tool, ToolRegistry,
};
use runifold::mcp::{Implementation, McpServer};
 
let weather = Arc::new(current_weather_tool());
let mut tools = ToolRegistry::new();
tools.register(weather.clone())?;
 
let mut capabilities = CapabilitySet::new();
capabilities.grant(weather.descriptor().capability());
let authority = RunContext::root(
    BudgetTracker::new(Budget::default()),
    capabilities,
);
 
let server = McpServer::new(
    Arc::new(tools),
    authority,
    Implementation::new("weather-server", "0.1.0"),
)
.with_instructions("Use current_weather only for weather questions.");

Registration and authority are separate gates:

  1. ToolRegistry::register determines which Tools the server knows.
  2. CapabilitySet::grant determines which Tools this MCP authority may list and call.

A registered but ungranted Tool is absent from tools/list and cannot be invoked by guessing its name.

Serve over stdio

A minimal MCP server binary is:

use runifold::mcp::serve_stdio;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let server = build_mcp_server()?;
    serve_stdio(server.session()).await?;
    Ok(())
}

Create a separate McpSession for every stdio connection. Never share one session across untrusted clients.

Serve over Streamable HTTP

McpHttpServer produces an Axum Router that can be merged into an existing application:

use std::sync::Arc;
 
use runifold::mcp::{
    McpHttpServer, McpHttpServerConfig, StaticBearerAuth,
};
use secrecy::SecretString;
use tokio::net::TcpListener;
 
let token = SecretString::from(std::env::var("MCP_TOKEN")?);
let auth = Arc::new(StaticBearerAuth::new(token));
let config = McpHttpServerConfig::new()
    .with_authorizer(auth)
    .with_allowed_origin("https://app.example.com")
    .with_max_body_bytes(1024 * 1024);
 
let router = McpHttpServer::new(build_mcp_server()?, config)
    .router("/mcp");
let listener = TcpListener::bind("127.0.0.1:3000").await?;
axum::serve(listener, router).await?;

The same /mcp endpoint handles POST, GET, and DELETE. JSON responses are the default; select HttpResponseMode::Sse when request responses need SSE.

For production:

  • authenticate every method, not only POST;
  • allow exact browser origins rather than a wildcard;
  • bound request bodies, notification buffers, replay, and sessions;
  • obtain rotating tokens from a secret store;
  • terminate TLS at a trusted proxy or application boundary;
  • treat McpError::SessionExpired as an explicit recovery decision;
  • never automatically retry a Tool call with possible side effects.

StaticBearerAuth fits small deployments. A multi-tenant service should implement HttpAuthProvider and HttpAuthorizer, then map identity to a tenant-scoped capability and audit context.

Other MCP capabilities

MCP extends beyond Tools:

  • list_resources() and read_resource() retrieve remote evidence that remains untrusted context;
  • list_prompts() and get_prompt() retrieve templates, not trusted system policy;
  • SamplingService lets a server request client-owned model sampling behind local approval and budget policy;
  • Task APIs preserve handles, polling, cancellation, and notifications for long-running protocol work;
  • ResourceRegistry, PromptRegistry, and CompletionRegistry expose those capabilities from a server.

See MCP context and Sampling for Resources, Prompts, Completion, Sampling, and caching. See MCP Tasks for long-running work. When you need checkpoints, leases, recovery, signals, and effect semantics, bridge the Task to a durable workflow.

Errors and troubleshooting

SymptomLikely causeFix
lifecycle error from list_toolsclient is not connectedcall client.connect().await? once
Cargo cannot find runifold-mcp 0.9.0a private registry mirror has not synchronized the complete workspace releaserefresh or wait for the mirror; keep all Runifold workspace crates on 0.9.0
Tool is missing from the listregistered without a capability grantgrant descriptor.capability()
-32601 method not foundwrong name, hidden capability, or unnegotiated featureinspect the live Tool list and server capabilities
-32602 invalid paramsarguments are not an object or violate the schemavalidate against tool.input_schema
is_error = truecompleted application-level Tool errorinspect safe content and decide whether the model may recover
SessionExpiredsession deletion or server restartdo not replay writes blindly; decide from the Effect Class
DeadlineExceededclient timeout or Run deadlineuse a justified timeout or an MCP Task
stdio JSON parse failuresserver logged to stdoutmove all diagnostics to stderr
HTTP 401 or 403bad token or rejected Origininspect auth and the exact Origin allowlist
rich media cannot be forwardedtarget Provider lacks that content formuse a compatible protocol or a safe text fallback

Before shipping: allowlist Tools, assign Effect and Risk locally, bound time and pagination, authenticate HTTP, limit body and result sizes, shut down stdio children, and test cancellation, lost sessions, application errors, and rich results without leaking secrets into logs or journals.