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



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

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



51
52
53
54
55
56
57
58
59
60
61
# File 'lib/mcp/server_session.rb', line 51

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.



83
84
85
# File 'lib/mcp/server_session.rb', line 83

def client_capabilities
  @client_capabilities || @server.client_capabilities
end

#configure_logging(logging_message_notification) ⇒ Object

Called by ‘Server#configure_logging_level`.



78
79
80
# File 'lib/mcp/server_session.rb', line 78

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.



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

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.



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

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.



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

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



63
64
65
# File 'lib/mcp/server_session.rb', line 63

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

#handle_json(request_json) ⇒ Object



67
68
69
# File 'lib/mcp/server_session.rb', line 67

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

#list_roots(related_request_id: nil) ⇒ Object

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



88
89
90
91
92
93
94
# File 'lib/mcp/server_session.rb', line 88

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



38
39
40
# File 'lib/mcp/server_session.rb', line 38

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

#notify_elicitation_complete(elicitation_id:) ⇒ Object

Sends an elicitation complete notification scoped to this session.



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

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.



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

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.



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

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.



149
150
151
152
153
# File 'lib/mcp/server_session.rb', line 149

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.



24
25
26
27
28
29
30
# File 'lib/mcp/server_session.rb', line 24

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.



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

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.



72
73
74
75
# File 'lib/mcp/server_session.rb', line 72

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

#unregister_in_flight(request_id) ⇒ Object



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

def unregister_in_flight(request_id)
  return if request_id.nil?

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