model-context-protocol-rb 0.4.0 → 0.5.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/CHANGELOG.md +9 -1
- data/README.md +155 -12
- data/lib/model_context_protocol/server/cancellable.rb +54 -0
- data/lib/model_context_protocol/server/configuration.rb +4 -9
- data/lib/model_context_protocol/server/progressable.rb +72 -0
- data/lib/model_context_protocol/server/prompt.rb +3 -1
- data/lib/model_context_protocol/server/redis_client_proxy.rb +134 -0
- data/lib/model_context_protocol/server/redis_config.rb +108 -0
- data/lib/model_context_protocol/server/redis_pool_manager.rb +110 -0
- data/lib/model_context_protocol/server/resource.rb +3 -0
- data/lib/model_context_protocol/server/router.rb +36 -3
- data/lib/model_context_protocol/server/stdio_transport/request_store.rb +102 -0
- data/lib/model_context_protocol/server/stdio_transport.rb +31 -6
- data/lib/model_context_protocol/server/streamable_http_transport/event_counter.rb +35 -0
- data/lib/model_context_protocol/server/streamable_http_transport/message_poller.rb +101 -0
- data/lib/model_context_protocol/server/streamable_http_transport/notification_queue.rb +80 -0
- data/lib/model_context_protocol/server/streamable_http_transport/request_store.rb +224 -0
- data/lib/model_context_protocol/server/streamable_http_transport/session_message_queue.rb +120 -0
- data/lib/model_context_protocol/server/{session_store.rb → streamable_http_transport/session_store.rb} +30 -16
- data/lib/model_context_protocol/server/streamable_http_transport/stream_registry.rb +119 -0
- data/lib/model_context_protocol/server/streamable_http_transport.rb +162 -79
- data/lib/model_context_protocol/server/tool.rb +4 -0
- data/lib/model_context_protocol/server.rb +9 -3
- data/lib/model_context_protocol/version.rb +1 -1
- data/tasks/templates/dev-http.erb +58 -14
- metadata +57 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5f97580e7f9c5723d25472b545c037bad650d82434865324c6d1dd450ac4031e
|
4
|
+
data.tar.gz: 1b73ad6036c7ded852210dc338190675e419c6f6354e027d2b7073a5e655c665
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3075cfc900a60fd1cb83b973c23f6f517373f6132ab043ea64079653bbc0ecc5c5aeb8a077e2c685396f9921d80ad341f9fa271fd343fb9b9f29ce93b6abda4b
|
7
|
+
data.tar.gz: c72619fea381f8968d7dd33e60c306449973bbeda9f5a8ffd6b8d5baf95d26df8278a2106b82daeef61982a950b74b5f168a8afb680baa6e44f10d26626cac69
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.5.0] - 2025-09-22
|
4
|
+
|
5
|
+
- Make streamable HTTP transport thread-safe by using Redis to manage state.
|
6
|
+
- Implement Redis connection pooling with robust management and configuration.
|
7
|
+
- Automatically upgrade connection to SSE to send notifications.
|
8
|
+
- Add support for cancellations and progress notifications via `cancellable` and `progressable` blocks in prompts, resources, and tools.
|
9
|
+
|
3
10
|
## [0.4.0] - 2025-09-07
|
4
11
|
|
5
12
|
- Implement pagination support.
|
@@ -67,7 +74,8 @@
|
|
67
74
|
|
68
75
|
- Initial release
|
69
76
|
|
70
|
-
[Unreleased]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.
|
77
|
+
[Unreleased]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.5.0...HEAD
|
78
|
+
[0.5.0]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.4.0...v0.5.0
|
71
79
|
[0.4.0]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.4...v0.4.0
|
72
80
|
[0.3.4]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.3...v0.3.4
|
73
81
|
[0.3.3]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.2...v0.3.3
|
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# model-context-protocol-rb
|
2
2
|
|
3
|
-
An implementation of the [Model Context Protocol (MCP)](https://spec.modelcontextprotocol.io/specification/2025-06-18/) in Ruby.
|
3
|
+
An implementation of the [Model Context Protocol (MCP)](https://spec.modelcontextprotocol.io/specification/2025-06-18/) in Ruby.
|
4
|
+
|
5
|
+
Provides simple abstractions that allow you to serve prompts, resources, resource templates, and tools via MCP locally (stdio) or in production (streamable HTTP backed by Redis) with minimal effort.
|
4
6
|
|
5
7
|
## Table of Contents
|
6
8
|
|
@@ -44,9 +46,9 @@ An implementation of the [Model Context Protocol (MCP)](https://spec.modelcontex
|
|
44
46
|
| ❌ | [List Changed Notification (Resources)](https://modelcontextprotocol.io/specification/2025-06-18/server/resources#list-changed-notification) |
|
45
47
|
| ❌ | [Subscriptions (Resources)](https://modelcontextprotocol.io/specification/2025-06-18/server/resources#subscriptions) |
|
46
48
|
| ❌ | [List Changed Notification (Tools)](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#list-changed-notification) |
|
47
|
-
|
|
49
|
+
| ✅ | [Cancellation](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation) |
|
48
50
|
| ✅ | [Ping](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/ping) |
|
49
|
-
|
|
51
|
+
| ✅ | [Progress](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress) |
|
50
52
|
|
51
53
|
## Usage
|
52
54
|
|
@@ -116,7 +118,7 @@ server = ModelContextProtocol::Server.new do |config|
|
|
116
118
|
# Optional: configure streamable HTTP transport if required
|
117
119
|
# config.transport = {
|
118
120
|
# type: :streamable_http,
|
119
|
-
#
|
121
|
+
# env: request.env,
|
120
122
|
# session_ttl: 3600 # Optional: session timeout in seconds (default: 3600)
|
121
123
|
# }
|
122
124
|
|
@@ -182,12 +184,33 @@ config.transport = { type: :stdio } # This is the default, can be omitted
|
|
182
184
|
```
|
183
185
|
|
184
186
|
##### Streamable HTTP Transport
|
185
|
-
|
187
|
+
The `:streamable_http` transport requires Redis to be configured globally before use:
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
ModelContextProtocol::Server.configure_redis do |config|
|
191
|
+
config.redis_url = ENV.fetch('REDIS_URL')
|
192
|
+
config.pool_size = 20
|
193
|
+
config.pool_timeout = 5
|
194
|
+
config.enable_reaper = true
|
195
|
+
config.reaper_interval = 60
|
196
|
+
config.idle_timeout = 300
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
| Option | Type | Required | Default | Description |
|
201
|
+
|--------|------|----------|---------|-------------|
|
202
|
+
| `redis_url` | String | Yes | - | Redis connection URL |
|
203
|
+
| `pool_size` | Integer | No | `20` | Connection pool size |
|
204
|
+
| `pool_timeout` | Integer | No | `5` | Pool checkout timeout in seconds |
|
205
|
+
| `enable_reaper` | Boolean | No | `true` | Enable connection reaping |
|
206
|
+
| `reaper_interval` | Integer | No | `60` | Reaper check interval in seconds |
|
207
|
+
| `idle_timeout` | Integer | No | `300` | Idle connection timeout in seconds |
|
208
|
+
|
209
|
+
When using `:streamable_http` transport, the following options are available:
|
186
210
|
|
187
211
|
| Option | Type | Required | Default | Description |
|
188
212
|
|--------|------|----------|---------|-------------|
|
189
213
|
| `type` | Symbol | Yes | `:stdio` | Must be `:streamable_http` for HTTP transport |
|
190
|
-
| `redis_client` | Redis | Yes | - | Redis client instance for session management |
|
191
214
|
| `session_ttl` | Integer | No | `3600` | Session timeout in seconds (1 hour) |
|
192
215
|
| `env` | Hash | No | - | Rack environment hash (for Rails integration) |
|
193
216
|
|
@@ -226,7 +249,21 @@ end
|
|
226
249
|
|
227
250
|
The streamable HTTP transport works with any valid Rack request. Here's an example of how you can integrate with Rails.
|
228
251
|
|
229
|
-
First,
|
252
|
+
First, configure Redis in an initializer:
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
# config/initializers/model_context_protocol.rb
|
256
|
+
ModelContextProtocol::Server.configure_redis do |config|
|
257
|
+
config.redis_url = ENV.fetch('REDIS_URL')
|
258
|
+
config.pool_size = 20
|
259
|
+
config.pool_timeout = 5
|
260
|
+
config.enable_reaper = true
|
261
|
+
config.reaper_interval = 60
|
262
|
+
config.idle_timeout = 300
|
263
|
+
end
|
264
|
+
```
|
265
|
+
|
266
|
+
Then, set the routes:
|
230
267
|
|
231
268
|
```ruby
|
232
269
|
constraints format: :json do
|
@@ -257,8 +294,7 @@ class ModelContextProtocolController < ApplicationController
|
|
257
294
|
config.registry = build_registry
|
258
295
|
config.transport = {
|
259
296
|
type: :streamable_http,
|
260
|
-
|
261
|
-
env: request.env # Rack environment hash
|
297
|
+
env: request.env
|
262
298
|
}
|
263
299
|
config.instructions = <<~INSTRUCTIONS
|
264
300
|
This server provides prompts, tools, and resources for interacting with my app.
|
@@ -330,12 +366,14 @@ Define any arguments using `argument` blocks nested within the `define` block. Y
|
|
330
366
|
|
331
367
|
#### Prompt Methods
|
332
368
|
|
333
|
-
Define your prompt properties and arguments, implement the `call` method using the `message_history` DSL to build prompt messages and `respond_with` to serialize them.
|
369
|
+
Define your prompt properties and arguments, implement the `call` method using the `message_history` DSL to build prompt messages and `respond_with` to serialize them. You can wrap long running operations in a `cancellable` block to allow clients to cancel the request. Also, you can automatically send progress notifications to clients by wrapping long-running operations in a `progressable` block.
|
334
370
|
|
335
371
|
| Method | Context | Description |
|
336
372
|
|--------|---------|-------------|
|
337
373
|
| `define` | Class definition | Block for defining prompt metadata and arguments |
|
338
374
|
| `call` | Instance method | Main method to implement prompt logic and build response |
|
375
|
+
| `cancellable` | Within `call` | Wrap long-running operations to allow client cancellation (e.g., `cancellable { slow_operation }`) |
|
376
|
+
| `progressable` | Within `call` | Wrap long-running operations to send clients progress notifications (e.g., `progressable { slow_operation }`) |
|
339
377
|
| `message_history` | Within `call` | DSL method to build an array of user and assistant messages |
|
340
378
|
| `respond_with` | Within `call` | Return properly formatted response data (e.g., `respond_with messages:`) |
|
341
379
|
|
@@ -480,12 +518,14 @@ Define any [resource annotations](https://modelcontextprotocol.io/specification/
|
|
480
518
|
|
481
519
|
#### Resource Methods
|
482
520
|
|
483
|
-
Define your resource properties and annotations, implement the `call` method to build resource content and `respond_with` to serialize the response.
|
521
|
+
Define your resource properties and annotations, implement the `call` method to build resource content and `respond_with` to serialize the response. You can wrap long running operations in a `cancellable` block to allow clients to cancel the request. Also, you can automatically send progress notifications to clients by wrapping long-running operations in a `progressable` block.
|
484
522
|
|
485
523
|
| Method | Context | Description |
|
486
524
|
|--------|---------|-------------|
|
487
525
|
| `define` | Class definition | Block for defining resource metadata and annotations |
|
488
526
|
| `call` | Instance method | Main method to implement resource logic and build response |
|
527
|
+
| `cancellable` | Within `call` | Wrap long-running operations to allow client cancellation (e.g., `cancellable { slow_operation }`) |
|
528
|
+
| `progressable` | Within `call` | Wrap long-running operations to send clients progress notifications (e.g., `progressable { slow_operation }`) |
|
489
529
|
| `respond_with` | Within `call` | Return properly formatted response data (e.g., `respond_with text:` or `respond_with binary:`) |
|
490
530
|
|
491
531
|
#### Available Instance Variables
|
@@ -648,12 +688,14 @@ Use the `define` block to set [tool properties](https://spec.modelcontextprotoco
|
|
648
688
|
|
649
689
|
#### Tool Methods
|
650
690
|
|
651
|
-
Define your tool properties and schemas, implement the `call` method using content helpers and `respond_with` to serialize responses.
|
691
|
+
Define your tool properties and schemas, implement the `call` method using content helpers and `respond_with` to serialize responses. You can wrap long running operations in a `cancellable` block to allow clients to cancel the request. Also, you can automatically send progress notifications to clients by wrapping long-running operations in a `progressable` block.
|
652
692
|
|
653
693
|
| Method | Context | Description |
|
654
694
|
|--------|---------|-------------|
|
655
695
|
| `define` | Class definition | Block for defining tool metadata and schemas |
|
656
696
|
| `call` | Instance method | Main method to implement tool logic and build response |
|
697
|
+
| `cancellable` | Within `call` | Wrap long-running operations to allow client cancellation (e.g., `cancellable { slow_operation }`) |
|
698
|
+
| `progressable` | Within `call` | Wrap long-running operations to send clients progress notifications (e.g., `progressable { slow_operation }`) |
|
657
699
|
| `respond_with` | Within `call` | Return properly formatted response data with various content types |
|
658
700
|
|
659
701
|
#### Content Blocks
|
@@ -973,6 +1015,87 @@ class TestToolWithToolErrorResponse < ModelContextProtocol::Server::Tool
|
|
973
1015
|
end
|
974
1016
|
```
|
975
1017
|
|
1018
|
+
This is an example of a tool that allows a client to cancel a long-running operation:
|
1019
|
+
|
1020
|
+
```ruby
|
1021
|
+
class TestToolWithCancellableSleep < ModelContextProtocol::Server::Tool
|
1022
|
+
define do
|
1023
|
+
name "cancellable_sleep"
|
1024
|
+
title "Cancellable Sleep Tool"
|
1025
|
+
description "Sleep for 3 seconds with cancellation support"
|
1026
|
+
input_schema do
|
1027
|
+
{
|
1028
|
+
type: "object",
|
1029
|
+
properties: {},
|
1030
|
+
additionalProperties: false
|
1031
|
+
}
|
1032
|
+
end
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
def call
|
1036
|
+
logger.info("Starting 3 second sleep operation")
|
1037
|
+
|
1038
|
+
result = cancellable do
|
1039
|
+
sleep 3
|
1040
|
+
"Sleep completed successfully"
|
1041
|
+
end
|
1042
|
+
|
1043
|
+
respond_with content: text_content(text: result)
|
1044
|
+
end
|
1045
|
+
end
|
1046
|
+
```
|
1047
|
+
|
1048
|
+
This is an example of a tool that automatically sends progress notifications to the client and allows the client to cancel the operation:
|
1049
|
+
|
1050
|
+
```ruby
|
1051
|
+
class TestToolWithProgressableAndCancellable < ModelContextProtocol::Server::Tool
|
1052
|
+
define do
|
1053
|
+
name "test_tool_with_progressable_and_cancellable"
|
1054
|
+
description "A test tool that demonstrates combined progressable and cancellable functionality"
|
1055
|
+
|
1056
|
+
input_schema do
|
1057
|
+
{
|
1058
|
+
type: "object",
|
1059
|
+
properties: {
|
1060
|
+
max_duration: {
|
1061
|
+
type: "number",
|
1062
|
+
description: "Expected maximum duration in seconds"
|
1063
|
+
},
|
1064
|
+
work_steps: {
|
1065
|
+
type: "number",
|
1066
|
+
description: "Number of work steps to perform"
|
1067
|
+
}
|
1068
|
+
},
|
1069
|
+
required: ["max_duration"]
|
1070
|
+
}
|
1071
|
+
end
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
def call
|
1075
|
+
max_duration = arguments[:max_duration] || 10
|
1076
|
+
work_steps = arguments[:work_steps] || 10
|
1077
|
+
logger.info("Starting progressable call with max_duration=#{max_duration}, work_steps=#{work_steps}")
|
1078
|
+
|
1079
|
+
result = progressable(max_duration:, message: "Processing #{work_steps} items") do
|
1080
|
+
cancellable do
|
1081
|
+
processed_items = []
|
1082
|
+
|
1083
|
+
work_steps.times do |i|
|
1084
|
+
sleep(max_duration / work_steps.to_f)
|
1085
|
+
processed_items << "item_#{i + 1}"
|
1086
|
+
end
|
1087
|
+
|
1088
|
+
processed_items
|
1089
|
+
end
|
1090
|
+
end
|
1091
|
+
|
1092
|
+
response = text_content(text: "Successfully processed #{result.length} items: #{result.join(", ")}")
|
1093
|
+
|
1094
|
+
respond_with content: response
|
1095
|
+
end
|
1096
|
+
end
|
1097
|
+
```
|
1098
|
+
|
976
1099
|
### Completions
|
977
1100
|
|
978
1101
|
The `ModelContextProtocol::Server::Completion` base class allows subclasses to define a completion that the MCP client can use to obtain hints or suggestions for arguments to prompts and resources.
|
@@ -1038,6 +1161,8 @@ gem install model-context-protocol-rb
|
|
1038
1161
|
|
1039
1162
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests.
|
1040
1163
|
|
1164
|
+
### Generate Development Servers
|
1165
|
+
|
1041
1166
|
Generate executables that you can use for testing:
|
1042
1167
|
|
1043
1168
|
```bash
|
@@ -1048,6 +1173,24 @@ bundle exec rake mcp:generate_stdio_server
|
|
1048
1173
|
bundle exec rake mcp:generate_streamable_http_server
|
1049
1174
|
```
|
1050
1175
|
|
1176
|
+
If you need to test with HTTPS (e.g., for clients that require SSL), generate self-signed certificates:
|
1177
|
+
|
1178
|
+
```bash
|
1179
|
+
# Create SSL directory and generate certificates
|
1180
|
+
mkdir -p tmp/ssl
|
1181
|
+
openssl req -x509 -newkey rsa:4096 -keyout tmp/ssl/server.key -out tmp/ssl/server.crt -days 365 -nodes -subj "/C=US/ST=Dev/L=Dev/O=Dev/CN=localhost"
|
1182
|
+
```
|
1183
|
+
|
1184
|
+
The HTTP server supports both HTTP and HTTPS:
|
1185
|
+
|
1186
|
+
```bash
|
1187
|
+
# Run HTTP server (default)
|
1188
|
+
bin/dev-http
|
1189
|
+
|
1190
|
+
# Run HTTPS server (requires SSL certificates in tmp/ssl/)
|
1191
|
+
SSL=true bin/dev-http
|
1192
|
+
```
|
1193
|
+
|
1051
1194
|
You can also run `bin/console` for an interactive prompt that will allow you to experiment. Execute command `rp` to reload the project.
|
1052
1195
|
|
1053
1196
|
To install this gem onto your local machine, run `bundle exec rake install`.
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require "concurrent-ruby"
|
2
|
+
|
3
|
+
module ModelContextProtocol
|
4
|
+
module Server::Cancellable
|
5
|
+
# Raised when a request has been cancelled by the client
|
6
|
+
class CancellationError < StandardError; end
|
7
|
+
|
8
|
+
# Execute a block with automatic cancellation support for blocking I/O operations.
|
9
|
+
# This method uses Concurrent::TimerTask to poll for cancellation every 100ms
|
10
|
+
# and can interrupt even blocking operations like HTTP requests or database queries.
|
11
|
+
#
|
12
|
+
# @param interval [Float] polling interval in seconds (default: 0.1)
|
13
|
+
# @yield block to execute with cancellation support
|
14
|
+
# @return [Object] the result of the block
|
15
|
+
# @raise [CancellationError] if the request is cancelled during execution
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# cancellable do
|
19
|
+
# response = Net::HTTP.get(URI('https://slow-api.example.com'))
|
20
|
+
# process_response(response)
|
21
|
+
# end
|
22
|
+
def cancellable(interval: 0.1, &block)
|
23
|
+
context = Thread.current[:mcp_context]
|
24
|
+
executing_thread = Concurrent::AtomicReference.new(nil)
|
25
|
+
|
26
|
+
timer_task = Concurrent::TimerTask.new(execution_interval: interval) do
|
27
|
+
if context && context[:request_store] && context[:request_id]
|
28
|
+
if context[:request_store].cancelled?(context[:request_id])
|
29
|
+
thread = executing_thread.get
|
30
|
+
thread&.raise(CancellationError, "Request was cancelled") if thread&.alive?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
begin
|
36
|
+
executing_thread.set(Thread.current)
|
37
|
+
|
38
|
+
if context && context[:request_store] && context[:request_id]
|
39
|
+
if context[:request_store].cancelled?(context[:request_id])
|
40
|
+
raise CancellationError, "Request #{context[:request_id]} was cancelled"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
timer_task.execute
|
45
|
+
|
46
|
+
result = block.call
|
47
|
+
result
|
48
|
+
ensure
|
49
|
+
executing_thread.set(nil)
|
50
|
+
timer_task&.shutdown if timer_task&.running?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -200,15 +200,10 @@ module ModelContextProtocol
|
|
200
200
|
end
|
201
201
|
|
202
202
|
def validate_streamable_http_transport!
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
end
|
208
|
-
|
209
|
-
redis_client = options[:redis_client]
|
210
|
-
unless redis_client.respond_to?(:hset) && redis_client.respond_to?(:expire)
|
211
|
-
raise InvalidTransportError, "redis_client must be a Redis-compatible client"
|
203
|
+
unless ModelContextProtocol::Server::RedisConfig.configured?
|
204
|
+
raise InvalidTransportError,
|
205
|
+
"streamable_http transport requires Redis configuration. " \
|
206
|
+
"Call ModelContextProtocol::Server.configure_redis in an initializer."
|
212
207
|
end
|
213
208
|
end
|
214
209
|
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "concurrent-ruby"
|
2
|
+
|
3
|
+
module ModelContextProtocol
|
4
|
+
module Server::Progressable
|
5
|
+
# Execute a block with automatic time-based progress reporting.
|
6
|
+
# Uses Concurrent::TimerTask to send progress notifications at regular intervals.
|
7
|
+
#
|
8
|
+
# @param max_duration [Numeric] Expected duration in seconds
|
9
|
+
# @param message [String, nil] Optional custom progress message
|
10
|
+
# @yield block to execute with progress tracking
|
11
|
+
# @return [Object] the result of the block
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# progressable(max_duration: 30) do # 30 seconds
|
15
|
+
# perform_long_operation
|
16
|
+
# end
|
17
|
+
def progressable(max_duration:, message: nil, &block)
|
18
|
+
context = Thread.current[:mcp_context]
|
19
|
+
|
20
|
+
return yield unless context && context[:progress_token] && context[:transport]
|
21
|
+
|
22
|
+
progress_token = context[:progress_token]
|
23
|
+
transport = context[:transport]
|
24
|
+
start_time = Time.now
|
25
|
+
update_interval = [1.0, max_duration * 0.05].max
|
26
|
+
|
27
|
+
timer_task = Concurrent::TimerTask.new(execution_interval: update_interval) do
|
28
|
+
elapsed_seconds = Time.now - start_time
|
29
|
+
progress_pct = [(elapsed_seconds / max_duration) * 100, 99].min
|
30
|
+
|
31
|
+
progress_message = if message
|
32
|
+
"#{message} (#{elapsed_seconds.round(1)}s / ~#{max_duration}s)"
|
33
|
+
else
|
34
|
+
"Processing... (#{elapsed_seconds.round(1)}s / ~#{max_duration}s)"
|
35
|
+
end
|
36
|
+
|
37
|
+
begin
|
38
|
+
transport.send_notification("notifications/progress", {
|
39
|
+
progressToken: progress_token,
|
40
|
+
progress: progress_pct.round(1),
|
41
|
+
total: 100,
|
42
|
+
message: progress_message
|
43
|
+
})
|
44
|
+
rescue
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
timer_task.shutdown if elapsed_seconds >= max_duration
|
49
|
+
end
|
50
|
+
|
51
|
+
begin
|
52
|
+
timer_task.execute
|
53
|
+
result = yield
|
54
|
+
|
55
|
+
begin
|
56
|
+
transport.send_notification("notifications/progress", {
|
57
|
+
progressToken: progress_token,
|
58
|
+
progress: 100,
|
59
|
+
total: 100,
|
60
|
+
message: "Completed"
|
61
|
+
})
|
62
|
+
rescue
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
result
|
67
|
+
ensure
|
68
|
+
timer_task&.shutdown if timer_task&.running?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module ModelContextProtocol
|
2
2
|
class Server::Prompt
|
3
|
-
include Server::
|
3
|
+
include ModelContextProtocol::Server::Cancellable
|
4
|
+
include ModelContextProtocol::Server::ContentHelpers
|
5
|
+
include ModelContextProtocol::Server::Progressable
|
4
6
|
|
5
7
|
attr_reader :arguments, :context, :logger
|
6
8
|
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ModelContextProtocol
|
4
|
+
class Server
|
5
|
+
class RedisClientProxy
|
6
|
+
def initialize(pool)
|
7
|
+
@pool = pool
|
8
|
+
end
|
9
|
+
|
10
|
+
def get(key)
|
11
|
+
with_connection { |redis| redis.get(key) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def set(key, value, **options)
|
15
|
+
with_connection { |redis| redis.set(key, value, **options) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def del(*keys)
|
19
|
+
with_connection { |redis| redis.del(*keys) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def exists(*keys)
|
23
|
+
with_connection { |redis| redis.exists(*keys) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def expire(key, seconds)
|
27
|
+
with_connection { |redis| redis.expire(key, seconds) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def ttl(key)
|
31
|
+
with_connection { |redis| redis.ttl(key) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def hget(key, field)
|
35
|
+
with_connection { |redis| redis.hget(key, field) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def hset(key, *args)
|
39
|
+
with_connection { |redis| redis.hset(key, *args) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def hgetall(key)
|
43
|
+
with_connection { |redis| redis.hgetall(key) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def lpush(key, *values)
|
47
|
+
with_connection { |redis| redis.lpush(key, *values) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def rpop(key)
|
51
|
+
with_connection { |redis| redis.rpop(key) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def lrange(key, start, stop)
|
55
|
+
with_connection { |redis| redis.lrange(key, start, stop) }
|
56
|
+
end
|
57
|
+
|
58
|
+
def llen(key)
|
59
|
+
with_connection { |redis| redis.llen(key) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def ltrim(key, start, stop)
|
63
|
+
with_connection { |redis| redis.ltrim(key, start, stop) }
|
64
|
+
end
|
65
|
+
|
66
|
+
def incr(key)
|
67
|
+
with_connection { |redis| redis.incr(key) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def decr(key)
|
71
|
+
with_connection { |redis| redis.decr(key) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def keys(pattern)
|
75
|
+
with_connection { |redis| redis.keys(pattern) }
|
76
|
+
end
|
77
|
+
|
78
|
+
def multi(&block)
|
79
|
+
with_connection do |redis|
|
80
|
+
redis.multi do |multi|
|
81
|
+
multi_wrapper = RedisMultiWrapper.new(multi)
|
82
|
+
block.call(multi_wrapper)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def pipelined(&block)
|
88
|
+
with_connection do |redis|
|
89
|
+
redis.pipelined do |pipeline|
|
90
|
+
pipeline_wrapper = RedisMultiWrapper.new(pipeline)
|
91
|
+
block.call(pipeline_wrapper)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def mget(*keys)
|
97
|
+
with_connection { |redis| redis.mget(*keys) }
|
98
|
+
end
|
99
|
+
|
100
|
+
def eval(script, keys: [], argv: [])
|
101
|
+
with_connection { |redis| redis.eval(script, keys: keys, argv: argv) }
|
102
|
+
end
|
103
|
+
|
104
|
+
def ping
|
105
|
+
with_connection { |redis| redis.ping }
|
106
|
+
end
|
107
|
+
|
108
|
+
def flushdb
|
109
|
+
with_connection { |redis| redis.flushdb }
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def with_connection(&block)
|
115
|
+
@pool.with(&block)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Wrapper for Redis multi/pipeline operations
|
119
|
+
class RedisMultiWrapper
|
120
|
+
def initialize(multi)
|
121
|
+
@multi = multi
|
122
|
+
end
|
123
|
+
|
124
|
+
def method_missing(method, *args, **kwargs, &block)
|
125
|
+
@multi.send(method, *args, **kwargs, &block)
|
126
|
+
end
|
127
|
+
|
128
|
+
def respond_to_missing?(method, include_private = false)
|
129
|
+
@multi.respond_to?(method, include_private)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|