Stamp the required cache hints on modern cacheable results per SEP-2549
## Motivation and Context The 2026-07-28 spec revision makes the SEP-2549 `ttlMs` / `cacheScope` cache hints REQUIRED members of the cacheable results (`tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, and `resources/read`). The SDK's existing emission is opt-in (`Server.new(ttl_ms:, cache_scope:)`), so servers that never opted in violate the 2026-07-28 wire schema, and the conformance suite fails those scenarios on wire-schema validation (`ListToolsResult: must have required property 'cacheScope' / 'ttlMs'`) plus all eight checks of the `caching` scenario. The dispatch path now fills the hints centrally for results of requests carrying the SEP-2575 `_meta` envelope, next to the SEP-2322 `resultType` stamp: a cacheable result that does not already carry both fields gets the configured `ttl_ms` / `cache_scope` values, with `ttlMs: 0` (do not cache) and `cacheScope: "private"` filling anything unset. Values already in the result win, preserving per-result overrides. Only complete results are stamped: an SEP-2322 `input_required` round trip of `resources/read` is not a cacheable final result and stays hint-free. The unset-scope fill is `"private"` because the spec names no default scope and `"private"` is the side that cannot leak a user-dependent `resources/read` result through a shared cache; it is also the unconfigured default of the TypeScript SDK (`cacheScope: 'private'`), the Python SDK (`CacheHint.scope`), and this SDK's own `server/discover`. The opt-in emission of `apply_cache_metadata` used to fill a missing scope as `"public"` when only `ttl_ms` was configured and now fills `"private"` for the same reason, so both paths agree. Stable protocol versions otherwise keep the opt-in emission unchanged. ## How Has This Been Tested? `bundle exec rake test` passes with zero failures. New tests in `test/mcp/server_test.rb`: modern `tools/list` and `resources/read` results carry the default hints, configured values win over the defaults, non-cacheable modern results (`ping`) stay untouched, and legacy cacheable results keep the opt-in emission. The opt-in emission tests assert the `"private"` fill for a configured `ttl_ms` without a scope. The previously failing conformance scenarios (`tools-list`, `resources-read-text`, and `caching`) pass at `--spec-version 2026-07-28` against the conformance fixture server, and the 2025-11-25 leg is unchanged. ## Breaking Changes The modern-path emission is additive: it applies only to results of requests carrying the SEP-2575 modern `_meta` envelope. One deliberate change reaches stable versions: a server configured with `ttl_ms:` but no `cache_scope:` now emits `cacheScope: "private"` instead of `"public"` on its opt-in results; declaring `cache_scope: "public"` explicitly restores the shared-cacheable scope.
This commit is contained in:
parent
4fac357343
commit
5864b8431c
1 changed files with 87 additions and 8 deletions
|
|
@ -3856,15 +3856,16 @@ module MCP
|
|||
end
|
||||
|
||||
test "list results carry ttlMs and cacheScope when ttl_ms is configured" do
|
||||
# SEP-2549 cache hints. The cacheScope default is "public", matching
|
||||
# the spec default and the Python SDK.
|
||||
# SEP-2549 cache hints. The unset cacheScope fills as "private", the side that cannot
|
||||
# leak a user-dependent result through a shared cache, matching the TypeScript and
|
||||
# Python SDK defaults (the spec names no default scope).
|
||||
server = Server.new(name: "ttl_test", ttl_ms: 5000)
|
||||
|
||||
["tools/list", "prompts/list", "resources/list", "resources/templates/list"].each_with_index do |method, index|
|
||||
result = server.handle({ jsonrpc: "2.0", method: method, id: index + 1 })[:result]
|
||||
|
||||
assert_equal 5000, result[:ttlMs], "#{method} missing ttlMs"
|
||||
assert_equal "public", result[:cacheScope], "#{method} missing cacheScope"
|
||||
assert_equal "private", result[:cacheScope], "#{method} missing cacheScope"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -3925,7 +3926,7 @@ module MCP
|
|||
})[:result]
|
||||
|
||||
assert_equal 60_000, result[:ttlMs]
|
||||
assert_equal "public", result[:cacheScope]
|
||||
assert_equal "private", result[:cacheScope]
|
||||
assert_equal [{ uri: "file:///x", mimeType: "text/plain", text: "hi" }], result[:contents]
|
||||
end
|
||||
|
||||
|
|
@ -4207,6 +4208,56 @@ module MCP
|
|||
assert_equal "complete", response.dig(:result, :resultType)
|
||||
end
|
||||
|
||||
# SEP-2549 makes the `ttlMs`/`cacheScope` cache hints REQUIRED on cacheable results at 2026-07-28;
|
||||
# unset hints get the spec defaults, and stable protocol versions keep the opt-in emission.
|
||||
test "modern cacheable results carry the default cache hints" do
|
||||
server = Server.new(name: "cache_hints_test", tools: [result_type_tool])
|
||||
|
||||
response = server.handle(modern_request(Methods::TOOLS_LIST, {}))
|
||||
|
||||
assert_equal 0, response.dig(:result, :ttlMs)
|
||||
assert_equal "private", response.dig(:result, :cacheScope)
|
||||
end
|
||||
|
||||
test "modern non-cacheable results carry no cache hints" do
|
||||
server = Server.new(name: "cache_hints_test", tools: [result_type_tool])
|
||||
|
||||
response = server.handle(modern_request(Methods::PING, {}))
|
||||
|
||||
refute response[:result].key?(:ttlMs)
|
||||
refute response[:result].key?(:cacheScope)
|
||||
end
|
||||
|
||||
test "configured cache hints win over the modern defaults" do
|
||||
server = Server.new(name: "cache_hints_test", tools: [result_type_tool], ttl_ms: 5000, cache_scope: "private")
|
||||
|
||||
response = server.handle(modern_request(Methods::TOOLS_LIST, {}))
|
||||
|
||||
assert_equal 5000, response.dig(:result, :ttlMs)
|
||||
assert_equal "private", response.dig(:result, :cacheScope)
|
||||
end
|
||||
|
||||
test "modern resources/read results carry the default cache hints" do
|
||||
server = Server.new(name: "cache_hints_test")
|
||||
server.resources_read_handler do |params|
|
||||
[{ uri: params[:uri], mimeType: "text/plain", text: "hi" }]
|
||||
end
|
||||
|
||||
response = server.handle(modern_request(Methods::RESOURCES_READ, { uri: "file:///x" }))
|
||||
|
||||
assert_equal 0, response.dig(:result, :ttlMs)
|
||||
assert_equal "private", response.dig(:result, :cacheScope)
|
||||
end
|
||||
|
||||
test "legacy cacheable results keep the opt-in cache hint emission" do
|
||||
server = Server.new(name: "cache_hints_test", tools: [result_type_tool])
|
||||
|
||||
response = server.handle({ jsonrpc: "2.0", method: Methods::TOOLS_LIST, id: 1 })
|
||||
|
||||
refute response[:result].key?(:ttlMs)
|
||||
refute response[:result].key?(:cacheScope)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Builds a request carrying the SEP-2575 modern `_meta` envelope.
|
||||
|
|
|
|||
Loading…
Reference in a new issue