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,197 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'redis'
4
- require 'json'
5
- require_relative 'error'
6
-
7
- module RubyMCP
8
- module Storage
9
- # Redis-based storage implementation for RubyMCP
10
- class Redis < Base
11
- def initialize(options = {})
12
- super
13
- @redis = if options[:connection].is_a?(::Redis)
14
- options[:connection]
15
- elsif options[:connection].is_a?(Hash)
16
- ::Redis.new(options[:connection])
17
- else
18
- ::Redis.new
19
- end
20
- @namespace = options[:namespace] || 'ruby_mcp'
21
- @ttl = options[:ttl] || 86_400 # Default 1 day TTL in seconds
22
- end
23
-
24
- # Context management
25
- def create_context(context)
26
- # Ensure context has an ID
27
- context_id = context['id']
28
- raise Error, 'Context must have an ID' unless context_id
29
-
30
- # Check if context already exists
31
- raise Error, "Context with ID '#{context_id}' already exists" if get_context(context_id)
32
-
33
- # Store the context
34
- store_context(context)
35
-
36
- # Add to index with timestamp for ordering
37
- @redis.zadd(
38
- contexts_index_key,
39
- Time.now.to_f,
40
- context_id
41
- )
42
-
43
- # Return the created context
44
- context
45
- end
46
-
47
- def get_context(context_id)
48
- # Get the context data
49
- context_data = @redis.get(context_key(context_id))
50
- return nil unless context_data
51
-
52
- # Parse the context
53
- context = JSON.parse(context_data)
54
-
55
- # Get messages if any
56
- messages = get_messages(context_id)
57
- context['messages'] = messages if messages.any?
58
-
59
- context
60
- end
61
-
62
- def update_context(context)
63
- context_id = context['id']
64
-
65
- # Check if context exists
66
- raise Error, "Context with ID '#{context_id}' does not exist" unless get_context(context_id)
67
-
68
- # Store the updated context
69
- store_context(context)
70
-
71
- context
72
- end
73
-
74
- def delete_context(context_id)
75
- # Remove from index
76
- @redis.zrem(contexts_index_key, context_id)
77
-
78
- # Delete context data
79
- @redis.del(context_key(context_id))
80
-
81
- # Delete messages
82
- @redis.del(messages_key(context_id))
83
-
84
- # Delete all content (using pattern matching)
85
- content_pattern = key(['context', context_id, 'content', '*'])
86
- content_keys = @redis.keys(content_pattern)
87
- @redis.del(*content_keys) if content_keys.any?
88
-
89
- true
90
- end
91
-
92
- def list_contexts(limit: 100, offset: 0)
93
- # Get context IDs from the index, sorted by score (timestamp) descending
94
- context_ids = @redis.zrevrange(contexts_index_key, offset, offset + limit - 1)
95
-
96
- # Return early if no contexts
97
- return [] if context_ids.empty?
98
-
99
- # Get each context
100
- context_ids.map { |id| get_context(id) }.compact
101
- end
102
-
103
- # Message handling
104
- def add_message(context_id, message)
105
- # Ensure context exists
106
- raise Error, "Context with ID '#{context_id}' does not exist" unless get_context(context_id)
107
-
108
- # Add message to the messages list
109
- message_json = JSON.generate(message)
110
- @redis.rpush(messages_key(context_id), message_json)
111
-
112
- # Set TTL on messages key
113
- @redis.expire(messages_key(context_id), @ttl)
114
-
115
- message
116
- end
117
-
118
- # Content handling
119
- def add_content(context_id, content_id, content_data)
120
- # Ensure context exists
121
- raise Error, "Context with ID '#{context_id}' does not exist" unless get_context(context_id)
122
-
123
- # Store content
124
- key = content_key(context_id, content_id)
125
-
126
- # If content is binary or complex, use Base64 encoding
127
- if content_data.is_a?(String) &&
128
- (content_data.encoding == Encoding::BINARY || content_data.include?("\0"))
129
- @redis.set(key, [content_data].pack('m0'))
130
- @redis.set("#{key}:encoding", 'base64')
131
- else
132
- @redis.set(key, content_data)
133
- end
134
-
135
- # Set TTL
136
- @redis.expire(key, @ttl)
137
- @redis.expire("#{key}:encoding", @ttl) if @redis.exists?("#{key}:encoding")
138
-
139
- content_data
140
- end
141
-
142
- def get_content(context_id, content_id)
143
- key = content_key(context_id, content_id)
144
- content = @redis.get(key)
145
- return nil unless content
146
-
147
- # Check if we need to decode from Base64
148
- encoding = @redis.get("#{key}:encoding")
149
- content = content.unpack1('m0') if encoding == 'base64'
150
-
151
- content
152
- end
153
-
154
- private
155
-
156
- def store_context(context)
157
- context_id = context['id']
158
-
159
- # Store the context
160
- context_json = JSON.generate(context)
161
- @redis.set(context_key(context_id), context_json)
162
-
163
- # Set TTL
164
- @redis.expire(context_key(context_id), @ttl)
165
- end
166
-
167
- def get_messages(context_id)
168
- # Get all messages from the list
169
- message_jsons = @redis.lrange(messages_key(context_id), 0, -1)
170
-
171
- # Parse each message
172
- message_jsons.map { |json| JSON.parse(json) }
173
- end
174
-
175
- # Helper methods for key generation
176
- def key(parts)
177
- [@namespace, *parts].join(':')
178
- end
179
-
180
- def context_key(context_id)
181
- key(['context', context_id])
182
- end
183
-
184
- def messages_key(context_id)
185
- key(['context', context_id, 'messages'])
186
- end
187
-
188
- def content_key(context_id, content_id)
189
- key(['context', context_id, 'content', content_id])
190
- end
191
-
192
- def contexts_index_key
193
- key(%w[contexts index])
194
- end
195
- end
196
- end
197
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyMCP
4
- class StorageFactory
5
- def self.create(config)
6
- # Support both old and new configuration interfaces
7
- storage_config = if config.respond_to?(:storage_config)
8
- config.storage_config
9
- else
10
- config.storage
11
- end
12
-
13
- case storage_config[:type]
14
- when :memory, nil
15
- Storage::Memory.new(storage_config)
16
- when :redis
17
- # Load Redis dependencies
18
- begin
19
- require 'redis'
20
- require_relative 'storage/redis'
21
- rescue LoadError
22
- raise LoadError, "Redis storage requires the redis gem. Add it to your Gemfile with: gem 'redis', '~> 5.0'"
23
- end
24
-
25
- Storage::Redis.new(storage_config)
26
- when :active_record
27
- # Load ActiveRecord dependencies
28
- begin
29
- require 'active_record'
30
- require_relative 'storage/active_record'
31
- rescue LoadError
32
- raise LoadError,
33
- "ActiveRecord storage requires the activerecord gem. Add it to your Gemfile with:
34
- gem 'activerecord', '~> 6.0'"
35
- end
36
-
37
- Storage::ActiveRecord.new(storage_config)
38
- else
39
- raise ArgumentError, "Unknown storage type: #{storage_config[:type]}"
40
- end
41
- end
42
- end
43
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'schemas'
4
-
5
- module RubyMCP
6
- class Validator
7
- def self.validate_context(params)
8
- validate(params, Schemas::ContextSchema)
9
- end
10
-
11
- def self.validate_message(params)
12
- validate(params, Schemas::MessageSchema)
13
- end
14
-
15
- def self.validate_generate(params)
16
- validate(params, Schemas::GenerateSchema)
17
- end
18
-
19
- def self.validate_content(params)
20
- validate(params, Schemas::ContentSchema)
21
- end
22
-
23
- def self.validate(params, schema)
24
- result = schema.call(params)
25
-
26
- if result.success?
27
- true
28
- else
29
- # This converts nested error hashes to strings properly
30
- error_messages = format_errors(result.errors.to_h)
31
- raise RubyMCP::Errors::ValidationError, "Validation failed: #{error_messages}"
32
- end
33
- end
34
-
35
- def self.format_errors(errors, prefix = '')
36
- errors.map do |key, value|
37
- if value.is_a?(Hash)
38
- format_errors(value, "#{prefix}#{key}.")
39
- else
40
- "#{prefix}#{key}: #{Array(value).join(', ')}"
41
- end
42
- end.join('; ')
43
- end
44
- end
45
- end
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # lib/ruby_mcp/version.rb
4
- module RubyMCP
5
- VERSION = '0.3.0'
6
- end
data/lib/ruby_mcp.rb DELETED
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'faraday'
4
- require 'jwt'
5
- require 'json'
6
-
7
- require 'concurrent'
8
- require 'logger'
9
- require 'dry-schema'
10
-
11
- require_relative 'ruby_mcp/version'
12
- require_relative 'ruby_mcp/errors'
13
- require_relative 'ruby_mcp/configuration'
14
-
15
- require_relative 'ruby_mcp/models/context'
16
- require_relative 'ruby_mcp/models/message'
17
- require_relative 'ruby_mcp/models/engine'
18
- require_relative 'ruby_mcp/providers/base'
19
- require_relative 'ruby_mcp/storage/base'
20
- require_relative 'ruby_mcp/storage/memory'
21
- require_relative 'ruby_mcp/server/router'
22
- require_relative 'ruby_mcp/server/base_controller'
23
- require_relative 'ruby_mcp/server/app'
24
- require_relative 'ruby_mcp/server/controller'
25
- require_relative 'ruby_mcp/server/engines_controller'
26
- require_relative 'ruby_mcp/server/contexts_controller'
27
- require_relative 'ruby_mcp/server/messages_controller'
28
- require_relative 'ruby_mcp/server/content_controller'
29
- require_relative 'ruby_mcp/server/generate_controller'
30
- require_relative 'ruby_mcp/providers/openai'
31
- require_relative 'ruby_mcp/providers/anthropic'
32
-
33
- # Optional storage backends - don't require them directly
34
- # require_relative 'ruby_mcp/storage/redis'
35
- # require_relative 'ruby_mcp/storage/active_record'
36
-
37
- require_relative 'ruby_mcp/schemas'
38
- require_relative 'ruby_mcp/validator'
39
- require_relative 'ruby_mcp/storage_factory'
40
- require_relative 'ruby_mcp/client'
41
-
42
- module RubyMCP
43
- class << self
44
- attr_accessor :configuration
45
- attr_writer :logger
46
-
47
- def client
48
- @client ||= begin
49
- initialize_components unless @storage
50
- Client.new(@storage)
51
- end
52
- end
53
-
54
- def configure
55
- self.configuration ||= Configuration.new
56
- yield(configuration) if block_given?
57
- end
58
-
59
- def logger
60
- @logger ||= Logger.new($stdout).tap do |log|
61
- log.progname = name
62
- end
63
- end
64
-
65
- private
66
-
67
- def initialize_components
68
- @storage = StorageFactory.create(configuration)
69
- end
70
- end
71
- end