mcp_on_ruby 0.3.0 → 1.0.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -28
  3. data/CODE_OF_CONDUCT.md +30 -58
  4. data/CONTRIBUTING.md +61 -67
  5. data/LICENSE.txt +2 -2
  6. data/README.md +159 -509
  7. data/bin/console +11 -0
  8. data/bin/setup +6 -0
  9. data/docs/advanced-usage.md +132 -0
  10. data/docs/api-reference.md +35 -0
  11. data/docs/testing.md +55 -0
  12. data/examples/claude/README.md +171 -0
  13. data/examples/claude/claude-bridge.js +122 -0
  14. data/lib/mcp_on_ruby/configuration.rb +74 -0
  15. data/lib/mcp_on_ruby/errors.rb +137 -0
  16. data/lib/mcp_on_ruby/generators/install_generator.rb +46 -0
  17. data/lib/mcp_on_ruby/generators/resource_generator.rb +63 -0
  18. data/lib/mcp_on_ruby/generators/templates/README +31 -0
  19. data/lib/mcp_on_ruby/generators/templates/application_resource.rb +20 -0
  20. data/lib/mcp_on_ruby/generators/templates/application_tool.rb +18 -0
  21. data/lib/mcp_on_ruby/generators/templates/initializer.rb +41 -0
  22. data/lib/mcp_on_ruby/generators/templates/resource.rb +50 -0
  23. data/lib/mcp_on_ruby/generators/templates/resource_spec.rb +67 -0
  24. data/lib/mcp_on_ruby/generators/templates/sample_resource.rb +57 -0
  25. data/lib/mcp_on_ruby/generators/templates/sample_tool.rb +59 -0
  26. data/lib/mcp_on_ruby/generators/templates/tool.rb +38 -0
  27. data/lib/mcp_on_ruby/generators/templates/tool_spec.rb +55 -0
  28. data/lib/mcp_on_ruby/generators/tool_generator.rb +51 -0
  29. data/lib/mcp_on_ruby/railtie.rb +108 -0
  30. data/lib/mcp_on_ruby/resource.rb +161 -0
  31. data/lib/mcp_on_ruby/server.rb +378 -0
  32. data/lib/mcp_on_ruby/tool.rb +134 -0
  33. data/lib/mcp_on_ruby/transport.rb +330 -0
  34. data/lib/mcp_on_ruby/version.rb +6 -0
  35. data/lib/mcp_on_ruby.rb +142 -0
  36. metadata +62 -173
  37. data/lib/ruby_mcp/client.rb +0 -43
  38. data/lib/ruby_mcp/configuration.rb +0 -90
  39. data/lib/ruby_mcp/errors.rb +0 -17
  40. data/lib/ruby_mcp/models/context.rb +0 -52
  41. data/lib/ruby_mcp/models/engine.rb +0 -31
  42. data/lib/ruby_mcp/models/message.rb +0 -60
  43. data/lib/ruby_mcp/providers/anthropic.rb +0 -269
  44. data/lib/ruby_mcp/providers/base.rb +0 -57
  45. data/lib/ruby_mcp/providers/openai.rb +0 -265
  46. data/lib/ruby_mcp/schemas.rb +0 -56
  47. data/lib/ruby_mcp/server/app.rb +0 -84
  48. data/lib/ruby_mcp/server/base_controller.rb +0 -49
  49. data/lib/ruby_mcp/server/content_controller.rb +0 -68
  50. data/lib/ruby_mcp/server/contexts_controller.rb +0 -67
  51. data/lib/ruby_mcp/server/controller.rb +0 -29
  52. data/lib/ruby_mcp/server/engines_controller.rb +0 -34
  53. data/lib/ruby_mcp/server/generate_controller.rb +0 -140
  54. data/lib/ruby_mcp/server/messages_controller.rb +0 -30
  55. data/lib/ruby_mcp/server/router.rb +0 -84
  56. data/lib/ruby_mcp/storage/active_record.rb +0 -414
  57. data/lib/ruby_mcp/storage/base.rb +0 -43
  58. data/lib/ruby_mcp/storage/error.rb +0 -8
  59. data/lib/ruby_mcp/storage/memory.rb +0 -69
  60. data/lib/ruby_mcp/storage/redis.rb +0 -197
  61. data/lib/ruby_mcp/storage_factory.rb +0 -43
  62. data/lib/ruby_mcp/validator.rb +0 -45
  63. data/lib/ruby_mcp/version.rb +0 -6
  64. data/lib/ruby_mcp.rb +0 -71
@@ -1,414 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- begin
4
- require 'active_record'
5
- rescue LoadError
6
- # ActiveRecord is an optional dependency
7
- # This error will be handled by the storage factory
8
- end
9
-
10
- require 'securerandom'
11
- require 'json'
12
- require_relative 'base'
13
-
14
- module RubyMCP
15
- module Storage
16
- # ActiveRecord-based storage implementation for RubyMCP
17
- class ActiveRecord < Base
18
- # Initialize ActiveRecord storage with options
19
- # @param options [Hash] Options for ActiveRecord storage
20
- # @option options [Hash] :connection ActiveRecord connection configuration
21
- # @option options [String] :table_prefix Prefix for table names (default: 'mcp_')
22
- def initialize(options = {})
23
- super
24
- @table_prefix = options[:table_prefix] || 'mcp_'
25
- @logger = options[:logger] || RubyMCP.logger
26
-
27
- # Set up ActiveRecord connection if provided
28
- ::ActiveRecord::Base.establish_connection(options[:connection]) if options[:connection].is_a?(Hash)
29
-
30
- setup_models
31
- ensure_tables_exist
32
- end
33
-
34
- # Create a new context
35
- # @param context [RubyMCP::Models::Context] Context to create
36
- # @return [RubyMCP::Models::Context] Created context
37
- def create_context(context)
38
- # Check if context already exists
39
- if @context_model.exists?(external_id: context.id)
40
- raise RubyMCP::Errors::ContextError, "Context already exists: #{context.id}"
41
- end
42
-
43
- # Create the context record
44
- ar_context = @context_model.create!(
45
- external_id: context.id,
46
- metadata: JSON.generate(context.metadata || {}),
47
- created_at: context.created_at,
48
- updated_at: context.updated_at
49
- )
50
-
51
- # Create message records if any
52
- context.messages.each do |message|
53
- create_message_record(ar_context.id, message)
54
- end
55
-
56
- # Create content records if any
57
- context.content_map.each do |content_id, content_data|
58
- create_content_record(ar_context.id, content_id, content_data)
59
- end
60
-
61
- context
62
- end
63
-
64
- # Get a context by ID
65
- # @param context_id [String] ID of the context to get
66
- # @return [RubyMCP::Models::Context] Found context
67
- # @raise [RubyMCP::Errors::ContextError] If context not found
68
- def get_context(context_id)
69
- ar_context = @context_model.find_by(external_id: context_id)
70
- raise RubyMCP::Errors::ContextError, "Context not found: #{context_id}" unless ar_context
71
-
72
- # Parse metadata
73
- metadata = begin
74
- json = JSON.parse(ar_context.metadata)
75
- symbolize_keys(json)
76
- rescue JSON::ParserError => e
77
- @logger.warn("Error parsing context metadata: #{e.message}")
78
- {}
79
- end
80
-
81
- # Create a new context object
82
- context = RubyMCP::Models::Context.new(
83
- id: ar_context.external_id,
84
- metadata: metadata
85
- )
86
-
87
- # Set timestamps
88
- context.instance_variable_set(:@created_at, ar_context.created_at)
89
- context.instance_variable_set(:@updated_at, ar_context.updated_at)
90
-
91
- # Load messages
92
- ar_messages = @message_model.where(context_id: ar_context.id).order(:created_at)
93
- ar_messages.each do |ar_message|
94
- # Parse message metadata
95
- msg_metadata = begin
96
- json = JSON.parse(ar_message.metadata)
97
- symbolize_keys(json)
98
- rescue JSON::ParserError => e
99
- @logger.warn("Error parsing message metadata: #{e.message}")
100
- {}
101
- end
102
-
103
- message = RubyMCP::Models::Message.new(
104
- id: ar_message.external_id,
105
- role: ar_message.role,
106
- content: ar_message.content,
107
- metadata: msg_metadata
108
- )
109
- message.instance_variable_set(:@created_at, ar_message.created_at)
110
- context.instance_variable_get(:@messages) << message
111
- end
112
-
113
- # Load content
114
- ar_contents = @content_model.where(context_id: ar_context.id)
115
- ar_contents.each do |ar_content|
116
- content_data = if ar_content.content_type == 'json'
117
- begin
118
- json = JSON.parse(ar_content.data_json)
119
- symbolize_keys(json)
120
- rescue JSON::ParserError => e
121
- @logger.warn("Error parsing content JSON: #{e.message}")
122
- {}
123
- end
124
- else
125
- ar_content.data_binary
126
- end
127
- context.instance_variable_get(:@content_map)[ar_content.external_id] = content_data
128
- end
129
-
130
- context
131
- end
132
-
133
- # Update a context
134
- # @param context [RubyMCP::Models::Context] Context to update
135
- # @return [RubyMCP::Models::Context] Updated context
136
- # @raise [RubyMCP::Errors::ContextError] If context not found
137
- def update_context(context)
138
- ar_context = @context_model.find_by(external_id: context.id)
139
- raise RubyMCP::Errors::ContextError, "Context not found: #{context.id}" unless ar_context
140
-
141
- # Update the context record
142
- ar_context.update!(
143
- metadata: JSON.generate(context.metadata || {}),
144
- updated_at: context.updated_at
145
- )
146
-
147
- # We don't update messages or content here as they are added separately
148
- context
149
- end
150
-
151
- # Delete a context
152
- # @param context_id [String] ID of the context to delete
153
- # @return [Boolean] True if deleted
154
- # @raise [RubyMCP::Errors::ContextError] If context not found
155
- def delete_context(context_id)
156
- ar_context = @context_model.find_by(external_id: context_id)
157
- raise RubyMCP::Errors::ContextError, "Context not found: #{context_id}" unless ar_context
158
-
159
- # Delete all related records manually since we can't rely on cascading in tests
160
- @message_model.where(context_id: ar_context.id).delete_all
161
- @content_model.where(context_id: ar_context.id).delete_all
162
- ar_context.destroy
163
-
164
- true
165
- end
166
-
167
- # List contexts with pagination
168
- # @param limit [Integer] Maximum number of contexts to return
169
- # @param offset [Integer] Number of contexts to skip
170
- # @return [Array<RubyMCP::Models::Context>] List of contexts
171
- def list_contexts(limit: 100, offset: 0)
172
- ar_contexts = @context_model.order(updated_at: :desc).limit(limit).offset(offset)
173
-
174
- ar_contexts.map do |ar_context|
175
- get_context(ar_context.external_id)
176
- end
177
- end
178
-
179
- # List all content for a context
180
- # @param context_id [String] ID of the context
181
- # @return [Hash] Map of content_id to content data
182
- # @raise [RubyMCP::Errors::ContextError] If context not found
183
- def list_content(context_id)
184
- ar_context = @context_model.find_by(external_id: context_id)
185
- raise RubyMCP::Errors::ContextError, "Context not found: #{context_id}" unless ar_context
186
-
187
- content_map = {}
188
- ar_contents = @content_model.where(context_id: ar_context.id)
189
-
190
- ar_contents.each do |ar_content|
191
- content_data = if ar_content.content_type == 'json'
192
- begin
193
- json = JSON.parse(ar_content.data_json)
194
- symbolize_keys(json)
195
- rescue JSON::ParserError => e
196
- raise RubyMCP::Errors::ContentError,
197
- "Invalid JSON in content #{ar_content.external_id}: #{e.message}"
198
- end
199
- else
200
- ar_content.data_binary
201
- end
202
- content_map[ar_content.external_id] = content_data
203
- end
204
-
205
- content_map
206
- end
207
-
208
- # Add a message to a context
209
- # @param context_id [String] ID of the context
210
- # @param message [RubyMCP::Models::Message] Message to add
211
- # @return [RubyMCP::Models::Message] Added message
212
- # @raise [RubyMCP::Errors::ContextError] If context not found
213
- def add_message(context_id, message)
214
- ar_context = @context_model.find_by(external_id: context_id)
215
- raise RubyMCP::Errors::ContextError, "Context not found: #{context_id}" unless ar_context
216
-
217
- # Create the message record
218
- create_message_record(ar_context.id, message)
219
-
220
- # Update the context's updated_at timestamp
221
- ar_context.touch
222
-
223
- message
224
- end
225
-
226
- # Add content to a context
227
- # @param context_id [String] ID of the context
228
- # @param content_id [String] ID of the content
229
- # @param content_data [Object] Content data
230
- # @return [String] Content ID
231
- # @raise [RubyMCP::Errors::ContextError] If context not found
232
- def add_content(context_id, content_id, content_data)
233
- ar_context = @context_model.find_by(external_id: context_id)
234
- raise RubyMCP::Errors::ContextError, "Context not found: #{context_id}" unless ar_context
235
-
236
- # Create the content record
237
- create_content_record(ar_context.id, content_id, content_data)
238
-
239
- # Update the context's updated_at timestamp
240
- ar_context.touch
241
-
242
- content_id
243
- end
244
-
245
- # Get content from a context
246
- # @param context_id [String] ID of the context
247
- # @param content_id [String] ID of the content
248
- # @return [Object] Content data
249
- # @raise [RubyMCP::Errors::ContextError] If context not found
250
- # @raise [RubyMCP::Errors::ContentError] If content not found
251
- def get_content(context_id, content_id)
252
- ar_context = @context_model.find_by(external_id: context_id)
253
- raise RubyMCP::Errors::ContextError, "Context not found: #{context_id}" unless ar_context
254
-
255
- ar_content = @content_model.find_by(context_id: ar_context.id, external_id: content_id)
256
- raise RubyMCP::Errors::ContentError, "Content not found: #{content_id}" unless ar_content
257
-
258
- if ar_content.content_type == 'json'
259
- begin
260
- json = JSON.parse(ar_content.data_json)
261
- symbolize_keys(json)
262
- rescue JSON::ParserError => e
263
- raise RubyMCP::Errors::ContentError, "Invalid JSON in content #{content_id}: #{e.message}"
264
- end
265
- else
266
- ar_content.data_binary
267
- end
268
- end
269
-
270
- private
271
-
272
- # Setup model classes
273
- def setup_models
274
- prefix = @table_prefix
275
-
276
- # Context model
277
- @context_model = Class.new(::ActiveRecord::Base) do
278
- self.table_name = "#{prefix}contexts"
279
- end
280
- Object.const_set("MCPContext#{SecureRandom.hex(4)}", @context_model)
281
-
282
- # Message model
283
- @message_model = Class.new(::ActiveRecord::Base) do
284
- self.table_name = "#{prefix}messages"
285
- end
286
- Object.const_set("MCPMessage#{SecureRandom.hex(4)}", @message_model)
287
-
288
- # Content model
289
- @content_model = Class.new(::ActiveRecord::Base) do
290
- self.table_name = "#{prefix}contents"
291
- end
292
- Object.const_set("MCPContent#{SecureRandom.hex(4)}", @content_model)
293
- end
294
-
295
- # Ensure necessary tables exist
296
- def ensure_tables_exist
297
- connection = ::ActiveRecord::Base.connection
298
-
299
- # Drop tables if they exist (for clean setup)
300
- connection.drop_table("#{@table_prefix}contents") if connection.table_exists?("#{@table_prefix}contents")
301
-
302
- connection.drop_table("#{@table_prefix}messages") if connection.table_exists?("#{@table_prefix}messages")
303
-
304
- connection.drop_table("#{@table_prefix}contexts") if connection.table_exists?("#{@table_prefix}contexts")
305
-
306
- # Create tables in proper order
307
- create_contexts_table
308
- create_messages_table
309
- create_contents_table
310
- end
311
-
312
- # Check if a table exists
313
- def table_exists?(table_name)
314
- ::ActiveRecord::Base.connection.table_exists?(table_name)
315
- end
316
-
317
- # Create the contexts table
318
- def create_contexts_table
319
- connection = ::ActiveRecord::Base.connection
320
- connection.create_table("#{@table_prefix}contexts") do |t|
321
- t.string :external_id, null: false
322
- t.text :metadata, default: '{}'
323
- t.timestamps
324
- end
325
- connection.add_index("#{@table_prefix}contexts", :external_id, unique: true)
326
- connection.add_index("#{@table_prefix}contexts", :updated_at)
327
- end
328
-
329
- # Create the messages table
330
- def create_messages_table
331
- connection = ::ActiveRecord::Base.connection
332
- connection.create_table("#{@table_prefix}messages") do |t|
333
- t.bigint :context_id, null: false
334
- t.string :external_id, null: false
335
- t.string :role, null: false
336
- t.text :content, null: false
337
- t.text :metadata, default: '{}'
338
- t.timestamps
339
- end
340
- connection.add_index("#{@table_prefix}messages", %i[context_id external_id], unique: true)
341
- connection.add_index("#{@table_prefix}messages", :context_id)
342
- end
343
-
344
- # Create the contents table
345
- def create_contents_table
346
- connection = ::ActiveRecord::Base.connection
347
- connection.create_table("#{@table_prefix}contents") do |t|
348
- t.bigint :context_id, null: false
349
- t.string :external_id, null: false
350
- t.binary :data_binary
351
- t.text :data_json
352
- t.string :content_type
353
- t.timestamps
354
- end
355
- connection.add_index("#{@table_prefix}contents", %i[context_id external_id], unique: true)
356
- connection.add_index("#{@table_prefix}contents", :context_id)
357
- end
358
-
359
- # Create a message record
360
- def create_message_record(context_id, message)
361
- @message_model.create!(
362
- context_id: context_id,
363
- external_id: message.id,
364
- role: message.role,
365
- content: message.content.to_s,
366
- metadata: JSON.generate(message.metadata || {}),
367
- created_at: message.created_at
368
- )
369
- end
370
-
371
- # Create a content record
372
- def create_content_record(context_id, content_id, content_data)
373
- # Validate data type first
374
- unless content_data.is_a?(String) || content_data.is_a?(Hash) ||
375
- content_data.is_a?(Array) || content_data.is_a?(Numeric) ||
376
- content_data.is_a?(TrueClass) || content_data.is_a?(FalseClass) ||
377
- content_data.is_a?(NilClass)
378
- raise RubyMCP::Errors::ContentError,
379
- "Invalid data type: #{content_data.class.name}. Must be String, Hash, Array, Numeric, Boolean, or nil."
380
- end
381
-
382
- if content_data.is_a?(Hash) || content_data.is_a?(Array)
383
- @content_model.create!(
384
- context_id: context_id,
385
- external_id: content_id,
386
- data_json: JSON.generate(content_data),
387
- content_type: 'json'
388
- )
389
- else
390
- @content_model.create!(
391
- context_id: context_id,
392
- external_id: content_id,
393
- data_binary: content_data.to_s,
394
- content_type: 'binary'
395
- )
396
- end
397
- end
398
-
399
- # Helper method to symbolize keys in hashes (including nested ones)
400
- def symbolize_keys(obj)
401
- case obj
402
- when Hash
403
- obj.each_with_object({}) do |(key, value), result|
404
- result[key.to_sym] = symbolize_keys(value)
405
- end
406
- when Array
407
- obj.map { |item| symbolize_keys(item) }
408
- else
409
- obj
410
- end
411
- end
412
- end
413
- end
414
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyMCP
4
- module Storage
5
- class Base
6
- def initialize(options = {})
7
- @options = options
8
- end
9
-
10
- def create_context(context)
11
- raise NotImplementedError, "#{self.class.name} must implement #create_context"
12
- end
13
-
14
- def get_context(context_id)
15
- raise NotImplementedError, "#{self.class.name} must implement #get_context"
16
- end
17
-
18
- def update_context(context)
19
- raise NotImplementedError, "#{self.class.name} must implement #update_context"
20
- end
21
-
22
- def delete_context(context_id)
23
- raise NotImplementedError, "#{self.class.name} must implement #delete_context"
24
- end
25
-
26
- def list_contexts(limit: 100, offset: 0)
27
- raise NotImplementedError, "#{self.class.name} must implement #list_contexts"
28
- end
29
-
30
- def add_message(context_id, message)
31
- raise NotImplementedError, "#{self.class.name} must implement #add_message"
32
- end
33
-
34
- def add_content(context_id, content_id, content_data)
35
- raise NotImplementedError, "#{self.class.name} must implement #add_content"
36
- end
37
-
38
- def get_content(context_id, content_id)
39
- raise NotImplementedError, "#{self.class.name} must implement #get_content"
40
- end
41
- end
42
- end
43
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyMCP
4
- module Storage
5
- # Error class for storage-related errors
6
- class Error < StandardError; end
7
- end
8
- end
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'base'
4
-
5
- module RubyMCP
6
- module Storage
7
- class Memory < Base
8
- def initialize(options = {})
9
- super
10
- @contexts = {}
11
- @contents = {}
12
- end
13
-
14
- def create_context(context)
15
- @contexts[context.id] = context
16
- context
17
- end
18
-
19
- def get_context(context_id)
20
- context = @contexts[context_id]
21
- raise RubyMCP::Errors::ContextError, "Context not found: #{context_id}" unless context
22
-
23
- context
24
- end
25
-
26
- def update_context(context)
27
- @contexts[context.id] = context
28
- context
29
- end
30
-
31
- def delete_context(context_id)
32
- context = get_context(context_id)
33
- @contexts.delete(context_id)
34
- @contents.delete(context_id)
35
- context
36
- end
37
-
38
- def list_contexts(limit: 100, offset: 0)
39
- @contexts.values
40
- .sort_by(&:updated_at)
41
- .reverse
42
- .slice(offset, limit) || []
43
- end
44
-
45
- def add_message(context_id, message)
46
- context = get_context(context_id)
47
- context.add_message(message)
48
- update_context(context)
49
- message
50
- end
51
-
52
- def add_content(context_id, content_id, content_data)
53
- get_context(context_id)
54
- @contents[context_id] ||= {}
55
- @contents[context_id][content_id] = content_data
56
- content_id
57
- end
58
-
59
- def get_content(context_id, content_id)
60
- get_context(context_id)
61
- contents = @contents[context_id] || {}
62
- content = contents[content_id]
63
- raise RubyMCP::Errors::ContentError, "Content not found: #{content_id}" unless content
64
-
65
- content
66
- end
67
- end
68
- end
69
- end