Class: MCP::ServerSession

Inherits:
Object
  • Object
show all
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.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(server:, transport:, session_id: nil) ⇒ ServerSession

Returns a new instance of ServerSession.



12
13
14
15
16
17
18
19
20
21
22
# File 'lib/mcp/server_session.rb', line 12

def initialize(server:, transport:, session_id: nil)
  @server = server
  @transport = transport
  @session_id = session_id
  @client = nil
  @client_capabilities = nil
  @logging_message_notification = nil
  @in_flight = {}
  @in_flight_mutex = Mutex.new
  @initialized = false
end

Instance Attribute Details

#clientObject (readonly)

Returns the value of attribute client.



10
11
12
# File 'lib/mcp/server_session.rb', line 10

def client
  @client
end

#logging_message_notificationObject (readonly)

Returns the value of attribute logging_message_notification.



10
11
12
# File 'lib/mcp/server_session.rb', line 10

def logging_message_notification
  @logging_message_notification
end

#session_idObject (readonly)

Returns the value of attribute session_id.



10
11
12
# File 'lib/mcp/server_session.rb', line 10

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).



57
58
59
60
# File 'lib/mcp/server_session.rb', line 57

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.



64
65
66
67
68
69
70
71
72
73
74
# File 'lib/mcp/server_session.rb', line 64

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_capabilitiesObject

Returns per-session client capabilities, falling back to global.



96
97
98
# File 'lib/mcp/server_session.rb', line 96

def client_capabilities
  @client_capabilities || @server.client_capabilities
end

#configure_logging(logging_message_notification) ⇒ Object

Called by Server#configure_logging_level.



91
92
93
# File 'lib/mcp/server_session.rb', line 91

def configure_logging(logging_message_notification)
  @logging_message_notification = logging_message_notification
end

#create_form_elicitation(message:, requested_schema:, related_request_id: nil) ⇒ Object

Sends an elicitation/create request (form mode) scoped to this session.



131
132
133
134
135
136
137
138
139
# File 'lib/mcp/server_session.rb', line 131

def create_form_elicitation(message:, requested_schema:, related_request_id: nil)
  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: message, requestedSchema: requested_schema }
  send_to_transport_request(Methods::ELICITATION_CREATE, params, related_request_id: related_request_id)
end

#create_sampling_message(related_request_id: nil, **kwargs) ⇒ Object

Deprecated.

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.



125
126
127
128
# File 'lib/mcp/server_session.rb', line 125

def create_sampling_message(related_request_id: nil, **kwargs)
  params = @server.build_sampling_params(client_capabilities, **kwargs)
  send_to_transport_request(Methods::SAMPLING_CREATE_MESSAGE, params, related_request_id: related_request_id)
end

#create_url_elicitation(message:, url:, elicitation_id:, related_request_id: nil) ⇒ Object

Sends an elicitation/create request (URL mode) scoped to this session.



142
143
144
145
146
147
148
149
150
# File 'lib/mcp/server_session.rb', line 142

def create_url_elicitation(message:, url:, elicitation_id:, related_request_id: nil)
  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: message, url: url, elicitationId: elicitation_id }
  send_to_transport_request(Methods::ELICITATION_CREATE, params, related_request_id: related_request_id)
end

#handle(request) ⇒ Object



76
77
78
# File 'lib/mcp/server_session.rb', line 76

def handle(request)
  @server.handle(request, session: self)
end

#handle_json(request_json) ⇒ Object



80
81
82
# File 'lib/mcp/server_session.rb', line 80

def handle_json(request_json)
  @server.handle_json(request_json, session: self)
end

#initialized?Boolean

Whether initialize has already completed for this session.

Returns:

  • (Boolean)


25
26
27
# File 'lib/mcp/server_session.rb', line 25

def initialized?
  @initialized
end

#list_roots(related_request_id: nil) ⇒ Object

Deprecated.

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.



105
106
107
108
109
110
111
# File 'lib/mcp/server_session.rb', line 105

def list_roots(related_request_id: nil)
  unless client_capabilities&.dig(:roots)
    raise "Client does not support roots."
  end

  send_to_transport_request(Methods::ROOTS_LIST, nil, related_request_id: related_request_id)
end

#lookup_in_flight(request_id) ⇒ Object



51
52
53
# File 'lib/mcp/server_session.rb', line 51

def lookup_in_flight(request_id)
  @in_flight_mutex.synchronize { @in_flight[request_id] }
end

#mark_initialized!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).



32
33
34
# File 'lib/mcp/server_session.rb', line 32

def mark_initialized!
  @initialized = true
end

#notify_elicitation_complete(elicitation_id:) ⇒ Object

Sends an elicitation complete notification scoped to this session.



170
171
172
173
174
# File 'lib/mcp/server_session.rb', line 170

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

Deprecated.

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.



201
202
203
204
205
206
207
208
209
210
211
# File 'lib/mcp/server_session.rb', line 201

def notify_log_message(data:, level:, logger: nil, related_request_id: nil)
  effective_logging = @logging_message_notification || @server.logging_message_notification
  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: 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.



184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/mcp/server_session.rb', line 184

def notify_progress(progress_token:, progress:, total: nil, message: nil, related_request_id: nil)
  params = {
    "progressToken" => progress_token,
    "progress" => progress,
    "total" => total,
    "message" => message,
  }.compact

  send_to_transport(Methods::NOTIFICATIONS_PROGRESS, params, related_request_id: 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.



177
178
179
180
181
# File 'lib/mcp/server_session.rb', line 177

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) ⇒ Object

Sends a ping request scoped to this session.



114
115
116
117
118
119
# File 'lib/mcp/server_session.rb', line 114

def ping(related_request_id: nil)
  result = send_to_transport_request(Methods::PING, nil, related_request_id: related_request_id)
  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.



37
38
39
40
41
42
43
# File 'lib/mcp/server_session.rb', line 37

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.



157
158
159
160
161
162
163
164
165
166
167
# File 'lib/mcp/server_session.rb', line 157

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: 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.



85
86
87
88
# File 'lib/mcp/server_session.rb', line 85

def store_client_info(client:, capabilities: nil)
  @client = client
  @client_capabilities = capabilities
end

#unregister_in_flight(request_id) ⇒ Object



45
46
47
48
49
# File 'lib/mcp/server_session.rb', line 45

def unregister_in_flight(request_id)
  return if request_id.nil?

  @in_flight_mutex.synchronize { @in_flight.delete(request_id) }
end