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
@@ -0,0 +1,288 @@
|
|
1
|
+
#!/usr/bin/env <%= @ruby_path %>
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "rack"
|
5
|
+
require 'rackup/handler/webrick'
|
6
|
+
require "webrick"
|
7
|
+
require "webrick/https"
|
8
|
+
require "openssl"
|
9
|
+
require "securerandom"
|
10
|
+
require "redis"
|
11
|
+
require "logger"
|
12
|
+
require "json"
|
13
|
+
require 'stringio'
|
14
|
+
|
15
|
+
require_relative "../lib/model_context_protocol"
|
16
|
+
|
17
|
+
ModelContextProtocol::Server.configure_redis do |config|
|
18
|
+
config.redis_url = "redis://localhost:6379/0"
|
19
|
+
config.pool_size = 10
|
20
|
+
config.enable_reaper = true
|
21
|
+
config.reaper_interval = 10
|
22
|
+
config.idle_timeout = 15
|
23
|
+
end
|
24
|
+
|
25
|
+
Dir[File.join(__dir__, "../spec/support/**/*.rb")].each { |file| require file }
|
26
|
+
|
27
|
+
logger = Logger.new(STDOUT)
|
28
|
+
logger.level = Logger::INFO
|
29
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
30
|
+
request_id = Thread.current[:request_id] || "----"
|
31
|
+
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity} [#{request_id}]: #{msg}\n"
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
class MCPHttpApp
|
36
|
+
def initialize(logger)
|
37
|
+
@logger = logger
|
38
|
+
end
|
39
|
+
|
40
|
+
def call(env)
|
41
|
+
request_id = SecureRandom.hex(4)
|
42
|
+
Thread.current[:request_id] = request_id
|
43
|
+
|
44
|
+
request = Rack::Request.new(env)
|
45
|
+
body_content = request.body.read
|
46
|
+
|
47
|
+
case env['REQUEST_METHOD']
|
48
|
+
when 'POST'
|
49
|
+
begin
|
50
|
+
request_json = JSON.parse(body_content)
|
51
|
+
method = request_json['method']
|
52
|
+
id = request_json['id']
|
53
|
+
|
54
|
+
if method&.start_with?('notifications/')
|
55
|
+
@logger.info("→ #{method} [NOTIFICATION]")
|
56
|
+
elsif id.nil?
|
57
|
+
@logger.info("→ #{method} [NOTIFICATION]")
|
58
|
+
else
|
59
|
+
@logger.info("→ #{method} (id: #{id}) [REQUEST]")
|
60
|
+
end
|
61
|
+
@logger.info(" Request: #{body_content}")
|
62
|
+
rescue JSON::ParserError
|
63
|
+
@logger.info("→ POST #{env['PATH_INFO']} [INVALID JSON]")
|
64
|
+
@logger.info(" Request: #{body_content}")
|
65
|
+
end
|
66
|
+
when 'GET'
|
67
|
+
accept_header = env['HTTP_ACCEPT'] || ''
|
68
|
+
if accept_header.include?('text/event-stream')
|
69
|
+
@logger.info("→ GET #{env['PATH_INFO']} [SSE STREAM REQUEST]")
|
70
|
+
else
|
71
|
+
@logger.info("→ GET #{env['PATH_INFO']}")
|
72
|
+
end
|
73
|
+
@logger.info(" Headers: Accept=#{accept_header}") unless accept_header.empty?
|
74
|
+
when 'DELETE'
|
75
|
+
session_id = env['HTTP_MCP_SESSION_ID']
|
76
|
+
if session_id
|
77
|
+
@logger.info("→ DELETE #{env['PATH_INFO']} [SESSION CLEANUP: #{session_id}]")
|
78
|
+
else
|
79
|
+
@logger.info("→ DELETE #{env['PATH_INFO']} [SESSION CLEANUP]")
|
80
|
+
end
|
81
|
+
else
|
82
|
+
@logger.info("→ #{env['REQUEST_METHOD']} #{env['PATH_INFO']}")
|
83
|
+
@logger.info(" Request: #{body_content}") unless body_content.empty?
|
84
|
+
end
|
85
|
+
|
86
|
+
if ModelContextProtocol::Server::RedisConfig.configured?
|
87
|
+
pool_stats = ModelContextProtocol::Server::RedisConfig.stats
|
88
|
+
@logger.info(" Redis Pool: #{pool_stats}")
|
89
|
+
end
|
90
|
+
|
91
|
+
env['rack.input'] = StringIO.new(body_content)
|
92
|
+
request = Rack::Request.new(env)
|
93
|
+
|
94
|
+
unless request.path == "/mcp"
|
95
|
+
return [404, {"Content-Type" => "application/json"}, ['{"error": "Not found"}']]
|
96
|
+
end
|
97
|
+
|
98
|
+
if request.request_method == "OPTIONS"
|
99
|
+
return [200, {
|
100
|
+
"Access-Control-Allow-Origin" => "*",
|
101
|
+
"Access-Control-Allow-Methods" => "GET, POST, DELETE, OPTIONS",
|
102
|
+
"Access-Control-Allow-Headers" => "Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version, Origin",
|
103
|
+
"Access-Control-Max-Age" => "86400"
|
104
|
+
}, [""]]
|
105
|
+
end
|
106
|
+
|
107
|
+
transport_config = {
|
108
|
+
type: :streamable_http,
|
109
|
+
env:,
|
110
|
+
require_sessions: false,
|
111
|
+
session_ttl: 3600,
|
112
|
+
allowed_origins: ["*"]
|
113
|
+
}
|
114
|
+
|
115
|
+
@logger.debug("Creating MCP server with transport config")
|
116
|
+
server = create_mcp_server(transport_config)
|
117
|
+
transport = nil
|
118
|
+
|
119
|
+
begin
|
120
|
+
@logger.debug("Starting MCP server")
|
121
|
+
result = server.start
|
122
|
+
|
123
|
+
if server.respond_to?(:transport)
|
124
|
+
transport = server.transport
|
125
|
+
end
|
126
|
+
|
127
|
+
case result
|
128
|
+
when Hash
|
129
|
+
if result[:stream]
|
130
|
+
@logger.info("← SSE STREAM OPENED [PERSISTENT CONNECTION]")
|
131
|
+
@logger.info(" Connection will remain open for real-time notifications")
|
132
|
+
headers = result[:headers] || {}
|
133
|
+
headers["Access-Control-Allow-Origin"] = "*"
|
134
|
+
headers["Access-Control-Allow-Methods"] = "GET, POST, DELETE, OPTIONS"
|
135
|
+
headers["Access-Control-Allow-Headers"] = "Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version, Origin"
|
136
|
+
|
137
|
+
return [200, headers, result[:stream_proc]]
|
138
|
+
elsif result[:json]
|
139
|
+
response_body = result[:json].to_json
|
140
|
+
status = result[:status] || 200
|
141
|
+
|
142
|
+
begin
|
143
|
+
response_json = result[:json]
|
144
|
+
if response_json[:error]
|
145
|
+
@logger.info("← ERROR RESPONSE (code: #{response_json[:error][:code]})")
|
146
|
+
elsif status == 202
|
147
|
+
@logger.info("← NOTIFICATION ACCEPTED [NO RESPONSE REQUIRED]")
|
148
|
+
elsif response_json[:accepted] == true && status == 200
|
149
|
+
method = request_json['method'] rescue 'unknown'
|
150
|
+
id = request_json['id'] rescue 'unknown'
|
151
|
+
@logger.info("← #{method} RESPONSE (id: #{id}) [DELIVERED VIA SSE STREAM]")
|
152
|
+
elsif response_json[:result]
|
153
|
+
method = request_json['method'] rescue 'unknown'
|
154
|
+
@logger.info("← #{method} RESPONSE (id: #{response_json[:id]})")
|
155
|
+
else
|
156
|
+
@logger.info("← RESPONSE (status: #{status})")
|
157
|
+
end
|
158
|
+
@logger.info(" Response: #{response_body}") unless status == 202 && response_body == '{}'
|
159
|
+
rescue
|
160
|
+
@logger.info("← RESPONSE (status: #{status})")
|
161
|
+
@logger.info(" Response: #{response_body}") unless response_body.empty?
|
162
|
+
end
|
163
|
+
|
164
|
+
headers = result[:headers] || {}
|
165
|
+
headers["Content-Type"] = "application/json"
|
166
|
+
headers["Access-Control-Allow-Origin"] = "*"
|
167
|
+
headers["Access-Control-Allow-Methods"] = "GET, POST, DELETE, OPTIONS"
|
168
|
+
headers["Access-Control-Allow-Headers"] = "Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version, Origin"
|
169
|
+
|
170
|
+
[result[:status] || 200, headers, [response_body]]
|
171
|
+
else
|
172
|
+
# Fallback
|
173
|
+
@logger.error("← Invalid transport response")
|
174
|
+
[500, {"Content-Type" => "application/json"}, ['{"error": "Invalid transport response"}']]
|
175
|
+
end
|
176
|
+
else
|
177
|
+
@logger.error("← Unexpected response format")
|
178
|
+
[500, {"Content-Type" => "application/json"}, ['{"error": "Unexpected response format"}']]
|
179
|
+
end
|
180
|
+
rescue => e
|
181
|
+
@logger.error("Error handling request: #{e.message}")
|
182
|
+
@logger.debug("Full backtrace:\n#{e.backtrace.join("\n")}")
|
183
|
+
[500, {"Content-Type" => "application/json"}, [%Q({"error": "Internal server error: #{e.message}"})]]
|
184
|
+
ensure
|
185
|
+
transport&.cleanup if transport&.respond_to?(:cleanup)
|
186
|
+
Thread.current[:request_id] = nil
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
|
192
|
+
def create_mcp_server(transport_config)
|
193
|
+
ModelContextProtocol::Server.new do |config|
|
194
|
+
config.name = "MCP Development Server"
|
195
|
+
config.version = "1.0.0"
|
196
|
+
config.logging_enabled = true
|
197
|
+
|
198
|
+
config.pagination = {
|
199
|
+
default_page_size: 2,
|
200
|
+
max_page_size: 3,
|
201
|
+
cursor_ttl: 1800
|
202
|
+
}
|
203
|
+
|
204
|
+
config.set_environment_variable("MCP_ENV", "development")
|
205
|
+
|
206
|
+
config.context = {
|
207
|
+
user_id: "123456",
|
208
|
+
request_id: Thread.current[:request_id]
|
209
|
+
}
|
210
|
+
|
211
|
+
config.transport = transport_config
|
212
|
+
|
213
|
+
config.registry = ModelContextProtocol::Server::Registry.new do
|
214
|
+
prompts list_changed: true do
|
215
|
+
register TestPrompt
|
216
|
+
register TestPromptWithCompletionClass
|
217
|
+
end
|
218
|
+
|
219
|
+
resources list_changed: true, subscribe: true do
|
220
|
+
register TestResource
|
221
|
+
register TestAnnotatedResource
|
222
|
+
register TestBinaryResource
|
223
|
+
end
|
224
|
+
|
225
|
+
resource_templates do
|
226
|
+
register TestResourceTemplate
|
227
|
+
end
|
228
|
+
|
229
|
+
tools list_changed: true do
|
230
|
+
register TestToolWithStructuredContentResponse
|
231
|
+
register TestToolWithTextResponse
|
232
|
+
register TestToolWithImageResponse
|
233
|
+
register TestToolWithMixedContentResponse
|
234
|
+
register TestToolWithResourceResponse
|
235
|
+
register TestToolWithToolErrorResponse
|
236
|
+
register TestToolWithCancellableSleep
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
use_ssl = ENV['SSL'] == 'true'
|
244
|
+
port = use_ssl ? 9293 : 9292
|
245
|
+
protocol = use_ssl ? 'https' : 'http'
|
246
|
+
|
247
|
+
logger.info("Starting MCP #{protocol.upcase} Development Server on #{protocol}://localhost:#{port}/mcp")
|
248
|
+
|
249
|
+
app = Rack::Builder.new do
|
250
|
+
map '/mcp' do
|
251
|
+
run MCPHttpApp.new(logger)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
server_options = {
|
256
|
+
Port: port,
|
257
|
+
Host: '0.0.0.0'
|
258
|
+
}
|
259
|
+
|
260
|
+
if use_ssl
|
261
|
+
cert_path = File.join(__dir__, '..', 'tmp', 'ssl', 'server.crt')
|
262
|
+
key_path = File.join(__dir__, '..', 'tmp', 'ssl', 'server.key')
|
263
|
+
|
264
|
+
unless File.exist?(cert_path) && File.exist?(key_path)
|
265
|
+
logger.error("SSL certificates not found at tmp/ssl/")
|
266
|
+
logger.error("Generate them with: 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\"")
|
267
|
+
exit(1)
|
268
|
+
end
|
269
|
+
|
270
|
+
server_options.merge!(
|
271
|
+
SSLEnable: true,
|
272
|
+
SSLCertificate: OpenSSL::X509::Certificate.new(File.read(cert_path)),
|
273
|
+
SSLPrivateKey: OpenSSL::PKey::RSA.new(File.read(key_path)),
|
274
|
+
SSLVerifyClient: OpenSSL::SSL::VERIFY_NONE
|
275
|
+
)
|
276
|
+
end
|
277
|
+
|
278
|
+
server = WEBrick::HTTPServer.new(server_options)
|
279
|
+
server.mount '/', Rackup::Handler::WEBrick, app
|
280
|
+
|
281
|
+
['INT', 'TERM'].each do |signal|
|
282
|
+
Signal.trap(signal) do
|
283
|
+
server.shutdown
|
284
|
+
exit(0)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
server.start
|
data/tasks/templates/dev.erb
CHANGED
@@ -11,6 +11,12 @@ server = ModelContextProtocol::Server.new do |config|
|
|
11
11
|
config.version = "1.0.0"
|
12
12
|
config.logging_enabled = true
|
13
13
|
|
14
|
+
config.pagination = {
|
15
|
+
default_page_size: 10,
|
16
|
+
max_page_size: 20,
|
17
|
+
cursor_ttl: 1800
|
18
|
+
}
|
19
|
+
|
14
20
|
config.set_environment_variable("MCP_ENV", "development")
|
15
21
|
|
16
22
|
config.context = {
|
@@ -25,6 +31,7 @@ server = ModelContextProtocol::Server.new do |config|
|
|
25
31
|
|
26
32
|
resources list_changed: true, subscribe: true do
|
27
33
|
register TestResource
|
34
|
+
register TestAnnotatedResource
|
28
35
|
register TestBinaryResource
|
29
36
|
end
|
30
37
|
|
@@ -37,7 +44,6 @@ server = ModelContextProtocol::Server.new do |config|
|
|
37
44
|
register TestToolWithImageResponse
|
38
45
|
register TestToolWithImageResponseDefaultMimeType
|
39
46
|
register TestToolWithResourceResponse
|
40
|
-
register TestToolWithResourceResponseDefaultMimeType
|
41
47
|
register TestToolWithToolErrorResponse
|
42
48
|
end
|
43
49
|
end
|
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.
|
4
|
+
version: 0.5.0
|
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-09-
|
11
|
+
date: 2025-09-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json-schema
|
@@ -38,6 +38,48 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '2.8'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: redis
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: connection_pool
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.4'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.4'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: concurrent-ruby
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.3'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.3'
|
41
83
|
description:
|
42
84
|
email:
|
43
85
|
- dick@hey.com
|
@@ -56,20 +98,36 @@ files:
|
|
56
98
|
- Rakefile
|
57
99
|
- lib/model_context_protocol.rb
|
58
100
|
- lib/model_context_protocol/server.rb
|
101
|
+
- lib/model_context_protocol/server/cancellable.rb
|
59
102
|
- lib/model_context_protocol/server/completion.rb
|
60
103
|
- lib/model_context_protocol/server/configuration.rb
|
104
|
+
- lib/model_context_protocol/server/content.rb
|
105
|
+
- lib/model_context_protocol/server/content_helpers.rb
|
61
106
|
- lib/model_context_protocol/server/mcp_logger.rb
|
107
|
+
- lib/model_context_protocol/server/pagination.rb
|
108
|
+
- lib/model_context_protocol/server/progressable.rb
|
62
109
|
- lib/model_context_protocol/server/prompt.rb
|
110
|
+
- lib/model_context_protocol/server/redis_client_proxy.rb
|
111
|
+
- lib/model_context_protocol/server/redis_config.rb
|
112
|
+
- lib/model_context_protocol/server/redis_pool_manager.rb
|
63
113
|
- lib/model_context_protocol/server/registry.rb
|
64
114
|
- lib/model_context_protocol/server/resource.rb
|
65
115
|
- lib/model_context_protocol/server/resource_template.rb
|
66
116
|
- lib/model_context_protocol/server/router.rb
|
67
|
-
- lib/model_context_protocol/server/session_store.rb
|
68
117
|
- lib/model_context_protocol/server/stdio_transport.rb
|
118
|
+
- lib/model_context_protocol/server/stdio_transport/request_store.rb
|
69
119
|
- lib/model_context_protocol/server/streamable_http_transport.rb
|
120
|
+
- lib/model_context_protocol/server/streamable_http_transport/event_counter.rb
|
121
|
+
- lib/model_context_protocol/server/streamable_http_transport/message_poller.rb
|
122
|
+
- lib/model_context_protocol/server/streamable_http_transport/notification_queue.rb
|
123
|
+
- lib/model_context_protocol/server/streamable_http_transport/request_store.rb
|
124
|
+
- lib/model_context_protocol/server/streamable_http_transport/session_message_queue.rb
|
125
|
+
- lib/model_context_protocol/server/streamable_http_transport/session_store.rb
|
126
|
+
- lib/model_context_protocol/server/streamable_http_transport/stream_registry.rb
|
70
127
|
- lib/model_context_protocol/server/tool.rb
|
71
128
|
- lib/model_context_protocol/version.rb
|
72
129
|
- tasks/mcp.rake
|
130
|
+
- tasks/templates/dev-http.erb
|
73
131
|
- tasks/templates/dev.erb
|
74
132
|
homepage: https://github.com/dickdavis/model-context-protocol-rb
|
75
133
|
licenses:
|