mcp_on_ruby 0.1.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.
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyMCP
4
+ module Server
5
+ class GenerateController < BaseController
6
+ def create
7
+ context_id = params[:context_id]
8
+ engine_id = params[:engine_id]
9
+
10
+ begin
11
+ # Get the context
12
+ context = storage.get_context(context_id)
13
+
14
+ # Find the provider
15
+ provider_name, model = parse_engine_id(engine_id)
16
+ provider = get_provider(provider_name)
17
+
18
+ # Generation options
19
+ options = {
20
+ model: model,
21
+ max_tokens: params[:max_tokens],
22
+ temperature: params[:temperature],
23
+ top_p: params[:top_p],
24
+ frequency_penalty: params[:frequency_penalty],
25
+ presence_penalty: params[:presence_penalty],
26
+ stop: params[:stop]
27
+ }.compact
28
+
29
+ # Generate the response
30
+ response = provider.generate(context, options)
31
+
32
+ # Add the assistant message to the context if requested
33
+ if params[:update_context] != false
34
+ message = RubyMCP::Models::Message.new(
35
+ role: 'assistant',
36
+ content: response[:content],
37
+ metadata: response[:metadata]
38
+ )
39
+ storage.add_message(context_id, message)
40
+ end
41
+
42
+ ok(response)
43
+ rescue RubyMCP::Errors::ContextError => e
44
+ not_found(e.message)
45
+ rescue RubyMCP::Errors::ProviderError, RubyMCP::Errors::EngineError => e
46
+ bad_request(e.message)
47
+ end
48
+ end
49
+
50
+ def stream
51
+ context_id = params[:context_id]
52
+ engine_id = params[:engine_id]
53
+
54
+ # Check that we have a streaming-compatible request
55
+ return bad_request('Streaming requires HTTP/1.1 or higher') unless request.env['HTTP_VERSION'] == 'HTTP/1.1'
56
+
57
+ begin
58
+ # Get the context
59
+ context = storage.get_context(context_id)
60
+
61
+ # Find the provider
62
+ provider_name, model = parse_engine_id(engine_id)
63
+ provider = get_provider(provider_name)
64
+
65
+ # Generation options
66
+ options = {
67
+ model: model,
68
+ max_tokens: params[:max_tokens],
69
+ temperature: params[:temperature],
70
+ top_p: params[:top_p],
71
+ frequency_penalty: params[:frequency_penalty],
72
+ presence_penalty: params[:presence_penalty],
73
+ stop: params[:stop]
74
+ }.compact
75
+
76
+ # Prepare streaming response
77
+ headers = {
78
+ 'Content-Type' => 'text/event-stream',
79
+ 'Cache-Control' => 'no-cache',
80
+ 'Connection' => 'keep-alive'
81
+ }
82
+
83
+ # Start streaming
84
+ chunked_body = Enumerator.new do |yielder|
85
+ complete_message = ''
86
+
87
+ # Stream the response
88
+ provider.generate_stream(context, options) do |chunk|
89
+ data = chunk.to_json
90
+ yielder << "data: #{data}\n\n"
91
+
92
+ # Accumulate content for final message
93
+ complete_message += chunk[:content] if chunk[:content]
94
+ end
95
+
96
+ # Add the complete message to context if requested
97
+ if params[:update_context] != false && !complete_message.empty?
98
+ message = RubyMCP::Models::Message.new(
99
+ role: 'assistant',
100
+ content: complete_message
101
+ )
102
+ storage.add_message(context_id, message)
103
+ end
104
+
105
+ # End the stream
106
+ yielder << "data: [DONE]\n\n"
107
+ end
108
+
109
+ [200, headers, chunked_body]
110
+ rescue RubyMCP::Errors::ContextError => e
111
+ not_found(e.message)
112
+ rescue RubyMCP::Errors::ProviderError, RubyMCP::Errors::EngineError => e
113
+ bad_request(e.message)
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def parse_engine_id(engine_id)
120
+ parts = engine_id.to_s.split('/', 2)
121
+ raise RubyMCP::Errors::ValidationError, 'Invalid engine_id format' unless parts.length == 2
122
+
123
+ parts
124
+ end
125
+
126
+ def get_provider(provider_name)
127
+ provider_config = RubyMCP.configuration.providers[provider_name.to_sym]
128
+ raise RubyMCP::Errors::ProviderError, "Provider not configured: #{provider_name}" unless provider_config
129
+
130
+ class_name = provider_name.to_s.capitalize
131
+ unless RubyMCP::Providers.const_defined?(class_name)
132
+ raise RubyMCP::Errors::ProviderError, "Provider not found: #{provider_name}"
133
+ end
134
+
135
+ provider_class = RubyMCP::Providers.const_get(class_name)
136
+ provider_class.new(provider_config)
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyMCP
4
+ module Server
5
+ class MessagesController < BaseController
6
+ def create
7
+ context_id = params[:context_id]
8
+
9
+ begin
10
+ # Create the message
11
+ message = RubyMCP::Models::Message.new(
12
+ role: params[:role],
13
+ content: params[:content],
14
+ id: params[:id],
15
+ metadata: params[:metadata]
16
+ )
17
+
18
+ # Add to the context
19
+ storage.add_message(context_id, message)
20
+
21
+ created(message.to_h)
22
+ rescue RubyMCP::Errors::ContextError => e
23
+ not_found(e.message)
24
+ rescue RubyMCP::Errors::ValidationError => e
25
+ bad_request(e.message)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyMCP
4
+ module Server
5
+ class Router
6
+ Route = Struct.new(:http_method, :path, :controller, :action)
7
+
8
+ def initialize
9
+ @routes = []
10
+ end
11
+
12
+ def add(method, path, controller, action)
13
+ @routes << Route.new(method, path, controller, action)
14
+ end
15
+
16
+ def route(request)
17
+ route = find_route(request.request_method, request.path)
18
+ return nil unless route
19
+
20
+ params = extract_params(route.path, request.path)
21
+
22
+ # Add body params for non-GET requests
23
+ if request.post? || request.put?
24
+ begin
25
+ body_params = JSON.parse(request.body.read, symbolize_names: true)
26
+ params.merge!(body_params)
27
+ rescue JSON::ParserError
28
+ # Handle empty or invalid JSON
29
+ end
30
+ end
31
+
32
+ # Add query params
33
+ params.merge!(extract_query_params(request))
34
+
35
+ # Initialize the controller and call the action
36
+ controller = route.controller.new(request, params)
37
+ controller.send(route.action)
38
+ end
39
+
40
+ private
41
+
42
+ def find_route(method, path)
43
+ @routes.find do |route|
44
+ route.http_method == method && path_matches?(route.path, path)
45
+ end
46
+ end
47
+
48
+ def path_matches?(route_path, request_path)
49
+ route_parts = route_path.split('/')
50
+ request_parts = request_path.split('/')
51
+
52
+ return false if route_parts.length != request_parts.length
53
+
54
+ route_parts.zip(request_parts).all? do |route_part, request_part|
55
+ route_part.start_with?(':') || route_part == request_part
56
+ end
57
+ end
58
+
59
+ def extract_params(route_path, request_path)
60
+ params = {}
61
+
62
+ route_parts = route_path.split('/')
63
+ request_parts = request_path.split('/')
64
+
65
+ route_parts.zip(request_parts).each do |route_part, request_part|
66
+ if route_part.start_with?(':')
67
+ param_name = route_part[1..].to_sym
68
+ params[param_name] = request_part
69
+ end
70
+ end
71
+
72
+ params
73
+ end
74
+
75
+ def extract_query_params(request)
76
+ params = {}
77
+ request.params.each do |key, value|
78
+ params[key.to_sym] = value
79
+ end
80
+ params
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,43 @@
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
@@ -0,0 +1,69 @@
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
@@ -0,0 +1,45 @@
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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/ruby_mcp/version.rb
4
+ module RubyMCP
5
+ VERSION = '0.1.0'
6
+ end
data/lib/ruby_mcp.rb ADDED
@@ -0,0 +1,52 @@
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
+ module RubyMCP
37
+ class << self
38
+ attr_accessor :configuration
39
+ attr_writer :logger
40
+
41
+ def configure
42
+ self.configuration ||= Configuration.new
43
+ yield(configuration) if block_given?
44
+ end
45
+
46
+ def logger
47
+ @logger ||= Logger.new($stdout).tap do |log|
48
+ log.progname = name
49
+ end
50
+ end
51
+ end
52
+ end