用 Rust 开发 MCP:从 0 到 1 写一个能被 Claude 调用的工具服务

  • King
  • 发布于 8小时前
  • 阅读 13

最近很多人把MCP(ModelContextProtocol)当成“给大模型装插件”的标准接口:你把自己的能力(工具、数据源、业务系统)封装成一个MCPServer,模型侧(MCPClient,比如ClaudeDesktop、Cursor等)就能发现并调用你的工具。这篇文

最近很多人把 MCP(Model Context Protocol) 当成“给大模型装插件”的标准接口:

你把自己的能力(工具、数据源、业务系统)封装成一个 MCP Server,模型侧(MCP Client,比如 Claude Desktop、Cursor 等)就能发现并调用你的工具。

这篇文章带你用 Rust 写一个最小可用 MCP Server:

  • 支持基础握手/初始化
  • 暴露一个 add 工具(可扩展成查数据库、发交易、查链上数据…)
  • 让 MCP 客户端能调用它并拿到结果

本文尽量“工程化”:结构清晰、代码可复制、方便你后续塞进真实业务。


MCP 是什么:一句话搞懂

MCP = 一个标准化协议,让模型以“工具调用”的方式连接外部能力。

你可以把 MCP Server 理解成一个“工具盒服务”,它向外声明:

  • 我有哪些 tools
  • 每个 tool 的参数 schema 是啥
  • 调用后返回什么结果

客户端会把模型的工具调用请求转成协议消息发给你,你返回结果即可。


Rust 适合写 MCP 吗?

适合,而且很香:

  • 稳定:长时间运行不崩(工具服务常驻)
  • 性能:高并发、低延迟(未来 tools 可能大量调用)
  • 生态:Tokio + serde_json + axum/warp 很成熟
  • 可扩展:后续接数据库、RPC、链上节点、缓存、队列都容易

选型:MCP 的传输方式怎么做?

MCP 常见传输方式有两类(你可能在不同客户端看到不同配置):

  1. stdio:客户端启动你的进程,通过 stdin/stdout 交换 JSON 消息
  2. HTTP / SSE / WebSocket:以网络服务形式通信

为了让读者“马上跑起来”,本文用 stdio 做最小可用版本(最通用、部署最简单)。


项目结构

mcp-rust-demo/
  Cargo.toml
  src/
    main.rs
    protocol.rs
    tools.rs

依赖(核心:tokio + serde):

# Cargo.toml
[package]
name = "mcp-rust-demo"
version = "0.1.0"
edition = "2024"

[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"

定义协议消息(最小集合)

我们先不追求“覆盖协议全部字段”,而是实现 MCP 常用最小链路:

  • initialize:客户端初始化
  • tools/list:列出工具
  • tools/call:调用工具

src/protocol.rs

use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Debug, Serialize, Deserialize)]
pub struct RpcRequest {
    pub id: Value,
    pub method: String,
    pub params: Option<Value>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct RpcResponse {
    pub id: Value,
    pub result: Option<Value>,
    pub error: Option<RpcError>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct RpcError {
    pub code: i32,
    pub message: String,
}

impl RpcResponse {
    pub fn ok(id: Value, result: Value) -> Self {
        Self { id, result: Some(result), error: None }
    }
    pub fn err(id: Value, code: i32, message: impl Into<String>) -> Self {
        Self { id, result: None, error: Some(RpcError { code, message: message.into() }) }
    }
}

写一个工具:add(但结构按真实业务来)

src/tools.rs

use anyhow::Result;
use serde_json::{json, Value};

pub fn tools_list() -> Value {
    // MCP 工具通常需要:name / description / inputSchema
    json!({
        "tools": [
            {
                "name": "add",
                "description": "Add two numbers and return the sum.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "a": { "type": "number" },
                        "b": { "type": "number" }
                    },
                    "required": ["a", "b"]
                }
            }
        ]
    })
}

pub fn call_tool(name: &str, args: Value) -> Result<Value> {
    match name {
        "add" => {
            let a = args.get("a").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let b = args.get("b").and_then(|v| v.as_f64()).unwrap_or(0.0);
            Ok(json!({
                "content": [
                    { "type": "text", "text": format!("sum = {}", a + b) }
                ]
            }))
        }
        _ => Ok(json!({
            "content": [
                { "type": "text", "text": format!("unknown tool: {}", name) }
            ]
        }))
    }
}

你会发现返回结构是 content 数组,这样更贴近“模型可读输出”:

  • text:文本
  • 后续你还可以扩展 imagejson 等类型(看客户端支持)

MCP 主循环:读 stdin,写 stdout

src/main.rs

mod protocol;
mod tools;

use protocol::{RpcRequest, RpcResponse};
use serde_json::{json, Value};
use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let stdin = io::stdin();
    let mut reader = io::BufReader::new(stdin).lines();

    let stdout = io::stdout();
    let mut writer = io::BufWriter::new(stdout);

    while let Some(line) = reader.next_line().await? {
        if line.trim().is_empty() {
            continue;
        }

        let req: RpcRequest = match serde_json::from_str(&line) {
            Ok(v) => v,
            Err(e) => {
                // 解析失败就忽略或回错(这里选择忽略)
                eprintln!("invalid json: {e}");
                continue;
            }
        };

        let id = req.id.clone();
        let resp = handle(req).unwrap_or_else(|e| {
            RpcResponse::err(id, -32000, format!("internal error: {e}"))
        });

        let out = serde_json::to_string(&resp)?;
        writer.write_all(out.as_bytes()).await?;
        writer.write_all(b"\n").await?;
        writer.flush().await?;
    }

    Ok(())
}

fn handle(req: RpcRequest) -> anyhow::Result<RpcResponse> {
    let id = req.id;

    match req.method.as_str() {
        "initialize" => {
            // 初始化时返回 server capabilities / name 等
            Ok(RpcResponse::ok(id, json!({
                "protocolVersion": "0.1",
                "serverInfo": { "name": "mcp-rust-demo", "version": "0.1.0" },
                "capabilities": { "tools": {} }
            })))
        }

        "tools/list" => {
            Ok(RpcResponse::ok(id, tools::tools_list()))
        }

        "tools/call" => {
            let params = req.params.unwrap_or(Value::Null);
            let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
            let args = params.get("arguments").cloned().unwrap_or(Value::Null);

            let result = tools::call_tool(name, args)?;
            Ok(RpcResponse::ok(id, result))
        }

        _ => Ok(RpcResponse::err(id, -32601, "Method not found")),
    }
}

本地测试:不用客户端也能自测

编译运行:

cargo run

然后手动喂一条 initialize

echo '{"id":1,"method":"initialize","params":{}}' | cargo run

你会看到 stdout 输出一个 JSON response。

再测工具列表:

echo '{"id":2,"method":"tools/list","params":{}}' | cargo run

测试工具调用:

echo '{"id":3,"method":"tools/call","params":{"name":"add","arguments":{"a":1,"b":2}}}' | cargo run

接入 Claude / MCP 客户端(思路)

不同客户端配置略有差异,但 stdio 方式基本都需要告诉客户端:

  • 命令:运行你的二进制文件
  • 参数:可选
  • 环境变量:可选(比如 API KEY)

你把服务写好以后,往往只需要把 cargo build --release 生成的可执行文件路径填进去即可。

小技巧:真实业务里通常会加上日志(stderr),避免污染 stdout(stdout 必须保持协议消息)。


工程化建议:把 demo 变成可用服务

当你从 add 进化到真实工具(比如“查链上价格/下单/访问内部系统”)时,建议直接按下面做:

1. 工具分层

  • tools/:只做参数校验 + 路由
  • services/:业务逻辑(RPC、DB、缓存)
  • clients/:外部依赖封装(HTTP、gRPC、Sui/EVM RPC)

2. 并发与限流

Rust + Tokio 很适合在 call_tool 内部:

  • Semaphore 控制并发
  • governor 做限流
  • 对 RPC 节点做池化与熔断

3. schema 一定要写好

工具调用成功率=参数 schema 清晰度。 强烈建议:把 schema 生成/维护做成常量或用宏管理,避免漂移。


结语:Rust 写 MCP 的价值

MCP 本质是“模型工具调用的标准接口”。Rust 的优势在于:

  • 你可以把它当成一个长跑型的工具服务
  • 随时扩展:更多工具、更重的并发、更复杂的 IO
  • 最终把它变成你团队的“模型能力入口”

如果你已经在做链上/日志/高并发任务系统,把这些能力封装成 MCP 工具,会非常自然:

模型负责“决策 + 调度”,MCP Server 负责“可靠执行”。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
King
King
0x56af...a0dd
擅长Rust/Solidity/FunC/Move开发