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
-
#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. -
#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) ⇒ Object
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).
82 83 84 85 |
# File 'lib/mcp/server_session.rb', line 82 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.
89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/mcp/server_session.rb', line 89 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.
121 122 123 |
# File 'lib/mcp/server_session.rb', line 121 def client_capabilities @client_capabilities || @server.client_capabilities end |
#configure_logging(logging_message_notification) ⇒ Object
Called by Server#configure_logging_level.
116 117 118 |
# File 'lib/mcp/server_session.rb', line 116 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.
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
# File 'lib/mcp/server_session.rb', line 177 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.
160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/mcp/server_session.rb', line 160 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.
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_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.
221 222 223 |
# File 'lib/mcp/server_session.rb', line 221 def fulfill_input_request(method, params, related_request_id:) send_to_transport_request(method, params, related_request_id: ) end |
#handle(request) ⇒ Object
101 102 103 |
# File 'lib/mcp/server_session.rb', line 101 def handle(request) @server.handle(request, session: self) end |
#handle_json(request_json) ⇒ Object
105 106 107 |
# File 'lib/mcp/server_session.rb', line 105 def handle_json(request_json) @server.handle_json(request_json, session: self) 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.
134 135 136 137 138 139 140 141 142 |
# File 'lib/mcp/server_session.rb', line 134 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
76 77 78 |
# File 'lib/mcp/server_session.rb', line 76 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.
243 244 245 246 247 |
# File 'lib/mcp/server_session.rb', line 243 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.
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 |
# File 'lib/mcp/server_session.rb', line 274 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.
257 258 259 260 261 262 263 264 265 266 267 268 |
# File 'lib/mcp/server_session.rb', line 257 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.
250 251 252 253 254 |
# File 'lib/mcp/server_session.rb', line 250 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.
145 146 147 148 149 150 |
# File 'lib/mcp/server_session.rb', line 145 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.
62 63 64 65 66 67 68 |
# File 'lib/mcp/server_session.rb', line 62 def register_in_flight(request_id) return if request_id.nil? cancellation = Cancellation.new(request_id: request_id) @in_flight_mutex.synchronize { @in_flight[request_id] = cancellation } cancellation 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.
230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/mcp/server_session.rb', line 230 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.
110 111 112 113 |
# File 'lib/mcp/server_session.rb', line 110 def store_client_info(client:, capabilities: nil) @client = client @client_capabilities = capabilities end |
#unregister_in_flight(request_id) ⇒ Object
70 71 72 73 74 |
# File 'lib/mcp/server_session.rb', line 70 def unregister_in_flight(request_id) return if request_id.nil? @in_flight_mutex.synchronize { @in_flight.delete(request_id) } end |