模块化模型上下文协议服务器

一个用Rust编写的模块化模型上下文协议服务器,支持stdio和Streamable HTTP两种传输方式,适用于需要模块化工具、资源和提示管理的开发场景。

  • by d-carmo
  • Rust
  • 混合部署
  • 更新于
在 GitHub 查看 d-carmo/mcp_server

使用说明

mcp服务器

]

模块化 模型上下文协议 用Rust编写的服务器。

支持两种传输方式(在运行时通过以下方式选择 .env):

运输 何时使用
标准 Claude Desktop,CLI客户端,默认
可流式传输的HTTP Web客户端、多会话、远程访问

目录


建筑

src/
├── main.rs               # Wires config → engine.  Nothing else lives here.
├── config.rs             # Reads .env / env vars → Config struct
├── server.rs             # McpServer — implements rmcp::ServerHandler using
│                         # all registered tools, resources, and prompts
├── engine/
│   ├── mod.rs            # Dispatches to stdio or HTTP based on Config
│   └── http.rs           # Streamable HTTP transport (Axum + rmcp)
├── tools/
│   ├── mod.rs            # McpTool trait + ToolRegistration + all_tools()
│   ├── count_lines.rs    # Counts lines in files by extension
│   └── query_db.rs       # Executes user-scoped SELECT queries against MySQL
├── resources/
│   └── mod.rs            # McpResource trait + ResourceRegistration + all_resources()
└── prompts/
    └── mod.rs            # McpPrompt trait + PromptRegistration + all_prompts()

自动发现的工作原理

三层协同工作,将文件放入文件夹就足够了:

  1. build.rs --跑步前 rustc 在每一个 cargo build.它扫描 src/tools/, src/resources/,以及 src/prompts/ 并写了一个 pub mod <name>; 线路/ .rs 鱼贯而入 OUT_DIR/<dir>_modules.rs.

  2. include! --每个 mod.rs 有一个单一 include!(concat!(env!("OUT_DIR"), "/<dir>_modules.rs")) 这就拉进来了 生成的声明。Rust编译器可以看到 文件夹,无需任何手动操作。

  3. inventory 板条箱 --每个插件文件调用 inventory::submit! 一次 在模块级别。在链接时,链接器会收集所有提交的内容。 all_tools() / all_resources() / all_prompts() 在以下位置迭代它们 启动。


配置(.env)

复制 .env 并根据需要进行编辑。所有值都有内置默认值。

变量 默认值 描述
MCP_PORT 3333 用于流式HTTP传输的TCP端口
MCP_LISTENING_ADDRESS 127.0.0.1 流式HTTP传输的绑定地址
MCP_COMMUNICATION stdio 运输: stdio 或 Streamable_HTTP
DB_URL -- MySQL主机(例如。 localhost)--需要 query_db
DB_NAME -- MySQL数据库名称--必填 query_db
DB_USER -- MySQL用户名--必填 query_db
DB_PASSWORD (空) MySQL密码——必填 query_db

示例——在所有接口上作为HTTP服务器运行:

MCP_COMMUNICATION=Streamable_HTTP
MCP_LISTENING_ADDRESS=0.0.0.0
MCP_PORT=3333

运输

标准

默认值。客户端将此二进制文件作为子进程生成。JSON-RPC 消息通过以换行符分隔的stdin/stdout进行交换。 eprintln! (stderr)用于所有日志记录,以免损坏 协议通道。

MCP客户端配置示例(Claude Desktop/ .mcp.json):

{
  "mcpServers": {
    "mcp-server": {
      "command": "/path/to/target/release/mcp_server"
    }
  }
}

可流式传输的HTTP

实施 可流式HTTP传输 来自MCP规范(2025-11-25)。

服务器在以下位置挂载一个端点 /mcp:

方法 目的
POST /mcp 发送JSON-RPC消息。响应为 application/json 或 text/event-stream 这取决于消息类型。
GET /mcp 为服务器发起的消息打开持久的SSE流。
DELETE /mcp 明确结束会议。

会话管理(MCP-Session-Id), MCP-Protocol-Version 头球 验证、SSE保持活动和流恢复都在内部处理 通过rmcp StreamableHttpService.


测试

以下示例涵盖了运输和锻炼工具、资源和 提示。根据您的需求调整工具名称、资源URI和提示名称 已注册。


标准

二进制文件从以下地址每行读取一条JSON-RPC消息 stdin 并写道 回应 stdout.直接用管道传输换行符分隔的消息序列 使用here文档将二进制文件转换为二进制文件。

初始化

./target/release/mcp_server << 'EOF'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl-test","version":"0.0.1"}}}
EOF

确认初始化(通知-无响应)

./target/release/mcp_server << 'EOF'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl-test","version":"0.0.1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
EOF

工具列表

./target/release/mcp_server << 'EOF'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl-test","version":"0.0.1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
EOF

工具--调用(count_lines)

./target/release/mcp_server << 'EOF'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl-test","version":"0.0.1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"count_lines","arguments":{"path":"/tmp","extension":"rs"}}}
EOF

工具--调用(query_db)

对MySQL执行用户范围的SELECT查询。查询必须包含 :user_id 只有一次,所以结果总是会被过滤到给定的用户。

./target/release/mcp_server << 'EOF'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl-test","version":"0.0.1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"query_db","arguments":{"user_id":"1","query":"SELECT * FROM Entries WHERE user_id = :user_id"}}}
EOF

资源-列表

./target/release/mcp_server << 'EOF'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl-test","version":"0.0.1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"resources/list"}
EOF

资源--阅读

将URI替换为返回的URI resources/list.

./target/release/mcp_server << 'EOF'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl-test","version":"0.0.1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"resources/read","params":{"uri":"file:///config/settings.json"}}
EOF

提示-列表

./target/release/mcp_server << 'EOF'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl-test","version":"0.0.1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"prompts/list"}
EOF

提示--获取

将名称和参数替换为返回的值 prompts/list.

./target/release/mcp_server << 'EOF'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl-test","version":"0.0.1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"prompts/get","params":{"name":"summarise","arguments":{"text":"Hello world"}}}
EOF

每个回复都写在 stdout 在加工过程中,它会自行生产。 诊断/日志输出转到 stderr 并且不会干扰。


可流式传输的HTTP

先启动服务器:

MCP_COMMUNICATION=Streamable_HTTP ./target/release/mcp_server

每一个请求都是 POST /mcp服务器返回一个 MCP-Session-Id 头球 在 initialize 回应;所有后续请求都必须包含该标头。

1.初始化--捕获会话ID

curl -si -X POST http://127.0.0.1:3333/mcp \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl-test","version":"0.0.1"}}}' \
  -D /tmp/mcp_headers.txt

# Extract the session ID from the saved headers
SESSION_ID=$(grep -i 'mcp-session-id' /tmp/mcp_headers.txt | awk '{print $2}' | tr -d '\r\n')
echo "Session: $SESSION_ID"

2.确认初始化(通知——无响应正文)

curl -s -X POST http://127.0.0.1:3333/mcp \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -H "MCP-Session-Id: $SESSION_ID" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

3.工具清单

curl -s -X POST http://127.0.0.1:3333/mcp \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -H "MCP-Session-Id: $SESSION_ID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

4.工具--调用(count_lines)

curl -s -X POST http://127.0.0.1:3333/mcp \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -H "MCP-Session-Id: $SESSION_ID" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"count_lines","arguments":{"path":"/tmp","extension":"rs"}}}'

5.工具--调用(query_db)

对MySQL执行用户范围的SELECT查询。查询必须包含 :user_id 只有一次,所以结果总是会被过滤到给定的用户。

curl -s -X POST http://127.0.0.1:3333/mcp \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -H "MCP-Session-Id: $SESSION_ID" \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"query_db","arguments":{"user_id":"1","query":"SELECT * FROM Entries WHERE user_id = :user_id"}}}'

7.资源----清单

curl -s -X POST http://127.0.0.1:3333/mcp \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -H "MCP-Session-Id: $SESSION_ID" \
  -d '{"jsonrpc":"2.0","id":5,"method":"resources/list"}'

8.资源----阅读

将URI替换为返回的URI resources/list.

curl -s -X POST http://127.0.0.1:3333/mcp \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -H "MCP-Session-Id: $SESSION_ID" \
  -d '{"jsonrpc":"2.0","id":6,"method":"resources/read","params":{"uri":"file:///config/settings.json"}}'

9.提示-列表

curl -s -X POST http://127.0.0.1:3333/mcp \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -H "MCP-Session-Id: $SESSION_ID" \
  -d '{"jsonrpc":"2.0","id":7,"method":"prompts/list"}'

10.提示--获取

将名称和参数替换为返回的值 prompts/list.

curl -s -X POST http://127.0.0.1:3333/mcp \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -H "MCP-Session-Id: $SESSION_ID" \
  -d '{"jsonrpc":"2.0","id":7,"method":"prompts/get","params":{"name":"summarise","arguments":{"text":"Hello world"}}}'

10.结束会话(可选)

curl -s -X DELETE http://127.0.0.1:3333/mcp \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -H "MCP-Session-Id: $SESSION_ID"

构建

cargo build --release

构建脚本会自动运行,不需要手动步骤。


添加新工具

  1. 创建 src/tools/<your_tool>.rs.

  2. 实施 这 McpTool 特质:

use anyhow::Result;
use serde_json::{json, Map, Value};
use crate::tools::{BoxFuture, McpTool, ToolRegistration};

pub struct MyTool;

impl McpTool for MyTool {
    fn name(&self) -> &'static str {
        "my_tool"   // unique snake_case identifier
    }

    fn description(&self) -> &'static str {
        "Does something useful"
    }

    fn schema(&self) -> Map<String, Value> {
        // Must be a JSON Schema object with "type": "object"
        json!({
            "type": "object",
            "properties": {
                "input": { "type": "string", "description": "The input value" }
            },
            "required": ["input"]
        })
        .as_object()
        .unwrap()
        .clone()
    }

    fn call(&self, params: Map<String, Value>) -> BoxFuture<'_, Result<String>> {
        Box::pin(async move {
            let input = params["input"].as_str().unwrap_or("");
            Ok(format!("You said: {}", input))
        })
    }
}
  1. 注册 它位于同一文件的底部:
inventory::submit! { ToolRegistration { factory: || Box::new(MyTool) } }
  1. 跑 cargo build --工具是活的。没有其他文件需要编辑。

编写测试

添加一个 #[cfg(test)] 同一文件中的模块和测试 call 直接:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn returns_expected_output() {
        let tool = MyTool;
        let params = serde_json::json!({ "input": "hello" });
        let result = tokio::runtime::Runtime::new()
            .unwrap()
            .block_on(tool.call(params.as_object().unwrap().clone()))
            .unwrap();
        assert_eq!(result, "You said: hello");
    }
}

添加新资源

  1. 创建 src/resources/<your_resource>.rs.

  2. 实施 这 McpResource 特质:

use anyhow::Result;
use crate::resources::{BoxFuture, McpResource, ResourceRegistration};

pub struct ConfigResource;

impl McpResource for ConfigResource {
    fn uri(&self) -> &'static str {
        "file:///config/settings.json"   // unique URI
    }

    fn name(&self) -> &'static str {
        "Application Settings"
    }

    fn description(&self) -> Option<&'static str> {
        Some("Current application configuration")
    }

    fn mime_type(&self) -> Option<&'static str> {
        Some("application/json")
    }

    fn read(&self) -> BoxFuture<'_, Result<String>> {
        Box::pin(async move {
            // Read from disk, database, network, etc.
            Ok(r#"{"theme": "dark"}"#.to_string())
        })
    }
}
  1. 注册 它
inventory::submit! { ResourceRegistration { factory: || Box::new(ConfigResource) } }
  1. 跑 cargo build.

添加新提示

  1. 创建 src/prompts/<your_prompt>.rs.

  2. 实施 这 McpPrompt 特质:

use anyhow::Result;
use serde_json::{Map, Value};
use crate::prompts::{
    BoxFuture, McpPrompt, PromptArg, PromptMessage, PromptRegistration, Role,
};

pub struct SummarisePrompt;

impl McpPrompt for SummarisePrompt {
    fn name(&self) -> &'static str {
        "summarise"   // unique identifier
    }

    fn description(&self) -> Option<&'static str> {
        Some("Summarise a piece of text")
    }

    fn arguments(&self) -> Vec<PromptArg> {
        vec![PromptArg {
            name: "text",
            description: Some("The text to summarise"),
            required: true,
        }]
    }

    fn get(&self, args: Map<String, Value>) -> BoxFuture<'_, Result<Vec<PromptMessage>>> {
        Box::pin(async move {
            let text = args["text"].as_str().unwrap_or("(no text provided)");
            Ok(vec![PromptMessage {
                role: Role::User,
                text: format!("Please summarise the following:\n\n{}", text),
            }])
        })
    }
}
  1. 注册 它
inventory::submit! { PromptRegistration { factory: || Box::new(SummarisePrompt) } }
  1. 跑 cargo build.

运行测试

cargo test

每个模块都有自己的单元测试。特定练习中的工具测试 使用真实的文件系统进行端到端的实际调用逻辑。