Skip to content

Commit 3f249a5

Browse files
committed
Merge remote-tracking branch 'upstream/main'
Brings in upstream rmcp v1.7.0: - fix(rmcp): flatten Resource variant of PromptMessageContent (modelcontextprotocol#843) - fix: reply -32700 on stdio parse errors instead of closing (modelcontextprotocol#833) -- JsonRpcError.id is now Option<RequestId> per MCP spec - chore(rmcp): remove dependency on chrono default features (modelcontextprotocol#829) - fix: idle-timeout log level demoted to debug (modelcontextprotocol#824) - feat: task-based stdio examples (modelcontextprotocol#839) - chore(deps): askama 0.15 -> 0.16 (modelcontextprotocol#830) - ci: extend semver check to all features except local (modelcontextprotocol#832) Conflict resolution: - crates/rmcp/CHANGELOG.md: kept fork's bare-boolean Unreleased entry, inserted upstream's 1.7.0 release section beneath it - crates/rmcp/Cargo.toml: kept fork's chrono 0.4.44 over upstream's 0.4.38 pin, but adopted upstream's default-features = false + features = ["serde", "now"] from modelcontextprotocol#829 -- both intents preserved - crates/rmcp/src/service.rs: kept fork's METHOD_NOT_FOUND demotion to debug (ab4ccdb) and applied upstream's JsonRpcMessage::error signature change to Some(id) per modelcontextprotocol#833 Workspace bumped to 1.7.0 by upstream's release-plz commit; fork crates rmcp + rmcp-macros track that automatically via workspace = true. anthropic-ext, JsonAndArtifact wrapper, bare-bool schema normalisation, channel permission relay, and the rest of the fork-only surface are unchanged. cargo check + cargo test pass with the full anthropic-ext + server feature set; test_message_schema absorbs the JsonRpcError.id Option change cleanly.
2 parents 85b81da + 3529c36 commit 3f249a5

29 files changed

Lines changed: 926 additions & 134 deletions

.github/workflows/ci.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,18 @@ jobs:
8888
--only-explicit-features \
8989
--features default
9090
91+
- name: Check rmcp (all features except local)
92+
run: |
93+
FEATURES=$(cargo metadata --no-deps --format-version 1 \
94+
| jq -r '[.packages[] | select(.name == "rmcp") | .features | keys[]
95+
| select(startswith("__") | not)
96+
| select(. != "local")] | join(",")')
97+
cargo semver-checks \
98+
--package rmcp \
99+
--baseline-rev ${{ github.event.pull_request.base.sha }} \
100+
--only-explicit-features \
101+
--features "$FEATURES"
102+
91103
spelling:
92104
name: spell check with typos
93105
runs-on: ubuntu-latest
@@ -247,7 +259,7 @@ jobs:
247259
if [ -f "$dir/Cargo.toml" ]; then
248260
if [[ "$dir" != *"wasi"* ]]; then
249261
echo "Testing $dir"
250-
cargo test --manifest-path "$dir/Cargo.toml" --all-features
262+
cargo test --manifest-path "$dir/Cargo.toml" --all-features --all-targets
251263
fi
252264
fi
253265
done

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ default-members = ["crates/rmcp", "crates/rmcp-macros"]
44
resolver = "2"
55

66
[workspace.dependencies]
7-
rmcp = { version = "1.6.0", path = "./crates/rmcp" }
8-
rmcp-macros = { version = "1.6.0", path = "./crates/rmcp-macros" }
7+
rmcp = { version = "1.7.0", path = "./crates/rmcp" }
8+
rmcp-macros = { version = "1.7.0", path = "./crates/rmcp-macros" }
99

1010
[workspace.package]
1111
edition = "2024"
12-
version = "1.6.0"
12+
version = "1.7.0"
1313
authors = ["4t145 <u4t145@163.com>"]
1414
license = "Apache-2.0"
1515
repository = "https://github.com/modelcontextprotocol/rust-sdk/"

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ For the full MCP specification, see [modelcontextprotocol.io](https://modelconte
3131
- [Completions](#completions)
3232
- [Notifications](#notifications)
3333
- [Subscriptions](#subscriptions)
34+
- [Tasks](#tasks-long-running-tool-invocations)
3435
- [Examples](#examples)
3536
- [OAuth Support](#oauth-support)
3637
- [Related Resources](#related-resources)
@@ -954,6 +955,28 @@ impl ClientHandler for MyClient {
954955

955956
---
956957

958+
## Tasks (long-running tool invocations)
959+
960+
`rmcp` supports the [task-based tool invocation](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks)
961+
flow defined in SEP-1319. Annotate a tool with `execution(task_support = "required" | "optional")`
962+
and add `#[task_handler]` to your `ServerHandler` impl — `enqueue_task`, `tasks/list`, `tasks/get`,
963+
`tasks/result`, and `tasks/cancel` are generated for you on top of an `OperationProcessor`.
964+
965+
```rust, ignore
966+
#[tool(
967+
description = "Sum two numbers after a 2-second delay",
968+
execution(task_support = "required")
969+
)]
970+
async fn slow_sum(/* ... */) -> Result<CallToolResult, McpError> { /* ... */ }
971+
972+
#[tool_handler]
973+
#[task_handler]
974+
impl ServerHandler for TaskDemo {}
975+
```
976+
977+
See [`servers_task_stdio`](examples/servers/src/task_stdio.rs) and the matching
978+
[`clients_task_stdio`](examples/clients/src/task_stdio.rs) for a runnable end-to-end example.
979+
957980
## Examples
958981

959982
See [examples](examples/README.md).

crates/rmcp-macros/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.7.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-macros-v1.6.0...rmcp-macros-v1.7.0) - 2026-05-13
11+
12+
### Added
13+
14+
- add task-based stdio examples ([#839](https://github.com/modelcontextprotocol/rust-sdk/pull/839))
15+
1016
## [1.6.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-macros-v1.5.0...rmcp-macros-v1.6.0) - 2026-05-01
1117

1218
### Fixed

crates/rmcp/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- *(server)* Normalise bare boolean subschemas (`true` / `false`) in generated `inputSchema`, `outputSchema`, and `ElicitationSchema` to their object-form equivalents (`{}` / `{"not": {}}`) before serialisation. Triggered by `serde_json::Value` field expansions, `Vec<serde_json::Value>`, `BTreeMap<String, serde_json::Value>`, and `#[serde(deny_unknown_fields)]`. Claude Code's `LocalMcpServerManager` schema walker throws `TypeError: Cannot use 'in' operator to search for 'properties' in <bool>` on bare booleans, silently dropping the entire server's tool list ([anthropics/claude-code#50194](https://github.com/anthropics/claude-code/issues/50194), [#25081](https://github.com/anthropics/claude-code/issues/25081)). The fix is unconditional — boolean subschemas are spec-legal per JSON Schema 2020-12 §4.3.2, but real-world MCP clients can't always handle them, so emit object form universally.
1313

14+
## [1.7.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-v1.6.0...rmcp-v1.7.0) - 2026-05-13
15+
16+
### Added
17+
18+
- add task-based stdio examples ([#839](https://github.com/modelcontextprotocol/rust-sdk/pull/839))
19+
20+
### Fixed
21+
22+
- *(rmcp)* flatten Resource variant of PromptMessageContent ([#843](https://github.com/modelcontextprotocol/rust-sdk/pull/843))
23+
- reply -32700 on stdio parse errors instead of closing ([#833](https://github.com/modelcontextprotocol/rust-sdk/pull/833))
24+
25+
### Other
26+
27+
- *(rmcp)* remove dependency on chrono default features ([#829](https://github.com/modelcontextprotocol/rust-sdk/pull/829))
28+
- Fix/issue 817 idle timeout log level ([#824](https://github.com/modelcontextprotocol/rust-sdk/pull/824))
29+
1430
## [1.6.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-v1.5.0...rmcp-v1.6.0) - 2026-05-01
1531

1632
### Added

crates/rmcp/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ hyper-util = { version = "0.1", features = ["tokio"], optional = true }
102102
# macro
103103
rmcp-macros = { workspace = true, optional = true }
104104
[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies]
105-
chrono = { version = "0.4.44", features = ["serde"] }
105+
chrono = { version = "0.4.44", default-features = false, features = ["serde", "now"] }
106106

107107
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
108108
chrono = { version = "0.4.44", default-features = false, features = [

crates/rmcp/src/model.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -461,13 +461,17 @@ pub struct JsonRpcResponse<R = JsonObject> {
461461
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
462462
pub struct JsonRpcError {
463463
pub jsonrpc: JsonRpcVersion2_0,
464-
pub id: RequestId,
464+
// MCP 2025-11-25 §Error Responses: `id` is optional and omitted when the
465+
// server cannot read the request id (e.g. parse error / invalid request).
466+
// https://modelcontextprotocol.io/specification/2025-11-25/basic#error-responses
467+
#[serde(default, skip_serializing_if = "Option::is_none")]
468+
pub id: Option<RequestId>,
465469
pub error: ErrorData,
466470
}
467471

468472
impl JsonRpcError {
469473
/// Create a new JsonRpcError.
470-
pub fn new(id: RequestId, error: ErrorData) -> Self {
474+
pub fn new(id: Option<RequestId>, error: ErrorData) -> Self {
471475
Self {
472476
jsonrpc: JsonRpcVersion2_0,
473477
id,
@@ -601,7 +605,7 @@ impl<Req, Resp, Not> JsonRpcMessage<Req, Resp, Not> {
601605
})
602606
}
603607
#[inline]
604-
pub const fn error(error: ErrorData, id: RequestId) -> Self {
608+
pub const fn error(error: ErrorData, id: Option<RequestId>) -> Self {
605609
JsonRpcMessage::Error(JsonRpcError {
606610
jsonrpc: JsonRpcVersion2_0,
607611
id,
@@ -633,15 +637,15 @@ impl<Req, Resp, Not> JsonRpcMessage<Req, Resp, Not> {
633637
_ => None,
634638
}
635639
}
636-
pub fn into_error(self) -> Option<(ErrorData, RequestId)> {
640+
pub fn into_error(self) -> Option<(ErrorData, Option<RequestId>)> {
637641
match self {
638642
JsonRpcMessage::Error(e) => Some((e.error, e.id)),
639643
_ => None,
640644
}
641645
}
642-
pub fn into_result(self) -> Option<(Result<Resp, ErrorData>, RequestId)> {
646+
pub fn into_result(self) -> Option<(Result<Resp, ErrorData>, Option<RequestId>)> {
643647
match self {
644-
JsonRpcMessage::Response(r) => Some((Ok(r.result), r.id)),
648+
JsonRpcMessage::Response(r) => Some((Ok(r.result), Some(r.id))),
645649
JsonRpcMessage::Error(e) => Some((Err(e.error), e.id)),
646650

647651
_ => None,

crates/rmcp/src/model/prompt.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,10 @@ pub enum PromptMessageContent {
158158
image: ImageContent,
159159
},
160160
/// Embedded server-side resource
161-
Resource { resource: EmbeddedResource },
161+
Resource {
162+
#[serde(flatten)]
163+
resource: EmbeddedResource,
164+
},
162165
/// A link to a resource that can be fetched separately
163166
ResourceLink {
164167
#[serde(flatten)]
@@ -321,6 +324,57 @@ mod tests {
321324
assert!(json.contains("\"name\":\"test.txt\""));
322325
}
323326

327+
#[test]
328+
fn test_prompt_message_resource_serialization_is_flat() {
329+
// Regression test: PromptMessageContent::Resource must serialize to
330+
// the spec-compliant flat shape `{ "type": "resource", "resource": { "uri", "mimeType", "text" } }`
331+
// and NOT the double-nested shape `{ "type": "resource", "resource": { "resource": {...} } }`.
332+
// See: https://modelcontextprotocol.io/specification/2025-06-18/server/prompts
333+
let message = PromptMessage::new_resource(
334+
PromptMessageRole::User,
335+
"alc://packages/sc/narrative".to_string(),
336+
Some("text/markdown".to_string()),
337+
Some("# Hello".to_string()),
338+
None,
339+
None,
340+
None,
341+
);
342+
343+
let value: serde_json::Value = serde_json::to_value(&message).unwrap();
344+
345+
// Drill into content
346+
let content = value.get("content").expect("content present");
347+
assert_eq!(
348+
content.get("type").and_then(|v| v.as_str()),
349+
Some("resource")
350+
);
351+
352+
let resource = content
353+
.get("resource")
354+
.expect("resource field present at content level");
355+
356+
// Spec-compliant: resource.uri / resource.mimeType / resource.text MUST be flat
357+
assert_eq!(
358+
resource.get("uri").and_then(|v| v.as_str()),
359+
Some("alc://packages/sc/narrative"),
360+
"expected flat resource.uri, got: {resource:#?}"
361+
);
362+
assert_eq!(
363+
resource.get("mimeType").and_then(|v| v.as_str()),
364+
Some("text/markdown")
365+
);
366+
assert_eq!(
367+
resource.get("text").and_then(|v| v.as_str()),
368+
Some("# Hello")
369+
);
370+
371+
// Regression guard: content.resource MUST NOT contain a nested `resource` key.
372+
assert!(
373+
resource.get("resource").is_none(),
374+
"double-nested resource detected (regression): {resource:#?}"
375+
);
376+
}
377+
324378
#[test]
325379
fn test_prompt_message_content_resource_link_deserialization() {
326380
let json = r#"{

crates/rmcp/src/service.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -881,7 +881,7 @@ where
881881
Event::ToSink(m) => {
882882
if let Some(id) = match &m {
883883
JsonRpcMessage::Response(response) => Some(&response.id),
884-
JsonRpcMessage::Error(error) => Some(&error.id),
884+
JsonRpcMessage::Error(error) => error.id.as_ref(),
885885
_ => None,
886886
} {
887887
if let Some(ct) = local_ct_pool.remove(id) {
@@ -977,7 +977,7 @@ where
977977
} else {
978978
tracing::warn!(%id, ?error, "response error");
979979
}
980-
JsonRpcMessage::error(error, id)
980+
JsonRpcMessage::error(error, Some(id))
981981
}
982982
};
983983
let _send_result = sink.send(response).await;
@@ -1034,6 +1034,12 @@ where
10341034
}
10351035
}
10361036
Event::PeerMessage(JsonRpcMessage::Error(JsonRpcError { error, id, .. })) => {
1037+
let Some(id) = id else {
1038+
// MCP error responses without an id (e.g. Parse error / Invalid Request)
1039+
// can't be routed back to a pending request — log and drop.
1040+
tracing::debug!(?error, "received id-less peer error");
1041+
continue;
1042+
};
10371043
if let Some(responder) = local_responder_pool.remove(&id) {
10381044
let _response_result = responder.send(Err(ServiceError::McpError(error)));
10391045
if let Err(_error) = _response_result {

crates/rmcp/src/service/server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ where
219219
}
220220
Err(e) => {
221221
transport
222-
.send(ServerJsonRpcMessage::error(e.clone(), id))
222+
.send(ServerJsonRpcMessage::error(e.clone(), Some(id)))
223223
.await
224224
.map_err(|error| {
225225
ServerInitializeError::transport::<T>(error, "sending error response")

0 commit comments

Comments
 (0)