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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -28
- data/CODE_OF_CONDUCT.md +30 -58
- data/CONTRIBUTING.md +61 -67
- data/LICENSE.txt +2 -2
- data/README.md +159 -509
- data/bin/console +11 -0
- data/bin/setup +6 -0
- data/docs/advanced-usage.md +132 -0
- data/docs/api-reference.md +35 -0
- data/docs/testing.md +55 -0
- data/examples/claude/README.md +171 -0
- data/examples/claude/claude-bridge.js +122 -0
- data/lib/mcp_on_ruby/configuration.rb +74 -0
- data/lib/mcp_on_ruby/errors.rb +137 -0
- data/lib/mcp_on_ruby/generators/install_generator.rb +46 -0
- data/lib/mcp_on_ruby/generators/resource_generator.rb +63 -0
- data/lib/mcp_on_ruby/generators/templates/README +31 -0
- data/lib/mcp_on_ruby/generators/templates/application_resource.rb +20 -0
- data/lib/mcp_on_ruby/generators/templates/application_tool.rb +18 -0
- data/lib/mcp_on_ruby/generators/templates/initializer.rb +41 -0
- data/lib/mcp_on_ruby/generators/templates/resource.rb +50 -0
- data/lib/mcp_on_ruby/generators/templates/resource_spec.rb +67 -0
- data/lib/mcp_on_ruby/generators/templates/sample_resource.rb +57 -0
- data/lib/mcp_on_ruby/generators/templates/sample_tool.rb +59 -0
- data/lib/mcp_on_ruby/generators/templates/tool.rb +38 -0
- data/lib/mcp_on_ruby/generators/templates/tool_spec.rb +55 -0
- data/lib/mcp_on_ruby/generators/tool_generator.rb +51 -0
- data/lib/mcp_on_ruby/railtie.rb +108 -0
- data/lib/mcp_on_ruby/resource.rb +161 -0
- data/lib/mcp_on_ruby/server.rb +378 -0
- data/lib/mcp_on_ruby/tool.rb +134 -0
- data/lib/mcp_on_ruby/transport.rb +330 -0
- data/lib/mcp_on_ruby/version.rb +6 -0
- data/lib/mcp_on_ruby.rb +142 -0
- metadata +62 -173
- data/lib/ruby_mcp/client.rb +0 -43
- data/lib/ruby_mcp/configuration.rb +0 -90
- data/lib/ruby_mcp/errors.rb +0 -17
- data/lib/ruby_mcp/models/context.rb +0 -52
- data/lib/ruby_mcp/models/engine.rb +0 -31
- data/lib/ruby_mcp/models/message.rb +0 -60
- data/lib/ruby_mcp/providers/anthropic.rb +0 -269
- data/lib/ruby_mcp/providers/base.rb +0 -57
- data/lib/ruby_mcp/providers/openai.rb +0 -265
- data/lib/ruby_mcp/schemas.rb +0 -56
- data/lib/ruby_mcp/server/app.rb +0 -84
- data/lib/ruby_mcp/server/base_controller.rb +0 -49
- data/lib/ruby_mcp/server/content_controller.rb +0 -68
- data/lib/ruby_mcp/server/contexts_controller.rb +0 -67
- data/lib/ruby_mcp/server/controller.rb +0 -29
- data/lib/ruby_mcp/server/engines_controller.rb +0 -34
- data/lib/ruby_mcp/server/generate_controller.rb +0 -140
- data/lib/ruby_mcp/server/messages_controller.rb +0 -30
- data/lib/ruby_mcp/server/router.rb +0 -84
- data/lib/ruby_mcp/storage/active_record.rb +0 -414
- data/lib/ruby_mcp/storage/base.rb +0 -43
- data/lib/ruby_mcp/storage/error.rb +0 -8
- data/lib/ruby_mcp/storage/memory.rb +0 -69
- data/lib/ruby_mcp/storage/redis.rb +0 -197
- data/lib/ruby_mcp/storage_factory.rb +0 -43
- data/lib/ruby_mcp/validator.rb +0 -45
- data/lib/ruby_mcp/version.rb +0 -6
- 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
|
data/lib/ruby_mcp/validator.rb
DELETED
@@ -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
|
data/lib/ruby_mcp/version.rb
DELETED
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
|