mcp_on_ruby 1.0.0 → 2.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 +39 -3
- data/README.md +359 -163
- data/config/routes.rb +5 -0
- data/lib/generators/mcp_on_ruby/expose/expose_generator.rb +108 -0
- data/lib/generators/mcp_on_ruby/install/install_generator.rb +37 -0
- data/lib/generators/mcp_on_ruby/install/templates/initializer.rb +22 -0
- data/lib/mcp_on_ruby/call_context.rb +29 -0
- data/lib/mcp_on_ruby/configuration.rb +63 -59
- data/lib/mcp_on_ruby/controller.rb +60 -0
- data/lib/mcp_on_ruby/declaration.rb +24 -0
- data/lib/mcp_on_ruby/dispatcher.rb +97 -0
- data/lib/mcp_on_ruby/doctor.rb +67 -0
- data/lib/mcp_on_ruby/endpoint.rb +43 -0
- data/lib/mcp_on_ruby/engine.rb +29 -0
- data/lib/mcp_on_ruby/registry.rb +146 -0
- data/lib/mcp_on_ruby/result.rb +83 -0
- data/lib/mcp_on_ruby/rspec.rb +8 -0
- data/lib/mcp_on_ruby/schema.rb +149 -0
- data/lib/mcp_on_ruby/server_factory.rb +66 -0
- data/lib/mcp_on_ruby/stdio.rb +42 -0
- data/lib/mcp_on_ruby/test_helper.rb +86 -0
- data/lib/mcp_on_ruby/tool_spec.rb +87 -0
- data/lib/mcp_on_ruby/tool_table.rb +31 -0
- data/lib/mcp_on_ruby/version.rb +2 -3
- data/lib/mcp_on_ruby/warden_hook.rb +21 -0
- data/lib/mcp_on_ruby.rb +44 -122
- data/lib/tasks/mcp_on_ruby.rake +20 -0
- metadata +65 -136
- data/CODE_OF_CONDUCT.md +0 -56
- data/CONTRIBUTING.md +0 -86
- data/bin/console +0 -11
- data/bin/setup +0 -6
- data/docs/advanced-usage.md +0 -132
- data/docs/api-reference.md +0 -35
- data/docs/testing.md +0 -55
- data/examples/claude/README.md +0 -171
- data/examples/claude/claude-bridge.js +0 -122
- data/lib/mcp_on_ruby/errors.rb +0 -137
- data/lib/mcp_on_ruby/generators/install_generator.rb +0 -46
- data/lib/mcp_on_ruby/generators/resource_generator.rb +0 -63
- data/lib/mcp_on_ruby/generators/templates/README +0 -31
- data/lib/mcp_on_ruby/generators/templates/application_resource.rb +0 -20
- data/lib/mcp_on_ruby/generators/templates/application_tool.rb +0 -18
- data/lib/mcp_on_ruby/generators/templates/initializer.rb +0 -41
- data/lib/mcp_on_ruby/generators/templates/resource.rb +0 -50
- data/lib/mcp_on_ruby/generators/templates/resource_spec.rb +0 -67
- data/lib/mcp_on_ruby/generators/templates/sample_resource.rb +0 -57
- data/lib/mcp_on_ruby/generators/templates/sample_tool.rb +0 -59
- data/lib/mcp_on_ruby/generators/templates/tool.rb +0 -38
- data/lib/mcp_on_ruby/generators/templates/tool_spec.rb +0 -55
- data/lib/mcp_on_ruby/generators/tool_generator.rb +0 -51
- data/lib/mcp_on_ruby/railtie.rb +0 -108
- data/lib/mcp_on_ruby/resource.rb +0 -161
- data/lib/mcp_on_ruby/server.rb +0 -378
- data/lib/mcp_on_ruby/tool.rb +0 -134
- data/lib/mcp_on_ruby/transport.rb +0 -330
data/lib/mcp_on_ruby/resource.rb
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module McpOnRuby
|
|
4
|
-
# Base class for MCP resources - data sources that AI can read
|
|
5
|
-
class Resource
|
|
6
|
-
attr_reader :uri, :name, :description, :mime_type, :metadata, :tags
|
|
7
|
-
|
|
8
|
-
# Create a new resource
|
|
9
|
-
# @param uri [String] The resource URI (supports templates with {param})
|
|
10
|
-
# @param name [String] Optional human-readable name
|
|
11
|
-
# @param description [String] Resource description
|
|
12
|
-
# @param mime_type [String] MIME type of the resource content
|
|
13
|
-
# @param metadata [Hash] Additional metadata
|
|
14
|
-
# @param tags [Array<String>] Tags for categorization
|
|
15
|
-
def initialize(uri:, name: nil, description: '', mime_type: 'application/json', metadata: {}, tags: [])
|
|
16
|
-
@uri = uri.to_s
|
|
17
|
-
@name = name
|
|
18
|
-
@description = description
|
|
19
|
-
@mime_type = mime_type
|
|
20
|
-
@metadata = metadata
|
|
21
|
-
@tags = Array(tags)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Read the resource content with given parameters
|
|
25
|
-
# @param params [Hash] URI template parameters
|
|
26
|
-
# @param context [Hash] Request context (headers, user info, etc.)
|
|
27
|
-
# @return [Hash] The resource content wrapped in MCP format
|
|
28
|
-
def read(params = {}, context = {})
|
|
29
|
-
# Validate parameters if this is a template
|
|
30
|
-
validate_template_params!(params) if template?
|
|
31
|
-
|
|
32
|
-
# Get the content
|
|
33
|
-
content = fetch_content(params, context)
|
|
34
|
-
|
|
35
|
-
# Wrap in MCP resource format
|
|
36
|
-
{
|
|
37
|
-
contents: [
|
|
38
|
-
{
|
|
39
|
-
uri: resolve_uri(params),
|
|
40
|
-
mimeType: mime_type,
|
|
41
|
-
text: serialize_content(content)
|
|
42
|
-
}
|
|
43
|
-
]
|
|
44
|
-
}
|
|
45
|
-
rescue => error
|
|
46
|
-
McpOnRuby.logger.error("Resource '#{uri}' read failed: #{error.message}")
|
|
47
|
-
McpOnRuby.logger.error(error.backtrace.join("\n"))
|
|
48
|
-
|
|
49
|
-
{
|
|
50
|
-
error: {
|
|
51
|
-
code: -32603,
|
|
52
|
-
message: "Resource read failed: #{error.message}",
|
|
53
|
-
data: { uri: uri, error_type: error.class.name }
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Get the resource schema for MCP protocol
|
|
59
|
-
# @return [Hash] The resource schema
|
|
60
|
-
def to_schema
|
|
61
|
-
schema = {
|
|
62
|
-
uri: uri,
|
|
63
|
-
mimeType: mime_type
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
schema[:name] = name if name
|
|
67
|
-
schema[:description] = description unless description.empty?
|
|
68
|
-
schema[:metadata] = metadata unless metadata.empty?
|
|
69
|
-
schema[:tags] = tags unless tags.empty?
|
|
70
|
-
|
|
71
|
-
schema
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Check if this resource is a template (contains {param} placeholders)
|
|
75
|
-
# @return [Boolean] True if resource URI contains template parameters
|
|
76
|
-
def template?
|
|
77
|
-
uri.include?('{') && uri.include?('}')
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Extract parameter names from template URI
|
|
81
|
-
# @return [Array<String>] Parameter names
|
|
82
|
-
def template_params
|
|
83
|
-
return [] unless template?
|
|
84
|
-
|
|
85
|
-
uri.scan(/\{([^}]+)\}/).flatten
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Check if resource is authorized for the given context
|
|
89
|
-
# @param context [Hash] Request context
|
|
90
|
-
# @return [Boolean] True if authorized
|
|
91
|
-
def authorized?(context = {})
|
|
92
|
-
return true unless respond_to?(:authorize, true)
|
|
93
|
-
|
|
94
|
-
authorize(context)
|
|
95
|
-
rescue => error
|
|
96
|
-
McpOnRuby.logger.warn("Authorization check failed for resource '#{uri}': #{error.message}")
|
|
97
|
-
false
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
protected
|
|
101
|
-
|
|
102
|
-
# Override this method to implement resource content fetching
|
|
103
|
-
# @param params [Hash] URI template parameters
|
|
104
|
-
# @param context [Hash] Request context
|
|
105
|
-
# @return [Object] Resource content (will be serialized)
|
|
106
|
-
def fetch_content(params, context)
|
|
107
|
-
raise NotImplementedError, "Resource '#{uri}' must implement #fetch_content method"
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# Override this method to implement authorization logic
|
|
111
|
-
# @param context [Hash] Request context
|
|
112
|
-
# @return [Boolean] True if authorized
|
|
113
|
-
def authorize(context)
|
|
114
|
-
true
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
private
|
|
118
|
-
|
|
119
|
-
# Validate template parameters
|
|
120
|
-
# @param params [Hash] Parameters to validate
|
|
121
|
-
# @raise [McpOnRuby::ValidationError] If required parameters are missing
|
|
122
|
-
def validate_template_params!(params)
|
|
123
|
-
required_params = template_params
|
|
124
|
-
missing_params = required_params - params.keys.map(&:to_s)
|
|
125
|
-
|
|
126
|
-
unless missing_params.empty?
|
|
127
|
-
raise McpOnRuby::ValidationError,
|
|
128
|
-
"Resource '#{uri}' missing required parameters: #{missing_params.join(', ')}"
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Resolve URI template with parameters
|
|
133
|
-
# @param params [Hash] Parameters to substitute
|
|
134
|
-
# @return [String] Resolved URI
|
|
135
|
-
def resolve_uri(params)
|
|
136
|
-
return uri unless template?
|
|
137
|
-
|
|
138
|
-
resolved = uri.dup
|
|
139
|
-
params.each do |key, value|
|
|
140
|
-
resolved.gsub!("{#{key}}", value.to_s)
|
|
141
|
-
end
|
|
142
|
-
resolved
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Serialize content based on MIME type
|
|
146
|
-
# @param content [Object] Content to serialize
|
|
147
|
-
# @return [String] Serialized content
|
|
148
|
-
def serialize_content(content)
|
|
149
|
-
case mime_type
|
|
150
|
-
when 'application/json'
|
|
151
|
-
content.is_a?(String) ? content : JSON.pretty_generate(content)
|
|
152
|
-
when 'text/plain', 'text/html', 'text/css', 'text/javascript'
|
|
153
|
-
content.to_s
|
|
154
|
-
else
|
|
155
|
-
# For other types, assume it's already in the correct format
|
|
156
|
-
content.to_s
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
end
|
data/lib/mcp_on_ruby/server.rb
DELETED
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module McpOnRuby
|
|
4
|
-
# Main MCP server class that handles JSON-RPC protocol and manages tools/resources
|
|
5
|
-
class Server
|
|
6
|
-
attr_reader :tools, :resources, :configuration, :logger
|
|
7
|
-
|
|
8
|
-
# Initialize a new MCP server
|
|
9
|
-
# @param options [Hash] Server configuration options
|
|
10
|
-
# @yield [Server] Server instance for configuration
|
|
11
|
-
def initialize(options = {}, &block)
|
|
12
|
-
@configuration = McpOnRuby.configuration || Configuration.new
|
|
13
|
-
@configuration.tap do |config|
|
|
14
|
-
options.each { |key, value| config.send("#{key}=", value) if config.respond_to?("#{key}=") }
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
@logger = McpOnRuby.logger
|
|
18
|
-
@tools = {}
|
|
19
|
-
@resources = {}
|
|
20
|
-
@rate_limiter = RateLimiter.new(@configuration.rate_limit_per_minute)
|
|
21
|
-
|
|
22
|
-
# Configure the server using the block
|
|
23
|
-
instance_eval(&block) if block_given?
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Register a tool
|
|
27
|
-
# @param tool [Tool] The tool to register
|
|
28
|
-
# @param name [String] Optional name override
|
|
29
|
-
# @return [Tool] The registered tool
|
|
30
|
-
def register_tool(tool, name = nil)
|
|
31
|
-
tool_name = name || tool.name
|
|
32
|
-
@tools[tool_name] = tool
|
|
33
|
-
@logger.debug("Registered tool: #{tool_name}")
|
|
34
|
-
tool
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Register a resource
|
|
38
|
-
# @param resource [Resource] The resource to register
|
|
39
|
-
# @param uri [String] Optional URI override
|
|
40
|
-
# @return [Resource] The registered resource
|
|
41
|
-
def register_resource(resource, uri = nil)
|
|
42
|
-
resource_uri = uri || resource.uri
|
|
43
|
-
@resources[resource_uri] = resource
|
|
44
|
-
@logger.debug("Registered resource: #{resource_uri}")
|
|
45
|
-
resource
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# DSL method to define a tool
|
|
49
|
-
# @param name [String] Tool name
|
|
50
|
-
# @param description [String] Tool description
|
|
51
|
-
# @param input_schema [Hash] JSON Schema for validation
|
|
52
|
-
# @param options [Hash] Additional options (metadata, tags)
|
|
53
|
-
# @param block [Proc] Tool implementation
|
|
54
|
-
# @return [Tool] The created and registered tool
|
|
55
|
-
def tool(name, description = '', input_schema = {}, **options, &block)
|
|
56
|
-
tool_instance = McpOnRuby.tool(name, description, input_schema, **options, &block)
|
|
57
|
-
register_tool(tool_instance)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# DSL method to define a resource
|
|
61
|
-
# @param uri [String] Resource URI
|
|
62
|
-
# @param options [Hash] Resource options (name, description, etc.)
|
|
63
|
-
# @param block [Proc] Resource implementation
|
|
64
|
-
# @return [Resource] The created and registered resource
|
|
65
|
-
def resource(uri, **options, &block)
|
|
66
|
-
resource_instance = McpOnRuby.resource(uri, **options, &block)
|
|
67
|
-
register_resource(resource_instance)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Handle a JSON-RPC request
|
|
71
|
-
# @param request_body [String] JSON request body
|
|
72
|
-
# @param context [Hash] Request context (headers, IP, etc.)
|
|
73
|
-
# @return [String, nil] JSON response or nil for notifications
|
|
74
|
-
def handle_request(request_body, context = {})
|
|
75
|
-
# Parse JSON request
|
|
76
|
-
request = JSON.parse(request_body)
|
|
77
|
-
|
|
78
|
-
# Rate limiting check
|
|
79
|
-
unless @rate_limiter.allowed?(context[:remote_ip])
|
|
80
|
-
return error_response(nil, -32603, "Rate limit exceeded")
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Handle the request
|
|
84
|
-
response = handle_json_rpc(request, context)
|
|
85
|
-
|
|
86
|
-
# Return JSON response for requests with ID, nil for notifications
|
|
87
|
-
response ? JSON.generate(response) : nil
|
|
88
|
-
|
|
89
|
-
rescue JSON::ParserError => e
|
|
90
|
-
@logger.warn("Invalid JSON request: #{e.message}")
|
|
91
|
-
JSON.generate(error_response(nil, -32700, "Parse error"))
|
|
92
|
-
rescue => e
|
|
93
|
-
@logger.error("Request handling failed: #{e.message}")
|
|
94
|
-
@logger.error(e.backtrace.join("\n"))
|
|
95
|
-
JSON.generate(error_response(nil, -32603, "Internal error"))
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# Get server capabilities for MCP initialization
|
|
99
|
-
# @return [Hash] Server capabilities
|
|
100
|
-
def capabilities
|
|
101
|
-
{
|
|
102
|
-
tools: tools.any? ? {} : nil,
|
|
103
|
-
resources: resources.any? ? { subscribe: @configuration.enable_sse } : nil
|
|
104
|
-
}.compact
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Get server information
|
|
108
|
-
# @return [Hash] Server info
|
|
109
|
-
def server_info
|
|
110
|
-
{
|
|
111
|
-
name: "mcp_on_ruby",
|
|
112
|
-
version: McpOnRuby::VERSION
|
|
113
|
-
}
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
private
|
|
117
|
-
|
|
118
|
-
# Handle JSON-RPC protocol
|
|
119
|
-
# @param request [Hash] Parsed JSON-RPC request
|
|
120
|
-
# @param context [Hash] Request context
|
|
121
|
-
# @return [Hash, nil] Response hash or nil for notifications
|
|
122
|
-
def handle_json_rpc(request, context)
|
|
123
|
-
method = request['method']
|
|
124
|
-
params = request['params'] || {}
|
|
125
|
-
id = request['id']
|
|
126
|
-
|
|
127
|
-
@logger.debug("Handling method: #{method}")
|
|
128
|
-
|
|
129
|
-
case method
|
|
130
|
-
when 'initialize'
|
|
131
|
-
success_response(id, handle_initialize(params))
|
|
132
|
-
when 'tools/list'
|
|
133
|
-
success_response(id, handle_tools_list(context))
|
|
134
|
-
when 'tools/call'
|
|
135
|
-
success_response(id, handle_tool_call(params, context))
|
|
136
|
-
when 'resources/list'
|
|
137
|
-
success_response(id, handle_resources_list(context))
|
|
138
|
-
when 'resources/read'
|
|
139
|
-
success_response(id, handle_resource_read(params, context))
|
|
140
|
-
when 'ping'
|
|
141
|
-
success_response(id, { pong: true })
|
|
142
|
-
else
|
|
143
|
-
id ? error_response(id, -32601, "Method not found: #{method}") : nil
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Handle initialization request
|
|
148
|
-
# @param params [Hash] Initialization parameters
|
|
149
|
-
# @return [Hash] Initialization response
|
|
150
|
-
def handle_initialize(params)
|
|
151
|
-
client_info = params['clientInfo'] || {}
|
|
152
|
-
protocol_version = params['protocolVersion']
|
|
153
|
-
|
|
154
|
-
@logger.info("Client connected: #{client_info['name']} #{client_info['version']}")
|
|
155
|
-
|
|
156
|
-
{
|
|
157
|
-
serverInfo: server_info,
|
|
158
|
-
protocolVersion: McpOnRuby::PROTOCOL_VERSION,
|
|
159
|
-
capabilities: capabilities
|
|
160
|
-
}
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
# Handle tools list request
|
|
164
|
-
# @param context [Hash] Request context
|
|
165
|
-
# @return [Hash] Tools list response
|
|
166
|
-
def handle_tools_list(context)
|
|
167
|
-
authorized_tools = @tools.select { |_, tool| tool.authorized?(context) }
|
|
168
|
-
|
|
169
|
-
{
|
|
170
|
-
tools: authorized_tools.values.map(&:to_schema)
|
|
171
|
-
}
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# Handle tool call request
|
|
175
|
-
# @param params [Hash] Tool call parameters
|
|
176
|
-
# @param context [Hash] Request context
|
|
177
|
-
# @return [Hash] Tool call response
|
|
178
|
-
def handle_tool_call(params, context)
|
|
179
|
-
tool_name = params['name']
|
|
180
|
-
arguments = params['arguments'] || {}
|
|
181
|
-
|
|
182
|
-
tool = @tools[tool_name]
|
|
183
|
-
unless tool
|
|
184
|
-
raise McpOnRuby::NotFoundError, "Tool not found: #{tool_name}"
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
unless tool.authorized?(context)
|
|
188
|
-
raise McpOnRuby::AuthorizationError, "Not authorized to call tool: #{tool_name}"
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
result = tool.call(arguments, context)
|
|
192
|
-
|
|
193
|
-
# Handle error results from tool execution
|
|
194
|
-
if result.key?(:error)
|
|
195
|
-
raise McpOnRuby::ToolExecutionError, result[:error][:message]
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
{
|
|
199
|
-
content: [
|
|
200
|
-
{
|
|
201
|
-
type: "text",
|
|
202
|
-
text: serialize_tool_result(result)
|
|
203
|
-
}
|
|
204
|
-
]
|
|
205
|
-
}
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# Handle resources list request
|
|
209
|
-
# @param context [Hash] Request context
|
|
210
|
-
# @return [Hash] Resources list response
|
|
211
|
-
def handle_resources_list(context)
|
|
212
|
-
authorized_resources = @resources.select { |_, resource| resource.authorized?(context) }
|
|
213
|
-
|
|
214
|
-
{
|
|
215
|
-
resources: authorized_resources.values.map(&:to_schema)
|
|
216
|
-
}
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
# Handle resource read request
|
|
220
|
-
# @param params [Hash] Resource read parameters
|
|
221
|
-
# @param context [Hash] Request context
|
|
222
|
-
# @return [Hash] Resource read response
|
|
223
|
-
def handle_resource_read(params, context)
|
|
224
|
-
uri = params['uri']
|
|
225
|
-
|
|
226
|
-
# Find exact match or template match
|
|
227
|
-
resource = find_resource(uri)
|
|
228
|
-
unless resource
|
|
229
|
-
raise McpOnRuby::NotFoundError, "Resource not found: #{uri}"
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
unless resource.authorized?(context)
|
|
233
|
-
raise McpOnRuby::AuthorizationError, "Not authorized to read resource: #{uri}"
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
# Extract parameters from URI if it's a template
|
|
237
|
-
template_params = extract_template_params(resource.uri, uri)
|
|
238
|
-
|
|
239
|
-
result = resource.read(template_params, context)
|
|
240
|
-
|
|
241
|
-
# Handle error results from resource reading
|
|
242
|
-
if result.key?(:error)
|
|
243
|
-
raise McpOnRuby::ResourceReadError, result[:error][:message]
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
result
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
# Find a resource by URI (exact match or template match)
|
|
250
|
-
# @param uri [String] The URI to find
|
|
251
|
-
# @return [Resource, nil] The matching resource
|
|
252
|
-
def find_resource(uri)
|
|
253
|
-
# Try exact match first
|
|
254
|
-
return @resources[uri] if @resources.key?(uri)
|
|
255
|
-
|
|
256
|
-
# Try template matching
|
|
257
|
-
@resources.each do |template_uri, resource|
|
|
258
|
-
next unless resource.template?
|
|
259
|
-
|
|
260
|
-
if uri_matches_template?(template_uri, uri)
|
|
261
|
-
return resource
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
nil
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
# Check if a URI matches a template
|
|
269
|
-
# @param template [String] Template URI with {param} placeholders
|
|
270
|
-
# @param uri [String] Actual URI to match
|
|
271
|
-
# @return [Boolean] True if URI matches template
|
|
272
|
-
def uri_matches_template?(template, uri)
|
|
273
|
-
# Convert template to regex
|
|
274
|
-
regex_pattern = template.gsub(/\{[^}]+\}/, '([^/]+)')
|
|
275
|
-
regex = /^#{regex_pattern}$/
|
|
276
|
-
|
|
277
|
-
uri =~ regex
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
# Extract parameters from URI using template
|
|
281
|
-
# @param template [String] Template URI
|
|
282
|
-
# @param uri [String] Actual URI
|
|
283
|
-
# @return [Hash] Extracted parameters
|
|
284
|
-
def extract_template_params(template, uri)
|
|
285
|
-
return {} unless template.include?('{')
|
|
286
|
-
|
|
287
|
-
# Get parameter names
|
|
288
|
-
param_names = template.scan(/\{([^}]+)\}/).flatten
|
|
289
|
-
|
|
290
|
-
# Convert template to regex with capture groups
|
|
291
|
-
regex_pattern = template.gsub(/\{[^}]+\}/, '([^/]+)')
|
|
292
|
-
regex = /^#{regex_pattern}$/
|
|
293
|
-
|
|
294
|
-
# Extract values
|
|
295
|
-
matches = uri.match(regex)
|
|
296
|
-
return {} unless matches
|
|
297
|
-
|
|
298
|
-
# Build parameter hash
|
|
299
|
-
param_names.zip(matches.captures).to_h
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
# Serialize tool result for response
|
|
303
|
-
# @param result [Object] Tool execution result
|
|
304
|
-
# @return [String] Serialized result
|
|
305
|
-
def serialize_tool_result(result)
|
|
306
|
-
case result
|
|
307
|
-
when String
|
|
308
|
-
result
|
|
309
|
-
when Hash, Array
|
|
310
|
-
JSON.pretty_generate(result)
|
|
311
|
-
else
|
|
312
|
-
result.to_s
|
|
313
|
-
end
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
# Create success response
|
|
317
|
-
# @param id [String, Integer] Request ID
|
|
318
|
-
# @param result [Object] Response result
|
|
319
|
-
# @return [Hash] Success response
|
|
320
|
-
def success_response(id, result)
|
|
321
|
-
{
|
|
322
|
-
jsonrpc: "2.0",
|
|
323
|
-
id: id,
|
|
324
|
-
result: result
|
|
325
|
-
}
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
# Create error response
|
|
329
|
-
# @param id [String, Integer, nil] Request ID
|
|
330
|
-
# @param code [Integer] Error code
|
|
331
|
-
# @param message [String] Error message
|
|
332
|
-
# @param data [Object] Additional error data
|
|
333
|
-
# @return [Hash] Error response
|
|
334
|
-
def error_response(id, code, message, data = nil)
|
|
335
|
-
response = {
|
|
336
|
-
jsonrpc: "2.0",
|
|
337
|
-
id: id,
|
|
338
|
-
error: {
|
|
339
|
-
code: code,
|
|
340
|
-
message: message
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
response[:error][:data] = data if data
|
|
344
|
-
response
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
# Simple rate limiter
|
|
348
|
-
class RateLimiter
|
|
349
|
-
def initialize(requests_per_minute)
|
|
350
|
-
@requests_per_minute = requests_per_minute
|
|
351
|
-
@requests = {}
|
|
352
|
-
@mutex = Mutex.new
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
def allowed?(ip)
|
|
356
|
-
return true if @requests_per_minute <= 0
|
|
357
|
-
|
|
358
|
-
@mutex.synchronize do
|
|
359
|
-
now = Time.now.to_i
|
|
360
|
-
minute = now / 60
|
|
361
|
-
|
|
362
|
-
@requests[ip] ||= {}
|
|
363
|
-
@requests[ip][minute] ||= 0
|
|
364
|
-
|
|
365
|
-
# Clean old entries
|
|
366
|
-
@requests[ip].delete_if { |m, _| m < minute }
|
|
367
|
-
|
|
368
|
-
if @requests[ip][minute] >= @requests_per_minute
|
|
369
|
-
false
|
|
370
|
-
else
|
|
371
|
-
@requests[ip][minute] += 1
|
|
372
|
-
true
|
|
373
|
-
end
|
|
374
|
-
end
|
|
375
|
-
end
|
|
376
|
-
end
|
|
377
|
-
end
|
|
378
|
-
end
|
data/lib/mcp_on_ruby/tool.rb
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module McpOnRuby
|
|
4
|
-
# Base class for MCP tools - functions that AI can execute
|
|
5
|
-
class Tool
|
|
6
|
-
attr_reader :name, :description, :input_schema, :metadata, :tags
|
|
7
|
-
|
|
8
|
-
# Create a new tool
|
|
9
|
-
# @param name [String] The tool name
|
|
10
|
-
# @param description [String] The tool description
|
|
11
|
-
# @param input_schema [Hash] JSON Schema for input validation
|
|
12
|
-
# @param metadata [Hash] Additional metadata
|
|
13
|
-
# @param tags [Array<String>] Tags for categorization
|
|
14
|
-
def initialize(name:, description: '', input_schema: {}, metadata: {}, tags: [])
|
|
15
|
-
@name = name.to_s
|
|
16
|
-
@description = description
|
|
17
|
-
@input_schema = normalize_schema(input_schema)
|
|
18
|
-
@metadata = metadata
|
|
19
|
-
@tags = Array(tags)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Execute the tool with given arguments
|
|
23
|
-
# @param arguments [Hash] The arguments to pass to the tool
|
|
24
|
-
# @param context [Hash] Request context (headers, user info, etc.)
|
|
25
|
-
# @return [Hash] The tool execution result
|
|
26
|
-
def call(arguments = {}, context = {})
|
|
27
|
-
# Validate arguments against schema
|
|
28
|
-
validate_arguments!(arguments)
|
|
29
|
-
|
|
30
|
-
# Call the implementation
|
|
31
|
-
execute(arguments, context)
|
|
32
|
-
rescue => error
|
|
33
|
-
McpOnRuby.logger.error("Tool '#{name}' execution failed: #{error.message}")
|
|
34
|
-
McpOnRuby.logger.error(error.backtrace.join("\n"))
|
|
35
|
-
|
|
36
|
-
{
|
|
37
|
-
error: {
|
|
38
|
-
code: -32603,
|
|
39
|
-
message: "Tool execution failed: #{error.message}",
|
|
40
|
-
data: { tool: name, error_type: error.class.name }
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Get the tool's schema for MCP protocol
|
|
46
|
-
# @return [Hash] The tool schema
|
|
47
|
-
def to_schema
|
|
48
|
-
{
|
|
49
|
-
name: name,
|
|
50
|
-
description: description,
|
|
51
|
-
inputSchema: input_schema
|
|
52
|
-
}.tap do |schema|
|
|
53
|
-
schema[:metadata] = metadata unless metadata.empty?
|
|
54
|
-
schema[:tags] = tags unless tags.empty?
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Check if tool is authorized for the given context
|
|
59
|
-
# @param context [Hash] Request context
|
|
60
|
-
# @return [Boolean] True if authorized
|
|
61
|
-
def authorized?(context = {})
|
|
62
|
-
return true unless respond_to?(:authorize, true)
|
|
63
|
-
|
|
64
|
-
authorize(context)
|
|
65
|
-
rescue => error
|
|
66
|
-
McpOnRuby.logger.warn("Authorization check failed for tool '#{name}': #{error.message}")
|
|
67
|
-
false
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
protected
|
|
71
|
-
|
|
72
|
-
# Override this method to implement tool functionality
|
|
73
|
-
# @param arguments [Hash] Validated arguments
|
|
74
|
-
# @param context [Hash] Request context
|
|
75
|
-
# @return [Hash] Tool result
|
|
76
|
-
def execute(arguments, context)
|
|
77
|
-
raise NotImplementedError, "Tool '#{name}' must implement #execute method"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Override this method to implement authorization logic
|
|
81
|
-
# @param context [Hash] Request context
|
|
82
|
-
# @return [Boolean] True if authorized
|
|
83
|
-
def authorize(context)
|
|
84
|
-
true
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
private
|
|
88
|
-
|
|
89
|
-
# Validate arguments against the input schema
|
|
90
|
-
# @param arguments [Hash] Arguments to validate
|
|
91
|
-
# @raise [McpOnRuby::ValidationError] If validation fails
|
|
92
|
-
def validate_arguments!(arguments)
|
|
93
|
-
return if input_schema.empty?
|
|
94
|
-
|
|
95
|
-
errors = JSON::Validator.fully_validate(input_schema, arguments)
|
|
96
|
-
return if errors.empty?
|
|
97
|
-
|
|
98
|
-
raise McpOnRuby::ValidationError, "Tool '#{name}' validation failed: #{errors.join(', ')}"
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Normalize schema to ensure it's a proper JSON Schema
|
|
102
|
-
# @param schema [Hash] Input schema
|
|
103
|
-
# @return [Hash] Normalized schema
|
|
104
|
-
def normalize_schema(schema)
|
|
105
|
-
return {} if schema.nil? || schema.empty?
|
|
106
|
-
|
|
107
|
-
# Ensure we have a proper JSON Schema structure
|
|
108
|
-
normalized = schema.is_a?(Hash) ? schema.dup : {}
|
|
109
|
-
|
|
110
|
-
# Set default type if not specified
|
|
111
|
-
normalized['type'] ||= 'object' if normalized.key?('properties') || normalized.key?('required')
|
|
112
|
-
|
|
113
|
-
# Convert symbol keys to strings for JSON Schema compatibility
|
|
114
|
-
deep_stringify_keys(normalized)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Convert all symbol keys to strings recursively
|
|
118
|
-
# @param obj [Object] Object to convert
|
|
119
|
-
# @return [Object] Object with string keys
|
|
120
|
-
def deep_stringify_keys(obj)
|
|
121
|
-
case obj
|
|
122
|
-
when Hash
|
|
123
|
-
obj.each_with_object({}) do |(key, value), hash|
|
|
124
|
-
hash[key.to_s] = deep_stringify_keys(value)
|
|
125
|
-
end
|
|
126
|
-
when Array
|
|
127
|
-
obj.map { |item| deep_stringify_keys(item) }
|
|
128
|
-
else
|
|
129
|
-
obj
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
end
|