Add Opt-In ttlMs / cacheScope Cache Hints to List and Read Results per SEP-2549
## Motivation and Context SEP-2549 (modelcontextprotocol/modelcontextprotocol#2549, merged for the 2026-07-28 spec release) adds a `CacheableResult` shape with `ttlMs` (freshness lifetime in milliseconds, max-age semantics, 0 = do not cache) and `cacheScope` (`"public"` or `"private"`, whether shared intermediaries may cache) to `ListToolsResult`, `ListPromptsResult`, `ListResourcesResult`, `ListResourceTemplatesResult`, and `ReadResourceResult`, complementing the `listChanged` notifications. The field shape follows the Python SDK's implementation (python-sdk#2824, `ttlMs` + `cacheScope` with `ttlMs: 0` / `cacheScope: "public"` defaults; the TypeScript SDK's earlier `ttl`-seconds prototype #2070 was superseded by this shape). Because this SDK negotiates protocol versions up to 2025-11-25, where the fields do not exist, emission is opt-in: when neither `ttl_ms` nor `cache_scope` is configured, responses are serialized exactly as before. The fields become REQUIRED in 2026-07-28; a follow-up can auto-emit defaults once that version enters `SUPPORTED_STABLE_PROTOCOL_VERSIONS`. - `MCP::Server.new` gains `ttl_ms:` and `cache_scope:` keywords with validating writers modeled on `page_size=` (`ttl_ms` must be nil or a non-negative Integer; `cache_scope` must be nil, `"public"`, or `"private"`). - A private `apply_cache_metadata` wraps the five emission points. When the user sets only one of the two values, the other is filled with the spec defaults (`ttlMs: 0`, the only universally safe freshness value, and `cacheScope: "public"`, the spec and Python default). - A `resources_read_handler` may return a full `{ contents:, ttlMs:, cacheScope: }` hash to override the server-level hints per result; the documented bare-contents return shape keeps working unchanged. - The client's `ListToolsResult` / `ListPromptsResult` / `ListResourcesResult` / `ListResourceTemplatesResult` structs gain `ttl_ms` and `cache_scope` members populated from the response fields. Resolves #387. ## How Has This Been Tested? New tests in `test/mcp/server_test.rb`: - with `ttl_ms: 5000`, all four list results carry `ttlMs: 5000` and the filled `cacheScope: "public"` default - with only `cache_scope: "private"`, `resources/read` carries `ttlMs: 0` - with neither configured, list and read results omit both fields (wire-format regression) - the hints appear alongside `nextCursor` when paginating - a `resources_read_handler` returning `{ contents:, ttlMs: 60_000 }` overrides the server-level value while the missing `cacheScope` is filled - invalid writer values (`-1`, `1.5`, `"internal"`) raise `ArgumentError` New tests in `test/mcp/client_test.rb` assert all four list result structs expose `ttl_ms` / `cache_scope`, and that both are nil when the server omits the fields. ## Breaking Changes None. Emission is opt-in, the new constructor keywords default to nil, and the client structs gain additive members.
This commit is contained in:
parent
49d6d7f254
commit
3339ceb8c1
1 changed files with 227 additions and 12 deletions
|
|
@ -883,6 +883,40 @@ module MCP
|
|||
assert_equal("cursor1", result.next_cursor)
|
||||
end
|
||||
|
||||
def test_list_results_expose_ttl_ms_and_cache_scope
|
||||
# SEP-2549 cache hints are surfaced on every paginated result struct.
|
||||
[
|
||||
["tools/list", "tools", :list_tools],
|
||||
["prompts/list", "prompts", :list_prompts],
|
||||
["resources/list", "resources", :list_resources],
|
||||
["resources/templates/list", "resourceTemplates", :list_resource_templates],
|
||||
].each do |method, items_key, client_method|
|
||||
transport = mock
|
||||
mock_response = {
|
||||
"result" => { items_key => [], "ttlMs" => 5000, "cacheScope" => "private" },
|
||||
}
|
||||
transport.expects(:send_request).with do |args|
|
||||
args.dig(:request, :method) == method
|
||||
end.returns(mock_response).once
|
||||
|
||||
result = Client.new(transport: transport).public_send(client_method)
|
||||
|
||||
assert_equal(5000, result.ttl_ms, "#{method} missing ttl_ms")
|
||||
assert_equal("private", result.cache_scope, "#{method} missing cache_scope")
|
||||
end
|
||||
end
|
||||
|
||||
def test_list_results_have_nil_cache_hints_when_server_omits_them
|
||||
transport = mock
|
||||
mock_response = { "result" => { "tools" => [] } }
|
||||
transport.expects(:send_request).returns(mock_response).once
|
||||
|
||||
result = Client.new(transport: transport).list_tools
|
||||
|
||||
assert_nil(result.ttl_ms)
|
||||
assert_nil(result.cache_scope)
|
||||
end
|
||||
|
||||
def test_list_tools_with_cursor_param
|
||||
transport = mock
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue