Skip to content

Commit dd30a70

Browse files
authored
feat!: add MRTR behavior support (SEP-2322) (#929)
* feat!: add MRTR behavior support * feat: harden SEP-2322 MRTR support * ci: diff public API only on features common to base and head cargo public-api builds both revisions with the same feature set, so a feature introduced (or removed) by a PR broke the build of the other revision. Restrict the all-features diff to features present in both the base and head revisions.
1 parent a719459 commit dd30a70

30 files changed

Lines changed: 2121 additions & 127 deletions

.github/workflows/ci.yml

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,18 +170,32 @@ jobs:
170170
171171
- name: Check rmcp (all features except local)
172172
run: |
173-
FEATURES=$(cargo metadata --no-deps --format-version 1 \
174-
| jq -r '[.packages[] | select(.name == "rmcp") | .features | keys[]
175-
| select(startswith("__") | not)
176-
| select(. != "local")] | join(",")')
173+
BASE_SHA=${{ github.event.pull_request.base.sha }}
174+
# `cargo public-api diff` builds both revisions with the same feature
175+
# set, so a feature that exists on only one side (e.g. a feature added
176+
# or removed by this PR) would fail to build the other revision. Diff
177+
# only the features present in BOTH the base and the head; features
178+
# unique to one side are necessarily pure additions/removals, which the
179+
# release-type deny flags already govern.
180+
list_features() {
181+
cargo metadata --no-deps --format-version 1 --manifest-path "$1/Cargo.toml" \
182+
| jq -r '.packages[] | select(.name == "rmcp") | .features | keys[]
183+
| select(startswith("__") | not)
184+
| select(. != "local")'
185+
}
186+
list_features "." | sort -u > "$RUNNER_TEMP/head_features"
187+
git worktree add --detach "$RUNNER_TEMP/rmcp-base" "$BASE_SHA"
188+
list_features "$RUNNER_TEMP/rmcp-base" | sort -u > "$RUNNER_TEMP/base_features"
189+
git worktree remove --force "$RUNNER_TEMP/rmcp-base"
190+
FEATURES=$(comm -12 "$RUNNER_TEMP/head_features" "$RUNNER_TEMP/base_features" | paste -sd, -)
177191
cargo public-api \
178192
--package rmcp \
179193
--features "$FEATURES" \
180194
-ss \
181195
diff \
182196
$DENY \
183197
--force \
184-
${{ github.event.pull_request.base.sha }}..${{ github.sha }}
198+
"$BASE_SHA"..${{ github.sha }}
185199
186200
spelling:
187201
name: spell check with typos

conformance/src/bin/server.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,9 @@ impl ServerHandler for ConformanceServer {
213213
&self,
214214
request: CallToolRequestParams,
215215
cx: RequestContext<RoleServer>,
216-
) -> Result<CallToolResult, ErrorData> {
216+
) -> Result<CallToolResponse, ErrorData> {
217217
let args = request.arguments.unwrap_or_default();
218-
match request.name.as_ref() {
218+
let result = match request.name.as_ref() {
219219
"test_simple_text" => Ok(CallToolResult::success(vec![ContentBlock::text(
220220
"This is a simple text response for testing.",
221221
)])),
@@ -530,7 +530,8 @@ impl ServerHandler for ConformanceServer {
530530
format!("Unknown tool: {}", request.name),
531531
None,
532532
)),
533-
}
533+
};
534+
result.map(Into::into)
534535
}
535536

536537
async fn list_resources(
@@ -555,9 +556,9 @@ impl ServerHandler for ConformanceServer {
555556
&self,
556557
request: ReadResourceRequestParams,
557558
_cx: RequestContext<RoleServer>,
558-
) -> Result<ReadResourceResult, ErrorData> {
559+
) -> Result<ReadResourceResponse, ErrorData> {
559560
let uri = request.uri.as_str();
560-
match uri {
561+
let result = match uri {
561562
"test://static-text" => Ok(ReadResourceResult::new(vec![
562563
ResourceContents::TextResourceContents {
563564
uri: uri.into(),
@@ -598,7 +599,8 @@ impl ServerHandler for ConformanceServer {
598599
))
599600
}
600601
}
601-
}
602+
};
603+
result.map(Into::into)
602604
}
603605

604606
async fn list_resource_templates(
@@ -679,8 +681,8 @@ impl ServerHandler for ConformanceServer {
679681
&self,
680682
request: GetPromptRequestParams,
681683
_cx: RequestContext<RoleServer>,
682-
) -> Result<GetPromptResult, ErrorData> {
683-
match request.name.as_str() {
684+
) -> Result<GetPromptResponse, ErrorData> {
685+
let result = match request.name.as_str() {
684686
"test_simple_prompt" => Ok(GetPromptResult::new(vec![PromptMessage::new_text(
685687
Role::User,
686688
"This is a simple test prompt.",
@@ -721,7 +723,8 @@ impl ServerHandler for ConformanceServer {
721723
format!("Unknown prompt: {}", request.name),
722724
None,
723725
)),
724-
}
726+
};
727+
result.map(Into::into)
725728
}
726729

727730
async fn complete(

crates/rmcp-macros/src/prompt_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ pub fn prompt_handler(attr: TokenStream, input: TokenStream) -> syn::Result<Toke
3535
&self,
3636
request: rmcp::model::GetPromptRequestParams,
3737
context: rmcp::service::RequestContext<rmcp::RoleServer>,
38-
) -> Result<rmcp::model::GetPromptResult, rmcp::ErrorData> {
38+
) -> Result<rmcp::model::GetPromptResponse, rmcp::ErrorData> {
3939
let prompt_context = rmcp::handler::server::prompt::PromptContext::new(
4040
self,
4141
request.name,

crates/rmcp-macros/src/task_handler.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,16 @@ pub fn task_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenS
7777

7878
let task_result_id = task_id.clone();
7979
let future = Box::pin(async move {
80-
let result = server.call_tool(future_request, future_context).await;
80+
let result = server
81+
.call_tool(future_request, future_context)
82+
.await
83+
.and_then(|response| match response {
84+
rmcp::model::CallToolResponse::Complete(result) => Ok(result),
85+
_ => Err(rmcp::ErrorData::internal_error(
86+
"input_required is not supported for task-based tool calls",
87+
None,
88+
)),
89+
});
8190
Ok(
8291
Box::new(ToolCallTaskResult::new(task_result_id, result))
8392
as Box<dyn OperationResultTransport>,

crates/rmcp-macros/src/tool_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub fn tool_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenS
4747
&self,
4848
request: rmcp::model::CallToolRequestParams,
4949
context: rmcp::service::RequestContext<rmcp::RoleServer>,
50-
) -> Result<rmcp::model::CallToolResult, rmcp::ErrorData> {
50+
) -> Result<rmcp::model::CallToolResponse, rmcp::ErrorData> {
5151
let tcc = rmcp::handler::server::tool::ToolCallContext::new(self, request, context);
5252
#router.call(tcc).await
5353
}

crates/rmcp/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ features = [
2222
"client-side-sse",
2323
"elicitation",
2424
"macros",
25+
"request-state",
2526
"reqwest",
2627
"reqwest-native-tls",
2728
"reqwest-tls-no-provider",
@@ -64,6 +65,10 @@ schemars = { version = "1.0", optional = true, features = ["chrono04"] }
6465
# for image encoding
6566
base64 = { version = "0.22", optional = true }
6667

68+
# for SEP-2322 requestState integrity sealing (opt-in via the `request-state` feature)
69+
hmac = { version = "0.12", optional = true }
70+
sha2 = { version = "0.10", optional = true }
71+
6772
# for HTTP client
6873
reqwest = { version = "0.13.2", default-features = false, features = [
6974
"json",
@@ -120,6 +125,9 @@ server = ["transport-async-rw", "schemars", "dep:pastey"]
120125
macros = ["dep:rmcp-macros", "dep:pastey"]
121126
elicitation = ["dep:url"]
122127

128+
# SEP-2322 requestState integrity helper (HMAC-SHA256 seal/open codec)
129+
request-state = ["dep:hmac", "dep:sha2", "base64"]
130+
123131
# reqwest http client
124132
__reqwest = ["dep:reqwest"]
125133

@@ -315,6 +323,11 @@ name = "test_trace_context"
315323
required-features = ["server", "client"]
316324
path = "tests/test_trace_context.rs"
317325

326+
[[test]]
327+
name = "test_mrtr_behavior"
328+
required-features = ["server", "client"]
329+
path = "tests/test_mrtr_behavior.rs"
330+
318331
[[test]]
319332
name = "test_prompt_macros"
320333
required-features = ["server", "client"]

crates/rmcp/src/handler/server.rs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ impl<H: ServerHandler> Service<RoleServer> for H {
2727
) -> Result<<RoleServer as ServiceRole>::Resp, McpError> {
2828
// `context` is moved into the dispatch below, so read the negotiated version first.
2929
let protocol_version = context.protocol_version();
30+
let mrtr_supported = protocol_version
31+
.as_ref()
32+
.is_some_and(|v| v.as_str() >= ProtocolVersion::V_2026_07_28.as_str());
3033
let result = match request {
3134
ClientRequest::InitializeRequest(request) => self
3235
.initialize(request.params, context)
@@ -46,7 +49,7 @@ impl<H: ServerHandler> Service<RoleServer> for H {
4649
ClientRequest::GetPromptRequest(request) => self
4750
.get_prompt(request.params, context)
4851
.await
49-
.map(ServerResult::GetPromptResult),
52+
.map(ServerResult::from),
5053
ClientRequest::ListPromptsRequest(request) => self
5154
.list_prompts(request.params, context)
5255
.await
@@ -62,7 +65,7 @@ impl<H: ServerHandler> Service<RoleServer> for H {
6265
ClientRequest::ReadResourceRequest(request) => self
6366
.read_resource(request.params, context)
6467
.await
65-
.map(ServerResult::ReadResourceResult),
68+
.map(ServerResult::from),
6669
ClientRequest::SubscribeRequest(request) => self
6770
.subscribe(request.params, context)
6871
.await
@@ -105,7 +108,7 @@ impl<H: ServerHandler> Service<RoleServer> for H {
105108
} else {
106109
self.call_tool(request.params, context)
107110
.await
108-
.map(ServerResult::CallToolResult)
111+
.map(ServerResult::from)
109112
}
110113
}
111114
ClientRequest::ListToolsRequest(request) => self
@@ -133,6 +136,17 @@ impl<H: ServerHandler> Service<RoleServer> for H {
133136
.await
134137
.map(ServerResult::CancelTaskResult),
135138
};
139+
let result = result.and_then(|result| {
140+
if matches!(result, ServerResult::InputRequiredResult(_)) && !mrtr_supported {
141+
Err(McpError::invalid_request(
142+
"InputRequiredResult requires negotiated protocol version 2026-07-28 or newer",
143+
None,
144+
))
145+
} else {
146+
Ok(result)
147+
}
148+
});
149+
136150
// SEP-2164: peers negotiating 2026-07-28+ get the standard INVALID_PARAMS code for
137151
// resource-not-found; older peers keep RESOURCE_NOT_FOUND. ISO `YYYY-MM-DD` versions
138152
// compare lexically the same as chronologically.
@@ -229,7 +243,7 @@ macro_rules! server_handler_methods {
229243
&self,
230244
request: GetPromptRequestParams,
231245
context: RequestContext<RoleServer>,
232-
) -> impl Future<Output = Result<GetPromptResult, McpError>> + MaybeSendFuture + '_ {
246+
) -> impl Future<Output = Result<GetPromptResponse, McpError>> + MaybeSendFuture + '_ {
233247
std::future::ready(Err(McpError::method_not_found::<GetPromptRequestMethod>()))
234248
}
235249
fn list_prompts(
@@ -259,7 +273,7 @@ macro_rules! server_handler_methods {
259273
&self,
260274
request: ReadResourceRequestParams,
261275
context: RequestContext<RoleServer>,
262-
) -> impl Future<Output = Result<ReadResourceResult, McpError>> + MaybeSendFuture + '_ {
276+
) -> impl Future<Output = Result<ReadResourceResponse, McpError>> + MaybeSendFuture + '_ {
263277
std::future::ready(Err(
264278
McpError::method_not_found::<ReadResourceRequestMethod>(),
265279
))
@@ -312,7 +326,7 @@ macro_rules! server_handler_methods {
312326
&self,
313327
request: CallToolRequestParams,
314328
context: RequestContext<RoleServer>,
315-
) -> impl Future<Output = Result<CallToolResult, McpError>> + MaybeSendFuture + '_ {
329+
) -> impl Future<Output = Result<CallToolResponse, McpError>> + MaybeSendFuture + '_ {
316330
std::future::ready(Err(McpError::method_not_found::<CallToolRequestMethod>()))
317331
}
318332
fn list_tools(
@@ -485,7 +499,7 @@ macro_rules! impl_server_handler_for_wrapper {
485499
&self,
486500
request: GetPromptRequestParams,
487501
context: RequestContext<RoleServer>,
488-
) -> impl Future<Output = Result<GetPromptResult, McpError>> + MaybeSendFuture + '_ {
502+
) -> impl Future<Output = Result<GetPromptResponse, McpError>> + MaybeSendFuture + '_ {
489503
(**self).get_prompt(request, context)
490504
}
491505

@@ -518,7 +532,7 @@ macro_rules! impl_server_handler_for_wrapper {
518532
&self,
519533
request: ReadResourceRequestParams,
520534
context: RequestContext<RoleServer>,
521-
) -> impl Future<Output = Result<ReadResourceResult, McpError>> + MaybeSendFuture + '_ {
535+
) -> impl Future<Output = Result<ReadResourceResponse, McpError>> + MaybeSendFuture + '_ {
522536
(**self).read_resource(request, context)
523537
}
524538

@@ -542,7 +556,7 @@ macro_rules! impl_server_handler_for_wrapper {
542556
&self,
543557
request: CallToolRequestParams,
544558
context: RequestContext<RoleServer>,
545-
) -> impl Future<Output = Result<CallToolResult, McpError>> + MaybeSendFuture + '_ {
559+
) -> impl Future<Output = Result<CallToolResponse, McpError>> + MaybeSendFuture + '_ {
546560
(**self).call_tool(request, context)
547561
}
548562

0 commit comments

Comments
 (0)