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,84 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rack'
4
- require 'rack/cors'
5
- require 'json'
6
-
7
- module RubyMCP
8
- module Server
9
- class App
10
- attr_reader :config
11
-
12
- def initialize(config = RubyMCP.configuration)
13
- @config = config
14
- @router = Router.new
15
- setup_routes
16
- end
17
-
18
- def call(env)
19
- request = Rack::Request.new(env)
20
-
21
- # Handle CORS preflight requests
22
- return [200, {}, []] if request.request_method == 'OPTIONS'
23
-
24
- # Authenticate if required
25
- if @config.auth_required && !authenticate(request)
26
- return [401, { 'Content-Type' => 'application/json' }, [{ error: 'Unauthorized' }.to_json]]
27
- end
28
-
29
- # Route the request
30
- response = @router.route(request)
31
-
32
- # Default to 404 if no route matched
33
- response || [404, { 'Content-Type' => 'application/json' }, [{ error: 'Not found' }.to_json]]
34
- end
35
-
36
- def rack_app
37
- app = self
38
-
39
- Rack::Builder.new do
40
- use Rack::Cors do
41
- allow do
42
- origins '*'
43
- resource '*',
44
- headers: :any,
45
- methods: %i[get post put delete options]
46
- end
47
- end
48
-
49
- run app
50
- end
51
- end
52
-
53
- private
54
-
55
- def setup_routes
56
- @router.add('GET', '/engines', EnginesController, :index)
57
- @router.add('POST', '/contexts', ContextsController, :create)
58
- @router.add('GET', '/contexts', ContextsController, :index)
59
- @router.add('GET', '/contexts/:id', ContextsController, :show)
60
- @router.add('DELETE', '/contexts/:id', ContextsController, :destroy)
61
- @router.add('POST', '/messages', MessagesController, :create)
62
- @router.add('POST', '/generate', GenerateController, :create)
63
- @router.add('POST', '/generate/stream', GenerateController, :stream)
64
- @router.add('POST', '/content', ContentController, :create)
65
- @router.add('GET', '/content/:context_id/:id', ContentController, :show)
66
- end
67
-
68
- def authenticate(request)
69
- auth_header = request.env['HTTP_AUTHORIZATION']
70
- return false unless auth_header
71
-
72
- token = auth_header.split(' ').last
73
- return false unless token
74
-
75
- begin
76
- JWT.decode(token, @config.jwt_secret, true, { algorithm: 'HS256' })
77
- true
78
- rescue JWT::DecodeError
79
- false
80
- end
81
- end
82
- end
83
- end
84
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyMCP
4
- module Server
5
- class BaseController
6
- attr_reader :request, :params
7
-
8
- def initialize(request, params = {})
9
- @request = request
10
- @params = params
11
- end
12
-
13
- protected
14
-
15
- def json_response(status, data)
16
- body = data.to_json
17
- headers = {
18
- 'Content-Type' => 'application/json',
19
- 'Content-Length' => body.bytesize.to_s
20
- }
21
- [status, headers, [body]]
22
- end
23
-
24
- def ok(data = {})
25
- json_response(200, data)
26
- end
27
-
28
- def created(data = {})
29
- json_response(201, data)
30
- end
31
-
32
- def bad_request(error = 'Bad request')
33
- json_response(400, { error: error })
34
- end
35
-
36
- def not_found(error = 'Not found')
37
- json_response(404, { error: error })
38
- end
39
-
40
- def server_error(error = 'Internal server error')
41
- json_response(500, { error: error })
42
- end
43
-
44
- def storage
45
- RubyMCP.configuration.storage_instance
46
- end
47
- end
48
- end
49
- end
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'base64'
4
-
5
- module RubyMCP
6
- module Server
7
- class ContentController < BaseController
8
- def create
9
- context_id = params[:context_id]
10
- content_id = params[:id] || "cnt_#{SecureRandom.hex(10)}"
11
- content_type = params[:type] || 'file'
12
-
13
- begin
14
- # Get context to ensure it exists
15
- storage.get_context(context_id)
16
-
17
- # Handle file data (base64 encoded)
18
- data = if params[:file_data]
19
- {
20
- filename: params[:filename],
21
- content_type: params[:content_type] || 'application/octet-stream',
22
- data: Base64.strict_decode64(params[:file_data])
23
- }
24
- else
25
- params[:data] || {}
26
- end
27
-
28
- # Store the content
29
- storage.add_content(context_id, content_id, data)
30
-
31
- created({
32
- id: content_id,
33
- context_id: context_id,
34
- type: content_type
35
- })
36
- rescue RubyMCP::Errors::ContextError => e
37
- not_found(e.message)
38
- rescue ArgumentError => e
39
- # Handle base64 decoding errors
40
- bad_request("Invalid file_data: #{e.message}")
41
- end
42
- end
43
-
44
- def show
45
- context_id = params[:context_id]
46
- content_id = params[:id]
47
-
48
- begin
49
- content = storage.get_content(context_id, content_id)
50
-
51
- if content[:filename] && content[:data]
52
- # Send file response
53
- headers = {
54
- 'Content-Type' => content[:content_type],
55
- 'Content-Disposition' => "attachment; filename=\"#{content[:filename]}\""
56
- }
57
- [200, headers, [content[:data]]]
58
- else
59
- # Send JSON response
60
- ok(content)
61
- end
62
- rescue RubyMCP::Errors::ContextError, RubyMCP::Errors::ContentError => e
63
- not_found(e.message)
64
- end
65
- end
66
- end
67
- end
68
- end
@@ -1,67 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyMCP
4
- module Server
5
- class ContextsController < BaseController
6
- def index
7
- limit = (params[:limit] || 50).to_i
8
- offset = (params[:offset] || 0).to_i
9
-
10
- contexts = storage.list_contexts(limit: limit, offset: offset)
11
- ok({ contexts: contexts.map(&:to_h) })
12
- end
13
-
14
- def show
15
- context = storage.get_context(params[:id])
16
- ok(context.to_h)
17
- rescue RubyMCP::Errors::ContextError => e
18
- not_found(e.message)
19
- end
20
-
21
- def create
22
- # Validate the request
23
- RubyMCP::Validator.validate_context(params)
24
-
25
- # Create a new context
26
- messages = []
27
-
28
- # If messages were provided, create message objects
29
- if params[:messages].is_a?(Array)
30
- params[:messages].each do |msg|
31
- messages << RubyMCP::Models::Message.new(
32
- role: msg[:role],
33
- content: msg[:content],
34
- id: msg[:id],
35
- metadata: msg[:metadata]
36
- )
37
- end
38
- end
39
-
40
- # Create the context
41
- context = RubyMCP::Models::Context.new(
42
- id: params[:id],
43
- messages: messages,
44
- metadata: params[:metadata]
45
- )
46
-
47
- # Store the context
48
- storage.create_context(context)
49
-
50
- created(context.to_h)
51
- rescue RubyMCP::Errors::ValidationError => e
52
- bad_request(e.message)
53
- end
54
-
55
- def destroy
56
- context_id = params[:id]
57
-
58
- begin
59
- storage.delete_context(context_id)
60
- ok({ success: true })
61
- rescue RubyMCP::Errors::ContextError => e
62
- not_found("Context not found: #{e.message}")
63
- end
64
- end
65
- end
66
- end
67
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rack'
4
- require 'rack/handler/webrick'
5
-
6
- module RubyMCP
7
- module Server
8
- class Controller
9
- def initialize(config = RubyMCP.configuration)
10
- @config = config
11
- @app = App.new(config)
12
- end
13
-
14
- def start
15
- options = {
16
- Host: @config.server_host,
17
- Port: @config.server_port
18
- }
19
-
20
- RubyMCP.logger.info "Starting RubyMCP server on #{@config.server_host}:#{@config.server_port}"
21
- Rack::Handler::WEBrick.run @app.rack_app, **options
22
- end
23
-
24
- def stop
25
- # Nothing to do here yet, but will be useful if we add more complex server
26
- end
27
- end
28
- end
29
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyMCP
4
- module Server
5
- class EnginesController < BaseController
6
- def index
7
- engines = []
8
-
9
- RubyMCP.configuration.providers.each do |provider_name, provider_config|
10
- provider_class = get_provider_class(provider_name)
11
- next unless provider_class
12
-
13
- provider = provider_class.new(provider_config)
14
- engines.concat(provider.list_engines)
15
- end
16
-
17
- ok({ engines: engines.map(&:to_h) })
18
- end
19
-
20
- private
21
-
22
- def get_provider_class(provider_name)
23
- class_name = provider_name.to_s.capitalize
24
-
25
- if RubyMCP::Providers.const_defined?(class_name)
26
- RubyMCP::Providers.const_get(class_name)
27
- else
28
- RubyMCP.logger.warn "Provider not found: #{provider_name}"
29
- nil
30
- end
31
- end
32
- end
33
- end
34
- end
@@ -1,140 +0,0 @@
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
@@ -1,30 +0,0 @@
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
@@ -1,84 +0,0 @@
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