model-context-protocol-rb 0.3.4 → 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 +26 -1
- data/README.md +886 -196
- data/lib/model_context_protocol/server/cancellable.rb +54 -0
- data/lib/model_context_protocol/server/configuration.rb +80 -8
- data/lib/model_context_protocol/server/content.rb +321 -0
- data/lib/model_context_protocol/server/content_helpers.rb +84 -0
- data/lib/model_context_protocol/server/pagination.rb +71 -0
- data/lib/model_context_protocol/server/progressable.rb +72 -0
- data/lib/model_context_protocol/server/prompt.rb +108 -14
- 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/registry.rb +94 -18
- data/lib/model_context_protocol/server/resource.rb +98 -25
- data/lib/model_context_protocol/server/resource_template.rb +26 -13
- 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 +352 -112
- data/lib/model_context_protocol/server/tool.rb +79 -53
- data/lib/model_context_protocol/server.rb +124 -21
- data/lib/model_context_protocol/version.rb +1 -1
- data/tasks/mcp.rake +28 -2
- data/tasks/templates/dev-http.erb +288 -0
- data/tasks/templates/dev.erb +7 -1
- metadata +61 -3
@@ -2,6 +2,13 @@ require "json-schema"
|
|
2
2
|
|
3
3
|
module ModelContextProtocol
|
4
4
|
class Server::Tool
|
5
|
+
# Raised when output schema validation fails.
|
6
|
+
class OutputSchemaValidationError < StandardError; end
|
7
|
+
|
8
|
+
include ModelContextProtocol::Server::Cancellable
|
9
|
+
include ModelContextProtocol::Server::ContentHelpers
|
10
|
+
include ModelContextProtocol::Server::Progressable
|
11
|
+
|
5
12
|
attr_reader :arguments, :context, :logger
|
6
13
|
|
7
14
|
def initialize(arguments, logger, context = {})
|
@@ -15,58 +22,58 @@ module ModelContextProtocol
|
|
15
22
|
raise NotImplementedError, "Subclasses must implement the call method"
|
16
23
|
end
|
17
24
|
|
18
|
-
|
19
|
-
def serialized
|
20
|
-
{content: [{type: "text", text:}], isError: false}
|
21
|
-
end
|
22
|
-
end
|
23
|
-
private_constant :TextResponse
|
24
|
-
|
25
|
-
ImageResponse = Data.define(:data, :mime_type) do
|
26
|
-
def initialize(data:, mime_type: "image/png")
|
27
|
-
super
|
28
|
-
end
|
29
|
-
|
25
|
+
Response = Data.define(:content) do
|
30
26
|
def serialized
|
31
|
-
|
27
|
+
serialized_contents = content.map(&:serialized)
|
28
|
+
{content: serialized_contents, isError: false}
|
32
29
|
end
|
33
30
|
end
|
34
|
-
private_constant :
|
35
|
-
|
36
|
-
ResourceResponse = Data.define(:uri, :text, :mime_type) do
|
37
|
-
def initialize(uri:, text:, mime_type: "text/plain")
|
38
|
-
super
|
39
|
-
end
|
31
|
+
private_constant :Response
|
40
32
|
|
33
|
+
StructuredContentResponse = Data.define(:structured_content, :tool) do
|
41
34
|
def serialized
|
42
|
-
|
35
|
+
json_text = JSON.generate(structured_content)
|
36
|
+
text_content = ModelContextProtocol::Server::Content::Text[
|
37
|
+
meta: nil,
|
38
|
+
annotations: nil,
|
39
|
+
text: json_text
|
40
|
+
]
|
41
|
+
|
42
|
+
validation_errors = JSON::Validator.fully_validate(
|
43
|
+
tool.class.definition[:outputSchema], structured_content
|
44
|
+
)
|
45
|
+
|
46
|
+
if validation_errors.empty?
|
47
|
+
{
|
48
|
+
structuredContent: structured_content,
|
49
|
+
content: [text_content.serialized],
|
50
|
+
isError: false
|
51
|
+
}
|
52
|
+
else
|
53
|
+
raise OutputSchemaValidationError, validation_errors.join(", ")
|
54
|
+
end
|
43
55
|
end
|
44
56
|
end
|
45
|
-
private_constant :
|
57
|
+
private_constant :StructuredContentResponse
|
46
58
|
|
47
|
-
|
59
|
+
ErrorResponse = Data.define(:error) do
|
48
60
|
def serialized
|
49
|
-
{content: [{type: "text", text:}], isError: true}
|
61
|
+
{content: [{type: "text", text: error}], isError: true}
|
50
62
|
end
|
51
63
|
end
|
52
|
-
private_constant :
|
53
|
-
|
54
|
-
private def respond_with(
|
55
|
-
case [
|
56
|
-
in [
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
ResourceResponse[mime_type:, text:, uri:]
|
64
|
-
in [:resource, {text:, uri:}]
|
65
|
-
ResourceResponse[text:, uri:]
|
66
|
-
in [:error, {text:}]
|
67
|
-
ToolErrorResponse[text:]
|
64
|
+
private_constant :ErrorResponse
|
65
|
+
|
66
|
+
private def respond_with(**kwargs)
|
67
|
+
case [kwargs]
|
68
|
+
in [{content:}]
|
69
|
+
content_array = content.is_a?(Array) ? content : [content]
|
70
|
+
Response[content: content_array]
|
71
|
+
in [{structured_content:}]
|
72
|
+
StructuredContentResponse[structured_content:, tool: self]
|
73
|
+
in [{error:}]
|
74
|
+
ErrorResponse[error:]
|
68
75
|
else
|
69
|
-
raise ModelContextProtocol::Server::ResponseArgumentsError, "Invalid arguments: #{
|
76
|
+
raise ModelContextProtocol::Server::ResponseArgumentsError, "Invalid arguments: #{kwargs.inspect}"
|
70
77
|
end
|
71
78
|
end
|
72
79
|
|
@@ -75,39 +82,48 @@ module ModelContextProtocol
|
|
75
82
|
end
|
76
83
|
|
77
84
|
class << self
|
78
|
-
attr_reader :name, :description, :input_schema
|
85
|
+
attr_reader :name, :description, :title, :input_schema, :output_schema
|
79
86
|
|
80
|
-
def
|
81
|
-
|
82
|
-
|
87
|
+
def define(&block)
|
88
|
+
definition_dsl = DefinitionDSL.new
|
89
|
+
definition_dsl.instance_eval(&block)
|
83
90
|
|
84
|
-
@name =
|
85
|
-
@description =
|
86
|
-
@
|
91
|
+
@name = definition_dsl.name
|
92
|
+
@description = definition_dsl.description
|
93
|
+
@title = definition_dsl.title
|
94
|
+
@input_schema = definition_dsl.input_schema
|
95
|
+
@output_schema = definition_dsl.output_schema
|
87
96
|
end
|
88
97
|
|
89
98
|
def inherited(subclass)
|
90
99
|
subclass.instance_variable_set(:@name, @name)
|
91
100
|
subclass.instance_variable_set(:@description, @description)
|
101
|
+
subclass.instance_variable_set(:@title, @title)
|
92
102
|
subclass.instance_variable_set(:@input_schema, @input_schema)
|
103
|
+
subclass.instance_variable_set(:@output_schema, @output_schema)
|
93
104
|
end
|
94
105
|
|
95
106
|
def call(arguments, logger, context = {})
|
96
107
|
new(arguments, logger, context).call
|
97
108
|
rescue JSON::Schema::ValidationError => validation_error
|
98
109
|
raise ModelContextProtocol::Server::ParameterValidationError, validation_error.message
|
99
|
-
rescue ModelContextProtocol::Server::ResponseArgumentsError =>
|
100
|
-
raise
|
110
|
+
rescue OutputSchemaValidationError, ModelContextProtocol::Server::ResponseArgumentsError => tool_error
|
111
|
+
raise tool_error, tool_error.message
|
112
|
+
rescue Server::Cancellable::CancellationError
|
113
|
+
raise
|
101
114
|
rescue => error
|
102
|
-
|
115
|
+
ErrorResponse[error: error.message]
|
103
116
|
end
|
104
117
|
|
105
|
-
def
|
106
|
-
{name: @name, description: @description, inputSchema: @input_schema}
|
118
|
+
def definition
|
119
|
+
result = {name: @name, description: @description, inputSchema: @input_schema}
|
120
|
+
result[:title] = @title if @title
|
121
|
+
result[:outputSchema] = @output_schema if @output_schema
|
122
|
+
result
|
107
123
|
end
|
108
124
|
end
|
109
125
|
|
110
|
-
class
|
126
|
+
class DefinitionDSL
|
111
127
|
def name(value = nil)
|
112
128
|
@name = value if value
|
113
129
|
@name
|
@@ -118,10 +134,20 @@ module ModelContextProtocol
|
|
118
134
|
@description
|
119
135
|
end
|
120
136
|
|
137
|
+
def title(value = nil)
|
138
|
+
@title = value if value
|
139
|
+
@title
|
140
|
+
end
|
141
|
+
|
121
142
|
def input_schema(&block)
|
122
143
|
@input_schema = instance_eval(&block) if block_given?
|
123
144
|
@input_schema
|
124
145
|
end
|
146
|
+
|
147
|
+
def output_schema(&block)
|
148
|
+
@output_schema = instance_eval(&block) if block_given?
|
149
|
+
@output_schema
|
150
|
+
end
|
125
151
|
end
|
126
152
|
end
|
127
153
|
end
|
@@ -8,7 +8,7 @@ module ModelContextProtocol
|
|
8
8
|
# Raised when invalid parameters are provided.
|
9
9
|
class ParameterValidationError < StandardError; end
|
10
10
|
|
11
|
-
attr_reader :configuration, :router
|
11
|
+
attr_reader :configuration, :router, :transport
|
12
12
|
|
13
13
|
def initialize
|
14
14
|
@configuration = Configuration.new
|
@@ -20,7 +20,7 @@ module ModelContextProtocol
|
|
20
20
|
def start
|
21
21
|
configuration.validate!
|
22
22
|
|
23
|
-
transport = case configuration.transport_type
|
23
|
+
@transport = case configuration.transport_type
|
24
24
|
when :stdio, nil
|
25
25
|
StdioTransport.new(router: @router, configuration: @configuration)
|
26
26
|
when :streamable_http
|
@@ -32,21 +32,26 @@ module ModelContextProtocol
|
|
32
32
|
raise ArgumentError, "Unknown transport: #{configuration.transport_type}"
|
33
33
|
end
|
34
34
|
|
35
|
-
transport.handle
|
35
|
+
@transport.handle
|
36
36
|
end
|
37
37
|
|
38
38
|
private
|
39
39
|
|
40
|
-
|
41
|
-
private_constant :
|
40
|
+
SUPPORTED_PROTOCOL_VERSIONS = ["2025-06-18"].freeze
|
41
|
+
private_constant :SUPPORTED_PROTOCOL_VERSIONS
|
42
42
|
|
43
|
-
|
43
|
+
LATEST_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS.first
|
44
|
+
private_constant :LATEST_PROTOCOL_VERSION
|
45
|
+
|
46
|
+
InitializeResponse = Data.define(:protocol_version, :capabilities, :server_info, :instructions) do
|
44
47
|
def serialized
|
45
|
-
{
|
48
|
+
response = {
|
46
49
|
protocolVersion: protocol_version,
|
47
50
|
capabilities: capabilities,
|
48
51
|
serverInfo: server_info
|
49
52
|
}
|
53
|
+
response[:instructions] = instructions if instructions
|
54
|
+
response
|
50
55
|
end
|
51
56
|
end
|
52
57
|
|
@@ -63,14 +68,26 @@ module ModelContextProtocol
|
|
63
68
|
end
|
64
69
|
|
65
70
|
def map_handlers
|
66
|
-
router.map("initialize") do |
|
71
|
+
router.map("initialize") do |message|
|
72
|
+
client_protocol_version = message["params"]&.dig("protocolVersion")
|
73
|
+
|
74
|
+
negotiated_version = if client_protocol_version && SUPPORTED_PROTOCOL_VERSIONS.include?(client_protocol_version)
|
75
|
+
client_protocol_version
|
76
|
+
else
|
77
|
+
LATEST_PROTOCOL_VERSION
|
78
|
+
end
|
79
|
+
|
80
|
+
server_info = {
|
81
|
+
name: configuration.name,
|
82
|
+
version: configuration.version
|
83
|
+
}
|
84
|
+
server_info[:title] = configuration.title if configuration.title
|
85
|
+
|
67
86
|
InitializeResponse[
|
68
|
-
protocol_version:
|
87
|
+
protocol_version: negotiated_version,
|
69
88
|
capabilities: build_capabilities,
|
70
|
-
server_info:
|
71
|
-
|
72
|
-
version: configuration.version
|
73
|
-
}
|
89
|
+
server_info: server_info,
|
90
|
+
instructions: configuration.instructions
|
74
91
|
]
|
75
92
|
end
|
76
93
|
|
@@ -112,8 +129,28 @@ module ModelContextProtocol
|
|
112
129
|
end
|
113
130
|
end
|
114
131
|
|
115
|
-
router.map("resources/list") do
|
116
|
-
|
132
|
+
router.map("resources/list") do |message|
|
133
|
+
params = message["params"] || {}
|
134
|
+
|
135
|
+
if configuration.pagination_enabled? && Server::Pagination.pagination_requested?(params)
|
136
|
+
opts = configuration.pagination_options
|
137
|
+
|
138
|
+
pagination_params = Server::Pagination.extract_pagination_params(
|
139
|
+
params,
|
140
|
+
default_page_size: opts[:default_page_size],
|
141
|
+
max_page_size: opts[:max_page_size]
|
142
|
+
)
|
143
|
+
|
144
|
+
configuration.registry.resources_data(
|
145
|
+
cursor: pagination_params[:cursor],
|
146
|
+
page_size: pagination_params[:page_size],
|
147
|
+
cursor_ttl: opts[:cursor_ttl]
|
148
|
+
)
|
149
|
+
else
|
150
|
+
configuration.registry.resources_data
|
151
|
+
end
|
152
|
+
rescue Server::Pagination::InvalidCursorError => e
|
153
|
+
raise ParameterValidationError, e.message
|
117
154
|
end
|
118
155
|
|
119
156
|
router.map("resources/read") do |message|
|
@@ -123,15 +160,55 @@ module ModelContextProtocol
|
|
123
160
|
raise ModelContextProtocol::Server::ParameterValidationError, "resource not found for #{uri}"
|
124
161
|
end
|
125
162
|
|
126
|
-
resource.call
|
163
|
+
resource.call
|
127
164
|
end
|
128
165
|
|
129
166
|
router.map("resources/templates/list") do |message|
|
130
|
-
|
167
|
+
params = message["params"] || {}
|
168
|
+
|
169
|
+
if configuration.pagination_enabled? && Server::Pagination.pagination_requested?(params)
|
170
|
+
opts = configuration.pagination_options
|
171
|
+
|
172
|
+
pagination_params = Server::Pagination.extract_pagination_params(
|
173
|
+
params,
|
174
|
+
default_page_size: opts[:default_page_size],
|
175
|
+
max_page_size: opts[:max_page_size]
|
176
|
+
)
|
177
|
+
|
178
|
+
configuration.registry.resource_templates_data(
|
179
|
+
cursor: pagination_params[:cursor],
|
180
|
+
page_size: pagination_params[:page_size],
|
181
|
+
cursor_ttl: opts[:cursor_ttl]
|
182
|
+
)
|
183
|
+
else
|
184
|
+
configuration.registry.resource_templates_data
|
185
|
+
end
|
186
|
+
rescue Server::Pagination::InvalidCursorError => e
|
187
|
+
raise ParameterValidationError, e.message
|
131
188
|
end
|
132
189
|
|
133
|
-
router.map("prompts/list") do
|
134
|
-
|
190
|
+
router.map("prompts/list") do |message|
|
191
|
+
params = message["params"] || {}
|
192
|
+
|
193
|
+
if configuration.pagination_enabled? && Server::Pagination.pagination_requested?(params)
|
194
|
+
opts = configuration.pagination_options
|
195
|
+
|
196
|
+
pagination_params = Server::Pagination.extract_pagination_params(
|
197
|
+
params,
|
198
|
+
default_page_size: opts[:default_page_size],
|
199
|
+
max_page_size: opts[:max_page_size]
|
200
|
+
)
|
201
|
+
|
202
|
+
configuration.registry.prompts_data(
|
203
|
+
cursor: pagination_params[:cursor],
|
204
|
+
page_size: pagination_params[:page_size],
|
205
|
+
cursor_ttl: opts[:cursor_ttl]
|
206
|
+
)
|
207
|
+
else
|
208
|
+
configuration.registry.prompts_data
|
209
|
+
end
|
210
|
+
rescue Server::Pagination::InvalidCursorError => e
|
211
|
+
raise ParameterValidationError, e.message
|
135
212
|
end
|
136
213
|
|
137
214
|
router.map("prompts/get") do |message|
|
@@ -143,8 +220,28 @@ module ModelContextProtocol
|
|
143
220
|
.call(symbolized_arguments, configuration.logger, configuration.context)
|
144
221
|
end
|
145
222
|
|
146
|
-
router.map("tools/list") do
|
147
|
-
|
223
|
+
router.map("tools/list") do |message|
|
224
|
+
params = message["params"] || {}
|
225
|
+
|
226
|
+
if configuration.pagination_enabled? && Server::Pagination.pagination_requested?(params)
|
227
|
+
opts = configuration.pagination_options
|
228
|
+
|
229
|
+
pagination_params = Server::Pagination.extract_pagination_params(
|
230
|
+
params,
|
231
|
+
default_page_size: opts[:default_page_size],
|
232
|
+
max_page_size: opts[:max_page_size]
|
233
|
+
)
|
234
|
+
|
235
|
+
configuration.registry.tools_data(
|
236
|
+
cursor: pagination_params[:cursor],
|
237
|
+
page_size: pagination_params[:page_size],
|
238
|
+
cursor_ttl: opts[:cursor_ttl]
|
239
|
+
)
|
240
|
+
else
|
241
|
+
configuration.registry.tools_data
|
242
|
+
end
|
243
|
+
rescue Server::Pagination::InvalidCursorError => e
|
244
|
+
raise ParameterValidationError, e.message
|
148
245
|
end
|
149
246
|
|
150
247
|
router.map("tools/call") do |message|
|
@@ -184,5 +281,11 @@ module ModelContextProtocol
|
|
184
281
|
end
|
185
282
|
end
|
186
283
|
end
|
284
|
+
|
285
|
+
class << self
|
286
|
+
def configure_redis(&block)
|
287
|
+
RedisConfig.configure(&block)
|
288
|
+
end
|
289
|
+
end
|
187
290
|
end
|
188
291
|
end
|
data/tasks/mcp.rake
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
require "fileutils"
|
2
2
|
|
3
3
|
namespace :mcp do
|
4
|
-
desc "Generate the development server executable with the correct Ruby path"
|
5
|
-
task :
|
4
|
+
desc "Generate the STDIO development server executable with the correct Ruby path"
|
5
|
+
task :generate_stdio_server do
|
6
6
|
destination_path = "bin/dev"
|
7
7
|
template_path = File.expand_path("templates/dev.erb", __dir__)
|
8
8
|
|
@@ -27,6 +27,32 @@ namespace :mcp do
|
|
27
27
|
puts "Using Ruby path: #{ruby_path}"
|
28
28
|
end
|
29
29
|
|
30
|
+
desc "Generate the streamable HTTP development server executable with the correct Ruby path"
|
31
|
+
task :generate_streamable_http_server do
|
32
|
+
destination_path = "bin/dev-http"
|
33
|
+
template_path = File.expand_path("templates/dev-http.erb", __dir__)
|
34
|
+
|
35
|
+
# Create directory if it doesn't exist
|
36
|
+
FileUtils.mkdir_p(File.dirname(destination_path))
|
37
|
+
|
38
|
+
# Get the Ruby path
|
39
|
+
ruby_path = detect_ruby_path
|
40
|
+
|
41
|
+
# Read and process the template
|
42
|
+
template = File.read(template_path)
|
43
|
+
content = template.gsub("<%= @ruby_path %>", ruby_path)
|
44
|
+
|
45
|
+
# Write the executable
|
46
|
+
File.write(destination_path, content)
|
47
|
+
|
48
|
+
# Set permissions
|
49
|
+
FileUtils.chmod(0o755, destination_path)
|
50
|
+
|
51
|
+
# Show success message
|
52
|
+
puts "\nCreated executable at: #{File.expand_path(destination_path)}"
|
53
|
+
puts "Using Ruby path: #{ruby_path}"
|
54
|
+
end
|
55
|
+
|
30
56
|
def detect_ruby_path
|
31
57
|
# Get Ruby version from project config
|
32
58
|
ruby_version = get_project_ruby_version
|