Class: MCP::ServerSession
- Inherits:
-
Object
- Object
- MCP::ServerSession
- Defined in:
- lib/mcp/server_session.rb
Overview
Holds per-connection state for a single client session.
Created by the transport layer; delegates request handling to the shared Server.
Constant Summary collapse
- ERAS =
[:legacy, :modern].freeze
Instance Attribute Summary collapse
-
#client ⇒ Object
readonly
Returns the value of attribute client.
-
#era ⇒ Object
readonly
Connection-era lock of the dual-era serving model (SEP-2575):
niluntil the first era-distinctive message succeeds, then:legacyor:modernfor the connection's lifetime. -
#logging_message_notification ⇒ Object
readonly
Returns the value of attribute logging_message_notification.
-
#protocol_version ⇒ Object
readonly
Returns the value of attribute protocol_version.
-
#session_id ⇒ Object
readonly
Returns the value of attribute session_id.
Instance Method Summary collapse
-
#cancel_incoming(request_id:, reason: nil) ⇒ Object
Flips the
Cancellationfor a matching in-flight request received from the peer. -
#cancel_request(request_id:, reason: nil) ⇒ Object
Sends
notifications/cancelledto the peer for a previously-issued request. -
#client_capabilities ⇒ Object
Returns per-session client capabilities, falling back to global.
-
#configure_logging(logging_message_notification) ⇒ Object
Called by
Server#configure_logging_level. -
#create_form_elicitation(message:, requested_schema:, related_request_id: nil, timeout: nil) ⇒ Object
Sends an
elicitation/createrequest (form mode) scoped to this session. -
#create_sampling_message(related_request_id: nil, timeout: nil, **kwargs) ⇒ Object
deprecated
Deprecated.
MCP Sampling (
sampling/createMessage) is deprecated as of MCP protocol version 2026-07-28 (SEP-2577). Use direct LLM provider APIs instead. -
#create_url_elicitation(message:, url:, elicitation_id:, related_request_id: nil, timeout: nil) ⇒ Object
Sends an
elicitation/createrequest (URL mode) scoped to this session. -
#fulfill_input_request(method, params, related_request_id:) ⇒ Object
Sends an embedded SEP-2322
inputRequestsentry as a real server-to-client request on the legacy wire, for the server's dual-era fulfilment shim. - #handle(request) ⇒ Object
- #handle_json(request_json) ⇒ Object
-
#in_flight?(request_id) ⇒ Boolean
Whether
request_idis currently in flight, so a transport can refuse a colliding request before registering any state of its own for it. -
#initialize(server:, transport:, session_id: nil, era: nil) ⇒ ServerSession
constructor
A new instance of ServerSession.
-
#initialized? ⇒ Boolean
Whether
initializehas already completed for this session. -
#list_roots(related_request_id: nil, timeout: nil) ⇒ Object
deprecated
Deprecated.
MCP Roots (
roots/listandnotifications/roots/list_changed) is deprecated as of MCP protocol version 2026-07-28 (SEP-2577). Use tool parameters, resource URIs, server configuration, or environment variables instead. -
#lock_era!(era) ⇒ Object
One-shot era lock.
- #lookup_in_flight(request_id) ⇒ Object
-
#mark_initialized!(protocol_version: nil) ⇒ Object
Called by
Server#initafter a successfulinitializeresponse, so subsequentinitializerequests on the same session can be rejected per MCP spec (the initialization phase MUST be the first interaction). -
#notify_elicitation_complete(elicitation_id:) ⇒ Object
Sends an elicitation complete notification scoped to this session.
-
#notify_log_message(data:, level:, logger: nil, related_request_id: nil) ⇒ Object
deprecated
Deprecated.
MCP Logging (
logging/setLevelandnotifications/message) is deprecated as of MCP protocol version 2026-07-28 (SEP-2577). Use stderr or OpenTelemetry instead. -
#notify_progress(progress_token:, progress:, total: nil, message: nil, related_request_id: nil) ⇒ Object
Sends a progress notification to this session only.
-
#notify_resources_updated(uri:) ⇒ Object
Sends a resource updated notification to this session only.
-
#ping(related_request_id: nil, timeout: nil) ⇒ Object
Sends a
pingrequest scoped to this session. -
#register_in_flight(request_id) ⇒ Object
Registers a
Cancellationtoken for an in-flight request, or returnsnilwhenrequest_idis already in flight. -
#send_peer_cancellation(nested_request_id:, related_request_id: nil, reason: nil) ⇒ Object
Sends
notifications/cancelledto the peer for a nested server-to-client request that was started inside a now-cancelled parent request. -
#store_client_info(client:, capabilities: nil) ⇒ Object
Called by
Server#initduring the initialization handshake. -
#unregister_in_flight(request_id, cancellation: nil) ⇒ Object
Removes an in-flight registration.
Constructor Details
#initialize(server:, transport:, session_id: nil, era: nil) ⇒ ServerSession
Returns a new instance of ServerSession.
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/mcp/server_session.rb', line 19 def initialize(server:, transport:, session_id: nil, era: nil) validate_era!(era) if era @server = server @transport = transport @session_id = session_id @era = era @client = nil @client_capabilities = nil @logging_message_notification = nil @protocol_version = nil @in_flight = {} @in_flight_mutex = Mutex.new @initialized = false end |
Instance Attribute Details
#client ⇒ Object (readonly)
Returns the value of attribute client.
12 13 14 |
# File 'lib/mcp/server_session.rb', line 12 def client @client end |
#era ⇒ Object (readonly)
Connection-era lock of the dual-era serving model (SEP-2575): nil until the first era-distinctive message succeeds,
then :legacy or :modern for the connection's lifetime. Modern-era transports construct their per-request sessions
with era: :modern up front.
17 18 19 |
# File 'lib/mcp/server_session.rb', line 17 def era @era end |
#logging_message_notification ⇒ Object (readonly)
Returns the value of attribute logging_message_notification.
12 13 14 |
# File 'lib/mcp/server_session.rb', line 12 def @logging_message_notification end |
#protocol_version ⇒ Object (readonly)
Returns the value of attribute protocol_version.
12 13 14 |
# File 'lib/mcp/server_session.rb', line 12 def protocol_version @protocol_version end |
#session_id ⇒ Object (readonly)
Returns the value of attribute session_id.
12 13 14 |
# File 'lib/mcp/server_session.rb', line 12 def session_id @session_id end |
Instance Method Details
#cancel_incoming(request_id:, reason: nil) ⇒ Object
Flips the Cancellation for a matching in-flight request received from the peer.
Silently ignores unknown IDs per MCP spec (cancellation utilities, item 5).
104 105 106 107 |
# File 'lib/mcp/server_session.rb', line 104 def cancel_incoming(request_id:, reason: nil) cancellation = lookup_in_flight(request_id) cancellation&.cancel(reason: reason) end |
#cancel_request(request_id:, reason: nil) ⇒ Object
Sends notifications/cancelled to the peer for a previously-issued request.
Also unblocks any transport-level send_request waiting on a response for request_id.
111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/mcp/server_session.rb', line 111 def cancel_request(request_id:, reason: nil) params = { requestId: request_id } params[:reason] = reason if reason send_to_transport(Methods::NOTIFICATIONS_CANCELLED, params) if @transport.respond_to?(:cancel_pending_request) @transport.cancel_pending_request(request_id, reason: reason) end rescue => e MCP.configuration.exception_reporter.call(e, { notification: "cancelled", request_id: request_id }) end |
#client_capabilities ⇒ Object
Returns per-session client capabilities, falling back to global.
143 144 145 |
# File 'lib/mcp/server_session.rb', line 143 def client_capabilities @client_capabilities || @server.client_capabilities end |
#configure_logging(logging_message_notification) ⇒ Object
Called by Server#configure_logging_level.
138 139 140 |
# File 'lib/mcp/server_session.rb', line 138 def configure_logging() @logging_message_notification = end |
#create_form_elicitation(message:, requested_schema:, related_request_id: nil, timeout: nil) ⇒ Object
Sends an elicitation/create request (form mode) scoped to this session.
Per SEP-2260, the request must be associated with an originating client
request; prefer server_context.create_form_elicitation inside a handler,
which stamps the association automatically.
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/mcp/server_session.rb', line 199 def create_form_elicitation(message:, requested_schema:, related_request_id: nil, timeout: nil) warn_unassociated_request(__method__, ) unless client_capabilities&.dig(:elicitation) raise "Client does not support elicitation. " \ "The client must declare the `elicitation` capability during initialization." end params = { mode: "form", message: , requestedSchema: requested_schema } send_to_transport_request( Methods::ELICITATION_CREATE, params, related_request_id: , timeout: timeout, ) end |
#create_sampling_message(related_request_id: nil, timeout: nil, **kwargs) ⇒ Object
MCP Sampling (sampling/createMessage) is deprecated as of
MCP protocol version 2026-07-28 (SEP-2577). Use direct LLM provider
APIs instead.
Sends a sampling/createMessage request scoped to this session.
Per SEP-2260, the request must be associated with an originating client
request; prefer server_context.create_sampling_message inside a handler,
which stamps the association automatically.
182 183 184 185 186 187 188 189 190 191 192 |
# File 'lib/mcp/server_session.rb', line 182 def (related_request_id: nil, timeout: nil, **kwargs) warn_unassociated_request(__method__, ) params = @server.build_sampling_params(client_capabilities, **kwargs) send_to_transport_request( Methods::SAMPLING_CREATE_MESSAGE, params, related_request_id: , timeout: timeout, ) end |
#create_url_elicitation(message:, url:, elicitation_id:, related_request_id: nil, timeout: nil) ⇒ Object
Sends an elicitation/create request (URL mode) scoped to this session.
Per SEP-2260, the request must be associated with an originating client
request; prefer server_context.create_url_elicitation inside a handler,
which stamps the association automatically.
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/mcp/server_session.rb', line 221 def create_url_elicitation(message:, url:, elicitation_id:, related_request_id: nil, timeout: nil) warn_unassociated_request(__method__, ) unless client_capabilities&.dig(:elicitation, :url) raise "Client does not support URL mode elicitation. " \ "The client must declare the `elicitation.url` capability during initialization." end params = { mode: "url", message: , url: url, elicitationId: elicitation_id } send_to_transport_request( Methods::ELICITATION_CREATE, params, related_request_id: , timeout: timeout, ) end |
#fulfill_input_request(method, params, related_request_id:) ⇒ Object
Sends an embedded SEP-2322 inputRequests entry as a real server-to-client request on the legacy wire,
for the server's dual-era fulfilment shim.
The entry is forwarded verbatim - per the spec, clients treat each entry exactly like the equivalent
standalone request - and stays associated with the originating client request per SEP-2260.
Returns the client's result.
243 244 245 |
# File 'lib/mcp/server_session.rb', line 243 def fulfill_input_request(method, params, related_request_id:) send_to_transport_request(method, params, related_request_id: ) end |
#handle(request) ⇒ Object
123 124 125 |
# File 'lib/mcp/server_session.rb', line 123 def handle(request) @server.handle(request, session: self) end |
#handle_json(request_json) ⇒ Object
127 128 129 |
# File 'lib/mcp/server_session.rb', line 127 def handle_json(request_json) @server.handle_json(request_json, session: self) end |
#in_flight?(request_id) ⇒ Boolean
Whether request_id is currently in flight, so a transport can refuse a colliding request
before registering any state of its own for it.
94 95 96 |
# File 'lib/mcp/server_session.rb', line 94 def in_flight?(request_id) !lookup_in_flight(request_id).nil? end |
#initialized? ⇒ Boolean
Whether initialize has already completed for this session.
36 37 38 |
# File 'lib/mcp/server_session.rb', line 36 def initialized? @initialized end |
#list_roots(related_request_id: nil, timeout: nil) ⇒ Object
MCP Roots (roots/list and
notifications/roots/list_changed) is deprecated as of MCP protocol
version 2026-07-28 (SEP-2577). Use tool parameters, resource URIs,
server configuration, or environment variables instead.
Sends a roots/list request scoped to this session.
Per SEP-2260, the request must be associated with an originating client
request; prefer server_context.list_roots inside a handler, which stamps
the association automatically.
156 157 158 159 160 161 162 163 164 |
# File 'lib/mcp/server_session.rb', line 156 def list_roots(related_request_id: nil, timeout: nil) warn_unassociated_request(__method__, ) unless client_capabilities&.dig(:roots) raise "Client does not support roots." end send_to_transport_request(Methods::ROOTS_LIST, nil, related_request_id: , timeout: timeout) end |
#lock_era!(era) ⇒ Object
One-shot era lock. Locking the already-locked era is a no-op; flipping an established era raises, because a connection can never change eras.
53 54 55 56 57 58 59 |
# File 'lib/mcp/server_session.rb', line 53 def lock_era!(era) validate_era!(era) return if @era == era raise "Session era already locked to #{@era}" if @era @era = era end |
#lookup_in_flight(request_id) ⇒ Object
98 99 100 |
# File 'lib/mcp/server_session.rb', line 98 def lookup_in_flight(request_id) @in_flight_mutex.synchronize { @in_flight[request_id] } end |
#mark_initialized!(protocol_version: nil) ⇒ Object
Called by Server#init after a successful initialize response, so subsequent
initialize requests on the same session can be rejected per MCP spec
(the initialization phase MUST be the first interaction).
43 44 45 46 47 48 49 |
# File 'lib/mcp/server_session.rb', line 43 def mark_initialized!(protocol_version: nil) @initialized = true @protocol_version = protocol_version # A successful `initialize` is the legacy-distinctive message of the dual-era serving model (SEP-2575), # so it also locks the connection era. @era ||= :legacy end |
#notify_elicitation_complete(elicitation_id:) ⇒ Object
Sends an elicitation complete notification scoped to this session.
265 266 267 268 269 |
# File 'lib/mcp/server_session.rb', line 265 def notify_elicitation_complete(elicitation_id:) send_to_transport(Methods::NOTIFICATIONS_ELICITATION_COMPLETE, { elicitationId: elicitation_id }) rescue => e @server.report_exception(e, notification: "elicitation_complete") end |
#notify_log_message(data:, level:, logger: nil, related_request_id: nil) ⇒ Object
MCP Logging (logging/setLevel and notifications/message)
is deprecated as of MCP protocol version 2026-07-28 (SEP-2577).
Use stderr or OpenTelemetry instead.
Sends a log message notification to this session only.
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 |
# File 'lib/mcp/server_session.rb', line 296 def (data:, level:, logger: nil, related_request_id: nil) # In the modern lifecycle, log delivery is authorized per request through the `_meta` envelope's `logLevel` member # (applied via `configure_logging`); without it no `notifications/message` is sent, and the server-wide level # does not apply (SEP-2575). effective_logging = if @era == :modern @logging_message_notification else @logging_message_notification || @server. end return unless effective_logging&.should_notify?(level) params = { "data" => data, "level" => level } params["logger"] = logger if logger send_to_transport(Methods::NOTIFICATIONS_MESSAGE, params, related_request_id: ) rescue => e @server.report_exception(e, { notification: "log_message" }) end |
#notify_progress(progress_token:, progress:, total: nil, message: nil, related_request_id: nil) ⇒ Object
Sends a progress notification to this session only.
279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/mcp/server_session.rb', line 279 def notify_progress(progress_token:, progress:, total: nil, message: nil, related_request_id: nil) params = { "progressToken" => progress_token, "progress" => progress, "total" => total, "message" => , }.compact send_to_transport(Methods::NOTIFICATIONS_PROGRESS, params, related_request_id: ) rescue => e @server.report_exception(e, notification: "progress") end |
#notify_resources_updated(uri:) ⇒ Object
Sends a resource updated notification to this session only.
272 273 274 275 276 |
# File 'lib/mcp/server_session.rb', line 272 def notify_resources_updated(uri:) send_to_transport(Methods::NOTIFICATIONS_RESOURCES_UPDATED, { "uri" => uri }) rescue => e @server.report_exception(e, notification: "resources_updated") end |
#ping(related_request_id: nil, timeout: nil) ⇒ Object
Sends a ping request scoped to this session.
167 168 169 170 171 172 |
# File 'lib/mcp/server_session.rb', line 167 def ping(related_request_id: nil, timeout: nil) result = send_to_transport_request(Methods::PING, nil, related_request_id: , timeout: timeout) raise Server::ValidationError, "Response validation failed: invalid `result`" unless result.is_a?(Hash) result end |
#register_in_flight(request_id) ⇒ Object
Registers a Cancellation token for an in-flight request, or returns nil when request_id is already in flight.
The request id is the only key that routes request-scoped notifications, server-to-client requests,
and notifications/cancelled back to the request that caused them, so a second live request under the same id
has no destination of its own. Rather than let the newcomer displace the registration, report the collision
and leave the first request intact; the caller turns that into an Invalid Request.
66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/mcp/server_session.rb', line 66 def register_in_flight(request_id) return if request_id.nil? cancellation = Cancellation.new(request_id: request_id) registered = @in_flight_mutex.synchronize do next false if @in_flight.key?(request_id) @in_flight[request_id] = cancellation true end registered ? cancellation : nil end |
#send_peer_cancellation(nested_request_id:, related_request_id: nil, reason: nil) ⇒ Object
Sends notifications/cancelled to the peer for a nested server-to-client request
that was started inside a now-cancelled parent request. related_request_id
is the parent request id so the notification is routed to the same stream
(e.g. the parent's POST response stream on StreamableHTTPTransport) rather than
the GET SSE stream.
252 253 254 255 256 257 258 259 260 261 262 |
# File 'lib/mcp/server_session.rb', line 252 def send_peer_cancellation(nested_request_id:, related_request_id: nil, reason: nil) params = { requestId: nested_request_id } params[:reason] = reason if reason send_to_transport(Methods::NOTIFICATIONS_CANCELLED, params, related_request_id: ) if @transport.respond_to?(:cancel_pending_request) @transport.cancel_pending_request(nested_request_id, reason: reason) end rescue => e MCP.configuration.exception_reporter.call(e, { notification: "cancelled", request_id: nested_request_id }) end |
#store_client_info(client:, capabilities: nil) ⇒ Object
Called by Server#init during the initialization handshake.
132 133 134 135 |
# File 'lib/mcp/server_session.rb', line 132 def store_client_info(client:, capabilities: nil) @client = client @client_capabilities = capabilities end |
#unregister_in_flight(request_id, cancellation: nil) ⇒ Object
Removes an in-flight registration. Passing the Cancellation that register_in_flight returned removes
the entry only while it is still that one, so a request can never evict a registration it does not own.
82 83 84 85 86 87 88 89 90 |
# File 'lib/mcp/server_session.rb', line 82 def unregister_in_flight(request_id, cancellation: nil) return if request_id.nil? @in_flight_mutex.synchronize do next if cancellation && !@in_flight[request_id].equal?(cancellation) @in_flight.delete(request_id) end end |