Module: MCPClient::RequestMetadata

Included in:
JsonRpcCommon
Defined in:
lib/mcp_client/request_metadata.rb

Overview

The metadata a request carries and the fingerprint a cached result is bound to (MCP 2026-07-28 basic/index "_meta", server/utilities/caching): the reserved protocol keys, the effective parameters of the request this thread last built, and the evaluation of the host's request_meta that a cache decision holds for the request it leads to.

Each slot is named after the transport's object_id, so a worker thread that builds and discards transports keeps nothing of theirs for its life.

Defined Under Namespace

Classes: HeldRequestMeta

Constant Summary collapse

CACHE_NEUTRAL_META_KEYS =

_meta keys that identify one request rather than what it asks for (the progress token and the W3C trace identifiers): a cached result does not depend on them. baggage is deliberately not among them -- it carries application-defined context (a tenant, a locale), which a server may well vary its result by, so a result cached under one baggage is never served under another.

%w[progressToken traceparent tracestate].freeze
UNRECORDED_PARAMS =

Marks an attempt that has not built its request yet: the parameters of the previous request on this thread say nothing about it.

:unrecorded
OPAQUE_PARAMS =

Marks a request whose effective parameters the transport cannot read: host middleware may rewrite the body after the transport built it, so the parameters the server answers are not the ones a fingerprint of the request would describe. It is not a fingerprint and matches none, so no result is served across it whatever its cacheScope — "public" permits sharing across callers, not across result-affecting parameters.

:opaque
META_PROTOCOL_VERSION =

Reserved _meta keys (MCP 2026-07-28 basic/index "_meta").

'io.modelcontextprotocol/protocolVersion'
META_CLIENT_INFO =
'io.modelcontextprotocol/clientInfo'
META_CLIENT_CAPABILITIES =
'io.modelcontextprotocol/clientCapabilities'
META_LOG_LEVEL =
'io.modelcontextprotocol/logLevel'
META_SERVER_INFO =
'io.modelcontextprotocol/serverInfo'
META_SUBSCRIPTION_ID =
'io.modelcontextprotocol/subscriptionId'
PROTECTED_META_KEYS =

Per-request protocol fields the client owns. A host-supplied _meta may carry anything else (progressToken, trace context, vendor keys), but these are always set from the transport's own state so the body can never disagree with what the transport negotiated (on HTTP the MCP-Protocol-Version header must match the body).

[META_PROTOCOL_VERSION, META_CLIENT_INFO, META_CLIENT_CAPABILITIES].freeze

Instance Method Summary collapse

Instance Method Details

#adoptable_request_meta_hold(offered, innermost, method) ⇒ MCPClient::RequestMetadata::HeldRequestMeta?

Returns the offer when it really is the reservation open here, still waiting for the request it was made for.

Parameters:

Returns:



132
133
134
135
136
# File 'lib/mcp_client/request_metadata.rb', line 132

def adoptable_request_meta_hold(offered, innermost, method)
  return nil if offered.nil? || !offered.equal?(innermost)

  offered if offered.request_method == method && !offered.spent
end

#claimable_request_meta_hold ⇒ MCPClient::RequestMetadata::HeldRequestMeta?

Returns that reservation while the request it was made for may still claim it.

Returns:



201
202
203
204
# File 'lib/mcp_client/request_metadata.rb', line 201

def claimable_request_meta_hold
  held = held_request_meta
  held unless held.nil? || held.spent
end

#close_request_meta_hold ⇒ void

This method returns an undefined value.

Close the innermost hold scope.



183
184
185
186
187
188
189
190
# File 'lib/mcp_client/request_metadata.rb', line 183

def close_request_meta_hold
  stack = Thread.current[held_request_meta_key]
  return nil unless stack.is_a?(Array)

  stack.pop
  Thread.current[held_request_meta_key] = nil if stack.empty?
  nil
end

#current_params_fingerprint ⇒ String

Returns the fingerprint of the effective parameters the next request on this transport would carry. Reading it evaluates the host's request_meta, and the open operation holds that evaluation for the request the decision leads to instead of spending it on the decision alone. Outside any operation nothing is held at all: an evaluation that no request is waiting for is never kept.

Returns:

  • (String) —

    the fingerprint of the effective parameters the next request on this transport would carry. Reading it evaluates the host's request_meta, and the open operation holds that evaluation for the request the decision leads to instead of spending it on the decision alone. Outside any operation nothing is held at all: an evaluation that no request is waiting for is never kept.



212
213
214
# File 'lib/mcp_client/request_metadata.rb', line 212

def current_params_fingerprint
  params_fingerprint_of(with_request_meta({}, claim: :model))
end

#deep_sort_keys(value) ⇒ Object

Returns the value with every nested Hash sorted by key.

Parameters:

  • value (Object)

Returns:

  • (Object) —

    the value with every nested Hash sorted by key



264
265
266
267
268
269
270
# File 'lib/mcp_client/request_metadata.rb', line 264

def deep_sort_keys(value)
  case value
  when Hash then value.map { |k, v| [k.to_s, deep_sort_keys(v)] }.sort_by(&:first).to_h
  when Array then value.map { |v| deep_sort_keys(v) }
  else value
  end
end

#held_request_meta ⇒ MCPClient::RequestMetadata::HeldRequestMeta?

Returns the reservation of the innermost operation open on this thread.

Returns:



194
195
196
197
# File 'lib/mcp_client/request_metadata.rb', line 194

def held_request_meta
  stack = Thread.current[held_request_meta_key]
  stack.last if stack.is_a?(Array)
end

#held_request_meta_key ⇒ Symbol

Returns this transport's thread-local key for held metadata.

Returns:

  • (Symbol) —

    this transport's thread-local key for held metadata



232
233
234
# File 'lib/mcp_client/request_metadata.rb', line 232

def held_request_meta_key
  :"mcp_client_held_request_meta_#{object_id}"
end

#holding_request_meta(method) { ... } ⇒ Object

Reserve the evaluation of the host's request_meta for the request method this operation leads to, for the operation's dynamic extent and no longer: however it ends -- a value returned, a reconnect that raised, a caller that swallowed the error -- the reservation goes with it, so no later request on this thread can carry it.

Parameters:

  • method (String) —

    the JSON-RPC method of the request the operation sends

Yields:

  • the operation

Returns:

  • (Object) —

    the block's value



95
96
97
98
99
100
101
102
# File 'lib/mcp_client/request_metadata.rb', line 95

def holding_request_meta(method)
  open_request_meta_hold(method)
  begin
    yield
  ensure
    close_request_meta_hold
  end
end

#note_request_params(params) ⇒ void

This method returns an undefined value.

Remember the effective parameters a request goes out with, so a result cached from it is bound to them (MCP 2026-07-28 caching: a server may vary a result by host metadata such as a vendor tenant key).

Parameters:

  • params (Hash, nil) —

    the effective (wire) parameters



29
30
31
# File 'lib/mcp_client/request_metadata.rb', line 29

def note_request_params(params)
  Thread.current[request_params_key] = params_fingerprint_of(params)
end

#note_request_params_pending ⇒ void

This method returns an undefined value.



70
71
72
# File 'lib/mcp_client/request_metadata.rb', line 70

def note_request_params_pending
  Thread.current[request_params_key] = UNRECORDED_PARAMS
end

#offer_request_meta_hold(reservation) ⇒ void

This method returns an undefined value.

Hand a reservation to the one operation it was opened for: the next operation this transport opens adopts it, and only if it really is the reservation currently innermost here. The offer is taken up once; an opener that never invokes the operation withdraws it.



144
145
146
147
# File 'lib/mcp_client/request_metadata.rb', line 144

def offer_request_meta_hold(reservation)
  Thread.current[offered_request_meta_key] = reservation
  nil
end

#offered_request_meta_key ⇒ Symbol

Returns this transport's thread-local key for the reservation offered to the operation about to be opened on it.

Returns:

  • (Symbol) —

    this transport's thread-local key for the reservation offered to the operation about to be opened on it



238
239
240
# File 'lib/mcp_client/request_metadata.rb', line 238

def offered_request_meta_key
  :"mcp_client_offered_request_meta_#{object_id}"
end

#open_request_meta_hold(method) ⇒ MCPClient::RequestMetadata::HeldRequestMeta

Open a hold scope without a block, for a caller whose operation spans several transports (a client listing across its servers). Every opener closes it from an ensure.

The operation adopts a reservation only when that very reservation was handed to it (#offer_request_meta_hold) -- the opener naming the operation it made it for, immediately before invoking it. Sharing a method name is not enough: an operation that begins meanwhile (a list a notification listener runs on a server the opener's loop has not reached) would otherwise spend an evaluation weighed for somebody else, sending its tenant, baggage or nonce on the wrong request and leaving the right one to go out under an evaluation nothing weighed.

Parameters:

  • method (String) —

    the JSON-RPC method of the request the operation sends

Returns:



118
119
120
121
122
123
124
# File 'lib/mcp_client/request_metadata.rb', line 118

def open_request_meta_hold(method)
  stack = (Thread.current[held_request_meta_key] ||= [])
  reservation = adoptable_request_meta_hold(take_offered_request_meta_hold, stack.last, method) ||
                HeldRequestMeta.new(method, false, nil, false)
  stack.push(reservation)
  reservation
end

#outside_request_meta_hold { ... } ⇒ Object

The boundary a transport crosses when it hands control to host code: a notification listener, a handler for a server-initiated request. The reservation the open operation holds is out of reach behind it, so whatever that code issues -- a nested list, a raw rpc_request, a fetch_prompts_list, whatever method it names -- reads the host afresh and is an operation of its own.

Yields:

  • the host code

Returns:

  • (Object) —

    the block's value



171
172
173
174
175
176
177
178
179
# File 'lib/mcp_client/request_metadata.rb', line 171

def outside_request_meta_hold
  stack = (Thread.current[held_request_meta_key] ||= [])
  stack.push(nil)
  begin
    yield
  ensure
    close_request_meta_hold
  end
end

#params_fingerprint_of(params) ⇒ String

A stable fingerprint of the metadata that shapes a result: the effective _meta without the protocol version, the log level and the per-request identifiers. The client identity and capabilities stay in: a server may vary a result by who asks and by the extensions and features a request advertises, and those change when the host sets client_info, drops it, declares an extension or registers a handler.

Parameters:

  • params (Hash, nil) —

    effective parameters

Returns:

  • (String)


250
251
252
253
254
255
# File 'lib/mcp_client/request_metadata.rb', line 250

def params_fingerprint_of(params)
  meta = params.is_a?(Hash) ? (params['_meta'] || params[:_meta]) : nil
  meta = meta.is_a?(Hash) ? meta.transform_keys(&:to_s) : {}
  meta = meta.except(META_PROTOCOL_VERSION, META_LOG_LEVEL, *CACHE_NEUTRAL_META_KEYS)
  Digest::SHA256.hexdigest(JSON.generate(deep_sort_keys(meta)))
end

#recorded_request_params ⇒ String, ...

The fingerprint this thread holds, exactly as it stands — never the transport's reading of it. Taken when an exchange starts and put back when it ends (HttpTransportBase::CacheSupport#exchange_jsonrpc): a request nested inside it notes parameters of its own, and the host may go on rewriting the metadata it handed the transport while the response is on its way back. A fingerprint is taken when the request is built, so what it describes is the request as sent.

Returns:

  • (String, Symbol, nil)


47
48
49
# File 'lib/mcp_client/request_metadata.rb', line 47

def recorded_request_params
  Thread.current[request_params_key]
end

#release_held_request_meta ⇒ void

This method returns an undefined value.

Drop a held evaluation of the host's request_meta: the decision that took it leads to no request of its own, so the next one evaluates afresh rather than sending metadata read some time ago. The scope drops it too when the operation ends; this is for a decision that settles before that.



222
223
224
225
226
227
228
229
# File 'lib/mcp_client/request_metadata.rb', line 222

def release_held_request_meta
  held = held_request_meta
  return nil unless held

  held.evaluated = false
  held.value = nil
  nil
end

#request_params_fingerprint ⇒ String?

Returns the fingerprint of the effective parameters of the request this thread last built.

Returns:

  • (String, nil) —

    the fingerprint of the effective parameters of the request this thread last built



35
36
37
# File 'lib/mcp_client/request_metadata.rb', line 35

def request_params_fingerprint
  Thread.current[request_params_key]
end

#request_params_key ⇒ Symbol

Returns this transport's thread-local key for the request parameters.

Returns:

  • (Symbol) —

    this transport's thread-local key for the request parameters



258
259
260
# File 'lib/mcp_client/request_metadata.rb', line 258

def request_params_key
  :"mcp_client_request_params_#{object_id}"
end

#restore_request_params(record) ⇒ void

This method returns an undefined value.

Parameters:



53
54
55
# File 'lib/mcp_client/request_metadata.rb', line 53

def restore_request_params(record)
  Thread.current[request_params_key] = record
end

#take_offered_request_meta_hold ⇒ MCPClient::RequestMetadata::HeldRequestMeta?

Returns the offer, which no later operation can take up again.

Returns:



157
158
159
160
161
# File 'lib/mcp_client/request_metadata.rb', line 157

def take_offered_request_meta_hold
  offered = Thread.current[offered_request_meta_key]
  Thread.current[offered_request_meta_key] = nil unless offered.nil?
  offered
end

#withdraw_request_meta_hold ⇒ void

This method returns an undefined value.



150
151
152
153
# File 'lib/mcp_client/request_metadata.rb', line 150

def withdraw_request_meta_hold
  Thread.current[offered_request_meta_key] = nil
  nil
end