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
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module McpOnRuby
|
4
|
+
# Base error class for MCP-related errors
|
5
|
+
class Error < StandardError
|
6
|
+
attr_reader :code, :data
|
7
|
+
|
8
|
+
def initialize(message, code: nil, data: nil)
|
9
|
+
super(message)
|
10
|
+
@code = code
|
11
|
+
@data = data
|
12
|
+
end
|
13
|
+
|
14
|
+
# Convert error to JSON-RPC error format
|
15
|
+
def to_json_rpc
|
16
|
+
{
|
17
|
+
code: @code || default_error_code,
|
18
|
+
message: message,
|
19
|
+
data: @data
|
20
|
+
}.compact
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def default_error_code
|
26
|
+
-32603 # Internal error
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# JSON-RPC parsing error
|
31
|
+
class ParseError < Error
|
32
|
+
private
|
33
|
+
|
34
|
+
def default_error_code
|
35
|
+
-32700
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Invalid JSON-RPC request
|
40
|
+
class InvalidRequestError < Error
|
41
|
+
private
|
42
|
+
|
43
|
+
def default_error_code
|
44
|
+
-32600
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Method not found
|
49
|
+
class MethodNotFoundError < Error
|
50
|
+
private
|
51
|
+
|
52
|
+
def default_error_code
|
53
|
+
-32601
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Invalid method parameters
|
58
|
+
class InvalidParamsError < Error
|
59
|
+
private
|
60
|
+
|
61
|
+
def default_error_code
|
62
|
+
-32602
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Tool or resource not found
|
67
|
+
class NotFoundError < Error
|
68
|
+
private
|
69
|
+
|
70
|
+
def default_error_code
|
71
|
+
-32603
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Authorization/authentication failed
|
76
|
+
class AuthorizationError < Error
|
77
|
+
private
|
78
|
+
|
79
|
+
def default_error_code
|
80
|
+
-32600
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Validation error for tool arguments or resource parameters
|
85
|
+
class ValidationError < Error
|
86
|
+
private
|
87
|
+
|
88
|
+
def default_error_code
|
89
|
+
-32602
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Tool execution error
|
94
|
+
class ToolExecutionError < Error
|
95
|
+
private
|
96
|
+
|
97
|
+
def default_error_code
|
98
|
+
-32603
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Resource read error
|
103
|
+
class ResourceReadError < Error
|
104
|
+
private
|
105
|
+
|
106
|
+
def default_error_code
|
107
|
+
-32603
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Rate limiting error
|
112
|
+
class RateLimitError < Error
|
113
|
+
private
|
114
|
+
|
115
|
+
def default_error_code
|
116
|
+
-32603
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Configuration error
|
121
|
+
class ConfigurationError < Error
|
122
|
+
private
|
123
|
+
|
124
|
+
def default_error_code
|
125
|
+
-32603
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Transport error
|
130
|
+
class TransportError < Error
|
131
|
+
private
|
132
|
+
|
133
|
+
def default_error_code
|
134
|
+
-32603
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
|
5
|
+
module McpOnRuby
|
6
|
+
module Generators
|
7
|
+
# Generator for installing MCP server in Rails application
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
9
|
+
source_root File.expand_path('templates', __dir__)
|
10
|
+
|
11
|
+
desc "Install MCP server in Rails application"
|
12
|
+
|
13
|
+
def create_initializer
|
14
|
+
template 'initializer.rb', 'config/initializers/mcp_on_ruby.rb'
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_application_classes
|
18
|
+
template 'application_tool.rb', 'app/tools/application_tool.rb'
|
19
|
+
template 'application_resource.rb', 'app/resources/application_resource.rb'
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_example_tool
|
23
|
+
template 'sample_tool.rb', 'app/tools/sample_tool.rb'
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_example_resource
|
27
|
+
template 'sample_resource.rb', 'app/resources/sample_resource.rb'
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_route
|
31
|
+
say "MCP server will be available at /mcp via middleware", :green
|
32
|
+
say "No route configuration needed - handled by Railtie", :blue
|
33
|
+
end
|
34
|
+
|
35
|
+
def show_readme
|
36
|
+
readme 'README' if behavior == :invoke
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def readme(path)
|
42
|
+
say IO.read(File.join(self.class.source_root, path)), :green if File.exist?(File.join(self.class.source_root, path))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
|
5
|
+
module McpOnRuby
|
6
|
+
module Generators
|
7
|
+
# Generator for creating MCP resources
|
8
|
+
class ResourceGenerator < Rails::Generators::NamedBase
|
9
|
+
source_root File.expand_path('templates', __dir__)
|
10
|
+
|
11
|
+
desc "Generate an MCP resource class"
|
12
|
+
|
13
|
+
argument :name, type: :string, desc: "Name of the resource"
|
14
|
+
|
15
|
+
class_option :uri, type: :string, desc: "URI pattern for the resource"
|
16
|
+
class_option :description, type: :string, desc: "Description of the resource"
|
17
|
+
class_option :mime_type, type: :string, default: 'application/json', desc: "MIME type of the resource"
|
18
|
+
class_option :template, type: :boolean, default: false, desc: "Create a templated resource with parameters"
|
19
|
+
|
20
|
+
def create_resource_file
|
21
|
+
template 'resource.rb', File.join('app/resources', "#{file_name}_resource.rb")
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_spec_file
|
25
|
+
return unless File.exist?(Rails.root.join('spec'))
|
26
|
+
|
27
|
+
template 'resource_spec.rb', File.join('spec/resources', "#{file_name}_resource_spec.rb")
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def resource_name
|
33
|
+
name.underscore
|
34
|
+
end
|
35
|
+
|
36
|
+
def resource_class_name
|
37
|
+
"#{name.camelize}Resource"
|
38
|
+
end
|
39
|
+
|
40
|
+
def resource_uri
|
41
|
+
options[:uri] || (options[:template] ? "#{resource_name}/{id}" : resource_name)
|
42
|
+
end
|
43
|
+
|
44
|
+
def resource_description
|
45
|
+
options[:description] || "#{name.humanize} resource"
|
46
|
+
end
|
47
|
+
|
48
|
+
def resource_mime_type
|
49
|
+
options[:mime_type]
|
50
|
+
end
|
51
|
+
|
52
|
+
def is_template?
|
53
|
+
options[:template] || resource_uri.include?('{')
|
54
|
+
end
|
55
|
+
|
56
|
+
def template_params
|
57
|
+
return [] unless is_template?
|
58
|
+
|
59
|
+
resource_uri.scan(/\{([^}]+)\}/).flatten
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
MCP on Ruby has been installed successfully!
|
2
|
+
|
3
|
+
Next steps:
|
4
|
+
|
5
|
+
1. Configure your MCP server in config/initializers/mcp_on_ruby.rb
|
6
|
+
2. Create tools in app/tools/ using: rails generate mcp_on_ruby:tool ToolName
|
7
|
+
3. Create resources in app/resources/ using: rails generate mcp_on_ruby:resource ResourceName
|
8
|
+
4. Start your Rails server and access MCP at http://localhost:3000/mcp
|
9
|
+
|
10
|
+
Example tool generation:
|
11
|
+
rails generate mcp_on_ruby:tool UserManager --description "Manage users"
|
12
|
+
|
13
|
+
Example resource generation:
|
14
|
+
rails generate mcp_on_ruby:resource UserData --uri "users/{id}" --template
|
15
|
+
|
16
|
+
Your MCP server will automatically discover and register all tools and resources
|
17
|
+
in app/tools/ and app/resources/ that inherit from ApplicationTool and ApplicationResource.
|
18
|
+
|
19
|
+
For manual setup, you can configure tools and resources in the initializer:
|
20
|
+
|
21
|
+
McpOnRuby.mount_in_rails(Rails.application) do |server|
|
22
|
+
server.tool 'custom_tool', 'Description' do |args|
|
23
|
+
{ result: 'Custom logic here' }
|
24
|
+
end
|
25
|
+
|
26
|
+
server.resource 'custom_resource' do
|
27
|
+
{ data: 'Custom resource data' }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Visit the documentation for more advanced usage patterns!
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Base class for all MCP resources in this application
|
4
|
+
class ApplicationResource < McpOnRuby::Resource
|
5
|
+
# Common functionality for all resources can be added here
|
6
|
+
|
7
|
+
# Example: Add common authorization logic
|
8
|
+
# def authorize(context)
|
9
|
+
# # Check if user is authenticated
|
10
|
+
# context[:authenticated] == true
|
11
|
+
# end
|
12
|
+
|
13
|
+
# Example: Add caching for all resource reads
|
14
|
+
# def read(params = {}, context = {})
|
15
|
+
# cache_key = "mcp:resource:#{uri}:#{params.to_json}"
|
16
|
+
# Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
|
17
|
+
# super
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Base class for all MCP tools in this application
|
4
|
+
class ApplicationTool < McpOnRuby::Tool
|
5
|
+
# Common functionality for all tools can be added here
|
6
|
+
|
7
|
+
# Example: Add common authorization logic
|
8
|
+
# def authorize(context)
|
9
|
+
# # Check if user is authenticated
|
10
|
+
# context[:authenticated] == true
|
11
|
+
# end
|
12
|
+
|
13
|
+
# Example: Add logging for all tool executions
|
14
|
+
# def call(arguments = {}, context = {})
|
15
|
+
# Rails.logger.info("Tool #{name} called with: #{arguments.inspect}")
|
16
|
+
# super
|
17
|
+
# end
|
18
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# MCP on Ruby configuration
|
2
|
+
McpOnRuby.configure do |config|
|
3
|
+
# Logging level
|
4
|
+
config.log_level = Rails.logger.level
|
5
|
+
|
6
|
+
# MCP endpoint path
|
7
|
+
config.path = '/mcp'
|
8
|
+
|
9
|
+
# Authentication (set to true for production)
|
10
|
+
config.authentication_required = false
|
11
|
+
config.authentication_token = ENV['MCP_AUTH_TOKEN']
|
12
|
+
|
13
|
+
# Security settings
|
14
|
+
config.allowed_origins = [] # Empty means allow all origins
|
15
|
+
config.localhost_only = Rails.env.development?
|
16
|
+
config.dns_rebinding_protection = true
|
17
|
+
|
18
|
+
# Rate limiting (requests per minute per IP)
|
19
|
+
config.rate_limit_per_minute = 60
|
20
|
+
|
21
|
+
# Features
|
22
|
+
config.enable_sse = true
|
23
|
+
config.cors_enabled = true
|
24
|
+
end
|
25
|
+
|
26
|
+
# Enable MCP server in Rails
|
27
|
+
Rails.application.configure do
|
28
|
+
config.mcp.enabled = true
|
29
|
+
config.mcp.auto_register_tools = true
|
30
|
+
config.mcp.auto_register_resources = true
|
31
|
+
end
|
32
|
+
|
33
|
+
# Mount MCP server (alternative to route-based mounting)
|
34
|
+
# Rails.application.config.after_initialize do
|
35
|
+
# McpOnRuby.mount_in_rails(Rails.application) do |server|
|
36
|
+
# # Manual tool/resource registration if needed
|
37
|
+
# # server.tool 'custom_tool', 'Description' do |args|
|
38
|
+
# # { result: 'Custom logic here' }
|
39
|
+
# # end
|
40
|
+
# end
|
41
|
+
# end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# <%= resource_description %>
|
4
|
+
class <%= resource_class_name %> < ApplicationResource
|
5
|
+
def initialize
|
6
|
+
super(
|
7
|
+
uri: '<%= resource_uri %>',
|
8
|
+
name: '<%= name.humanize %>',
|
9
|
+
description: '<%= resource_description %>',
|
10
|
+
mime_type: '<%= resource_mime_type %>',
|
11
|
+
metadata: {
|
12
|
+
category: 'custom',
|
13
|
+
version: '1.0.0'
|
14
|
+
},
|
15
|
+
tags: ['<%= resource_name %>']
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def fetch_content(params, context)
|
22
|
+
<% if is_template? -%>
|
23
|
+
# This is a templated resource with parameters: <%= template_params.join(', ') %>
|
24
|
+
# Access parameters via params hash:
|
25
|
+
<% template_params.each do |param| -%>
|
26
|
+
# <%= param %> = params['<%= param %>']
|
27
|
+
<% end -%>
|
28
|
+
<% end -%>
|
29
|
+
|
30
|
+
# Implement your resource content fetching logic here
|
31
|
+
# Return data that will be serialized according to mime_type
|
32
|
+
|
33
|
+
{
|
34
|
+
resource: '<%= resource_name %>',
|
35
|
+
<% if is_template? -%>
|
36
|
+
parameters: params,
|
37
|
+
<% end -%>
|
38
|
+
data: {
|
39
|
+
# Your resource data here
|
40
|
+
},
|
41
|
+
generated_at: Time.current.iso8601
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
# Optional: Add authorization logic
|
46
|
+
# def authorize(context)
|
47
|
+
# # Return true/false based on context (user, permissions, etc.)
|
48
|
+
# true
|
49
|
+
# end
|
50
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
RSpec.describe <%= resource_class_name %>, type: :model do
|
6
|
+
subject(:resource) { described_class.new }
|
7
|
+
|
8
|
+
describe '#read' do
|
9
|
+
<% if is_template? -%>
|
10
|
+
let(:params) { { <%= template_params.map { |p| "'#{p}' => 'test_#{p}'" }.join(', ') %> } }
|
11
|
+
<% else -%>
|
12
|
+
let(:params) { {} }
|
13
|
+
<% end -%>
|
14
|
+
let(:context) { { remote_ip: '127.0.0.1' } }
|
15
|
+
|
16
|
+
it 'returns resource content' do
|
17
|
+
result = resource.read(params, context)
|
18
|
+
|
19
|
+
expect(result).to be_a(Hash)
|
20
|
+
expect(result).to have_key(:contents)
|
21
|
+
expect(result[:contents]).to be_an(Array)
|
22
|
+
|
23
|
+
content = result[:contents].first
|
24
|
+
expect(content).to include(:uri, :mimeType, :text)
|
25
|
+
end
|
26
|
+
|
27
|
+
<% if is_template? -%>
|
28
|
+
it 'uses template parameters' do
|
29
|
+
result = resource.read(params, context)
|
30
|
+
content = JSON.parse(result[:contents].first[:text])
|
31
|
+
|
32
|
+
expect(content['parameters']).to eq(params.stringify_keys)
|
33
|
+
end
|
34
|
+
<% end -%>
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#authorize' do
|
38
|
+
let(:context) { { authenticated: true } }
|
39
|
+
|
40
|
+
it 'returns true for authorized context' do
|
41
|
+
expect(resource.authorized?(context)).to be true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#to_schema' do
|
46
|
+
it 'returns valid schema' do
|
47
|
+
schema = resource.to_schema
|
48
|
+
|
49
|
+
expect(schema).to include(:uri, :mimeType)
|
50
|
+
expect(schema[:uri]).to eq('<%= resource_uri %>')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
<% if is_template? -%>
|
55
|
+
describe '#template?' do
|
56
|
+
it 'returns true' do
|
57
|
+
expect(resource.template?).to be true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#template_params' do
|
62
|
+
it 'returns parameter names' do
|
63
|
+
expect(resource.template_params).to match_array(<%= template_params.inspect %>)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
<% end -%>
|
67
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Sample resource demonstrating MCP resource creation
|
4
|
+
class SampleResource < ApplicationResource
|
5
|
+
def initialize
|
6
|
+
super(
|
7
|
+
uri: 'sample_data',
|
8
|
+
name: 'Sample Data',
|
9
|
+
description: 'A sample resource that provides application statistics',
|
10
|
+
mime_type: 'application/json',
|
11
|
+
metadata: {
|
12
|
+
category: 'sample',
|
13
|
+
version: '1.0.0'
|
14
|
+
},
|
15
|
+
tags: ['sample', 'stats']
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def fetch_content(params, context)
|
22
|
+
# Example: Fetch data from Rails models
|
23
|
+
{
|
24
|
+
application: {
|
25
|
+
name: Rails.application.class.module_parent_name,
|
26
|
+
environment: Rails.env,
|
27
|
+
version: '1.0.0'
|
28
|
+
},
|
29
|
+
statistics: {
|
30
|
+
# users_count: User.count,
|
31
|
+
# posts_count: Post.count,
|
32
|
+
uptime: uptime_info
|
33
|
+
},
|
34
|
+
timestamp: Time.current.iso8601,
|
35
|
+
request_info: {
|
36
|
+
remote_ip: context[:remote_ip],
|
37
|
+
user_agent: context[:user_agent]
|
38
|
+
}
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
# Optional: Add authorization logic
|
43
|
+
# def authorize(context)
|
44
|
+
# # Example: Only allow authenticated users to read this resource
|
45
|
+
# context[:authenticated] == true
|
46
|
+
# end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def uptime_info
|
51
|
+
load_avg = `uptime`.strip rescue 'unavailable'
|
52
|
+
{
|
53
|
+
server_uptime: load_avg,
|
54
|
+
rails_uptime: Time.current - Rails.application.config.time_zone.parse('2024-01-01')
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Sample tool demonstrating MCP tool creation
|
4
|
+
class SampleTool < ApplicationTool
|
5
|
+
def initialize
|
6
|
+
super(
|
7
|
+
name: 'sample_tool',
|
8
|
+
description: 'A sample tool that demonstrates basic functionality',
|
9
|
+
input_schema: {
|
10
|
+
type: 'object',
|
11
|
+
properties: {
|
12
|
+
message: {
|
13
|
+
type: 'string',
|
14
|
+
description: 'Message to process'
|
15
|
+
},
|
16
|
+
count: {
|
17
|
+
type: 'integer',
|
18
|
+
description: 'Number of times to repeat',
|
19
|
+
minimum: 1,
|
20
|
+
maximum: 10,
|
21
|
+
default: 1
|
22
|
+
}
|
23
|
+
},
|
24
|
+
required: ['message']
|
25
|
+
},
|
26
|
+
metadata: {
|
27
|
+
category: 'sample',
|
28
|
+
version: '1.0.0'
|
29
|
+
},
|
30
|
+
tags: ['sample', 'demo']
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def execute(arguments, context)
|
37
|
+
message = arguments['message']
|
38
|
+
count = arguments['count'] || 1
|
39
|
+
|
40
|
+
# Example: Access Rails models or services
|
41
|
+
# user_count = User.count
|
42
|
+
|
43
|
+
{
|
44
|
+
success: true,
|
45
|
+
result: message * count,
|
46
|
+
processed_at: Time.current.iso8601,
|
47
|
+
context_info: {
|
48
|
+
remote_ip: context[:remote_ip],
|
49
|
+
user_agent: context[:user_agent]
|
50
|
+
}
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
# Optional: Add authorization logic
|
55
|
+
# def authorize(context)
|
56
|
+
# # Example: Only allow authenticated users
|
57
|
+
# context[:authenticated] == true
|
58
|
+
# end
|
59
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# <%= tool_description %>
|
4
|
+
class <%= tool_class_name %> < ApplicationTool
|
5
|
+
def initialize
|
6
|
+
super(
|
7
|
+
name: '<%= tool_name %>',
|
8
|
+
description: '<%= tool_description %>',
|
9
|
+
<%= input_schema_code %>,
|
10
|
+
metadata: {
|
11
|
+
category: 'custom',
|
12
|
+
version: '1.0.0'
|
13
|
+
},
|
14
|
+
tags: ['<%= tool_name %>']
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def execute(arguments, context)
|
21
|
+
# Implement your tool logic here
|
22
|
+
# Arguments are validated according to input_schema
|
23
|
+
# Context contains request information (IP, headers, etc.)
|
24
|
+
|
25
|
+
{
|
26
|
+
success: true,
|
27
|
+
result: "Tool <%= tool_name %> executed successfully",
|
28
|
+
arguments: arguments,
|
29
|
+
processed_at: Time.current.iso8601
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
# Optional: Add authorization logic
|
34
|
+
# def authorize(context)
|
35
|
+
# # Return true/false based on context (user, permissions, etc.)
|
36
|
+
# true
|
37
|
+
# end
|
38
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
RSpec.describe <%= tool_class_name %>, type: :model do
|
6
|
+
subject(:tool) { described_class.new }
|
7
|
+
|
8
|
+
describe '#execute' do
|
9
|
+
let(:arguments) { {} }
|
10
|
+
let(:context) { { remote_ip: '127.0.0.1' } }
|
11
|
+
|
12
|
+
it 'executes successfully' do
|
13
|
+
result = tool.call(arguments, context)
|
14
|
+
|
15
|
+
expect(result).to be_a(Hash)
|
16
|
+
expect(result[:success]).to be true
|
17
|
+
end
|
18
|
+
|
19
|
+
# Add more specific tests based on your tool's functionality
|
20
|
+
# context 'with valid arguments' do
|
21
|
+
# let(:arguments) { { param: 'value' } }
|
22
|
+
#
|
23
|
+
# it 'returns expected result' do
|
24
|
+
# result = tool.call(arguments, context)
|
25
|
+
#
|
26
|
+
# expect(result[:result]).to eq('expected_value')
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
|
30
|
+
# context 'with invalid arguments' do
|
31
|
+
# let(:arguments) { { invalid: 'param' } }
|
32
|
+
#
|
33
|
+
# it 'raises validation error' do
|
34
|
+
# expect { tool.call(arguments, context) }.to raise_error(McpOnRuby::ValidationError)
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '#authorize' do
|
40
|
+
let(:context) { { authenticated: true } }
|
41
|
+
|
42
|
+
it 'returns true for authorized context' do
|
43
|
+
expect(tool.authorized?(context)).to be true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#to_schema' do
|
48
|
+
it 'returns valid schema' do
|
49
|
+
schema = tool.to_schema
|
50
|
+
|
51
|
+
expect(schema).to include(:name, :description, :inputSchema)
|
52
|
+
expect(schema[:name]).to eq('<%= tool_name %>')
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|