Class: MCPClient::CachedResult
- Inherits:
-
Object
- Object
- MCPClient::CachedResult
- Defined in:
- lib/mcp_client/cached_result.rb
Overview
A cached server result with its freshness hint (MCP 2026-07-28
server/utilities/caching): ttlMs says how long the client MAY consider
the result fresh after receipt (0 = immediately stale; absent = no hint,
only older servers), and cacheScope whether the response may be shared
across authorization contexts ("public") or not ("private").
Constant Summary collapse
- SCOPES =
%w[public private].freeze
- MIXED_CONTEXT =
The context of a private list whose pages were fetched under different credentials: it belongs to no context and never matches one.
Object.new.freeze
- UNKNOWN_CONTEXT =
The context of an entry whose request nothing could record: an unknown request is not an anonymous one, so this belongs to no context either and is never served across one.
Object.new.freeze
- MIXED_PARAMS =
The params fingerprint of a list whose pages were fetched under differing effective parameters: no request's parameters match it.
Object.new.freeze
Instance Attribute Summary collapse
-
#authorization_context ⇒ String?
The authorization context (the Authorization header) of the request that produced a privately scoped entry; such an entry is served only in that context ("MUST NOT be shared across authorization contexts").
-
#cache_scope ⇒ String?
readonly
"public", "private", or nil when absent/unknown.
-
#fetch_token ⇒ Object?
Identity of the fetch that recorded this entry: the list that fetch converts afterwards attaches to this entry and to no other.
-
#params_fingerprint ⇒ String?
Fingerprint of the effective request parameters (host
_meta) the request that produced this entry went out with; the entry is served only to requests that would carry the same. -
#received_at ⇒ Float
readonly
Monotonic receipt time (seconds).
-
#ttl_ms ⇒ Integer, ...
readonly
The server's ttlMs (nil when it sent none).
-
#value ⇒ Object
The cached value (whatever the caller stored).
Class Method Summary collapse
-
.combine(entries, value, now:) ⇒ CachedResult
Combine the hints of several pages of one list into one entry: the shortest TTL wins, and one private page makes the whole list private.
-
.from_result(result, value, now:, assume_zero: false) ⇒ CachedResult
Build an entry from a CacheableResult.
-
.normalize_ttl(raw) ⇒ Integer, Float
"Servers MUST provide a ttlMs value that is >= 0"; anything else is treated as 0 (immediately stale).
-
.stale(now:, like: nil) ⇒ CachedResult
An entry that is stale from the start: what a change notification leaves behind so the kind reads as "known and stale", not "unknown".
Instance Method Summary collapse
-
#fresh?(now:) ⇒ Boolean
Fresh while now < t_received + ttlMs.
-
#hint? ⇒ Boolean
Whether the server gave a ttlMs at all.
-
#initialize(value:, received_at:, ttl_ms:, cache_scope:) ⇒ CachedResult
constructor
A new instance of CachedResult.
-
#to_info(now:) ⇒ Hash
Ttl_ms, cache_scope, received_at, fresh.
Constructor Details
#initialize(value:, received_at:, ttl_ms:, cache_scope:) ⇒ CachedResult
Returns a new instance of CachedResult.
113 114 115 116 117 118 119 120 |
# File 'lib/mcp_client/cached_result.rb', line 113 def initialize(value:, received_at:, ttl_ms:, cache_scope:) @value = value @received_at = received_at @ttl_ms = ttl_ms # Kept frozen: the scope is compared against the exact sentinels and # is handed out through #to_info. @cache_scope = cache_scope.is_a?(String) ? -cache_scope : cache_scope end |
Instance Attribute Details
#authorization_context ⇒ String?
The authorization context (the Authorization header) of the request that produced a privately scoped entry; such an entry is served only in that context ("MUST NOT be shared across authorization contexts").
36 37 38 |
# File 'lib/mcp_client/cached_result.rb', line 36 def @authorization_context end |
#cache_scope ⇒ String? (readonly)
Returns "public", "private", or nil when absent/unknown.
30 31 32 |
# File 'lib/mcp_client/cached_result.rb', line 30 def cache_scope @cache_scope end |
#fetch_token ⇒ Object?
Identity of the fetch that recorded this entry: the list that fetch converts afterwards attaches to this entry and to no other.
41 42 43 |
# File 'lib/mcp_client/cached_result.rb', line 41 def fetch_token @fetch_token end |
#params_fingerprint ⇒ String?
Fingerprint of the effective request parameters (host _meta) the
request that produced this entry went out with; the entry is served
only to requests that would carry the same.
47 48 49 |
# File 'lib/mcp_client/cached_result.rb', line 47 def params_fingerprint @params_fingerprint end |
#received_at ⇒ Float (readonly)
Returns monotonic receipt time (seconds).
26 27 28 |
# File 'lib/mcp_client/cached_result.rb', line 26 def received_at @received_at end |
#ttl_ms ⇒ Integer, ... (readonly)
Returns the server's ttlMs (nil when it sent none).
28 29 30 |
# File 'lib/mcp_client/cached_result.rb', line 28 def ttl_ms @ttl_ms end |
#value ⇒ Object
Returns the cached value (whatever the caller stored).
13 14 15 |
# File 'lib/mcp_client/cached_result.rb', line 13 def value @value end |
Class Method Details
.combine(entries, value, now:) ⇒ CachedResult
Combine the hints of several pages of one list into one entry: the shortest TTL wins, and one private page makes the whole list private.
75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/mcp_client/cached_result.rb', line 75 def self.combine(entries, value, now:) hinted = entries.select(&:hint?) # Each page expires at its own received_at + ttlMs; the combined entry # (received now) lives until the earliest of those. ttl = hinted.map { |e| [e.ttl_ms - ((now - e.received_at) * 1000.0), 0].max }.min scope = if entries.any? { |e| e.cache_scope == 'private' } 'private' else entries.map(&:cache_scope).compact.first end combined = new(value: value, received_at: now, ttl_ms: ttl, cache_scope: scope) combined.params_fingerprint = entries.first&.params_fingerprint combined end |
.from_result(result, value, now:, assume_zero: false) ⇒ CachedResult
Build an entry from a CacheableResult.
56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/mcp_client/cached_result.rb', line 56 def self.from_result(result, value, now:, assume_zero: false) ttl = if result.is_a?(Hash) && result.key?('ttlMs') normalize_ttl(result['ttlMs']) elsif assume_zero 0 end # Cross-context reuse needs an explicit "public": an absent or unknown # cacheScope keeps the entry within the authorization context that # produced it. scope = result.is_a?(Hash) ? result['cacheScope'] : nil new(value: value, received_at: now, ttl_ms: ttl, cache_scope: SCOPES.include?(scope) ? scope : 'private') end |
.normalize_ttl(raw) ⇒ Integer, Float
"Servers MUST provide a ttlMs value that is >= 0"; anything else is treated as 0 (immediately stale).
107 108 109 110 111 |
# File 'lib/mcp_client/cached_result.rb', line 107 def self.normalize_ttl(raw) return 0 unless raw.is_a?(Numeric) && raw.finite? [raw, 0].max end |
.stale(now:, like: nil) ⇒ CachedResult
An entry that is stale from the start: what a change notification leaves behind so the kind reads as "known and stale", not "unknown".
96 97 98 99 100 101 |
# File 'lib/mcp_client/cached_result.rb', line 96 def self.stale(now:, like: nil) entry = new(value: nil, received_at: now, ttl_ms: 0, cache_scope: like&.cache_scope) entry. = like&. entry.params_fingerprint = like&.params_fingerprint entry end |
Instance Method Details
#fresh?(now:) ⇒ Boolean
Fresh while now < t_received + ttlMs. Without a hint the client keeps its own heuristic (cache until a change notification), so the entry counts as fresh.
132 133 134 135 136 |
# File 'lib/mcp_client/cached_result.rb', line 132 def fresh?(now:) return true unless hint? now < @received_at + (@ttl_ms / 1000.0) end |
#hint? ⇒ Boolean
Returns whether the server gave a ttlMs at all.
123 124 125 |
# File 'lib/mcp_client/cached_result.rb', line 123 def hint? !@ttl_ms.nil? end |
#to_info(now:) ⇒ Hash
Returns ttl_ms, cache_scope, received_at, fresh.
140 141 142 143 |
# File 'lib/mcp_client/cached_result.rb', line 140 def to_info(now:) # Detached values: nothing a caller does to them reaches the entry. { ttl_ms: @ttl_ms, cache_scope: @cache_scope&.dup, received_at: @received_at, fresh: fresh?(now: now) } end |