model-context-protocol-rb 0.3.1 → 0.3.3
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 +19 -1
- data/README.md +254 -67
- data/lib/model_context_protocol/server/completion.rb +51 -0
- data/lib/model_context_protocol/server/configuration.rb +94 -2
- data/lib/model_context_protocol/server/mcp_logger.rb +109 -0
- data/lib/model_context_protocol/server/prompt.rb +72 -15
- data/lib/model_context_protocol/server/registry.rb +22 -0
- data/lib/model_context_protocol/server/resource.rb +35 -10
- data/lib/model_context_protocol/server/resource_template.rb +93 -0
- data/lib/model_context_protocol/server/session_store.rb +108 -0
- data/lib/model_context_protocol/server/stdio_transport.rb +26 -11
- data/lib/model_context_protocol/server/streamable_http_transport.rb +291 -0
- data/lib/model_context_protocol/server/tool.rb +28 -8
- data/lib/model_context_protocol/server.rb +80 -7
- data/lib/model_context_protocol/version.rb +1 -1
- data/lib/model_context_protocol.rb +1 -1
- data/tasks/templates/dev.erb +14 -1
- metadata +21 -2
@@ -12,16 +12,19 @@ module ModelContextProtocol
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
attr_reader :
|
15
|
+
attr_reader :router, :configuration
|
16
16
|
|
17
|
-
def initialize(
|
18
|
-
@logger = logger
|
17
|
+
def initialize(router:, configuration:)
|
19
18
|
@router = router
|
19
|
+
@configuration = configuration
|
20
20
|
end
|
21
21
|
|
22
|
-
def
|
22
|
+
def handle
|
23
|
+
# Connect logger to transport
|
24
|
+
@configuration.logger.connect_transport(self)
|
25
|
+
|
23
26
|
loop do
|
24
|
-
line =
|
27
|
+
line = receive_message
|
25
28
|
break unless line
|
26
29
|
|
27
30
|
begin
|
@@ -31,18 +34,17 @@ module ModelContextProtocol
|
|
31
34
|
result = router.route(message)
|
32
35
|
send_message(Response[id: message["id"], result: result.serialized])
|
33
36
|
rescue ModelContextProtocol::Server::ParameterValidationError => validation_error
|
34
|
-
|
37
|
+
@configuration.logger.error("Validation error", error: validation_error.message)
|
35
38
|
send_message(
|
36
39
|
ErrorResponse[id: message["id"], error: {code: -32602, message: validation_error.message}]
|
37
40
|
)
|
38
41
|
rescue JSON::ParserError => parser_error
|
39
|
-
|
42
|
+
@configuration.logger.error("Parser error", error: parser_error.message)
|
40
43
|
send_message(
|
41
44
|
ErrorResponse[id: "", error: {code: -32700, message: parser_error.message}]
|
42
45
|
)
|
43
46
|
rescue => error
|
44
|
-
|
45
|
-
log(error.backtrace)
|
47
|
+
@configuration.logger.error("Internal error", error: error.message, backtrace: error.backtrace.first(5))
|
46
48
|
send_message(
|
47
49
|
ErrorResponse[id: message["id"], error: {code: -32603, message: error.message}]
|
48
50
|
)
|
@@ -50,10 +52,23 @@ module ModelContextProtocol
|
|
50
52
|
end
|
51
53
|
end
|
52
54
|
|
55
|
+
def send_notification(method, params)
|
56
|
+
notification = {
|
57
|
+
jsonrpc: "2.0",
|
58
|
+
method: method,
|
59
|
+
params: params
|
60
|
+
}
|
61
|
+
$stdout.puts(JSON.generate(notification))
|
62
|
+
$stdout.flush
|
63
|
+
rescue IOError => e
|
64
|
+
# Handle broken pipe gracefully
|
65
|
+
@configuration.logger.debug("Failed to send notification", error: e.message) if @configuration.logging_enabled?
|
66
|
+
end
|
67
|
+
|
53
68
|
private
|
54
69
|
|
55
|
-
def
|
56
|
-
|
70
|
+
def receive_message
|
71
|
+
$stdin.gets
|
57
72
|
end
|
58
73
|
|
59
74
|
def send_message(message)
|
@@ -0,0 +1,291 @@
|
|
1
|
+
require "json"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
module ModelContextProtocol
|
5
|
+
class Server::StreamableHttpTransport
|
6
|
+
Response = Data.define(:id, :result) do
|
7
|
+
def serialized
|
8
|
+
{jsonrpc: "2.0", id:, result:}
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
ErrorResponse = Data.define(:id, :error) do
|
13
|
+
def serialized
|
14
|
+
{jsonrpc: "2.0", id:, error:}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
def initialize(router:, configuration:)
|
18
|
+
@router = router
|
19
|
+
@configuration = configuration
|
20
|
+
|
21
|
+
transport_options = @configuration.transport_options
|
22
|
+
@redis = transport_options[:redis_client]
|
23
|
+
|
24
|
+
@session_store = ModelContextProtocol::Server::SessionStore.new(
|
25
|
+
@redis,
|
26
|
+
ttl: transport_options[:session_ttl] || 3600
|
27
|
+
)
|
28
|
+
|
29
|
+
@server_instance = "#{Socket.gethostname}-#{Process.pid}-#{SecureRandom.hex(4)}"
|
30
|
+
@local_streams = {}
|
31
|
+
@notification_queue = []
|
32
|
+
|
33
|
+
setup_redis_subscriber
|
34
|
+
end
|
35
|
+
|
36
|
+
def handle
|
37
|
+
@configuration.logger.connect_transport(self)
|
38
|
+
|
39
|
+
request = @configuration.transport_options[:request]
|
40
|
+
response = @configuration.transport_options[:response]
|
41
|
+
|
42
|
+
unless request && response
|
43
|
+
raise ArgumentError, "StreamableHTTP transport requires request and response objects in transport_options"
|
44
|
+
end
|
45
|
+
|
46
|
+
case request.method
|
47
|
+
when "POST"
|
48
|
+
handle_post_request(request)
|
49
|
+
when "GET"
|
50
|
+
handle_sse_request(request, response)
|
51
|
+
when "DELETE"
|
52
|
+
handle_delete_request(request)
|
53
|
+
else
|
54
|
+
error_response = ErrorResponse[id: nil, error: {code: -32601, message: "Method not allowed"}]
|
55
|
+
{json: error_response.serialized, status: 405}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def send_notification(method, params)
|
60
|
+
notification = {
|
61
|
+
jsonrpc: "2.0",
|
62
|
+
method: method,
|
63
|
+
params: params
|
64
|
+
}
|
65
|
+
|
66
|
+
if has_active_streams?
|
67
|
+
deliver_to_active_streams(notification)
|
68
|
+
else
|
69
|
+
@notification_queue << notification
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def handle_post_request(request)
|
76
|
+
body_string = request.body.read
|
77
|
+
body = JSON.parse(body_string)
|
78
|
+
session_id = request.headers["Mcp-Session-Id"]
|
79
|
+
|
80
|
+
case body["method"]
|
81
|
+
when "initialize"
|
82
|
+
handle_initialization(body)
|
83
|
+
else
|
84
|
+
handle_regular_request(body, session_id)
|
85
|
+
end
|
86
|
+
rescue JSON::ParserError
|
87
|
+
error_response = ErrorResponse[id: "", error: {code: -32700, message: "Parse error"}]
|
88
|
+
{json: error_response.serialized, status: 400}
|
89
|
+
rescue ModelContextProtocol::Server::ParameterValidationError => validation_error
|
90
|
+
@configuration.logger.error("Validation error", error: validation_error.message)
|
91
|
+
error_response = ErrorResponse[id: body&.dig("id"), error: {code: -32602, message: validation_error.message}]
|
92
|
+
{json: error_response.serialized, status: 400}
|
93
|
+
rescue => e
|
94
|
+
@configuration.logger.error("Error handling POST request", error: e.message, backtrace: e.backtrace.first(5))
|
95
|
+
error_response = ErrorResponse[id: body&.dig("id"), error: {code: -32603, message: "Internal error"}]
|
96
|
+
{json: error_response.serialized, status: 500}
|
97
|
+
end
|
98
|
+
|
99
|
+
def handle_initialization(body)
|
100
|
+
session_id = SecureRandom.uuid
|
101
|
+
|
102
|
+
@session_store.create_session(session_id, {
|
103
|
+
server_instance: @server_instance,
|
104
|
+
context: @configuration.context || {},
|
105
|
+
created_at: Time.now.to_f
|
106
|
+
})
|
107
|
+
|
108
|
+
result = @router.route(body)
|
109
|
+
response = Response[id: body["id"], result: result.serialized]
|
110
|
+
|
111
|
+
{
|
112
|
+
json: response.serialized,
|
113
|
+
status: 200,
|
114
|
+
headers: {"Mcp-Session-Id" => session_id}
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
def handle_regular_request(body, session_id)
|
119
|
+
unless session_id && @session_store.session_exists?(session_id)
|
120
|
+
error_response = ErrorResponse[id: body["id"], error: {code: -32600, message: "Invalid or missing session ID"}]
|
121
|
+
return {json: error_response.serialized, status: 400}
|
122
|
+
end
|
123
|
+
|
124
|
+
result = @router.route(body)
|
125
|
+
response = Response[id: body["id"], result: result.serialized]
|
126
|
+
|
127
|
+
if @session_store.session_has_active_stream?(session_id)
|
128
|
+
deliver_to_session_stream(session_id, response.serialized)
|
129
|
+
{json: {accepted: true}, status: 200}
|
130
|
+
else
|
131
|
+
{json: response.serialized, status: 200}
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def handle_sse_request(request, response)
|
136
|
+
session_id = request.headers["Mcp-Session-Id"]
|
137
|
+
|
138
|
+
unless session_id && @session_store.session_exists?(session_id)
|
139
|
+
error_response = ErrorResponse[id: nil, error: {code: -32600, message: "Invalid or missing session ID"}]
|
140
|
+
return {json: error_response.serialized, status: 400}
|
141
|
+
end
|
142
|
+
|
143
|
+
@session_store.mark_stream_active(session_id, @server_instance)
|
144
|
+
|
145
|
+
{
|
146
|
+
stream: true,
|
147
|
+
headers: {
|
148
|
+
"Content-Type" => "text/event-stream",
|
149
|
+
"Cache-Control" => "no-cache",
|
150
|
+
"Connection" => "keep-alive"
|
151
|
+
},
|
152
|
+
stream_proc: create_sse_stream_proc(session_id)
|
153
|
+
}
|
154
|
+
end
|
155
|
+
|
156
|
+
def handle_delete_request(request)
|
157
|
+
session_id = request.headers["Mcp-Session-Id"]
|
158
|
+
|
159
|
+
if session_id
|
160
|
+
cleanup_session(session_id)
|
161
|
+
end
|
162
|
+
|
163
|
+
{json: {success: true}, status: 200}
|
164
|
+
end
|
165
|
+
|
166
|
+
def create_sse_stream_proc(session_id)
|
167
|
+
proc do |stream|
|
168
|
+
register_local_stream(session_id, stream)
|
169
|
+
|
170
|
+
flush_notifications_to_stream(stream)
|
171
|
+
|
172
|
+
start_keepalive_thread(session_id, stream)
|
173
|
+
|
174
|
+
loop do
|
175
|
+
break unless stream_connected?(stream)
|
176
|
+
sleep 0.1
|
177
|
+
end
|
178
|
+
ensure
|
179
|
+
cleanup_local_stream(session_id)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def register_local_stream(session_id, stream)
|
184
|
+
@local_streams[session_id] = stream
|
185
|
+
end
|
186
|
+
|
187
|
+
def cleanup_local_stream(session_id)
|
188
|
+
@local_streams.delete(session_id)
|
189
|
+
@session_store.mark_stream_inactive(session_id)
|
190
|
+
end
|
191
|
+
|
192
|
+
def stream_connected?(stream)
|
193
|
+
return false unless stream
|
194
|
+
|
195
|
+
begin
|
196
|
+
stream.write(": ping\n\n")
|
197
|
+
stream.flush if stream.respond_to?(:flush)
|
198
|
+
true
|
199
|
+
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
200
|
+
false
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def start_keepalive_thread(session_id, stream)
|
205
|
+
Thread.new do
|
206
|
+
loop do
|
207
|
+
sleep 30
|
208
|
+
break unless stream_connected?(stream)
|
209
|
+
|
210
|
+
begin
|
211
|
+
send_ping_to_stream(stream)
|
212
|
+
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
213
|
+
break
|
214
|
+
end
|
215
|
+
end
|
216
|
+
rescue => e
|
217
|
+
@configuration.logger.error("Keepalive thread error", error: e.message)
|
218
|
+
ensure
|
219
|
+
cleanup_local_stream(session_id)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def send_ping_to_stream(stream)
|
224
|
+
stream.write(": ping #{Time.now.iso8601}\n\n")
|
225
|
+
stream.flush if stream.respond_to?(:flush)
|
226
|
+
end
|
227
|
+
|
228
|
+
def send_to_stream(stream, data)
|
229
|
+
message = data.is_a?(String) ? data : data.to_json
|
230
|
+
stream.write("data: #{message}\n\n")
|
231
|
+
stream.flush if stream.respond_to?(:flush)
|
232
|
+
end
|
233
|
+
|
234
|
+
def deliver_to_session_stream(session_id, data)
|
235
|
+
if @local_streams[session_id]
|
236
|
+
begin
|
237
|
+
send_to_stream(@local_streams[session_id], data)
|
238
|
+
return true
|
239
|
+
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
240
|
+
cleanup_local_stream(session_id)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
@session_store.route_message_to_session(session_id, data)
|
245
|
+
end
|
246
|
+
|
247
|
+
def cleanup_session(session_id)
|
248
|
+
cleanup_local_stream(session_id)
|
249
|
+
@session_store.cleanup_session(session_id)
|
250
|
+
end
|
251
|
+
|
252
|
+
def setup_redis_subscriber
|
253
|
+
Thread.new do
|
254
|
+
@session_store.subscribe_to_server(@server_instance) do |data|
|
255
|
+
session_id = data["session_id"]
|
256
|
+
message = data["message"]
|
257
|
+
|
258
|
+
if @local_streams[session_id]
|
259
|
+
begin
|
260
|
+
send_to_stream(@local_streams[session_id], message)
|
261
|
+
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
262
|
+
cleanup_local_stream(session_id)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
rescue => e
|
267
|
+
@configuration.logger.error("Redis subscriber error", error: e.message, backtrace: e.backtrace.first(5))
|
268
|
+
sleep 5
|
269
|
+
retry
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def has_active_streams?
|
274
|
+
@local_streams.any?
|
275
|
+
end
|
276
|
+
|
277
|
+
def deliver_to_active_streams(notification)
|
278
|
+
@local_streams.each do |session_id, stream|
|
279
|
+
send_to_stream(stream, notification)
|
280
|
+
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
281
|
+
cleanup_local_stream(session_id)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def flush_notifications_to_stream(stream)
|
286
|
+
while (notification = @notification_queue.shift)
|
287
|
+
send_to_stream(stream, notification)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
@@ -2,11 +2,13 @@ require "json-schema"
|
|
2
2
|
|
3
3
|
module ModelContextProtocol
|
4
4
|
class Server::Tool
|
5
|
-
attr_reader :params
|
5
|
+
attr_reader :params, :context, :logger
|
6
6
|
|
7
|
-
def initialize(params)
|
7
|
+
def initialize(params, logger, context = {})
|
8
8
|
validate!(params)
|
9
9
|
@params = params
|
10
|
+
@context = context
|
11
|
+
@logger = logger
|
10
12
|
end
|
11
13
|
|
12
14
|
def call
|
@@ -76,11 +78,12 @@ module ModelContextProtocol
|
|
76
78
|
attr_reader :name, :description, :input_schema
|
77
79
|
|
78
80
|
def with_metadata(&block)
|
79
|
-
|
81
|
+
metadata_dsl = MetadataDSL.new
|
82
|
+
metadata_dsl.instance_eval(&block)
|
80
83
|
|
81
|
-
@name =
|
82
|
-
@description =
|
83
|
-
@input_schema =
|
84
|
+
@name = metadata_dsl.name
|
85
|
+
@description = metadata_dsl.description
|
86
|
+
@input_schema = metadata_dsl.input_schema
|
84
87
|
end
|
85
88
|
|
86
89
|
def inherited(subclass)
|
@@ -89,8 +92,8 @@ module ModelContextProtocol
|
|
89
92
|
subclass.instance_variable_set(:@input_schema, @input_schema)
|
90
93
|
end
|
91
94
|
|
92
|
-
def call(params)
|
93
|
-
new(params).call
|
95
|
+
def call(params, logger, context = {})
|
96
|
+
new(params, logger, context).call
|
94
97
|
rescue JSON::Schema::ValidationError => validation_error
|
95
98
|
raise ModelContextProtocol::Server::ParameterValidationError, validation_error.message
|
96
99
|
rescue ModelContextProtocol::Server::ResponseArgumentsError => response_arguments_error
|
@@ -103,5 +106,22 @@ module ModelContextProtocol
|
|
103
106
|
{name: @name, description: @description, inputSchema: @input_schema}
|
104
107
|
end
|
105
108
|
end
|
109
|
+
|
110
|
+
class MetadataDSL
|
111
|
+
def name(value = nil)
|
112
|
+
@name = value if value
|
113
|
+
@name
|
114
|
+
end
|
115
|
+
|
116
|
+
def description(value = nil)
|
117
|
+
@description = value if value
|
118
|
+
@description
|
119
|
+
end
|
120
|
+
|
121
|
+
def input_schema(&block)
|
122
|
+
@input_schema = instance_eval(&block) if block_given?
|
123
|
+
@input_schema
|
124
|
+
end
|
125
|
+
end
|
106
126
|
end
|
107
127
|
end
|
@@ -19,13 +19,25 @@ module ModelContextProtocol
|
|
19
19
|
|
20
20
|
def start
|
21
21
|
configuration.validate!
|
22
|
-
|
23
|
-
|
22
|
+
|
23
|
+
transport = case configuration.transport_type
|
24
|
+
when :stdio, nil
|
25
|
+
StdioTransport.new(router: @router, configuration: @configuration)
|
26
|
+
when :streamable_http
|
27
|
+
StreamableHttpTransport.new(
|
28
|
+
router: @router,
|
29
|
+
configuration: @configuration
|
30
|
+
)
|
31
|
+
else
|
32
|
+
raise ArgumentError, "Unknown transport: #{configuration.transport_type}"
|
33
|
+
end
|
34
|
+
|
35
|
+
transport.handle
|
24
36
|
end
|
25
37
|
|
26
38
|
private
|
27
39
|
|
28
|
-
PROTOCOL_VERSION = "
|
40
|
+
PROTOCOL_VERSION = "2025-06-18".freeze
|
29
41
|
private_constant :PROTOCOL_VERSION
|
30
42
|
|
31
43
|
InitializeResponse = Data.define(:protocol_version, :capabilities, :server_info) do
|
@@ -44,6 +56,12 @@ module ModelContextProtocol
|
|
44
56
|
end
|
45
57
|
end
|
46
58
|
|
59
|
+
LoggingSetLevelResponse = Data.define do
|
60
|
+
def serialized
|
61
|
+
{}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
47
65
|
def map_handlers
|
48
66
|
router.map("initialize") do |_message|
|
49
67
|
InitializeResponse[
|
@@ -60,12 +78,56 @@ module ModelContextProtocol
|
|
60
78
|
PingResponse[]
|
61
79
|
end
|
62
80
|
|
81
|
+
router.map("logging/setLevel") do |message|
|
82
|
+
level = message["params"]["level"]
|
83
|
+
|
84
|
+
unless Configuration::VALID_LOG_LEVELS.include?(level)
|
85
|
+
raise ParameterValidationError, "Invalid log level: #{level}. Valid levels are: #{Configuration::VALID_LOG_LEVELS.join(", ")}"
|
86
|
+
end
|
87
|
+
|
88
|
+
configuration.logger.set_mcp_level(level)
|
89
|
+
LoggingSetLevelResponse[]
|
90
|
+
end
|
91
|
+
|
92
|
+
router.map("completion/complete") do |message|
|
93
|
+
type = message["params"]["ref"]["type"]
|
94
|
+
|
95
|
+
completion_source = case type
|
96
|
+
when "ref/prompt"
|
97
|
+
name = message["params"]["ref"]["name"]
|
98
|
+
configuration.registry.find_prompt(name)
|
99
|
+
when "ref/resource"
|
100
|
+
uri = message["params"]["ref"]["uri"]
|
101
|
+
configuration.registry.find_resource_template(uri)
|
102
|
+
else
|
103
|
+
raise ModelContextProtocol::Server::ParameterValidationError, "ref/type invalid"
|
104
|
+
end
|
105
|
+
|
106
|
+
arg_name, arg_value = message["params"]["argument"].values_at("name", "value")
|
107
|
+
|
108
|
+
if completion_source
|
109
|
+
completion_source.complete_for(arg_name, arg_value)
|
110
|
+
else
|
111
|
+
ModelContextProtocol::Server::NullCompletion.call(arg_name, arg_value)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
63
115
|
router.map("resources/list") do
|
64
116
|
configuration.registry.resources_data
|
65
117
|
end
|
66
118
|
|
67
119
|
router.map("resources/read") do |message|
|
68
|
-
|
120
|
+
uri = message["params"]["uri"]
|
121
|
+
resource = configuration.registry.find_resource(uri)
|
122
|
+
unless resource
|
123
|
+
raise ModelContextProtocol::Server::ParameterValidationError, "resource not found for #{uri}"
|
124
|
+
end
|
125
|
+
|
126
|
+
resource.call(configuration.logger, configuration.context)
|
127
|
+
end
|
128
|
+
|
129
|
+
router.map("resources/templates/list") do |message|
|
130
|
+
configuration.registry.resource_templates_data
|
69
131
|
end
|
70
132
|
|
71
133
|
router.map("prompts/list") do
|
@@ -73,7 +135,12 @@ module ModelContextProtocol
|
|
73
135
|
end
|
74
136
|
|
75
137
|
router.map("prompts/get") do |message|
|
76
|
-
|
138
|
+
arguments = message["params"]["arguments"]
|
139
|
+
symbolized_arguments = arguments.transform_keys(&:to_sym)
|
140
|
+
configuration
|
141
|
+
.registry
|
142
|
+
.find_prompt(message["params"]["name"])
|
143
|
+
.call(symbolized_arguments, configuration.logger, configuration.context)
|
77
144
|
end
|
78
145
|
|
79
146
|
router.map("tools/list") do
|
@@ -81,12 +148,18 @@ module ModelContextProtocol
|
|
81
148
|
end
|
82
149
|
|
83
150
|
router.map("tools/call") do |message|
|
84
|
-
|
151
|
+
arguments = message["params"]["arguments"]
|
152
|
+
symbolized_arguments = arguments.transform_keys(&:to_sym)
|
153
|
+
configuration
|
154
|
+
.registry
|
155
|
+
.find_tool(message["params"]["name"])
|
156
|
+
.call(symbolized_arguments, configuration.logger, configuration.context)
|
85
157
|
end
|
86
158
|
end
|
87
159
|
|
88
160
|
def build_capabilities
|
89
161
|
{}.tap do |capabilities|
|
162
|
+
capabilities[:completions] = {}
|
90
163
|
capabilities[:logging] = {} if configuration.logging_enabled?
|
91
164
|
|
92
165
|
registry = configuration.registry
|
@@ -94,7 +167,7 @@ module ModelContextProtocol
|
|
94
167
|
if registry.prompts_options.any? && !registry.instance_variable_get(:@prompts).empty?
|
95
168
|
capabilities[:prompts] = {
|
96
169
|
listChanged: registry.prompts_options[:list_changed]
|
97
|
-
}.compact
|
170
|
+
}.except(:completions).compact
|
98
171
|
end
|
99
172
|
|
100
173
|
if registry.resources_options.any? && !registry.instance_variable_get(:@resources).empty?
|
data/tasks/templates/dev.erb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
#!/usr/bin/env <%= @ruby_path %>
|
2
2
|
|
3
3
|
require "bundler/setup"
|
4
|
+
require "securerandom"
|
4
5
|
require_relative "../lib/model_context_protocol"
|
5
6
|
|
6
7
|
Dir[File.join(__dir__, "../spec/support/**/*.rb")].each { |file| require file }
|
@@ -8,7 +9,15 @@ Dir[File.join(__dir__, "../spec/support/**/*.rb")].each { |file| require file }
|
|
8
9
|
server = ModelContextProtocol::Server.new do |config|
|
9
10
|
config.name = "MCP Development Server"
|
10
11
|
config.version = "1.0.0"
|
11
|
-
config.
|
12
|
+
config.logging_enabled = true
|
13
|
+
|
14
|
+
config.set_environment_variable("MCP_ENV", "development")
|
15
|
+
|
16
|
+
config.context = {
|
17
|
+
user_id: "123456",
|
18
|
+
request_id: SecureRandom.uuid
|
19
|
+
}
|
20
|
+
|
12
21
|
config.registry = ModelContextProtocol::Server::Registry.new do
|
13
22
|
prompts list_changed: true do
|
14
23
|
register TestPrompt
|
@@ -19,6 +28,10 @@ server = ModelContextProtocol::Server.new do |config|
|
|
19
28
|
register TestBinaryResource
|
20
29
|
end
|
21
30
|
|
31
|
+
resource_templates do
|
32
|
+
register TestResourceTemplate
|
33
|
+
end
|
34
|
+
|
22
35
|
tools list_changed: true do
|
23
36
|
register TestToolWithTextResponse
|
24
37
|
register TestToolWithImageResponse
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: model-context-protocol-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dick Davis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-09-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json-schema
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '5.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: addressable
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.8'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.8'
|
27
41
|
description:
|
28
42
|
email:
|
29
43
|
- dick@hey.com
|
@@ -42,12 +56,17 @@ files:
|
|
42
56
|
- Rakefile
|
43
57
|
- lib/model_context_protocol.rb
|
44
58
|
- lib/model_context_protocol/server.rb
|
59
|
+
- lib/model_context_protocol/server/completion.rb
|
45
60
|
- lib/model_context_protocol/server/configuration.rb
|
61
|
+
- lib/model_context_protocol/server/mcp_logger.rb
|
46
62
|
- lib/model_context_protocol/server/prompt.rb
|
47
63
|
- lib/model_context_protocol/server/registry.rb
|
48
64
|
- lib/model_context_protocol/server/resource.rb
|
65
|
+
- lib/model_context_protocol/server/resource_template.rb
|
49
66
|
- lib/model_context_protocol/server/router.rb
|
67
|
+
- lib/model_context_protocol/server/session_store.rb
|
50
68
|
- lib/model_context_protocol/server/stdio_transport.rb
|
69
|
+
- lib/model_context_protocol/server/streamable_http_transport.rb
|
51
70
|
- lib/model_context_protocol/server/tool.rb
|
52
71
|
- lib/model_context_protocol/version.rb
|
53
72
|
- tasks/mcp.rake
|