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.



116
117
118
119
120
121
122
123
124
# File 'lib/mcp/server_session.rb', line 116

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

Sends a ‘sampling/createMessage` request scoped to this session.



110
111
112
113
# File 'lib/mcp/server_session.rb', line 110

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.



127
128
129
130
131
132
133
134
135
# File 'lib/mcp/server_session.rb', line 127

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

Sends a ‘roots/list` request scoped to this session.



101
102
103
104
105
106
107
# File 'lib/mcp/server_session.rb', line 101

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.



155
156
157
158
159
# File 'lib/mcp/server_session.rb', line 155

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

Sends a log message notification to this session only.



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

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.



169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/mcp/server_session.rb', line 169

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.



162
163
164
165
166
# File 'lib/mcp/server_session.rb', line 162

def notify_resources_updated(uri:)
  send_to_transport(Methods::NOTIFICATIONS_RESOURCES_UPDATED, { "uri" => uri })
rescue => e
  @server.report_exception(e, notification: "resources_updated")
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.



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

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