mcp 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +17 -15
- data/lib/mcp/progress.rb +4 -3
- data/lib/mcp/server/transports/stdio_transport.rb +5 -3
- data/lib/mcp/server/transports/streamable_http_transport.rb +118 -13
- data/lib/mcp/server.rb +50 -54
- data/lib/mcp/server_context.rb +19 -1
- data/lib/mcp/server_session.rb +79 -0
- data/lib/mcp/transport.rb +2 -2
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 29a39b8c5bb27a2fcdc8d084dce2cd79dfada5981a18d156bc1de78604035b2e
|
|
4
|
+
data.tar.gz: d969675cb0bb08b9ee3971a9bd90891767c7b28d68fe60579cf4857a06ff3680
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ff992068b54cc35acd43bc9e93edb3959f270a19eedf52540748344f2a94488829cbfec65dedb2772d67dd883f7536523af25d6419ded2cc71944359ff2d016
|
|
7
|
+
data.tar.gz: 0f716e3f54ca10619f95787c65dfe01fecc2356474ddf2909dfb2918f544f3fc8aa2a1a798836b2c556a9178ad842472fd79820ad83e96e855e4ca1ded4f5d8b
|
data/README.md
CHANGED
|
@@ -113,9 +113,17 @@ The server provides the following notification methods:
|
|
|
113
113
|
- `notify_tools_list_changed` - Send a notification when the tools list changes
|
|
114
114
|
- `notify_prompts_list_changed` - Send a notification when the prompts list changes
|
|
115
115
|
- `notify_resources_list_changed` - Send a notification when the resources list changes
|
|
116
|
-
- `notify_progress` - Send a progress notification for long-running operations
|
|
117
116
|
- `notify_log_message` - Send a structured logging notification message
|
|
118
117
|
|
|
118
|
+
#### Session Scoping
|
|
119
|
+
|
|
120
|
+
When using Streamable HTTP transport with multiple clients, each client connection gets its own session. Notifications are scoped as follows:
|
|
121
|
+
|
|
122
|
+
- **`report_progress`** and **`notify_log_message`** called via `server_context` inside a tool handler are automatically sent only to the requesting client.
|
|
123
|
+
No extra configuration is needed.
|
|
124
|
+
- **`notify_tools_list_changed`**, **`notify_prompts_list_changed`**, and **`notify_resources_list_changed`** are always broadcast to all connected clients,
|
|
125
|
+
as they represent server-wide state changes. These should be called on the `server` instance directly.
|
|
126
|
+
|
|
119
127
|
#### Notification Format
|
|
120
128
|
|
|
121
129
|
Notifications follow the JSON-RPC 2.0 specification and use these method names:
|
|
@@ -169,24 +177,10 @@ The `server_context.report_progress` method accepts:
|
|
|
169
177
|
- `total:` (optional) — total expected value, so clients can display a percentage
|
|
170
178
|
- `message:` (optional) — human-readable status message
|
|
171
179
|
|
|
172
|
-
#### Server-Side: Direct `notify_progress` Usage
|
|
173
|
-
|
|
174
|
-
You can also call `notify_progress` directly on the server instance:
|
|
175
|
-
|
|
176
|
-
```ruby
|
|
177
|
-
server.notify_progress(
|
|
178
|
-
progress_token: "token-123",
|
|
179
|
-
progress: 50,
|
|
180
|
-
total: 100, # optional
|
|
181
|
-
message: "halfway" # optional
|
|
182
|
-
)
|
|
183
|
-
```
|
|
184
|
-
|
|
185
180
|
**Key Features:**
|
|
186
181
|
|
|
187
182
|
- Tools report progress via `server_context.report_progress`
|
|
188
183
|
- `report_progress` is a no-op when no `progressToken` was provided by the client
|
|
189
|
-
- `notify_progress` is a no-op when no transport is configured
|
|
190
184
|
- Supports both numeric and string progress tokens
|
|
191
185
|
|
|
192
186
|
### Logging
|
|
@@ -293,6 +287,14 @@ Set `stateless: true` in `MCP::Server::Transports::StreamableHTTPTransport.new`
|
|
|
293
287
|
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
|
|
294
288
|
```
|
|
295
289
|
|
|
290
|
+
By default, sessions do not expire. To mitigate session hijacking risks, you can set a `session_idle_timeout` (in seconds).
|
|
291
|
+
When configured, sessions that receive no HTTP requests for this duration are automatically expired and cleaned up:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
# Session timeout of 30 minutes
|
|
295
|
+
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, session_idle_timeout: 1800)
|
|
296
|
+
```
|
|
297
|
+
|
|
296
298
|
### Unsupported Features (to be implemented in future versions)
|
|
297
299
|
|
|
298
300
|
- Resource subscriptions
|
data/lib/mcp/progress.rb
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module MCP
|
|
4
4
|
class Progress
|
|
5
|
-
def initialize(
|
|
6
|
-
@
|
|
5
|
+
def initialize(notification_target:, progress_token:)
|
|
6
|
+
@notification_target = notification_target
|
|
7
7
|
@progress_token = progress_token
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def report(progress, total: nil, message: nil)
|
|
11
11
|
return unless @progress_token
|
|
12
|
+
return unless @notification_target
|
|
12
13
|
|
|
13
|
-
@
|
|
14
|
+
@notification_target.notify_progress(
|
|
14
15
|
progress_token: @progress_token,
|
|
15
16
|
progress: progress,
|
|
16
17
|
total: total,
|
|
@@ -10,17 +10,19 @@ module MCP
|
|
|
10
10
|
STATUS_INTERRUPTED = Signal.list["INT"] + 128
|
|
11
11
|
|
|
12
12
|
def initialize(server)
|
|
13
|
-
|
|
13
|
+
super(server)
|
|
14
14
|
@open = false
|
|
15
|
+
@session = nil
|
|
15
16
|
$stdin.set_encoding("UTF-8")
|
|
16
17
|
$stdout.set_encoding("UTF-8")
|
|
17
|
-
super
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def open
|
|
21
21
|
@open = true
|
|
22
|
+
@session = ServerSession.new(server: @server, transport: self)
|
|
22
23
|
while @open && (line = $stdin.gets)
|
|
23
|
-
|
|
24
|
+
response = @session.handle_json(line.strip)
|
|
25
|
+
send_response(response) if response
|
|
24
26
|
end
|
|
25
27
|
rescue Interrupt
|
|
26
28
|
warn("\nExiting...")
|
|
@@ -8,18 +8,30 @@ module MCP
|
|
|
8
8
|
class Server
|
|
9
9
|
module Transports
|
|
10
10
|
class StreamableHTTPTransport < Transport
|
|
11
|
-
def initialize(server, stateless: false)
|
|
11
|
+
def initialize(server, stateless: false, session_idle_timeout: nil)
|
|
12
12
|
super(server)
|
|
13
|
-
#
|
|
13
|
+
# Maps `session_id` to `{ stream: stream_object, server_session: ServerSession, last_active_at: float_from_monotonic_clock }`.
|
|
14
14
|
@sessions = {}
|
|
15
15
|
@mutex = Mutex.new
|
|
16
16
|
|
|
17
17
|
@stateless = stateless
|
|
18
|
+
@session_idle_timeout = session_idle_timeout
|
|
19
|
+
|
|
20
|
+
if @session_idle_timeout
|
|
21
|
+
if @stateless
|
|
22
|
+
raise ArgumentError, "session_idle_timeout is not supported in stateless mode."
|
|
23
|
+
elsif @session_idle_timeout <= 0
|
|
24
|
+
raise ArgumentError, "session_idle_timeout must be a positive number."
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
start_reaper_thread if @session_idle_timeout
|
|
18
29
|
end
|
|
19
30
|
|
|
20
31
|
REQUIRED_POST_ACCEPT_TYPES = ["application/json", "text/event-stream"].freeze
|
|
21
32
|
REQUIRED_GET_ACCEPT_TYPES = ["text/event-stream"].freeze
|
|
22
33
|
STREAM_WRITE_ERRORS = [IOError, Errno::EPIPE, Errno::ECONNRESET].freeze
|
|
34
|
+
SESSION_REAP_INTERVAL = 60
|
|
23
35
|
|
|
24
36
|
def handle_request(request)
|
|
25
37
|
case request.env["REQUEST_METHOD"]
|
|
@@ -35,6 +47,9 @@ module MCP
|
|
|
35
47
|
end
|
|
36
48
|
|
|
37
49
|
def close
|
|
50
|
+
@reaper_thread&.kill
|
|
51
|
+
@reaper_thread = nil
|
|
52
|
+
|
|
38
53
|
@mutex.synchronize do
|
|
39
54
|
@sessions.each_key { |session_id| cleanup_session_unsafe(session_id) }
|
|
40
55
|
end
|
|
@@ -56,6 +71,11 @@ module MCP
|
|
|
56
71
|
session = @sessions[session_id]
|
|
57
72
|
return false unless session && session[:stream]
|
|
58
73
|
|
|
74
|
+
if session_expired?(session)
|
|
75
|
+
cleanup_session_unsafe(session_id)
|
|
76
|
+
return false
|
|
77
|
+
end
|
|
78
|
+
|
|
59
79
|
begin
|
|
60
80
|
send_to_stream(session[:stream], notification)
|
|
61
81
|
true
|
|
@@ -75,6 +95,11 @@ module MCP
|
|
|
75
95
|
@sessions.each do |sid, session|
|
|
76
96
|
next unless session[:stream]
|
|
77
97
|
|
|
98
|
+
if session_expired?(session)
|
|
99
|
+
failed_sessions << sid
|
|
100
|
+
next
|
|
101
|
+
end
|
|
102
|
+
|
|
78
103
|
begin
|
|
79
104
|
send_to_stream(session[:stream], notification)
|
|
80
105
|
sent_count += 1
|
|
@@ -97,6 +122,39 @@ module MCP
|
|
|
97
122
|
|
|
98
123
|
private
|
|
99
124
|
|
|
125
|
+
def start_reaper_thread
|
|
126
|
+
@reaper_thread = Thread.new do
|
|
127
|
+
loop do
|
|
128
|
+
sleep(SESSION_REAP_INTERVAL)
|
|
129
|
+
reap_expired_sessions
|
|
130
|
+
rescue StandardError => e
|
|
131
|
+
MCP.configuration.exception_reporter.call(e, error: "Session reaper error")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def reap_expired_sessions
|
|
137
|
+
return unless @session_idle_timeout
|
|
138
|
+
|
|
139
|
+
expired_streams = @mutex.synchronize do
|
|
140
|
+
@sessions.each_with_object([]) do |(session_id, session), streams|
|
|
141
|
+
next unless session_expired?(session)
|
|
142
|
+
|
|
143
|
+
streams << session[:stream] if session[:stream]
|
|
144
|
+
@sessions.delete(session_id)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
expired_streams.each do |stream|
|
|
149
|
+
# Closing outside the mutex is safe because expired sessions are already
|
|
150
|
+
# removed from `@sessions` above, so other threads will not find them
|
|
151
|
+
# and will not attempt to close the same stream.
|
|
152
|
+
stream.close
|
|
153
|
+
rescue
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
100
158
|
def send_to_stream(stream, data)
|
|
101
159
|
message = data.is_a?(String) ? data : data.to_json
|
|
102
160
|
stream.write("data: #{message}\n\n")
|
|
@@ -120,10 +178,14 @@ module MCP
|
|
|
120
178
|
|
|
121
179
|
if body["method"] == "initialize"
|
|
122
180
|
handle_initialization(body_string, body)
|
|
123
|
-
elsif notification?(body) || response?(body)
|
|
124
|
-
handle_accepted
|
|
125
181
|
else
|
|
126
|
-
|
|
182
|
+
return missing_session_id_response if !@stateless && !session_id
|
|
183
|
+
|
|
184
|
+
if notification?(body) || response?(body)
|
|
185
|
+
handle_accepted
|
|
186
|
+
else
|
|
187
|
+
handle_regular_request(body_string, session_id)
|
|
188
|
+
end
|
|
127
189
|
end
|
|
128
190
|
rescue StandardError => e
|
|
129
191
|
MCP.configuration.exception_reporter.call(e, { request: body_string })
|
|
@@ -141,7 +203,9 @@ module MCP
|
|
|
141
203
|
session_id = extract_session_id(request)
|
|
142
204
|
|
|
143
205
|
return missing_session_id_response unless session_id
|
|
144
|
-
|
|
206
|
+
|
|
207
|
+
error_response = validate_and_touch_session(session_id)
|
|
208
|
+
return error_response if error_response
|
|
145
209
|
return session_already_connected_response if get_session_stream(session_id)
|
|
146
210
|
|
|
147
211
|
setup_sse_stream(session_id)
|
|
@@ -228,18 +292,26 @@ module MCP
|
|
|
228
292
|
|
|
229
293
|
def handle_initialization(body_string, body)
|
|
230
294
|
session_id = nil
|
|
295
|
+
server_session = nil
|
|
231
296
|
|
|
232
297
|
unless @stateless
|
|
233
298
|
session_id = SecureRandom.uuid
|
|
299
|
+
server_session = ServerSession.new(server: @server, transport: self, session_id: session_id)
|
|
234
300
|
|
|
235
301
|
@mutex.synchronize do
|
|
236
302
|
@sessions[session_id] = {
|
|
237
303
|
stream: nil,
|
|
304
|
+
server_session: server_session,
|
|
305
|
+
last_active_at: Process.clock_gettime(Process::CLOCK_MONOTONIC),
|
|
238
306
|
}
|
|
239
307
|
end
|
|
240
308
|
end
|
|
241
309
|
|
|
242
|
-
response =
|
|
310
|
+
response = if server_session
|
|
311
|
+
server_session.handle_json(body_string)
|
|
312
|
+
else
|
|
313
|
+
@server.handle_json(body_string)
|
|
314
|
+
end
|
|
243
315
|
|
|
244
316
|
headers = {
|
|
245
317
|
"Content-Type" => "application/json",
|
|
@@ -255,16 +327,27 @@ module MCP
|
|
|
255
327
|
end
|
|
256
328
|
|
|
257
329
|
def handle_regular_request(body_string, session_id)
|
|
330
|
+
server_session = nil
|
|
331
|
+
stream = nil
|
|
332
|
+
|
|
258
333
|
unless @stateless
|
|
259
|
-
if session_id
|
|
260
|
-
|
|
334
|
+
if session_id
|
|
335
|
+
error_response = validate_and_touch_session(session_id)
|
|
336
|
+
return error_response if error_response
|
|
337
|
+
|
|
338
|
+
@mutex.synchronize do
|
|
339
|
+
session = @sessions[session_id]
|
|
340
|
+
server_session = session[:server_session] if session
|
|
341
|
+
stream = session[:stream] if session
|
|
342
|
+
end
|
|
261
343
|
end
|
|
262
344
|
end
|
|
263
345
|
|
|
264
|
-
response =
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
346
|
+
response = if server_session
|
|
347
|
+
server_session.handle_json(body_string)
|
|
348
|
+
else
|
|
349
|
+
@server.handle_json(body_string)
|
|
350
|
+
end
|
|
268
351
|
|
|
269
352
|
if stream
|
|
270
353
|
send_response_to_stream(stream, response, session_id)
|
|
@@ -273,6 +356,22 @@ module MCP
|
|
|
273
356
|
end
|
|
274
357
|
end
|
|
275
358
|
|
|
359
|
+
def validate_and_touch_session(session_id)
|
|
360
|
+
@mutex.synchronize do
|
|
361
|
+
return session_not_found_response unless (session = @sessions[session_id])
|
|
362
|
+
return unless @session_idle_timeout
|
|
363
|
+
|
|
364
|
+
if session_expired?(session)
|
|
365
|
+
cleanup_session_unsafe(session_id)
|
|
366
|
+
return session_not_found_response
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
session[:last_active_at] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
nil
|
|
373
|
+
end
|
|
374
|
+
|
|
276
375
|
def get_session_stream(session_id)
|
|
277
376
|
@mutex.synchronize { @sessions[session_id]&.fetch(:stream, nil) }
|
|
278
377
|
end
|
|
@@ -378,6 +477,12 @@ module MCP
|
|
|
378
477
|
)
|
|
379
478
|
raise # Re-raise to exit the keepalive loop
|
|
380
479
|
end
|
|
480
|
+
|
|
481
|
+
def session_expired?(session)
|
|
482
|
+
return false unless @session_idle_timeout
|
|
483
|
+
|
|
484
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - session[:last_active_at] > @session_idle_timeout
|
|
485
|
+
end
|
|
381
486
|
end
|
|
382
487
|
end
|
|
383
488
|
end
|
data/lib/mcp/server.rb
CHANGED
|
@@ -111,15 +111,29 @@ module MCP
|
|
|
111
111
|
@transport = transport
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
# Processes a parsed JSON-RPC request and returns the response as a Hash.
|
|
115
|
+
#
|
|
116
|
+
# @param request [Hash] A parsed JSON-RPC request.
|
|
117
|
+
# @param session [ServerSession, nil] Per-connection session. Passed by
|
|
118
|
+
# `ServerSession#handle` for session-scoped notification delivery.
|
|
119
|
+
# When `nil`, progress and logging notifications from tool handlers are silently skipped.
|
|
120
|
+
# @return [Hash, nil] The JSON-RPC response, or `nil` for notifications.
|
|
121
|
+
def handle(request, session: nil)
|
|
115
122
|
JsonRpcHandler.handle(request) do |method|
|
|
116
|
-
handle_request(request, method)
|
|
123
|
+
handle_request(request, method, session: session)
|
|
117
124
|
end
|
|
118
125
|
end
|
|
119
126
|
|
|
120
|
-
|
|
127
|
+
# Processes a JSON-RPC request string and returns the response as a JSON string.
|
|
128
|
+
#
|
|
129
|
+
# @param request [String] A JSON-RPC request as a JSON string.
|
|
130
|
+
# @param session [ServerSession, nil] Per-connection session. Passed by
|
|
131
|
+
# `ServerSession#handle_json` for session-scoped notification delivery.
|
|
132
|
+
# When `nil`, progress and logging notifications from tool handlers are silently skipped.
|
|
133
|
+
# @return [String, nil] The JSON-RPC response as JSON, or `nil` for notifications.
|
|
134
|
+
def handle_json(request, session: nil)
|
|
121
135
|
JsonRpcHandler.handle_json(request) do |method|
|
|
122
|
-
handle_request(request, method)
|
|
136
|
+
handle_request(request, method, session: session)
|
|
123
137
|
end
|
|
124
138
|
end
|
|
125
139
|
|
|
@@ -172,21 +186,6 @@ module MCP
|
|
|
172
186
|
report_exception(e, { notification: "resources_list_changed" })
|
|
173
187
|
end
|
|
174
188
|
|
|
175
|
-
def notify_progress(progress_token:, progress:, total: nil, message: nil)
|
|
176
|
-
return unless @transport
|
|
177
|
-
|
|
178
|
-
params = {
|
|
179
|
-
"progressToken" => progress_token,
|
|
180
|
-
"progress" => progress,
|
|
181
|
-
"total" => total,
|
|
182
|
-
"message" => message,
|
|
183
|
-
}.compact
|
|
184
|
-
|
|
185
|
-
@transport.send_notification(Methods::NOTIFICATIONS_PROGRESS, params)
|
|
186
|
-
rescue => e
|
|
187
|
-
report_exception(e, notification: "progress")
|
|
188
|
-
end
|
|
189
|
-
|
|
190
189
|
def notify_log_message(data:, level:, logger: nil)
|
|
191
190
|
return unless @transport
|
|
192
191
|
return unless logging_message_notification&.should_notify?(level)
|
|
@@ -199,34 +198,16 @@ module MCP
|
|
|
199
198
|
report_exception(e, { notification: "log_message" })
|
|
200
199
|
end
|
|
201
200
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
# Sets a custom handler for `resources/read` requests.
|
|
202
|
+
# The block receives the parsed request params and should return resource
|
|
203
|
+
# contents. The return value is set as the `contents` field of the response.
|
|
204
|
+
#
|
|
205
|
+
# @yield [params] The request params containing `:uri`.
|
|
206
|
+
# @yieldreturn [Array<Hash>, Hash] Resource contents.
|
|
206
207
|
def resources_read_handler(&block)
|
|
207
208
|
@handlers[Methods::RESOURCES_READ] = block
|
|
208
209
|
end
|
|
209
210
|
|
|
210
|
-
def resources_templates_list_handler(&block)
|
|
211
|
-
@handlers[Methods::RESOURCES_TEMPLATES_LIST] = block
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def tools_list_handler(&block)
|
|
215
|
-
@handlers[Methods::TOOLS_LIST] = block
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def tools_call_handler(&block)
|
|
219
|
-
@handlers[Methods::TOOLS_CALL] = block
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
def prompts_list_handler(&block)
|
|
223
|
-
@handlers[Methods::PROMPTS_LIST] = block
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def prompts_get_handler(&block)
|
|
227
|
-
@handlers[Methods::PROMPTS_GET] = block
|
|
228
|
-
end
|
|
229
|
-
|
|
230
211
|
private
|
|
231
212
|
|
|
232
213
|
def validate!
|
|
@@ -297,11 +278,12 @@ module MCP
|
|
|
297
278
|
end
|
|
298
279
|
end
|
|
299
280
|
|
|
300
|
-
def handle_request(request, method)
|
|
281
|
+
def handle_request(request, method, session: nil)
|
|
301
282
|
handler = @handlers[method]
|
|
302
283
|
unless handler
|
|
303
284
|
instrument_call("unsupported_method") do
|
|
304
|
-
|
|
285
|
+
client = session&.client || @client
|
|
286
|
+
add_instrumentation_data(client: client) if client
|
|
305
287
|
end
|
|
306
288
|
return
|
|
307
289
|
end
|
|
@@ -311,6 +293,8 @@ module MCP
|
|
|
311
293
|
->(params) {
|
|
312
294
|
instrument_call(method) do
|
|
313
295
|
result = case method
|
|
296
|
+
when Methods::INITIALIZE
|
|
297
|
+
init(params, session: session)
|
|
314
298
|
when Methods::TOOLS_LIST
|
|
315
299
|
{ tools: @handlers[Methods::TOOLS_LIST].call(params) }
|
|
316
300
|
when Methods::PROMPTS_LIST
|
|
@@ -321,10 +305,15 @@ module MCP
|
|
|
321
305
|
{ contents: @handlers[Methods::RESOURCES_READ].call(params) }
|
|
322
306
|
when Methods::RESOURCES_TEMPLATES_LIST
|
|
323
307
|
{ resourceTemplates: @handlers[Methods::RESOURCES_TEMPLATES_LIST].call(params) }
|
|
308
|
+
when Methods::TOOLS_CALL
|
|
309
|
+
call_tool(params, session: session)
|
|
310
|
+
when Methods::LOGGING_SET_LEVEL
|
|
311
|
+
configure_logging_level(params, session: session)
|
|
324
312
|
else
|
|
325
313
|
@handlers[method].call(params)
|
|
326
314
|
end
|
|
327
|
-
|
|
315
|
+
client = session&.client || @client
|
|
316
|
+
add_instrumentation_data(client: client) if client
|
|
328
317
|
|
|
329
318
|
result
|
|
330
319
|
rescue => e
|
|
@@ -360,8 +349,14 @@ module MCP
|
|
|
360
349
|
}.compact
|
|
361
350
|
end
|
|
362
351
|
|
|
363
|
-
def init(params)
|
|
364
|
-
|
|
352
|
+
def init(params, session: nil)
|
|
353
|
+
if params
|
|
354
|
+
if session
|
|
355
|
+
session.store_client_info(client: params[:clientInfo], capabilities: params[:capabilities])
|
|
356
|
+
else
|
|
357
|
+
@client = params[:clientInfo]
|
|
358
|
+
end
|
|
359
|
+
end
|
|
365
360
|
|
|
366
361
|
protocol_version = params[:protocolVersion] if params
|
|
367
362
|
negotiated_version = if Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(protocol_version)
|
|
@@ -389,7 +384,7 @@ module MCP
|
|
|
389
384
|
}.compact
|
|
390
385
|
end
|
|
391
386
|
|
|
392
|
-
def configure_logging_level(request)
|
|
387
|
+
def configure_logging_level(request, session: nil)
|
|
393
388
|
if capabilities[:logging].nil?
|
|
394
389
|
raise RequestHandlerError.new("Server does not support logging", request, error_type: :internal_error)
|
|
395
390
|
end
|
|
@@ -399,6 +394,7 @@ module MCP
|
|
|
399
394
|
raise RequestHandlerError.new("Invalid log level #{request[:level]}", request, error_type: :invalid_params)
|
|
400
395
|
end
|
|
401
396
|
|
|
397
|
+
session&.configure_logging(logging_message_notification)
|
|
402
398
|
@logging_message_notification = logging_message_notification
|
|
403
399
|
|
|
404
400
|
{}
|
|
@@ -408,7 +404,7 @@ module MCP
|
|
|
408
404
|
@tools.values.map(&:to_h)
|
|
409
405
|
end
|
|
410
406
|
|
|
411
|
-
def call_tool(request)
|
|
407
|
+
def call_tool(request, session: nil)
|
|
412
408
|
tool_name = request[:name]
|
|
413
409
|
|
|
414
410
|
tool = tools[tool_name]
|
|
@@ -440,7 +436,7 @@ module MCP
|
|
|
440
436
|
|
|
441
437
|
progress_token = request.dig(:_meta, :progressToken)
|
|
442
438
|
|
|
443
|
-
call_tool_with_args(tool, arguments, server_context_with_meta(request), progress_token: progress_token)
|
|
439
|
+
call_tool_with_args(tool, arguments, server_context_with_meta(request), progress_token: progress_token, session: session)
|
|
444
440
|
rescue RequestHandlerError
|
|
445
441
|
raise
|
|
446
442
|
rescue => e
|
|
@@ -509,12 +505,12 @@ module MCP
|
|
|
509
505
|
parameters.any? { |type, name| type == :keyrest || name == :server_context }
|
|
510
506
|
end
|
|
511
507
|
|
|
512
|
-
def call_tool_with_args(tool, arguments, context, progress_token: nil)
|
|
508
|
+
def call_tool_with_args(tool, arguments, context, progress_token: nil, session: nil)
|
|
513
509
|
args = arguments&.transform_keys(&:to_sym) || {}
|
|
514
510
|
|
|
515
511
|
if accepts_server_context?(tool.method(:call))
|
|
516
|
-
progress = Progress.new(
|
|
517
|
-
server_context = ServerContext.new(context, progress: progress)
|
|
512
|
+
progress = Progress.new(notification_target: session, progress_token: progress_token)
|
|
513
|
+
server_context = ServerContext.new(context, progress: progress, notification_target: session)
|
|
518
514
|
tool.call(**args, server_context: server_context).to_h
|
|
519
515
|
else
|
|
520
516
|
tool.call(**args).to_h
|
data/lib/mcp/server_context.rb
CHANGED
|
@@ -2,15 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
module MCP
|
|
4
4
|
class ServerContext
|
|
5
|
-
def initialize(context, progress:)
|
|
5
|
+
def initialize(context, progress:, notification_target:)
|
|
6
6
|
@context = context
|
|
7
7
|
@progress = progress
|
|
8
|
+
@notification_target = notification_target
|
|
8
9
|
end
|
|
9
10
|
|
|
11
|
+
# Reports progress for the current tool operation.
|
|
12
|
+
# The notification is automatically scoped to the originating session.
|
|
13
|
+
#
|
|
14
|
+
# @param progress [Numeric] Current progress value.
|
|
15
|
+
# @param total [Numeric, nil] Total expected value.
|
|
16
|
+
# @param message [String, nil] Human-readable status message.
|
|
10
17
|
def report_progress(progress, total: nil, message: nil)
|
|
11
18
|
@progress.report(progress, total: total, message: message)
|
|
12
19
|
end
|
|
13
20
|
|
|
21
|
+
# Sends a log message notification scoped to the originating session.
|
|
22
|
+
#
|
|
23
|
+
# @param data [Object] The log data to send.
|
|
24
|
+
# @param level [String] Log level (e.g., `"debug"`, `"info"`, `"error"`).
|
|
25
|
+
# @param logger [String, nil] Logger name.
|
|
26
|
+
def notify_log_message(data:, level:, logger: nil)
|
|
27
|
+
return unless @notification_target
|
|
28
|
+
|
|
29
|
+
@notification_target.notify_log_message(data: data, level: level, logger: logger)
|
|
30
|
+
end
|
|
31
|
+
|
|
14
32
|
def method_missing(name, ...)
|
|
15
33
|
if @context.respond_to?(name)
|
|
16
34
|
@context.public_send(name, ...)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "methods"
|
|
4
|
+
|
|
5
|
+
module MCP
|
|
6
|
+
# Holds per-connection state for a single client session.
|
|
7
|
+
# Created by the transport layer; delegates request handling to the shared `Server`.
|
|
8
|
+
class ServerSession
|
|
9
|
+
attr_reader :session_id, :client, :logging_message_notification
|
|
10
|
+
|
|
11
|
+
def initialize(server:, transport:, session_id: nil)
|
|
12
|
+
@server = server
|
|
13
|
+
@transport = transport
|
|
14
|
+
@session_id = session_id
|
|
15
|
+
@client = nil
|
|
16
|
+
@client_capabilities = nil # TODO: Use for per-session capability validation.
|
|
17
|
+
@logging_message_notification = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def handle(request)
|
|
21
|
+
@server.handle(request, session: self)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def handle_json(request_json)
|
|
25
|
+
@server.handle_json(request_json, session: self)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Called by `Server#init` during the initialization handshake.
|
|
29
|
+
def store_client_info(client:, capabilities: nil)
|
|
30
|
+
@client = client
|
|
31
|
+
@client_capabilities = capabilities
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Called by `Server#configure_logging_level`.
|
|
35
|
+
def configure_logging(logging_message_notification)
|
|
36
|
+
@logging_message_notification = logging_message_notification
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Sends a progress notification to this session only.
|
|
40
|
+
def notify_progress(progress_token:, progress:, total: nil, message: nil)
|
|
41
|
+
params = {
|
|
42
|
+
"progressToken" => progress_token,
|
|
43
|
+
"progress" => progress,
|
|
44
|
+
"total" => total,
|
|
45
|
+
"message" => message,
|
|
46
|
+
}.compact
|
|
47
|
+
|
|
48
|
+
send_to_transport(Methods::NOTIFICATIONS_PROGRESS, params)
|
|
49
|
+
rescue => e
|
|
50
|
+
@server.report_exception(e, notification: "progress")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Sends a log message notification to this session only.
|
|
54
|
+
def notify_log_message(data:, level:, logger: nil)
|
|
55
|
+
effective_logging = @logging_message_notification || @server.logging_message_notification
|
|
56
|
+
return unless effective_logging&.should_notify?(level)
|
|
57
|
+
|
|
58
|
+
params = { "data" => data, "level" => level }
|
|
59
|
+
params["logger"] = logger if logger
|
|
60
|
+
|
|
61
|
+
send_to_transport(Methods::NOTIFICATIONS_MESSAGE, params)
|
|
62
|
+
rescue => e
|
|
63
|
+
@server.report_exception(e, { notification: "log_message" })
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# TODO: When Ruby 2.7 support is dropped, replace with a direct call:
|
|
69
|
+
# `@transport.send_notification(method, params, session_id: @session_id)` and
|
|
70
|
+
# add `**` to `Transport#send_notification` and `StdioTransport#send_notification`.
|
|
71
|
+
def send_to_transport(method, params)
|
|
72
|
+
if @session_id
|
|
73
|
+
@transport.send_notification(method, params, session_id: @session_id)
|
|
74
|
+
else
|
|
75
|
+
@transport.send_notification(method, params)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/mcp/transport.rb
CHANGED
|
@@ -36,8 +36,8 @@ module MCP
|
|
|
36
36
|
send_response(response) if response
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
# Send a notification to the client
|
|
40
|
-
# Returns true if the notification was sent successfully
|
|
39
|
+
# Send a notification to the client.
|
|
40
|
+
# Returns true if the notification was sent successfully.
|
|
41
41
|
def send_notification(method, params = nil)
|
|
42
42
|
raise NotImplementedError, "Subclasses must implement send_notification"
|
|
43
43
|
end
|
data/lib/mcp/version.rb
CHANGED
data/lib/mcp.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Model Context Protocol
|
|
@@ -60,6 +60,7 @@ files:
|
|
|
60
60
|
- lib/mcp/server/transports/stdio_transport.rb
|
|
61
61
|
- lib/mcp/server/transports/streamable_http_transport.rb
|
|
62
62
|
- lib/mcp/server_context.rb
|
|
63
|
+
- lib/mcp/server_session.rb
|
|
63
64
|
- lib/mcp/string_utils.rb
|
|
64
65
|
- lib/mcp/tool.rb
|
|
65
66
|
- lib/mcp/tool/annotations.rb
|
|
@@ -75,7 +76,7 @@ licenses:
|
|
|
75
76
|
- Apache-2.0
|
|
76
77
|
metadata:
|
|
77
78
|
allowed_push_host: https://rubygems.org
|
|
78
|
-
changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.
|
|
79
|
+
changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.10.0
|
|
79
80
|
homepage_uri: https://github.com/modelcontextprotocol/ruby-sdk
|
|
80
81
|
source_code_uri: https://github.com/modelcontextprotocol/ruby-sdk
|
|
81
82
|
bug_tracker_uri: https://github.com/modelcontextprotocol/ruby-sdk/issues
|