正在统计访客…
浏览全部文档
开始 · 9找到适合你的 Runifold 学习路径45 分钟掌握 Runifold理解完整的 Runifold 平台第一次可信运行选择正确的执行 API选择 Crate 与 Cargo Feature构建常见 Runifold 应用Runifold 常见问题排查 Runifold 应用故障
执行内核 · 7理解 RunContext安全协调外部副作用使用预算与取消限制工作安全地处理错误与重试事件、Journal 与执行证据设计 Capability 安全执行从 Checkpoint 安全恢复
模型与服务商 · 7在避免重复输出的前提下路由模型选择并配置模型服务商使用服务商中立的模型协议基于 Provider Runtime 契约构建使用 OpenAI 控制面与 Realtime API测试与 Benchmark Provider Adapter配置 OpenAI、Anthropic、Gemini 与 Ollama
Agent · 7构建并配置 Agent为 Agent 添加类型化工具加入会话与语义记忆安全地委派给子 Agent返回结构化 Rust 值在不丢失语义的前提下流式输出使用检索为 Agent 提供事实依据
持久工作流 · 7组合确定性工作流让工作流持久化运行持久工作流 Worker协调 Timer、Signal 与持久等待运行多租户工作流基础设施运行并行 Branch 与安全 Race对持久 Workflow 进行版本管理
集成 · 7通过 MCP 连接外部能力选择存储与持久化边界通过 MCP Tasks 暴露持久工作构建并评估检索流水线使用 MCP Resources、Prompts 与 Sampling在不跨越权限的前提下缓存 MCP 响应在 Rust Web Service 中部署 Runifold
质量与运维 · 10在没有网络的情况下测试评估质量并阻止回归观测完整运行树在浏览器与边缘环境安全运行准确理解可靠性声明在 CI 中运行可复现评测使用 SLO 运维 Runifold治理 Task 保留与删除把审计证据归档到 S3-Compatible WORM 存储管理兼容性与可信发布
文档/集成
第一次使用?通过 45 分钟核心课程建立完整心智模型
集成

通过 MCP 连接外部能力

连接 MCP Server、调用并导入远程 Tool,或通过 stdio 与 Streamable HTTP 暴露 Runifold Tool。

实践指南·24 min

先确认你的方向

Runifold 的 MCP 支持是双向的。开始写代码前,先确认自己需要哪一条路径:

目标你使用的核心类型从哪里开始
直接调用远程 MCP ToolMcpClientCallToolParams连接 HTTP Server
让 Agent 自动选择远程 ToolMcpRemoteToolRemoteToolPolicy导入 Agent
启动本地 MCP 子进程StdioTransport连接 stdio
把 Runifold Tool 暴露给其他 MCP ClientMcpServerserve_stdio构建 Server
通过 HTTP 提供 MCP EndpointMcpHttpServer暴露 HTTP
使用 Resources、Prompts、Sampling对应 Registry 与 Client API其他 MCP 能力
使用长时间运行的 MCP TaskCallToolOutcome、Task Client APIMCP Tasks

无论选择哪一条路径,MCP 只是协议和集成边界。Runifold 仍然负责 Run 身份、预算、 Deadline、取消、Capability、Effect 与可观测性。

安装与 Feature

Client 与 Server 都通过 mcp Feature 提供:

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

如果要定义类型化 Tool,再加入:

cargo add serde --features derive
cargo add schemars

Streamable HTTP Bearer Token 示例还需要:

cargo add secrecy
cargo add axum

所有 Runifold Workspace Crate 都应保持 0.9.0。如果私有 Registry Mirror 暂时报 runifold-mcp 0.9.0 不存在,请刷新或等待 Mirror 同步,不要混用不同版本的 Workspace Crate。

所有 MCP 类型都位于 runifold::mcp

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

五分钟连接远程 MCP Server

下面是一个带 Bearer Token、15 秒超时和分页上限的 Streamable HTTP Client:

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:?}");

优先调用 connect():它先执行无状态 Discovery,然后选择双方支持的最新模式。现代 Server 使用无状态协议;只支持 2025-11-25 的 Server 会自动走 Legacy Initialize。 只有在你明确只支持旧协议时,才直接调用 initialize()

McpClient 应该作为长生命周期基础设施对象复用。不要在每次 Agent Turn 前重新连接。 Client 可以安全 Clone;Clone 共享同一个协议状态、缓存与 Transport。

连接后可以检查 Server 真正协商出的信息:

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 名称与描述属于远端自我声明信息,适合显示与诊断,不能用于授权。

发现并检查 Tool

list_tools() 会自动跟随 Cursor,并受到 with_max_pagination_pages 限制:

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);
}

不要把远端 Tool List 直接全部交给 Agent。生产应用至少要检查:

  • Tool Name 是否在本地 Allowlist 中;
  • Input/Output JSON Schema 是否符合预期;
  • 描述与 Annotation 是否发生了未经审核的变化;
  • 当前用户或租户是否确实有权使用该 Tool;
  • Tool 是否读取、写入、付款、发消息或删除数据;
  • 远端返回体与分页数量是否在本地上限内。

需要自己控制 Cursor 或缓存行为时,使用 list_tools_page()list_tools_page_with_cache()list_tools() 更适合大多数应用初始化流程。

直接调用 MCP Tool

MCP Tool 参数必须是 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?;

正确处理 CallToolResult 的三个通道:

if result.is_error {
    // Tool 已经成功执行到应用边界,但业务请求失败;不要当作 Transport 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 是有序的模型可见内容,可能包含文本、图片、音频、文档或 Resource Link; structured_content 是独立的结构化结果;is_error 是应用级失败。Transport、协议、 Deadline 或 Session 失败则由 McpError 返回,不会伪装成 is_error

call_tool() 使用 Client 配置的超时。它不会自动重试 HTTP Tool Call;对于可能产生副作用 的调用,这可以避免不明确响应导致的重复执行。

把远程 Tool 导入 Agent

McpRemoteTool 把远端 MCP Descriptor 适配成 Runifold 的标准 Tool。关键点是:Effect 与 Risk 必须由本地主机决定,不能信任远端 Annotation 自动授权。

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()?;

适配完成后,远端 Tool 会走普通 Runifold Tool 路径:输入 Schema 校验、Capability Policy、 Budget、Deadline、取消与 Tool Result 上限都仍然生效。Agent 的 RunContext Deadline 会 缩短远端请求超时;取消 Run 也会取消正在等待的 MCP 调用。

推荐逐个导入:

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 应来自你的静态配置或策略系统,而不是远端返回值。

连接本地 stdio MCP Server

stdio 适合由当前应用监督的本地子进程,例如文件搜索器或开发工具:

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?;

Server 的 stdout 专用于逐行 JSON-RPC Frame。诊断日志必须写入 stderr,否则会破坏 协议。StdioTransport 支持并发请求;shutdown() 会先关闭 stdin,再在超时后终止没有 退出的子进程。不要把 Secret 放进命令行参数,优先使用受控环境或 IPC。

把 Runifold Tool 构建成 MCP Server

先定义一个普通 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 })
}

然后注册 Tool,并显式授予 MCP Server 的 Authority:

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.");

注册和授权是两个不同的门:

  1. ToolRegistry::register 决定 Server 知道哪些 Tool;
  2. CapabilitySet::grant 决定这个 MCP Authority 可以列出和调用哪些 Tool。

只注册、不授权的 Tool 不会出现在 tools/list 中,也不能通过名称绕过调用。这是 Runifold MCP Server 最重要的安全边界。

通过 stdio 暴露 Server

一个最小 MCP Server Binary 的入口只有几行:

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(())
}

每条 stdio 连接使用独立的 McpSession。不要在多个不可信 Client 之间复用同一个 Session;为每个连接创建新的 server.session()

通过 Streamable HTTP 暴露 Server

McpHttpServer 返回一个可以合并到现有 Axum 应用的 Router:

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?;

同一个 /mcp Endpoint 会处理 POSTGETDELETE。默认响应使用 JSON;需要 SSE Response 时可配置 HttpResponseMode::Sse

生产环境至少要做到:

  • 所有方法都认证,而不只是 POST
  • 浏览器请求只允许精确 Origin,不使用通配符;
  • 限制 Request Body、通知缓冲区、Replay 数量与 Session 数;
  • Token 从 Secret Store 读取,并支持轮换;
  • TLS 在可信反向代理或应用边界终止;
  • Session 丢失返回 McpError::SessionExpired 后,由应用明确决定是否重连;
  • 不自动重试可能产生副作用的 Tool Call。

StaticBearerAuth 适合最小示例与单 Token 部署。多租户生产系统应实现自己的 HttpAuthProviderHttpAuthorizer,把身份映射到租户级 Capability 与审计上下文。

Resources、Prompts、Sampling 与 Tasks

MCP 不只有 Tool:

  • client.list_resources() / read_resource():读取远端证据;内容仍是不可信上下文;
  • client.list_prompts() / get_prompt():取得模板;模板不会自动成为 System Policy;
  • SamplingService:允许 Server 请求 Client 侧模型 Sampling,但必须经过本地审批与预算;
  • McpTask API:保留协议级长任务句柄、轮询、取消与通知;
  • ResourceRegistryPromptRegistryCompletionRegistry:在 Server 端暴露对应能力。

完整的 Resource、Prompt、Completion、Sampling 与缓存策略见 MCP 上下文与 Sampling;长任务见 MCP Tasks。需要 Checkpoint、Lease、Signal、恢复与 Effect 语义时,应把 MCP Task 接到 Runifold 持久工作流,而不是只依赖远端轮询。

错误与排查

现象常见原因处理方式
list_tools 报 Lifecycle Error尚未连接先调用一次 client.connect().await?
Cargo 找不到 runifold-mcp 0.9.0私有 Registry Mirror 尚未同步完整 Workspace Release刷新或等待 Mirror;所有 Runifold Workspace Crate 保持 0.9.0
Tool 不在列表中Server 注册了但没有 Capability Grant检查 capabilities.grant(descriptor.capability())
-32601 / Method Not Found名称错误、能力未暴露或协议能力未协商重新检查 Tool List 与 Server Capability
-32602 / Invalid Params参数不是 Object 或不符合 Input Schema对照 tool.input_schema 生成参数
is_error = trueTool 的应用级错误读取安全的 Content,按业务决定是否让模型修正
SessionExpiredHTTP Session 被删除或服务端重启不要重放写操作;按 Effect Class 决定重连或人工确认
DeadlineExceededClient Timeout 或 Run Deadline 到期增大合理上限,或把长任务改为 MCP Task
stdio 出现 JSON Parse ErrorServer 把日志写到了 stdout把所有日志移到 stderr
HTTP 401 / 403Token 无效或 Origin 未加入 Allowlist检查认证提供器与精确 Origin
媒体结果无法转发当前 Provider 不支持该 Content Block使用支持的 Provider 协议,或让 Tool 提供安全文本回退

上线前最后检查:只导入 Allowlist Tool;本地指定 Effect/Risk;设置超时和分页上限; 限制 Body 与结果大小;认证远程 HTTP;关闭 stdio 子进程;测试 Session 丢失、取消、 应用错误与富媒体结果;确认日志和 Journal 不包含 Secret 或完整敏感 Tool Payload。