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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -1
  3. data/README.md +886 -196
  4. data/lib/model_context_protocol/server/cancellable.rb +54 -0
  5. data/lib/model_context_protocol/server/configuration.rb +80 -8
  6. data/lib/model_context_protocol/server/content.rb +321 -0
  7. data/lib/model_context_protocol/server/content_helpers.rb +84 -0
  8. data/lib/model_context_protocol/server/pagination.rb +71 -0
  9. data/lib/model_context_protocol/server/progressable.rb +72 -0
  10. data/lib/model_context_protocol/server/prompt.rb +108 -14
  11. data/lib/model_context_protocol/server/redis_client_proxy.rb +134 -0
  12. data/lib/model_context_protocol/server/redis_config.rb +108 -0
  13. data/lib/model_context_protocol/server/redis_pool_manager.rb +110 -0
  14. data/lib/model_context_protocol/server/registry.rb +94 -18
  15. data/lib/model_context_protocol/server/resource.rb +98 -25
  16. data/lib/model_context_protocol/server/resource_template.rb +26 -13
  17. data/lib/model_context_protocol/server/router.rb +36 -3
  18. data/lib/model_context_protocol/server/stdio_transport/request_store.rb +102 -0
  19. data/lib/model_context_protocol/server/stdio_transport.rb +31 -6
  20. data/lib/model_context_protocol/server/streamable_http_transport/event_counter.rb +35 -0
  21. data/lib/model_context_protocol/server/streamable_http_transport/message_poller.rb +101 -0
  22. data/lib/model_context_protocol/server/streamable_http_transport/notification_queue.rb +80 -0
  23. data/lib/model_context_protocol/server/streamable_http_transport/request_store.rb +224 -0
  24. data/lib/model_context_protocol/server/streamable_http_transport/session_message_queue.rb +120 -0
  25. data/lib/model_context_protocol/server/{session_store.rb → streamable_http_transport/session_store.rb} +30 -16
  26. data/lib/model_context_protocol/server/streamable_http_transport/stream_registry.rb +119 -0
  27. data/lib/model_context_protocol/server/streamable_http_transport.rb +352 -112
  28. data/lib/model_context_protocol/server/tool.rb +79 -53
  29. data/lib/model_context_protocol/server.rb +124 -21
  30. data/lib/model_context_protocol/version.rb +1 -1
  31. data/tasks/mcp.rake +28 -2
  32. data/tasks/templates/dev-http.erb +288 -0
  33. data/tasks/templates/dev.erb +7 -1
  34. 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
@@ -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.3.4
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-02 00:00:00.000000000 Z
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: