mcp_on_ruby 0.2.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -16
  3. data/CODE_OF_CONDUCT.md +30 -58
  4. data/CONTRIBUTING.md +61 -67
  5. data/LICENSE.txt +2 -2
  6. data/README.md +160 -442
  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 +64 -146
  37. data/lib/ruby_mcp/client.rb +0 -15
  38. data/lib/ruby_mcp/configuration.rb +0 -69
  39. data/lib/ruby_mcp/errors.rb +0 -17
  40. data/lib/ruby_mcp/models/context.rb +0 -51
  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 -63
  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/base.rb +0 -43
  57. data/lib/ruby_mcp/storage/error.rb +0 -8
  58. data/lib/ruby_mcp/storage/memory.rb +0 -69
  59. data/lib/ruby_mcp/storage/redis.rb +0 -197
  60. data/lib/ruby_mcp/storage_factory.rb +0 -32
  61. data/lib/ruby_mcp/validator.rb +0 -45
  62. data/lib/ruby_mcp/version.rb +0 -6
  63. data/lib/ruby_mcp.rb +0 -68
@@ -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
@@ -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,32 +0,0 @@
1
- # lib/ruby_mcp/storage_factory.rb
2
- # frozen_string_literal: true
3
-
4
- module RubyMCP
5
- class StorageFactory
6
- def self.create(config)
7
- # Support both old and new configuration interfaces
8
- storage_config = if config.respond_to?(:storage_config)
9
- config.storage_config
10
- else
11
- config.storage
12
- end
13
-
14
- case storage_config[:type]
15
- when :memory, nil
16
- Storage::Memory.new(storage_config)
17
- when :redis
18
- # Load Redis dependencies
19
- begin
20
- require 'redis'
21
- require_relative 'storage/redis'
22
- rescue LoadError => e
23
- raise LoadError, "Redis storage requires the redis gem. Add it to your Gemfile: #{e.message}"
24
- end
25
-
26
- Storage::Redis.new(storage_config)
27
- else
28
- raise ArgumentError, "Unknown storage type: #{storage_config[:type]}"
29
- end
30
- end
31
- end
32
- 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.2.0'
6
- end
data/lib/ruby_mcp.rb DELETED
@@ -1,68 +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
- require_relative 'ruby_mcp/schemas'
34
- require_relative 'ruby_mcp/validator'
35
-
36
- require_relative 'ruby_mcp/client'
37
-
38
- module RubyMCP
39
- class << self
40
- attr_accessor :configuration
41
- attr_writer :logger
42
-
43
- def client
44
- @client ||= begin
45
- initialize_components unless @storage
46
- Client.new(@storage)
47
- end
48
- end
49
-
50
- def configure
51
- self.configuration ||= Configuration.new
52
- yield(configuration) if block_given?
53
- end
54
-
55
- def logger
56
- @logger ||= Logger.new($stdout).tap do |log|
57
- log.progname = name
58
- end
59
- end
60
-
61
- private
62
-
63
- def initialize_components
64
- require_relative 'ruby_mcp/storage_factory'
65
- @storage = StorageFactory.create(configuration)
66
- end
67
- end
68
- end