Skip to content

Commit e5ff7e6

Browse files
committed
Merge remote-tracking branch 'upstream/main'
# Conflicts: # examples/servers/Cargo.toml
2 parents 0186de2 + 5d00e20 commit e5ff7e6

10 files changed

Lines changed: 645 additions & 26 deletions

File tree

conformance/src/bin/server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ impl ServerHandler for ConformanceServer {
615615
} else {
616616
Err(ErrorData::resource_not_found(
617617
format!("Resource not found: {}", uri),
618-
None,
618+
Some(json!({ "uri": uri })),
619619
))
620620
}
621621
}

crates/rmcp/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,11 @@ name = "test_progress_subscriber"
255255
required-features = ["server", "client", "macros"]
256256
path = "tests/test_progress_subscriber.rs"
257257

258+
[[test]]
259+
name = "test_request_timeout_progress"
260+
required-features = ["server", "client", "macros"]
261+
path = "tests/test_request_timeout_progress.rs"
262+
258263
[[test]]
259264
name = "test_elicitation"
260265
required-features = ["elicitation", "client", "server"]

crates/rmcp/src/handler/server.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ impl<H: ServerHandler> Service<RoleServer> for H {
2222
request: <RoleServer as ServiceRole>::PeerReq,
2323
context: RequestContext<RoleServer>,
2424
) -> Result<<RoleServer as ServiceRole>::Resp, McpError> {
25-
match request {
25+
// `context` is moved into the dispatch below, so read the negotiated version first.
26+
let protocol_version = context.protocol_version();
27+
let result = match request {
2628
ClientRequest::InitializeRequest(request) => self
2729
.initialize(request.params, context)
2830
.await
@@ -127,7 +129,18 @@ impl<H: ServerHandler> Service<RoleServer> for H {
127129
.cancel_task(request.params, context)
128130
.await
129131
.map(ServerResult::CancelTaskResult),
130-
}
132+
};
133+
// SEP-2164: peers negotiating 2026-07-28+ get the standard INVALID_PARAMS code for
134+
// resource-not-found; older peers keep RESOURCE_NOT_FOUND. ISO `YYYY-MM-DD` versions
135+
// compare lexically the same as chronologically.
136+
let use_invalid_params =
137+
protocol_version.is_some_and(|v| v.as_str() >= ProtocolVersion::V_2026_07_28.as_str());
138+
result.map_err(|mut error| {
139+
if use_invalid_params && error.code == ErrorCode::RESOURCE_NOT_FOUND {
140+
error.code = ErrorCode::INVALID_PARAMS;
141+
}
142+
error
143+
})
131144
}
132145

133146
async fn handle_notification(
@@ -256,6 +269,34 @@ macro_rules! server_handler_methods {
256269
McpError::method_not_found::<UnsubscribeRequestMethod>(),
257270
))
258271
}
272+
/// Handle a `tools/call` request from a client.
273+
///
274+
/// # Choosing a return value
275+
///
276+
/// MCP distinguishes two failure modes; the API forces you to pick
277+
/// the right one explicitly because they reach the caller's UI very
278+
/// differently:
279+
///
280+
/// - `Ok(`[`CallToolResult::error`]`(...))` — the tool ran (or tried
281+
/// to) and produced a failure the caller should see. The
282+
/// `content` you supply is rendered in the caller's MCP client,
283+
/// so the user gets your message. **This is the right return
284+
/// value for almost every "the tool didn't work" path** — empty
285+
/// results, validation failures the user can fix, downstream
286+
/// service unavailability, etc.
287+
///
288+
/// - `Err(`[`McpError`]`)` — a JSON-RPC protocol error. Use this
289+
/// only when the request itself is unroutable: unknown tool
290+
/// ([`ErrorCode::METHOD_NOT_FOUND`]), malformed request shape that
291+
/// cannot be treated as a valid `tools/call`, or a server-internal
292+
/// failure that means the server cannot serve any request right now
293+
/// ([`ErrorCode::INTERNAL_ERROR`], `-32603`). MCP clients
294+
/// typically render protocol errors opaquely; **the caller will
295+
/// not see your message** — they see something like "Tool result
296+
/// missing due to internal error". If you want the caller to read
297+
/// your error, use `Ok(CallToolResult::error(...))`.
298+
///
299+
/// See [`CallToolResult::error`] for a worked example.
259300
fn call_tool(
260301
&self,
261302
request: CallToolRequestParams,

crates/rmcp/src/model.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ impl std::fmt::Display for ProtocolVersion {
152152
}
153153

154154
impl ProtocolVersion {
155+
pub const V_2026_07_28: Self = Self(Cow::Borrowed("2026-07-28"));
155156
pub const V_2025_11_25: Self = Self(Cow::Borrowed("2025-11-25"));
156157
pub const V_2025_06_18: Self = Self(Cow::Borrowed("2025-06-18"));
157158
pub const V_2025_03_26: Self = Self(Cow::Borrowed("2025-03-26"));
@@ -164,6 +165,7 @@ impl ProtocolVersion {
164165
Self::V_2025_03_26,
165166
Self::V_2025_06_18,
166167
Self::V_2025_11_25,
168+
Self::V_2026_07_28,
167169
];
168170

169171
/// Returns the string representation of this protocol version.
@@ -193,6 +195,7 @@ impl<'de> Deserialize<'de> for ProtocolVersion {
193195
"2025-03-26" => return Ok(ProtocolVersion::V_2025_03_26),
194196
"2025-06-18" => return Ok(ProtocolVersion::V_2025_06_18),
195197
"2025-11-25" => return Ok(ProtocolVersion::V_2025_11_25),
198+
"2026-07-28" => return Ok(ProtocolVersion::V_2026_07_28),
196199
_ => {}
197200
}
198201
Ok(ProtocolVersion(Cow::Owned(s)))
@@ -541,9 +544,12 @@ impl ErrorData {
541544
data,
542545
}
543546
}
547+
/// Resource-not-found error (`-32002`). The server upgrades this to `INVALID_PARAMS`
548+
/// (`-32602`) for peers negotiating protocol `2026-07-28` or newer (SEP-2164).
544549
pub fn resource_not_found(message: impl Into<Cow<'static, str>>, data: Option<Value>) -> Self {
545550
Self::new(ErrorCode::RESOURCE_NOT_FOUND, message, data)
546551
}
552+
547553
pub fn parse_error(message: impl Into<Cow<'static, str>>, data: Option<Value>) -> Self {
548554
Self::new(ErrorCode::PARSE_ERROR, message, data)
549555
}
@@ -2838,7 +2844,55 @@ impl CallToolResult {
28382844
meta: None,
28392845
}
28402846
}
2841-
/// Create an error tool result with unstructured content
2847+
2848+
/// Create a tool-level error result with caller-visible content.
2849+
///
2850+
/// # When to use this vs `Err(ErrorData)`
2851+
///
2852+
/// MCP distinguishes two failure modes for a `call_tool` invocation, and
2853+
/// the right one to use depends on **whose problem it is**:
2854+
///
2855+
/// - **Tool-level error** — `Ok(CallToolResult::error(...))`.
2856+
/// The request was valid and routed to your tool, but executing the
2857+
/// tool failed in a way the caller should see (a query returned no
2858+
/// rows, an external API returned 500, the user's input is plausible
2859+
/// but produced no result, etc.). The caller's MCP client renders the
2860+
/// `content` you provide; your message reaches the user. **This is the
2861+
/// right choice for almost every "the tool ran and didn't work" case.**
2862+
///
2863+
/// - **Protocol error** — `Err(ErrorData)` with a JSON-RPC code.
2864+
/// The server cannot route the request at all, or an infrastructure
2865+
/// error makes the server itself unusable
2866+
/// ([`ErrorCode::INTERNAL_ERROR`], `-32603`). MCP clients typically
2867+
/// render protocol errors opaquely (e.g. "Tool result missing due to
2868+
/// internal error") — the caller does **not** see your message.
2869+
///
2870+
/// # Example
2871+
///
2872+
/// ```rust,ignore
2873+
/// use rmcp::model::{CallToolResult, Content, ErrorData};
2874+
///
2875+
/// async fn lookup(query: &str) -> Result<CallToolResult, ErrorData> {
2876+
/// // Caller passed a malformed query — the server can't run anything.
2877+
/// // This is a protocol error, the caller's client will render it
2878+
/// // as -32602 invalid_params:
2879+
/// if query.is_empty() {
2880+
/// return Err(ErrorData::invalid_params("query must be non-empty", None));
2881+
/// }
2882+
///
2883+
/// // Tool ran, no result. Caller should see the explanation:
2884+
/// let rows = run_query(query).await;
2885+
/// if rows.is_empty() {
2886+
/// return Ok(CallToolResult::error(vec![Content::text(
2887+
/// format!("no rows matched '{query}'"),
2888+
/// )]));
2889+
/// }
2890+
///
2891+
/// Ok(CallToolResult::success(vec![Content::text(format_rows(&rows))]))
2892+
/// }
2893+
/// # async fn run_query(_: &str) -> Vec<&'static str> { vec![] }
2894+
/// # fn format_rows(_: &[&str]) -> String { String::new() }
2895+
/// ```
28422896
pub fn error(content: Vec<Content>) -> Self {
28432897
CallToolResult {
28442898
content,

0 commit comments

Comments
 (0)