Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,18 +170,32 @@ jobs:

- name: Check rmcp (all features except local)
run: |
FEATURES=$(cargo metadata --no-deps --format-version 1 \
| jq -r '[.packages[] | select(.name == "rmcp") | .features | keys[]
| select(startswith("__") | not)
| select(. != "local")] | join(",")')
BASE_SHA=${{ github.event.pull_request.base.sha }}
# `cargo public-api diff` builds both revisions with the same feature
# set, so a feature that exists on only one side (e.g. a feature added
# or removed by this PR) would fail to build the other revision. Diff
# only the features present in BOTH the base and the head; features
# unique to one side are necessarily pure additions/removals, which the
# release-type deny flags already govern.
list_features() {
cargo metadata --no-deps --format-version 1 --manifest-path "$1/Cargo.toml" \
| jq -r '.packages[] | select(.name == "rmcp") | .features | keys[]
| select(startswith("__") | not)
| select(. != "local")'
}
list_features "." | sort -u > "$RUNNER_TEMP/head_features"
git worktree add --detach "$RUNNER_TEMP/rmcp-base" "$BASE_SHA"
list_features "$RUNNER_TEMP/rmcp-base" | sort -u > "$RUNNER_TEMP/base_features"
git worktree remove --force "$RUNNER_TEMP/rmcp-base"
FEATURES=$(comm -12 "$RUNNER_TEMP/head_features" "$RUNNER_TEMP/base_features" | paste -sd, -)
cargo public-api \
--package rmcp \
--features "$FEATURES" \
-ss \
diff \
$DENY \
--force \
${{ github.event.pull_request.base.sha }}..${{ github.sha }}
"$BASE_SHA"..${{ github.sha }}

spelling:
name: spell check with typos
Expand Down
21 changes: 12 additions & 9 deletions conformance/src/bin/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,9 @@ impl ServerHandler for ConformanceServer {
&self,
request: CallToolRequestParams,
cx: RequestContext<RoleServer>,
) -> Result<CallToolResult, ErrorData> {
) -> Result<CallToolResponse, ErrorData> {
let args = request.arguments.unwrap_or_default();
match request.name.as_ref() {
let result = match request.name.as_ref() {
"test_simple_text" => Ok(CallToolResult::success(vec![ContentBlock::text(
"This is a simple text response for testing.",
)])),
Expand Down Expand Up @@ -530,7 +530,8 @@ impl ServerHandler for ConformanceServer {
format!("Unknown tool: {}", request.name),
None,
)),
}
};
result.map(Into::into)
}

async fn list_resources(
Expand All @@ -555,9 +556,9 @@ impl ServerHandler for ConformanceServer {
&self,
request: ReadResourceRequestParams,
_cx: RequestContext<RoleServer>,
) -> Result<ReadResourceResult, ErrorData> {
) -> Result<ReadResourceResponse, ErrorData> {
let uri = request.uri.as_str();
match uri {
let result = match uri {
"test://static-text" => Ok(ReadResourceResult::new(vec![
ResourceContents::TextResourceContents {
uri: uri.into(),
Expand Down Expand Up @@ -598,7 +599,8 @@ impl ServerHandler for ConformanceServer {
))
}
}
}
};
result.map(Into::into)
}

async fn list_resource_templates(
Expand Down Expand Up @@ -679,8 +681,8 @@ impl ServerHandler for ConformanceServer {
&self,
request: GetPromptRequestParams,
_cx: RequestContext<RoleServer>,
) -> Result<GetPromptResult, ErrorData> {
match request.name.as_str() {
) -> Result<GetPromptResponse, ErrorData> {
let result = match request.name.as_str() {
"test_simple_prompt" => Ok(GetPromptResult::new(vec![PromptMessage::new_text(
Role::User,
"This is a simple test prompt.",
Expand Down Expand Up @@ -721,7 +723,8 @@ impl ServerHandler for ConformanceServer {
format!("Unknown prompt: {}", request.name),
None,
)),
}
};
result.map(Into::into)
}

async fn complete(
Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp-macros/src/prompt_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub fn prompt_handler(attr: TokenStream, input: TokenStream) -> syn::Result<Toke
&self,
request: rmcp::model::GetPromptRequestParams,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::GetPromptResult, rmcp::ErrorData> {
) -> Result<rmcp::model::GetPromptResponse, rmcp::ErrorData> {
let prompt_context = rmcp::handler::server::prompt::PromptContext::new(
self,
request.name,
Expand Down
11 changes: 10 additions & 1 deletion crates/rmcp-macros/src/task_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,16 @@ pub fn task_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenS

let task_result_id = task_id.clone();
let future = Box::pin(async move {
let result = server.call_tool(future_request, future_context).await;
let result = server
.call_tool(future_request, future_context)
.await
.and_then(|response| match response {
rmcp::model::CallToolResponse::Complete(result) => Ok(result),
_ => Err(rmcp::ErrorData::internal_error(
"input_required is not supported for task-based tool calls",
None,
)),
});
Ok(
Box::new(ToolCallTaskResult::new(task_result_id, result))
as Box<dyn OperationResultTransport>,
Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp-macros/src/tool_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pub fn tool_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenS
&self,
request: rmcp::model::CallToolRequestParams,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::CallToolResult, rmcp::ErrorData> {
) -> Result<rmcp::model::CallToolResponse, rmcp::ErrorData> {
let tcc = rmcp::handler::server::tool::ToolCallContext::new(self, request, context);
#router.call(tcc).await
}
Expand Down
13 changes: 13 additions & 0 deletions crates/rmcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ features = [
"client-side-sse",
"elicitation",
"macros",
"request-state",
"reqwest",
"reqwest-native-tls",
"reqwest-tls-no-provider",
Expand Down Expand Up @@ -64,6 +65,10 @@ schemars = { version = "1.0", optional = true, features = ["chrono04"] }
# for image encoding
base64 = { version = "0.22", optional = true }

# for SEP-2322 requestState integrity sealing (opt-in via the `request-state` feature)
hmac = { version = "0.12", optional = true }
sha2 = { version = "0.10", optional = true }

# for HTTP client
reqwest = { version = "0.13.2", default-features = false, features = [
"json",
Expand Down Expand Up @@ -120,6 +125,9 @@ server = ["transport-async-rw", "dep:schemars", "dep:pastey"]
macros = ["dep:rmcp-macros", "dep:pastey"]
elicitation = ["dep:url"]

# SEP-2322 requestState integrity helper (HMAC-SHA256 seal/open codec)
request-state = ["dep:hmac", "dep:sha2", "base64"]

# reqwest http client
__reqwest = ["dep:reqwest"]

Expand Down Expand Up @@ -309,6 +317,11 @@ name = "test_trace_context"
required-features = ["server", "client"]
path = "tests/test_trace_context.rs"

[[test]]
name = "test_mrtr_behavior"
required-features = ["server", "client"]
path = "tests/test_mrtr_behavior.rs"

[[test]]
name = "test_prompt_macros"
required-features = ["server", "client"]
Expand Down
32 changes: 23 additions & 9 deletions crates/rmcp/src/handler/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ impl<H: ServerHandler> Service<RoleServer> for H {
) -> Result<<RoleServer as ServiceRole>::Resp, McpError> {
// `context` is moved into the dispatch below, so read the negotiated version first.
let protocol_version = context.protocol_version();
let mrtr_supported = protocol_version
.as_ref()
.is_some_and(|v| v.as_str() >= ProtocolVersion::V_2026_07_28.as_str());
let result = match request {
ClientRequest::InitializeRequest(request) => self
.initialize(request.params, context)
Expand All @@ -46,7 +49,7 @@ impl<H: ServerHandler> Service<RoleServer> for H {
ClientRequest::GetPromptRequest(request) => self
.get_prompt(request.params, context)
.await
.map(ServerResult::GetPromptResult),
.map(ServerResult::from),
ClientRequest::ListPromptsRequest(request) => self
.list_prompts(request.params, context)
.await
Expand All @@ -62,7 +65,7 @@ impl<H: ServerHandler> Service<RoleServer> for H {
ClientRequest::ReadResourceRequest(request) => self
.read_resource(request.params, context)
.await
.map(ServerResult::ReadResourceResult),
.map(ServerResult::from),
ClientRequest::SubscribeRequest(request) => self
.subscribe(request.params, context)
.await
Expand Down Expand Up @@ -105,7 +108,7 @@ impl<H: ServerHandler> Service<RoleServer> for H {
} else {
self.call_tool(request.params, context)
.await
.map(ServerResult::CallToolResult)
.map(ServerResult::from)
}
}
ClientRequest::ListToolsRequest(request) => self
Expand Down Expand Up @@ -133,6 +136,17 @@ impl<H: ServerHandler> Service<RoleServer> for H {
.await
.map(ServerResult::CancelTaskResult),
};
let result = result.and_then(|result| {
if matches!(result, ServerResult::InputRequiredResult(_)) && !mrtr_supported {
Err(McpError::invalid_request(
"InputRequiredResult requires negotiated protocol version 2026-07-28 or newer",
None,
))
} else {
Ok(result)
}
});

// SEP-2164: peers negotiating 2026-07-28+ get the standard INVALID_PARAMS code for
// resource-not-found; older peers keep RESOURCE_NOT_FOUND. ISO `YYYY-MM-DD` versions
// compare lexically the same as chronologically.
Expand Down Expand Up @@ -229,7 +243,7 @@ macro_rules! server_handler_methods {
&self,
request: GetPromptRequestParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<GetPromptResult, McpError>> + MaybeSendFuture + '_ {
) -> impl Future<Output = Result<GetPromptResponse, McpError>> + MaybeSendFuture + '_ {
std::future::ready(Err(McpError::method_not_found::<GetPromptRequestMethod>()))
}
fn list_prompts(
Expand Down Expand Up @@ -259,7 +273,7 @@ macro_rules! server_handler_methods {
&self,
request: ReadResourceRequestParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<ReadResourceResult, McpError>> + MaybeSendFuture + '_ {
) -> impl Future<Output = Result<ReadResourceResponse, McpError>> + MaybeSendFuture + '_ {
std::future::ready(Err(
McpError::method_not_found::<ReadResourceRequestMethod>(),
))
Expand Down Expand Up @@ -312,7 +326,7 @@ macro_rules! server_handler_methods {
&self,
request: CallToolRequestParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<CallToolResult, McpError>> + MaybeSendFuture + '_ {
) -> impl Future<Output = Result<CallToolResponse, McpError>> + MaybeSendFuture + '_ {
std::future::ready(Err(McpError::method_not_found::<CallToolRequestMethod>()))
}
fn list_tools(
Expand Down Expand Up @@ -485,7 +499,7 @@ macro_rules! impl_server_handler_for_wrapper {
&self,
request: GetPromptRequestParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<GetPromptResult, McpError>> + MaybeSendFuture + '_ {
) -> impl Future<Output = Result<GetPromptResponse, McpError>> + MaybeSendFuture + '_ {
(**self).get_prompt(request, context)
}

Expand Down Expand Up @@ -518,7 +532,7 @@ macro_rules! impl_server_handler_for_wrapper {
&self,
request: ReadResourceRequestParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<ReadResourceResult, McpError>> + MaybeSendFuture + '_ {
) -> impl Future<Output = Result<ReadResourceResponse, McpError>> + MaybeSendFuture + '_ {
(**self).read_resource(request, context)
}

Expand All @@ -542,7 +556,7 @@ macro_rules! impl_server_handler_for_wrapper {
&self,
request: CallToolRequestParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<CallToolResult, McpError>> + MaybeSendFuture + '_ {
) -> impl Future<Output = Result<CallToolResponse, McpError>> + MaybeSendFuture + '_ {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on all these renames? Just wondering why the move from Result to Response

@DaleSeo DaleSeo Jul 1, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexhancock Not a rename. CallToolResult, GetPromptResult, ReadResourceResult are unchanged. CallToolResponse is a new outcome enum mirroring what the spec's draft schema reference already models. Each method's response result is a union InputRequiredResult | CallToolResult. Rust has no anonymous unions, so I introduced one named enum per method to express that union at the handler return type.

Existing handlers don't need to change because the IntoCallToolResult conversions still accept a plain CallToolResult, Json<T>, Content, Result<T, E>, etc., and there are From<CallToolResult> / From<InputRequiredResult> impls, so .into() covers both arms. Only the trait-level return type widened.

(**self).call_tool(request, context)
}

Expand Down
Loading