Skip to content

fix(rmcp): tolerate empty cacheScope instead of silently dropping the whole result - #1281

Open
WAHIB-EL-KHADIRI wants to merge 2 commits into
modelcontextprotocol:mainfrom
WAHIB-EL-KHADIRI:fix/empty-cache-scope-untagged-fallthrough
Open

WAHIB-EL-KHADIRI wants to merge 2 commits into
modelcontextprotocol:mainfrom
WAHIB-EL-KHADIRI:fix/empty-cache-scope-untagged-fallthrough

Conversation

@WAHIB-EL-KHADIRI

Copy link
Copy Markdown

ListToolsResult/ReadResourceResult.cache_scope: Option<CacheScope> has no
custom deserializer, unlike the sibling ttl_ms field, which already
tolerates out-of-range input via deserialize_ttl_ms. A server sending
cacheScope: "" (SEP-2549 only permits "public"/"private"/absent) fails
deserialization of the whole result — and because ServerResult is
#[serde(untagged)], that failure doesn't surface as an error. It falls
through variant-by-variant to CustomResult (the catch-all), so callers
silently lose typed access to .tools / .contents instead of getting a
clear error or a usable result.

Fix mirrors the existing deserialize_ttl_ms normalize-don't-error pattern:
a new deserialize_cache_scope treats ""/null as absent, delegates
everything else to CacheScope's normal deserialization (so a genuinely
invalid value like "PUBLIC" still errors, it just no longer takes the
whole result down with it).

Test: added repro_empty_cache_scope_drops_every_tool_via_untagged_fallthrough
to test_cache_hints.rs, asserting both direct ListToolsResult
deserialization and the full ServerResult untagged path preserve the tool
list instead of degrading to CustomResult.

Test results: cargo test -p rmcp --test test_cache_hints → 6/6 pass (5
pre-existing + 1 new), no regressions.

Closes #1242.

… whole result

ListToolsResult/ReadResourceResult.cache_scope: Option<CacheScope> had no
custom deserializer, unlike the sibling ttl_ms field, which already
tolerates out-of-range input via deserialize_ttl_ms. A server sending
cacheScope: "" (SEP-2549 only permits "public"/"private"/absent) failed
deserialization of the whole result - and because ServerResult is
#[serde(untagged)], that failure doesn't surface as an error. It falls
through variant-by-variant to CustomResult (the catch-all), so callers
silently lose typed access to .tools/.contents instead of getting a clear
error or a usable result.

Add deserialize_cache_scope, mirroring the existing deserialize_ttl_ms
normalize-don't-error pattern: "" and null are treated as absent,
everything else delegates to CacheScope's normal deserialization, so a
genuinely invalid value (e.g. "PUBLIC") still errors - it just no longer
takes the whole result down with it.

Add a regression test covering both direct ListToolsResult deserialization
and the full ServerResult untagged path, asserting the tool list survives
instead of degrading to CustomResult.

Signed-off-by: Wahib El Khadiri <wahibelkhadiri06@gmail.com>
@WAHIB-EL-KHADIRI
WAHIB-EL-KHADIRI requested a review from a team as a code owner September 16, 2026 18:51
@github-actions github-actions Bot added T-test Testing related changes T-core Core library changes labels Sep 16, 2026
The Code Formatting job runs `cargo +nightly fmt --all -- --check`, and the
single-line `use rmcp::model::{...}` in this test exceeded the configured
width, failing the only red check on the PR. No behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WAHIB-EL-KHADIRI <wahibelkhadiri06@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-core Core library changes T-test Testing related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Empty cacheScope on tools/list drops every tool

2 participants