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
data/lib/ruby_mcp/server/app.rb
DELETED
@@ -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
|