model-context-protocol-rb 0.3.3 → 0.4.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.
@@ -37,16 +37,21 @@ module ModelContextProtocol
37
37
 
38
38
  private
39
39
 
40
- PROTOCOL_VERSION = "2025-06-18".freeze
41
- private_constant :PROTOCOL_VERSION
40
+ SUPPORTED_PROTOCOL_VERSIONS = ["2025-06-18"].freeze
41
+ private_constant :SUPPORTED_PROTOCOL_VERSIONS
42
42
 
43
- InitializeResponse = Data.define(:protocol_version, :capabilities, :server_info) do
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 |_message|
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: PROTOCOL_VERSION,
87
+ protocol_version: negotiated_version,
69
88
  capabilities: build_capabilities,
70
- server_info: {
71
- name: configuration.name,
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
- configuration.registry.resources_data
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(configuration.logger, configuration.context)
163
+ resource.call
127
164
  end
128
165
 
129
166
  router.map("resources/templates/list") do |message|
130
- configuration.registry.resource_templates_data
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
- configuration.registry.prompts_data
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
- configuration.registry.tools_data
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|
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module ModelContextProtocol
4
- VERSION = "0.3.3"
2
+ VERSION = "0.4.0"
5
3
  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 :generate_executable do
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
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env <%= @ruby_path %>
2
+
3
+ require "bundler/setup"
4
+ require "rack"
5
+ require 'rackup/handler/webrick'
6
+ require "webrick"
7
+ require "securerandom"
8
+ require "redis"
9
+ require "logger"
10
+ require "json"
11
+ require 'stringio'
12
+
13
+ require_relative "../lib/model_context_protocol"
14
+
15
+ Dir[File.join(__dir__, "../spec/support/**/*.rb")].each { |file| require file }
16
+
17
+ logger = Logger.new(STDOUT)
18
+ logger.level = Logger::INFO
19
+ logger.formatter = proc do |severity, datetime, progname, msg|
20
+ request_id = Thread.current[:request_id] || "----"
21
+ "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity} [#{request_id}]: #{msg}\n"
22
+ end
23
+
24
+
25
+ class MCPHttpApp
26
+ def initialize(logger)
27
+ @logger = logger
28
+ end
29
+
30
+ def call(env)
31
+ request_id = SecureRandom.hex(4)
32
+ Thread.current[:request_id] = request_id
33
+
34
+ request = Rack::Request.new(env)
35
+ body_content = request.body.read
36
+
37
+ case env['REQUEST_METHOD']
38
+ when 'POST'
39
+ begin
40
+ request_json = JSON.parse(body_content)
41
+ method = request_json['method']
42
+ id = request_json['id']
43
+
44
+ if method&.start_with?('notifications/')
45
+ @logger.info("→ #{method} [NOTIFICATION]")
46
+ elsif id.nil?
47
+ @logger.info("→ #{method} [NOTIFICATION]")
48
+ else
49
+ @logger.info("→ #{method} (id: #{id}) [REQUEST]")
50
+ end
51
+ @logger.info(" Request: #{body_content}")
52
+ rescue JSON::ParserError
53
+ @logger.info("→ POST #{env['PATH_INFO']} [INVALID JSON]")
54
+ @logger.info(" Request: #{body_content}")
55
+ end
56
+ when 'GET'
57
+ accept_header = env['HTTP_ACCEPT'] || ''
58
+ if accept_header.include?('text/event-stream')
59
+ @logger.info("→ GET #{env['PATH_INFO']} [SSE STREAM REQUEST]")
60
+ else
61
+ @logger.info("→ GET #{env['PATH_INFO']}")
62
+ end
63
+ @logger.info(" Headers: Accept=#{accept_header}") unless accept_header.empty?
64
+ when 'DELETE'
65
+ session_id = env['HTTP_MCP_SESSION_ID']
66
+ if session_id
67
+ @logger.info("→ DELETE #{env['PATH_INFO']} [SESSION CLEANUP: #{session_id}]")
68
+ else
69
+ @logger.info("→ DELETE #{env['PATH_INFO']} [SESSION CLEANUP]")
70
+ end
71
+ else
72
+ @logger.info("→ #{env['REQUEST_METHOD']} #{env['PATH_INFO']}")
73
+ @logger.info(" Request: #{body_content}") unless body_content.empty?
74
+ end
75
+
76
+ env['rack.input'] = StringIO.new(body_content)
77
+ request = Rack::Request.new(env)
78
+
79
+ unless request.path == "/mcp"
80
+ return [404, {"Content-Type" => "application/json"}, ['{"error": "Not found"}']]
81
+ end
82
+
83
+ if request.request_method == "OPTIONS"
84
+ return [200, {
85
+ "Access-Control-Allow-Origin" => "*",
86
+ "Access-Control-Allow-Methods" => "GET, POST, DELETE, OPTIONS",
87
+ "Access-Control-Allow-Headers" => "Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version, Origin",
88
+ "Access-Control-Max-Age" => "86400"
89
+ }, [""]]
90
+ end
91
+
92
+ begin
93
+ redis_client = Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
94
+ @logger.debug("Testing Redis connection...")
95
+ redis_client.ping
96
+ @logger.info("Redis connected successfully")
97
+ end
98
+
99
+ transport_config = {
100
+ type: :streamable_http,
101
+ env: env,
102
+ redis_client: redis_client,
103
+ require_sessions: false, # Optional sessions for easier testing
104
+ session_ttl: 3600,
105
+ validate_origin: false, # Disable for testing
106
+ allowed_origins: ["*"]
107
+ }
108
+
109
+ @logger.debug("Creating MCP server with transport config")
110
+ server = create_mcp_server(transport_config)
111
+
112
+ begin
113
+ @logger.debug("Starting MCP server")
114
+ result = server.start
115
+
116
+ case result
117
+ when Hash
118
+ if result[:stream]
119
+ @logger.info("← SSE STREAM OPENED [PERSISTENT CONNECTION]")
120
+ @logger.info(" Connection will remain open for real-time notifications")
121
+ headers = result[:headers] || {}
122
+ headers["Access-Control-Allow-Origin"] = "*"
123
+ headers["Access-Control-Allow-Methods"] = "GET, POST, DELETE, OPTIONS"
124
+ headers["Access-Control-Allow-Headers"] = "Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version, Origin"
125
+
126
+ return [200, headers, result[:stream_proc]]
127
+ elsif result[:json]
128
+ response_body = result[:json].to_json
129
+ status = result[:status] || 200
130
+
131
+ begin
132
+ response_json = result[:json]
133
+ if response_json[:error]
134
+ @logger.info("← ERROR RESPONSE (code: #{response_json[:error][:code]})")
135
+ elsif status == 202
136
+ @logger.info("← NOTIFICATION ACCEPTED [NO RESPONSE REQUIRED]")
137
+ elsif response_json[:result]
138
+ method = request_json['method'] rescue 'unknown'
139
+ @logger.info("← #{method} RESPONSE (id: #{response_json[:id]})")
140
+ else
141
+ @logger.info("← RESPONSE (status: #{status})")
142
+ end
143
+ @logger.info(" Response: #{response_body}") unless status == 202 && response_body == '{}'
144
+ rescue
145
+ @logger.info("← RESPONSE (status: #{status})")
146
+ @logger.info(" Response: #{response_body}") unless response_body.empty?
147
+ end
148
+
149
+ headers = result[:headers] || {}
150
+ headers["Content-Type"] = "application/json"
151
+ headers["Access-Control-Allow-Origin"] = "*"
152
+ headers["Access-Control-Allow-Methods"] = "GET, POST, DELETE, OPTIONS"
153
+ headers["Access-Control-Allow-Headers"] = "Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version, Origin"
154
+
155
+ [result[:status] || 200, headers, [response_body]]
156
+ else
157
+ # Fallback
158
+ @logger.error("← Invalid transport response")
159
+ [500, {"Content-Type" => "application/json"}, ['{"error": "Invalid transport response"}']]
160
+ end
161
+ else
162
+ @logger.error("← Unexpected response format")
163
+ [500, {"Content-Type" => "application/json"}, ['{"error": "Unexpected response format"}']]
164
+ end
165
+ rescue => e
166
+ @logger.error("Error handling request: #{e.message}")
167
+ @logger.debug("Full backtrace:\n#{e.backtrace.join("\n")}")
168
+ [500, {"Content-Type" => "application/json"}, [%Q({"error": "Internal server error: #{e.message}"})]]
169
+ ensure
170
+ Thread.current[:request_id] = nil
171
+ end
172
+ end
173
+
174
+ private
175
+
176
+ def create_mcp_server(transport_config)
177
+ ModelContextProtocol::Server.new do |config|
178
+ config.name = "MCP Development Server"
179
+ config.version = "1.0.0"
180
+ config.logging_enabled = true
181
+
182
+ config.pagination = {
183
+ default_page_size: 2,
184
+ max_page_size: 3,
185
+ cursor_ttl: 1800
186
+ }
187
+
188
+ config.set_environment_variable("MCP_ENV", "development")
189
+
190
+ config.context = {
191
+ user_id: "123456",
192
+ request_id: Thread.current[:request_id]
193
+ }
194
+
195
+ config.transport = transport_config
196
+
197
+ config.registry = ModelContextProtocol::Server::Registry.new do
198
+ prompts list_changed: true do
199
+ register TestPrompt
200
+ register TestPromptWithCompletionClass
201
+ end
202
+
203
+ resources list_changed: true, subscribe: true do
204
+ register TestResource
205
+ register TestAnnotatedResource
206
+ register TestBinaryResource
207
+ end
208
+
209
+ resource_templates do
210
+ register TestResourceTemplate
211
+ end
212
+
213
+ tools list_changed: true do
214
+ register TestToolWithStructuredContentResponse
215
+ register TestToolWithTextResponse
216
+ register TestToolWithImageResponse
217
+ register TestToolWithMixedContentResponse
218
+ register TestToolWithResourceResponse
219
+ register TestToolWithToolErrorResponse
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
225
+
226
+ logger.info("Starting MCP HTTP Development Server on http://localhost:9292/mcp")
227
+
228
+ app = Rack::Builder.new do
229
+ map '/mcp' do
230
+ run MCPHttpApp.new(logger)
231
+ end
232
+ end
233
+
234
+ server = WEBrick::HTTPServer.new(Port: 9292, Host: '0.0.0.0')
235
+ server.mount '/', Rackup::Handler::WEBrick, app
236
+
237
+ ['INT', 'TERM'].each do |signal|
238
+ Signal.trap(signal) do
239
+ server.shutdown
240
+ exit(0)
241
+ end
242
+ end
243
+
244
+ 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.3
4
+ version: 0.4.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-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json-schema
@@ -58,7 +58,10 @@ files:
58
58
  - lib/model_context_protocol/server.rb
59
59
  - lib/model_context_protocol/server/completion.rb
60
60
  - lib/model_context_protocol/server/configuration.rb
61
+ - lib/model_context_protocol/server/content.rb
62
+ - lib/model_context_protocol/server/content_helpers.rb
61
63
  - lib/model_context_protocol/server/mcp_logger.rb
64
+ - lib/model_context_protocol/server/pagination.rb
62
65
  - lib/model_context_protocol/server/prompt.rb
63
66
  - lib/model_context_protocol/server/registry.rb
64
67
  - lib/model_context_protocol/server/resource.rb
@@ -70,6 +73,7 @@ files:
70
73
  - lib/model_context_protocol/server/tool.rb
71
74
  - lib/model_context_protocol/version.rb
72
75
  - tasks/mcp.rake
76
+ - tasks/templates/dev-http.erb
73
77
  - tasks/templates/dev.erb
74
78
  homepage: https://github.com/dickdavis/model-context-protocol-rb
75
79
  licenses: