MCP服务器中间件
用于实现模型上下文协议(MCP)服务器的Rust中间件库。该中间件处理MCP协议通信、会话管理和工具调用执行,使为任何用例构建兼容MCP的服务器变得容易。
中间件提供了一个灵活的、基于训练的体系结构,允许您为任何域实现自定义工具调用,无论是数据库访问、文件操作、API集成,还是您希望通过MCP协议公开的任何其他功能。
关于模型上下文协议(MCP)
模型上下文协议(MCP)是一种标准化协议(规范版本2025-11-25),使AI应用程序能够安全地访问外部数据源和工具。MCP为AI代理提供了一个统一的接口,用于与外部系统、数据库、API和服务进行交互。
核心概念
MCP服务器通过三种主要机制公开功能:
- 工具:AI代理可以调用的可执行函数来执行操作
- 示例:执行SQL查询、读/写文件、调用REST API、运行shell命令 - 每个工具都有一个名称、描述和定义输入/输出类型的JSON模式 - 工具是通过以下方式发现的 tools/list 并通过以下方式执行 tools/call
- 提示:带有变量替换的预配置提示模板
- 通过结构化、可重用的提示帮助指导人工智能交互 - 支持自定义的必需和可选参数 - 通过发现 prompts/list 并通过以下方式检索 prompts/get 有争论
- 资源:AI代理可以读取以获取上下文的数据源
- 示例:文件、数据库模式、文档、配置文件 - 每个资源都有一个URI、名称、描述、MIME类型和可选元数据 - 支持对大型资源列表进行分页 - 通过发现 resources/list 并通过阅读 resources/read
协议架构
传输层:MCP使用JSON-RPC 2.0通过HTTP与服务器发送事件(SSE)进行流式响应。这提供了:
- 标准化请求/响应格式
- 实时流媒体功能
- 与现有HTTP基础架构的兼容性
会话管理:
- 每个客户端连接通过以下方式建立会话
initialize请求 - 会话由中返回的唯一会话ID标识
mcp-session-id头球 - 后续请求需要会话ID进行身份验证
- GET请求为服务器到客户端的通知建立SSE流
能力发现:
- 服务器在初始化过程中声明功能(工具、提示、资源)
- 客户端通过列表端点发现可用功能
- 动态模式生成确保客户端始终拥有最新的工具定义
错误处理:
- 标准JSON-RPC错误代码(-32002表示找不到资源,-32603表示内部错误)
- 带有错误代码、消息和可选数据的结构化错误响应
这个实现
这个中间件(mcp-server-middleware)是a Rust库 它提供了MCP协议规范的完整、生产就绪的实现。它提供:
基于特性的架构:
McpToolCall:实现工具执行逻辑的特性ToolDefinition:提供工具元数据的特性(名称、描述)McpPromptService:实现提示模板的特性PromptDefinition:提供提示元数据的特性ResourceDefinition&McpResourceService:资源管理特征(静态、编译时URI)- 动态资源注册表:在挂载中间件后,使用运行时URI注册/注销资源
类型安全:
- 使用以下命令从Rust类型自动生成JSON模式
ApplyJsonSchema宏 - 编译时类型检查确保模式与实现匹配
- 支持基于运行时数据的动态枚举值
协议遵从:
- 全面实施MCP协议规范(2025-11-25)
- 所有必需的协议方法(
initialize,tools/list,tools/call,prompts/list,prompts/get,resources/list,resources/read,ping) - 正确的JSON-RPC 2.0格式
- SSE流媒体支持
- 具有安全会话ID的会话管理
整合:
- 与无缝集成
my-http-server作为HTTP中间件 - 轻松注册工具、提示和资源
- 协议细节的自动处理(会话管理、错误格式化、模式生成)
主要特点:
- 零样板工具注册-只需实现特征并注册
- 自动模式生成-无需手动编写JSON模式
- 基于会话的安全性-每个客户端都有隔离的会话
- 流媒体支持-通过SSE实时更新
- 资源分页-高效处理大型资源列表
- 动态资源-在运行时注册/注销资源(例如,每个上传的文件或生成的工件一个),用作
blob(base64)或text - 提示模板-具有变量替换的可重用提示
特性
- MCP协议支持:全面实施MCP协议,包括初始化、工具调用、提示和通知
- 会话管理:使用基于会话的身份验证自动创建和管理会话
- 工具调用框架:易于使用的基于特征的系统,用于实现自定义工具调用
- 快速支持:注册并公开MCP客户端可以发现和使用的提示
- 资源支持:公开客户端可以读取以获取上下文的数据源(文件、模式等)
- HTTP集成:与无缝集成
my-http-server作为中间件 - 类型安全工具定义:杠杆
my-ai-agent用于类型安全的JSON模式生成 - 动态枚举:支持基于运行时数据动态生成枚举值
- 引出 (服务器→客户端用户输入):实现
McpToolCallEx可以在执行过程中通过以下方式向用户请求值ToolCallContext::elicit().要求客户做广告capabilities.elicitation初始化时。对于不应进入LLM上下文的凭据和确认非常有用。
安装
将依赖项添加到您的 Cargo.toml:
[dependencies]
mcp-server-middleware = { git = "https://github.com/my-ai-utils/mcp-server-middleware.git" }
my-http-server = { tag = "0.8.3", git = "https://github.com/MyJetTools/my-http-server.git"}
my-ai-agent = { tag = "0.1.0", git = "https://github.com/my-ai-utils/my-ai-agent.git", features = ["agent"] }
tokio = { version = "*", features = ["full"] }
serde = { version = "*", features = ["derive"] }
serde_json = "*"
async-trait = "*"快速开始
1.创建中间件
创建一个实例 McpMiddleware 根据您的服务器配置:
use mcp_server_middleware::McpMiddleware;
use std::sync::Arc;
let mut mcp_middleware = McpMiddleware::new(
"/mcp", // MCP endpoint path
"My MCP Server", // Server name
"0.1.0", // Server version
"Instructions for using this MCP server", // Instructions
);2.实施工具服务
创建一个实现 McpToolCall 特质:
use mcp_server_middleware::{McpToolCall, ToolDefinition};
use my_ai_agent::{macros::ApplyJsonSchema, json_schema::*};
use serde::{Deserialize, Serialize};
use async_trait::async_trait;
use std::sync::Arc;
// Define your input and output types with JSON schema
#[derive(ApplyJsonSchema, Debug, Serialize, Deserialize)]
pub struct MyToolRequest {
#[property(description = "Input parameter description")]
pub input_field: String,
}
#[derive(ApplyJsonSchema, Debug, Serialize, Deserialize)]
pub struct MyToolResponse {
#[property(description = "Output parameter description")]
pub output_field: String,
}
// Create your handler struct
pub struct MyToolHandler {
// Add any dependencies you need (e.g., app context, database connection, etc.)
}
impl MyToolHandler {
pub fn new() -> Self {
Self {}
}
}
// Implement ToolDefinition to provide metadata
impl ToolDefinition for MyToolHandler {
const FUNC_NAME: &'static str = "my_tool";
const DESCRIPTION: &'static str = "Description of what this tool does";
}
// Implement McpToolCall to handle tool execution
#[async_trait::async_trait]
impl McpToolCall for MyToolHandler {
async fn execute_tool_call(
&self,
request: MyToolRequest,
) -> Result {
// Your implementation here
let result = format!("Processed: {}", request.input_field);
Ok(MyToolResponse {
output_field: result,
})
}
}3.注册工具调用
使用中间件注册您的服务:
let service = Arc::new(MyToolHandler::new());
mcp_middleware.register_tool_call(service);4.注册提示(可选)
您还可以注册MCP客户端可以发现和使用的提示:
use mcp_server_middleware::{McpPromptService, PromptDefinition};
use std::collections::HashMap;
use async_trait::async_trait;
// Implement the prompt service
pub struct MyPromptService;
impl PromptDefinition for MyPromptService {
const PROMPT_NAME: &'static str = "example_prompt";
const DESCRIPTION: &'static str = "An example prompt that demonstrates prompt functionality";
fn get_argument_descriptions() -> Vec {
vec![
mcp_server_middleware::PromptArgumentDescription {
name: "variable_name".to_string(),
description: "Description of what this variable represents".to_string(),
required: true,
},
mcp_server_middleware::PromptArgumentDescription {
name: "optional_param".to_string(),
description: "An optional parameter".to_string(),
required: false,
},
]
}
}
#[async_trait]
impl McpPromptService for MyPromptService {
async fn execute_prompt(
&self,
arguments: &HashMap,
) -> Result {
let var_value = arguments.get("variable_name")
.ok_or("variable_name is required")?;
Ok(mcp_server_middleware::PromptExecutionResult {
description: "Example prompt result".to_string(),
message: format!("Processing with variable: {}", var_value),
})
}
}
// Register the prompt
let prompt_service = Arc::new(MyPromptService);
mcp_middleware.register_prompt(prompt_service);5.注册资源(可选)
资源允许客户端读取数据源。实现资源服务:
use mcp_server_middleware::{McpResourceService, ResourceDefinition, ResourceReadResult, ResourceContent};
use async_trait::async_trait;
pub struct MyResourceService;
impl ResourceDefinition for MyResourceService {
const RESOURCE_URI: &'static str = "file:///example.txt";
const RESOURCE_NAME: &'static str = "example.txt";
const DESCRIPTION: &'static str = "An example resource file";
const MIME_TYPE: &'static str = "text/plain";
// Optional: Override for additional metadata
fn get_title(&self) -> Option {
Some("Example Resource")
}
fn get_size(&self) -> Option {
Some(1024) // Size in bytes
}
}
#[async_trait]
impl McpResourceService for MyResourceService {
async fn read_resource(&self) -> Result {
// Read your resource content here
let content = "Resource content here".to_string();
Ok(ResourceReadResult {
contents: vec![ResourceContent {
uri: Self::RESOURCE_URI.to_string(),
mime_type: "text/plain".to_string(),
text: Some(content),
blob: None,
}],
})
}
}
// Register the resource
let resource_service = Arc::new(MyResourceService);
mcp_middleware.register_resource(resource_service);5b。注册动态资源(运行时)
ResourceDefinition 将URI固定到 const &'static str,所以它可以 仅描述编译时已知的资源。对于在以下地点铸造的资源 运行时——每个上传的文件、每个数据库行、每个生成的文件一个 工件——使用动态注册表。中间件将其置于一个 RwLock,因此即使在中间件完成后,您也可以注册/注销 包裹在 Arc 并安装在HTTP服务器上。 resources/list, resources/read, resources/subscribe,以及 initialize 能力广告在静态和动态上都呈扇形展开 动态注册表。
use mcp_server_middleware::{McpResourceService, ResourceReadResult, ResourceContent};
use async_trait::async_trait;
use std::sync::Arc;
pub struct BlobResource {
uri: String,
bytes_base64: String,
mime_type: String,
}
#[async_trait]
impl McpResourceService for BlobResource {
async fn read_resource(&self) -> Result {
Ok(ResourceReadResult {
contents: vec![ResourceContent {
uri: self.uri.clone(),
mime_type: self.mime_type.clone(),
text: None,
blob: Some(self.bytes_base64.clone()), // base64-encoded payload
}],
})
}
}
// `mcp_middleware: Arc` — fine to call after it's mounted.
let uri = format!("app://blob/{id}");
let svc = Arc::new(BlobResource {
uri: uri.clone(),
bytes_base64,
mime_type: "image/png".to_string(),
});
// Minimal form:
mcp_middleware
.register_dynamic_resource(
uri.clone(),
"blob name".to_string(),
"Generated blob".to_string(),
"image/png".to_string(),
svc.clone(),
)
.await;
// Or with optional title / size / icons:
mcp_middleware
.register_dynamic_resource_full(
uri.clone(),
"blob name".to_string(),
"Generated blob".to_string(),
"image/png".to_string(),
Some("Blob Title".to_string()),
Some(4096), // size in bytes
Vec::new(), // icons
svc,
)
.await;
// Push the change to live MCP sessions:
mcp_middleware.notify_resources_changed().await;
// Remove it later (true if it was present):
let _ = mcp_middleware.unregister_dynamic_resource(&uri).await;笔记:
- 注册同一URI两次会覆盖之前的条目。
- 动态注册表没有分页;
resources/list表面每
静态资源耗尽后,页面上的动态资源。 适用于“数万至数千”的参赛作品。
- 返回
blob(base64)与图像MIME类型允许MCP客户端
将资源渲染为图像内容块——用于 二进制有效载荷,而不是将base64填充到工具调用JSON中。
6.与HTTP服务器集成
将中间件添加到HTTP服务器:
use my_http_server::MyHttpServer;
use std::net::SocketAddr;
let mut http_server = MyHttpServer::new(SocketAddr::from(([0, 0, 0, 0], 8005)));
let mcp_middleware = Arc::new(mcp_middleware);
http_server.add_middleware(mcp_middleware);
http_server.start(app_states, logger);从工具调用返回指令
默认情况下,工具调用只返回结构化数据——模型通过 structuredContent 领域 tools/call 回应。有时工具想要附加一个短 *内联指令* 对于数据之上的模型:如何解释结果,下一步做什么,问用户什么。中间件通过以下方式公开了这一点 ToolCallOutput 第二个特点 McpToolCallWithInstruction.
McpToolCall 保持不变——现有实现无需任何编辑即可继续工作。要使用新功能,请执行 McpToolCallWithInstruction 相反,返回a ToolCallOutput:
use mcp_server_middleware::{
McpToolCallWithInstruction, ToolCallOutput, ToolDefinition,
};
#[async_trait::async_trait]
impl McpToolCallWithInstruction for MyHandler {
async fn execute_tool_call_with_instruction(
&self,
req: MyReq,
) -> Result, String> {
let resp = MyResp { items: vec![/* ... */] };
if resp.items.is_empty() {
return Ok(ToolCallOutput::with_instruction(
resp,
"Result is empty. Suggest the user widen the filter.",
));
}
Ok(ToolCallOutput::new(resp))
}
}ToolCallOutput 施工人员:
ToolCallOutput::new(data)--仅数据,无指令(相当于传统Ok(data)行为)。ToolCallOutput::with_instruction(data, text)--数据加上模型的内联指令。From for ToolCallOutput已实施,因此data.into()作为快捷方式ToolCallOutput::new(data).
McpToolCallWithInstruction 用毯子包裹 McpToolCall,因此任何现有 McpToolCall 实现是自动的 McpToolCallWithInstruction 那就回来了 ToolCallOutput::new(data)。只有当你想附加指令时,你才能直接实现新特性。注册使用相同 register_tool_call(...) 方法。
指令如何到达模型
当工具返回指令时 tools/call 响应包括:
result.structuredContent--结构化data(与之前相同);result.content[0]—{ "type": "text", "text": "" },这是读取的标准MCP信道模型。
当 instruction 是 None,行为与以前的版本没有变化: content[0].text 携带JSON字符串化数据 structuredContent 在结构上携带相同的数据。
服务器级指令与每次调用指令
这是两种不同的机制,不要混淆:
- 服务器说明 --the
instructions论点McpMiddleware::new(path, name, version, instructions)。它们在initialize并适用于整个会话。使用它们进行全局指导(“此服务器公开了一个Postgres数据库,查询应该是只读的”)。 - 每次通话说明 —
ToolCallOutput::with_instruction(...)。它们是在对特定内容的回复中返回的tools/call并且仅限于该调用的上下文。将它们用于取决于实际工具结果的情境提示(“搜索没有返回任何行——建议扩大过滤器”)。
服务器→客户启发(要求用户输入)
有时工具需要一个模型值 不得 请参阅——数据库密码、2FA代码、破坏性操作的明确确认。MCP称之为 *引出*:在工具调用过程中,服务器通过SSE流发回JSON-RPC请求,要求连接的客户端提示用户。用户的答案返回给工具;只有工具的最终结果才能到达模型。
这是作为一个 独立特征 — McpToolCallEx --以及a 单独登记法 — register_tool_call_with_context.平原 McpToolCall impls未受影响。
客户端先决条件
连接的MCP客户端必须通告 elicitation 能力期间 initialize:
{
"capabilities": {
"elicitation": {}
}
}如果没有, ctx.elicit(...) 回报 Err("MCP client does not support elicitation")许多客户(和临时客户) curl-样式集成)不支持启发式——始终处理错误路径。
实施上下文感知工具
实施 McpToolCallEx 而不是 McpToolCallexecute方法接收 &ToolCallContext 在输入旁边:
use std::sync::Arc;
use std::time::Duration;
use mcp_server_middleware::{
ElicitationAction, McpToolCallEx, ToolCallContext, ToolDefinition,
};
use my_ai_agent::macros::ApplyJsonSchema;
use serde::{Deserialize, Serialize};
#[derive(ApplyJsonSchema, Debug, Serialize, Deserialize)]
pub struct ConnectDbRequest {
#[property(description = "Database host, e.g. db.internal:5432")]
pub host: String,
#[property(description = "Database name")]
pub database: String,
#[property(description = "DB user")]
pub user: String,
}
#[derive(ApplyJsonSchema, Debug, Serialize, Deserialize)]
pub struct ConnectDbResponse {
#[property(description = "Server version reported by the database")]
pub server_version: String,
}
pub struct ConnectDbHandler;
impl ToolDefinition for ConnectDbHandler {
const FUNC_NAME: &'static str = "connect_db";
const DESCRIPTION: &'static str =
"Connect to a Postgres database. The password is asked from the user and never enters the model context.";
}
#[async_trait::async_trait]
impl McpToolCallEx for ConnectDbHandler {
async fn execute_tool_call(
&self,
req: ConnectDbRequest,
ctx: &ToolCallContext,
) -> Result {
// Flat JSON schema for what we want from the user.
// MCP elicitation only supports flat objects of primitive
// properties (string / number / integer / boolean / enum).
let schema = serde_json::json!({
"type": "object",
"properties": {
"password": {
"type": "string",
"description": format!("Password for {}@{}", req.user, req.host),
"format": "password",
},
},
"required": ["password"],
});
let resp = ctx
.elicit("Enter database password", schema, Duration::from_secs(60))
.await?;
match resp.action {
ElicitationAction::Accept => {
let password = resp
.content
.as_ref()
.and_then(|c| c.get("password"))
.and_then(|v| v.as_str())
.ok_or("Elicitation accepted but `password` is missing")?
.to_string();
let version = open_pg_connection(&req, &password).await?;
Ok(ConnectDbResponse { server_version: version })
}
ElicitationAction::Decline => {
Err("User declined to share the password".to_string())
}
ElicitationAction::Cancel => {
Err("Elicitation cancelled".to_string())
}
}
}
}
// Registration uses `register_tool_call_with_context` — NOT `register_tool_call`.
mcp_middleware.register_tool_call_with_context(Arc::new(ConnectDbHandler));什么 ToolCallContext 暴露
pub struct ToolCallContext {
pub session_id: String, // mcp-session-id of the originating call
pub supports_elicitation: bool, // did the client advertise the capability?
// ...plus internal handles to the elicitation registry and the SSE sender
}
impl ToolCallContext {
pub async fn elicit(
&self,
message: &str,
requested_schema: serde_json::Value,
timeout: Duration,
) -> Result;
}什么 elicit(...) 在引擎盖下:
- 分配a 负面的 请求id(故意为负数——永远不会与客户端为自己的请求分配的id冲突)。
- 推一个
elicitation/create此会话的SSE流上的JSON-RPC请求:{ id, message, requestedSchema }. - 将电话挂在
oneshot由该id键入,并具有提供的超时时间。 - 当客户端POST返回相同的响应时
id,中间件将其解析为ElicitationResponse并唤醒挂起的电话。
错误路径:
"MCP client does not support elicitation"--客户没有做广告capabilities.elicitation在initialize."No active SSE channel for this MCP session"--客户端打开了一个会话,但没有保留其GET /mcpSSE流打开。"Failed to deliver elicitation/create — SSE channel closed"--SSE流在分配和发送之间死亡。"Elicitation timed out — client did not reply in time"--客户端响应之前超时。
你可以用泡泡把这些直接吹起来 ?,或在返回之前将它们重新映射到特定于工具的消息。
回复形状
pub enum ElicitationAction { Accept, Decline, Cancel }
pub struct ElicitationResponse {
pub action: ElicitationAction,
/// Present when `action == Accept`. JSON object matching the
/// `requested_schema` you sent. `None` for Decline / Cancel.
pub content: Option,
}根据MCP规范:
Accept--用户提供的值。content是与模式匹配的JSON对象;用以下方法拉出田地content.get("field_name").Decline--用户主动拒绝(例如点击“不分享”)。视为 *拒绝*,不是内部错误——向模型返回一个干净的解释。Cancel--用户取消了提示(关闭对话框,切换开)。通常对待一样Decline;你可以分辨出你的用户体验是否在乎。
如果客户端返回格式错误或错误的有效负载,中间件会将其强制转换为 Cancel 随着 content == None --所以a Cancel 该分支涵盖了“用户取消”和“客户端崩溃”
警告:上下文感知路径上没有内联指令
McpToolCallEx::execute_tool_call 回报 Result 直接——确实如此 不 经历 ToolCallOutput因此,上下文感知工具无法通过 result.content[0].text 上述“返回说明”部分中描述的通道。如果你需要两个启发 *和* 内联指令,在内部对提示进行编码 OutputData 或者将工作分成两个工具。
动态枚举字段
对于需要接受动态生成列表中的值的工具调用参数(例如按可用城市、国家或其他运行时确定的选项进行筛选),可以使用动态枚举。此功能允许在运行时根据应用程序的当前数据状态生成枚举值。
要使用动态枚举,请指定 enum 参数在 #[property] 具有将生成枚举值的异步函数名称的属性。此函数必须返回 Option>> 并且当MCP客户端请求工具模式时将自动调用。
use my_ai_agent::macros::ApplyJsonSchema;
use serde::{Deserialize, Serialize};
use service_sdk::rust_extensions::StrOrString;
#[derive(ApplyJsonSchema, Serialize, Deserialize, Debug)]
pub struct FilterPropertiesToolCallModel {
#[property(enum: "get_city_enum", description: "Filter properties by city location")]
pub city: Option,
#[property(enum: "get_country_enum", description: "Filter by country using ISO2 code")]
pub country: Option,
#[property(enum: "get_project_name_enum", description: "Filter by development project name")]
pub project_name: Option,
}
// Implement the enum generation functions
async fn get_city_enum() -> Option>> {
let data_access = DATA_HOLDER.read().await;
data_access.units.group_by_project(|unit| &unit.city)
}
async fn get_country_enum() -> Option>> {
let data_access = DATA_HOLDER.read().await;
data_access.units.group_by_project(|unit| &unit.country)
}
async fn get_project_name_enum() -> Option>> {
let data_access = DATA_HOLDER.read().await;
data_access.units.group_by_project(|project| &project.title)
}在为工具生成JSON模式时,枚举函数会被自动发现和调用。返回的值将作为枚举约束包含在工具的输入模式中,为客户端提供每个参数的可用选项。这对于依赖于应用程序当前状态的参数特别有用,例如按可用城市过滤、从活动项目中选择或从动态加载的配置选项中选择。
创建工具调用和提示
工具调用分步指南
- 创建工具调用文件 (例如。,
my_tool_call.rs)
- 定义输入和输出结构 随着
ApplyJsonSchema:
#[derive(ApplyJsonSchema, Debug, Serialize, Deserialize)]
pub struct MyToolInputData {
#[property(description = "Description of the parameter")]
pub parameter_name: String,
#[property(description = "Another parameter")]
pub another_param: Option,
}
#[derive(ApplyJsonSchema, Debug, Serialize, Deserialize)]
pub struct MyToolResponse {
#[property(description = "Result description")]
pub result: String,
#[property(description = "Status code")]
pub status: i32,
}- 创建处理程序结构:
pub struct MyToolHandler {
// Add dependencies if needed (e.g., app context)
}
impl MyToolHandler {
pub fn new() -> Self {
Self {}
}
}- 实施
ToolDefinition特质:
impl ToolDefinition for MyToolHandler {
const FUNC_NAME: &'static str = "my_tool_name";
const DESCRIPTION: &'static str = "Clear description of what this tool does";
}- 实施
McpToolCall特质:
#[async_trait::async_trait]
impl McpToolCall for MyToolHandler {
async fn execute_tool_call(
&self,
model: MyToolInputData,
) -> Result {
// Your implementation here
let result = MyToolResponse {
result: "Success".to_string(),
status: 200,
};
Ok(result)
}
}- 在您的启动代码中注册:
mcp_middleware.register_tool_call(Arc::new(MyToolHandler::new()));提示分步指南
- 创建提示处理程序结构:
pub struct MyPromptHandler;- 实施
PromptDefinition特质:
impl PromptDefinition for MyPromptHandler {
const PROMPT_NAME: &'static str = "my_prompt_name";
const DESCRIPTION: &'static str = "Description of what this prompt provides";
fn get_argument_descriptions() -> Vec
{
vec![
PromptArgumentDescription {
name: "param1".to_string(),
description: "Description of param1".to_string(),
required: true,
},
PromptArgumentDescription {
name: "param2".to_string(),
description: "Description of param2".to_string(),
required: false,
},
]
}
}- 实施
McpPromptService特质:
#[async_trait::async_trait]
impl McpPromptService for MyPromptHandler {
async fn execute_prompt(
&self,
arguments: &HashMap,
) -> Result
{
// Access arguments if needed
let param1 = arguments.get("param1");
// Build your prompt content
let prompt_content = format!(
r#"
# Your Prompt Title
## Section 1
Content here...
"#
);
let result = PromptExecutionResult {
description: "What this prompt provides".to_string(),
message: prompt_content,
};
Ok(result)
}
}- 在您的启动代码中注册:
mcp_middleware.register_prompt(Arc::new(MyPromptHandler));完整示例:Postgres MCP服务器
以下示例演示了一个真实世界的实现——一个允许AI代理执行SQL查询的Postgres MCP服务器。这可作为构建自己的MCP服务器的具体参考:
use std::sync::Arc;
use mcp_server_middleware::{McpMiddleware, McpToolCall, ToolDefinition};
use my_http_server::MyHttpServer;
use my_ai_agent::{macros::ApplyJsonSchema, json_schema::*};
use serde::{Deserialize, Serialize};
use async_trait::async_trait;
use std::net::SocketAddr;
// Define your service
pub struct PostgresMcpService {
// Your service dependencies
}
impl PostgresMcpService {
pub fn new() -> Self {
Self {}
}
}
#[derive(ApplyJsonSchema, Debug, Serialize, Deserialize)]
pub struct SqlRequest {
#[property(description = "SQL query to execute")]
pub sql: String,
}
#[derive(ApplyJsonSchema, Debug, Serialize, Deserialize)]
pub struct SqlResponse {
#[property(description = "Query result as JSON")]
pub result: String,
}
impl ToolDefinition for PostgresMcpService {
const FUNC_NAME: &'static str = "sql_request";
const DESCRIPTION: &'static str = "Execute SQL queries";
}
#[async_trait::async_trait]
impl McpToolCall for PostgresMcpService {
async fn execute_tool_call(
&self,
model: SqlRequest,
) -> Result {
// Execute your SQL query
let result = execute_query(&model.sql).await?;
Ok(SqlResponse { result })
}
}
// Setup function
async fn setup_server() {
let mut http_server = MyHttpServer::new(SocketAddr::from(([0, 0, 0, 0], 8005)));
// Create middleware
let mut mcp_middleware = McpMiddleware::new(
"/postgres",
"Postgres MCP Server",
"0.1.0",
"Execute SQL queries on your database",
);
// Register tool
let service = Arc::new(PostgresMcpService::new());
mcp_middleware.register_tool_call(service);
// Add to server
let mcp_middleware = Arc::new(mcp_middleware);
http_server.add_middleware(mcp_middleware);
// Start server
http_server.start(app_states, logger);
}API 参考
McpMiddleware
处理MCP协议通信的主要中间件结构。
new(path, name, version, instructions)
创建新的中间件实例。
path:将处理MCP请求的HTTP路径(例如。,/mcp,/api/mcp)name:向客户端显示的服务器名称version:服务器版本字符串instructions:使用此服务器的AI代理的说明
register_tool_call(service)
注册工具调用服务。该服务必须实现:
McpToolCall特质ToolDefinition特质- 输入和输出类型必须实现
JsonTypeDescription,Serialize,以及DeserializeOwned
register_tool_call_with_context(service)
同 register_tool_call,但对于需要追溯到 执行过程中的客户端(服务器→客户启发等)。这 服务工具\[McpToolCallEx\]而不是 McpToolCall 和 收到a &ToolCallContext 其execute方法中的参数。请参阅 “服务器→上面的“客户启发”部分是一个工作示例。
register_prompt(prompt)
注册快速服务。该服务必须实现:
McpPromptService特质PromptDefinition特质- 这
PromptDefinition特质要求:
- PROMPT_NAME:提示的唯一标识符(const) - DESCRIPTION:人类可读描述(const) - get_argument_descriptions():退货 Vec 带参数元数据
register_resource(service)
注册一个静态资源,其URI在编译时是已知的。这 服务必须实现 ResourceDefinition (提供 RESOURCE_URI, RESOURCE_NAME, DESCRIPTION, MIME_TYPE consts plus可选 get_title / get_size / get_icons)以及 McpResourceService (提供 read_resource).
register_dynamic_resource(uri, name, description, mime_type, service) *(异步)*
注册在运行时生成的资源。URI是 String 选择由 呼叫者。只有服务 McpResourceService 需要impl-- ResourceDefinition 不是。Idempotent:重新注册相同的URI 覆盖之前的条目。由a支持 RwLock,所以这是安全的 中间件封装后的调用 Arc 并安装。
register_dynamic_resource_full(uri, name, description, mime_type, title, size, icons, service) *(异步)*
同 register_dynamic_resource 但接受可选 title: Option, size: Option,以及 icons: Vec 元数据。
unregister_dynamic_resource(uri) *(异步)*
删除动态资源。退货 true 如果资源具有该URI 当时在场。跟进 notify_resources_changed() 所以客户 刷新他们的资源列表。
McpToolCall 特质
您的工具服务必须实现的特性:
#[async_trait::async_trait]
pub trait McpToolCall
where
InputData: JsonTypeDescription + Sized + Send + Sync + 'static,
OutputData: JsonTypeDescription + Sized + Send + Sync + 'static,
{
async fn execute_tool_call(&self, model: InputData) -> Result;
}McpToolCallEx 特质
上下文感知变体 McpToolCall.执行此工具时 需要执行服务器→执行过程中的客户端交互 (今天的启发;未来的采样/进展)。注册 服务伴随 register_tool_call_with_context.
#[async_trait::async_trait]
pub trait McpToolCallEx
where
InputData: JsonTypeDescription + Sized + Send + Sync + 'static,
OutputData: JsonTypeDescription + Sized + Send + Sync + 'static,
{
async fn execute_tool_call(
&self,
model: InputData,
ctx: &ToolCallContext,
) -> Result;
}注意:此特征返回 OutputData 直接——没有 ToolCallOutput 包装器,因此内联指令( content[0].text 频道)在此路径上不可用。请参阅“警告:无内联 上面的上下文感知路径说明。
ToolCallContext
由中间件根据工具调用构建并传递给 McpToolCallEx::execute_tool_call.显示会话id 客户的 elicitation 能力标志,以及 elicit(...) 方法 记录在“服务器→客户启发”部分。
pub struct ToolCallContext {
pub session_id: String,
pub supports_elicitation: bool,
// ...internal handles
}
impl ToolCallContext {
pub async fn elicit(
&self,
message: &str,
requested_schema: serde_json::Value,
timeout: Duration,
) -> Result;
}ElicitationAction / ElicitationResponse
返回者 ToolCallContext::elicit根据MCP规范,客户 用三个动作中的一个进行回复; content 是 Some 仅在 Accept.
pub enum ElicitationAction { Accept, Decline, Cancel }
pub struct ElicitationResponse {
pub action: ElicitationAction,
pub content: Option,
}ToolDefinition 特质
提供有关工具的元数据:
pub trait ToolDefinition {
const FUNC_NAME: &'static str;
const DESCRIPTION: &'static str;
}PromptDefinition 特质
您的快速服务必须实现的特性:
pub trait PromptDefinition {
const PROMPT_NAME: &'static str;
const DESCRIPTION: &'static str;
fn get_argument_descriptions() -> Vec
;
}PromptArgumentDescription 结构体
表示提示参数描述:
pub struct PromptArgumentDescription {
pub name: String,
pub description: String,
pub required: bool,
}McpPromptService 特质
您的快速服务必须实现的特性:
#[async_trait::async_trait]
pub trait McpPromptService {
async fn execute_prompt(
&self,
arguments: &HashMap,
) -> Result
;
}MCP协议支持
中间件完全实现了MCP协议规范(2025-11-25),并处理以下协议方法:
核心协议方法
initialize:初始化新的MCP会话并返回服务器功能
- 声明支持工具、提示和资源 - 返回协议版本和服务器信息 - 创建具有唯一会话ID的新会话
tools/list:返回可用工具及其JSON模式的列表
- 包括每个工具的输入和输出模式 - 使用以下命令从Rust类型自动生成 ApplyJsonSchema
tools/call:使用提供的参数执行工具调用
- 根据工具的模式验证输入 - 执行您的服务实现 - 返回结构化结果或错误
prompts/list:返回可用提示及其参数的列表
- 显示提示名称、描述和参数定义 - 包括每个参数的必需/可选状态
prompts/get:检索带有变量替换的提示
- 使用提供的参数执行提示模板 - 返回格式化的提示消息,供AI使用
resources/list:返回包含元数据的可用资源
- 支持通过基于光标的导航进行分页 - 包括资源URI、名称、描述、MIME类型和可选元数据(标题、大小、图标)
resources/read:读取资源内容
- 根据资源类型返回文本或二进制内容 - 每个资源支持多个内容块
resources/subscribe:订阅资源更改
- 返回资源的初始版本 - 当前仅返回第一个版本(更新通知尚未实现)
ping:连接测试的健康检查端点
notifications/initialized:处理客户端初始化确认
elicitation/create*(服务器→客户)*:由服务器发送,要求连接的客户端提示用户输入。携带一条消息和一个描述预期回复的JSON模式。通过工具代码触发ToolCallContext::elicit(...).要求客户做广告capabilities.elicitation在initialize客户端通过常规POST端点使用匹配的请求id进行响应,中间件唤醒已暂停的工具调用。请参阅“服务器→全流程的“客户启发”部分。
协议特性
- JSON-RPC 2.0:所有请求/响应均遵循JSON-RPC 2.0格式
- 服务器发送事件(SSE):实时更新的流式响应
- 会话管理:通过以下方式进行基于会话的安全身份验证
mcp-session-id头球 - 类型安全:从Rust类型自动生成JSON模式
- 错误处理:符合MCP规范的标准化错误代码和消息
会话管理
会话由中间件自动管理:
- 每
initialize请求创建具有唯一会话ID的新会话 - 会话ID在
mcp-session-idHTTP标头 - 后续请求必须在
mcp-session-id头球 - 对MCP路径的GET请求为通知建立服务器发送事件(SSE)流
类型安全
中间件利用 my-ai-agents ApplyJsonSchema 宏自动为您的输入和输出类型生成JSON模式。这确保了MCP工具定义的类型安全和自动模式生成。使用 #[property(description = "...")] 用于记录字段的属性:
#[derive(ApplyJsonSchema, Serialize, Deserialize)]
pub struct MyRequest {
#[property(description: "A description of this field")]
pub field: String,
}当客户端调用时,会自动使用生成的模式 tools/list 以发现可用的工具。同样,当客户端调用时,会显示已注册的提示 prompts/list.
错误处理
工具执行错误应返回为 Err(String) 从 execute_tool_call中间件将以MCP响应格式适当地格式化这些内容,确保客户端收到结构正确的错误信息。
最佳实践
命名约定
- 工具文件:
{snake_case_name}_tool_call.rs - 提示文件:
{snake_case_name}_prompt.rs或添加到现有提示文件中 - 处理程序结构:
{PascalCaseName}Handler - 输入/输出结构:
{PascalCaseName}InputData/{PascalCaseName}Response
错误处理
- 始终返回描述性错误消息
- 使用
Result-字符串将被发送到客户端 - 优雅地处理错误并提供上下文
文档
- 使用
#[property(description = "...")]对于输入/输出结构中的所有字段 - 写清楚
DESCRIPTION工具和提示的常数 - 在注释中记录复杂的逻辑
项目结构
使用此中间件构建MCP服务器时,请按如下方式组织代码:
src/
├── lib.rs or main.rs
├── mcp/ # MCP tool calls and prompts
│ ├── mod.rs # Export all MCP components
│ ├── {tool_name}_tool_call.rs # Individual tool implementations
│ └── {prompt_name}_prompt.rs # Individual prompt implementations
└── http/
└── start_up.rs # Register tools and prompts here注册顺序
在启动代码中,按以下顺序注册组件:
- 创建
McpMiddleware例子 - 使用注册所有工具调用
register_tool_call() - 使用注册所有提示
register_prompt() - 使用注册所有资源
register_resource() - 将中间件添加到HTTP服务器
let mut mcp = McpMiddleware::new(
"/mcp",
"My MCP Server",
"0.1.0",
"Server description",
);
// Register tools
mcp.register_tool_call(Arc::new(Tool1Handler::new()));
mcp.register_tool_call(Arc::new(Tool2Handler::new()));
// Register prompts
mcp.register_prompt(Arc::new(Prompt1Handler));
mcp.register_prompt(Arc::new(Prompt2Handler));
// Register static resources
mcp.register_resource(Arc::new(Resource1Handler));
// Add to HTTP server
let mcp = Arc::new(mcp);
http_server.add_middleware(mcp.clone());
// Dynamic resources can be registered any time after this, e.g. from
// a tool handler that just produced an artifact:
// mcp.register_dynamic_resource(uri, name, desc, mime, svc).await;
// mcp.notify_resources_changed().await;用例
此中间件可用于构建用于各种目的的MCP服务器:
- 数据库访问:公开数据库操作(SQL查询、模式检查等)
- 文件系统操作:提供文件和目录管理功能
- API集成:将外部API和服务包装为MCP工具
- 开发工具:公开构建、测试和部署操作
- 自定义业务逻辑:为您的应用程序实施特定于域的工具
- 提示模板:注册可重用的提示模板,AI代理可以使用变量替换
上面的Postgres示例演示了一个这样的用例。您可以调整相同的模式来实现所需的任何功能的工具。提示对于提供客户端可以与不同变量值一起使用的预配置提示模板非常有用。
依赖项
my-http-server:HTTP服务器框架my-ai-agent:AI代理实用程序和JSON模式生成tokio:异步运行时serde/serde_json:序列化async-trait:异步特性支持
许可证
\[在此处添加您的许可证\]
贡献
\[在此处添加贡献指南\]
