active_mcp 0.5.0 → 0.6.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/README.md +55 -1
- data/app/controllers/active_mcp/base_controller.rb +1 -0
- data/app/controllers/concerns/active_mcp/request_handlable.rb +10 -2
- data/app/controllers/concerns/active_mcp/resource_readable.rb +5 -1
- data/app/{models/active_mcp/tool_executor.rb → controllers/concerns/active_mcp/tool_executable.rb} +7 -3
- data/app/views/active_mcp/resource_templates_list.json.jbuilder +24 -0
- data/lib/active_mcp/server/fetcher.rb +56 -0
- data/lib/active_mcp/server/method.rb +1 -1
- data/lib/active_mcp/server/protocol_handler.rb +58 -22
- data/lib/active_mcp/server.rb +18 -7
- data/lib/active_mcp/version.rb +1 -1
- metadata +4 -4
- data/lib/active_mcp/server/resource_manager.rb +0 -150
- data/lib/active_mcp/server/tool_manager.rb +0 -163
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 33e83423b0e7e4bdf722b5473ec742e92efe3af946270f3fc1edf2ee9806c8d2
|
4
|
+
data.tar.gz: a9fe38a30a53fbdc4810b2c5d08884730f71cd4ace372188c69916f6db61742a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b20d1f9baf3a7b48cab071135ca15fc182fb3d1cdc3570bcd0b0fb337906dd9e5fa0dc1f646823b281a9600911e3ef9ef7d5cf67eaab80a19a2cad27304f04bb
|
7
|
+
data.tar.gz: 5036f8e8b6056255e81b693cb5fc436d78bf04b8f0546c186110eb4774e6b7b7fe36d4e755c30e84884cadb75a4455af469fbe27d869b502a5ee5d0c79c9d167
|
data/README.md
CHANGED
@@ -36,6 +36,8 @@ A Ruby on Rails engine for the [Model Context Protocol (MCP)](https://modelconte
|
|
36
36
|
- [📦 MCP Resources](#-mcp-resources)
|
37
37
|
- [Creating Resources](#creating-resources)
|
38
38
|
- [Resource Types](#resource-types)
|
39
|
+
- [📦 MCP Resource Templates](#-mcp-resource-templates)
|
40
|
+
- [Creating Resource Templates](#creating-resource-templates)
|
39
41
|
- [⚙️ Advanced Configuration](#️-advanced-configuration)
|
40
42
|
- [Custom Controller](#custom-controller)
|
41
43
|
- [💡 Best Practices](#-best-practices)
|
@@ -311,7 +313,7 @@ MCP Resources allow you to share data and files with AI assistants. Resources ha
|
|
311
313
|
|
312
314
|
### Creating Resources
|
313
315
|
|
314
|
-
Resources are Ruby classes
|
316
|
+
Resources are Ruby classes `**Resource`:
|
315
317
|
|
316
318
|
```ruby
|
317
319
|
class UserResource
|
@@ -348,6 +350,18 @@ class UserResource
|
|
348
350
|
end
|
349
351
|
```
|
350
352
|
|
353
|
+
```ruby
|
354
|
+
class McpController < ActiveMcp::BaseController
|
355
|
+
private
|
356
|
+
|
357
|
+
def resource_list
|
358
|
+
User.all.map do |user|
|
359
|
+
UserResource.new(id: user.id)
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
363
|
+
```
|
364
|
+
|
351
365
|
### Resource Types
|
352
366
|
|
353
367
|
Resources can return two types of content:
|
@@ -400,6 +414,46 @@ def visible?
|
|
400
414
|
end
|
401
415
|
```
|
402
416
|
|
417
|
+
## 📦 MCP Resource Templates
|
418
|
+
|
419
|
+
MCP Resource Teamplates allow you to define template of resources.
|
420
|
+
|
421
|
+
### Creating Resource Templates
|
422
|
+
|
423
|
+
Resources are Ruby classes `**ResourceTemplates`:
|
424
|
+
|
425
|
+
```ruby
|
426
|
+
class UserResourceTemplate
|
427
|
+
def name
|
428
|
+
"Users"
|
429
|
+
end
|
430
|
+
|
431
|
+
def uri_template
|
432
|
+
"data://localhost/users/{id}"
|
433
|
+
end
|
434
|
+
|
435
|
+
def mime_type
|
436
|
+
"application/json"
|
437
|
+
end
|
438
|
+
|
439
|
+
def description
|
440
|
+
"This is a test."
|
441
|
+
end
|
442
|
+
end
|
443
|
+
```
|
444
|
+
|
445
|
+
```ruby
|
446
|
+
class McpController < ActiveMcp::BaseController
|
447
|
+
private
|
448
|
+
|
449
|
+
def resource_templates_list
|
450
|
+
[
|
451
|
+
UserResourceTemplate.new
|
452
|
+
]
|
453
|
+
end
|
454
|
+
end
|
455
|
+
```
|
456
|
+
|
403
457
|
## ⚙️ Advanced Configuration
|
404
458
|
|
405
459
|
### Custom Controller
|
@@ -39,6 +39,10 @@ module ActiveMcp
|
|
39
39
|
@resources = resources_list
|
40
40
|
@format = :jsonrpc
|
41
41
|
render 'active_mcp/resources_list', formats: :json
|
42
|
+
when Method::RESOURCES_TEMPLATES_LIST
|
43
|
+
@resource_templates = resource_templates_list
|
44
|
+
@format = :jsonrpc
|
45
|
+
render 'active_mcp/resource_templates_list', formats: :json
|
42
46
|
when Method::RESOURCES_READ
|
43
47
|
@resource = read_resource(params:, auth_info:)
|
44
48
|
@format = :jsonrpc
|
@@ -48,7 +52,7 @@ module ActiveMcp
|
|
48
52
|
@format = :jsonrpc
|
49
53
|
render 'active_mcp/tools_list', formats: :json
|
50
54
|
when Method::TOOLS_CALL
|
51
|
-
@tool_result =
|
55
|
+
@tool_result = execute_tool(params: params, auth_info: auth_info)
|
52
56
|
@format = :jsonrpc
|
53
57
|
render 'active_mcp/tools_call', formats: :json
|
54
58
|
else
|
@@ -69,12 +73,16 @@ module ActiveMcp
|
|
69
73
|
@resource = read_resource(params:, auth_info:)
|
70
74
|
@format = :json
|
71
75
|
render 'active_mcp/resources_read', formats: :json
|
76
|
+
when Method::RESOURCES_TEMPLATES_LIST
|
77
|
+
@resource_templates = resource_templates_list
|
78
|
+
@format = :json
|
79
|
+
render 'active_mcp/resource_templates_list', formats: :json
|
72
80
|
when Method::TOOLS_LIST
|
73
81
|
@tools = ActiveMcp::Tool.authorized_tools(auth_info)
|
74
82
|
@format = :json
|
75
83
|
render 'active_mcp/tools_list', formats: :json
|
76
84
|
when Method::TOOLS_CALL
|
77
|
-
@tool_result =
|
85
|
+
@tool_result = execute_tool(params: params, auth_info: auth_info)
|
78
86
|
@format = :json
|
79
87
|
render 'active_mcp/tools_call', formats: :json
|
80
88
|
else
|
@@ -4,6 +4,10 @@ module ActiveMcp
|
|
4
4
|
|
5
5
|
private
|
6
6
|
|
7
|
+
def resource_templates_list
|
8
|
+
[]
|
9
|
+
end
|
10
|
+
|
7
11
|
def resources_list
|
8
12
|
[]
|
9
13
|
end
|
@@ -41,7 +45,7 @@ module ActiveMcp
|
|
41
45
|
end
|
42
46
|
|
43
47
|
begin
|
44
|
-
if content = resource.text
|
48
|
+
if resource.respond_to?(:text) && content = resource.text
|
45
49
|
return {
|
46
50
|
contents: [
|
47
51
|
{
|
data/app/{models/active_mcp/tool_executor.rb → controllers/concerns/active_mcp/tool_executable.rb}
RENAMED
@@ -1,6 +1,10 @@
|
|
1
1
|
module ActiveMcp
|
2
|
-
|
3
|
-
|
2
|
+
module ToolExecutable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def execute_tool(params:, auth_info:)
|
4
8
|
if params[:jsonrpc].present?
|
5
9
|
tool_name = params[:params][:name]
|
6
10
|
tool_params = params[:params][:arguments]
|
@@ -105,7 +109,7 @@ module ActiveMcp
|
|
105
109
|
end
|
106
110
|
end
|
107
111
|
|
108
|
-
def
|
112
|
+
def formatted(object)
|
109
113
|
case object
|
110
114
|
when String
|
111
115
|
object
|
@@ -0,0 +1,24 @@
|
|
1
|
+
json.jsonrpc ActiveMcp::JSON_RPC_VERSION if @format == :jsonrpc
|
2
|
+
json.id @id if @format == :jsonrpc && @id.present?
|
3
|
+
|
4
|
+
if @format == :jsonrpc
|
5
|
+
json.result do
|
6
|
+
json.resourceTemplates do
|
7
|
+
json.array!(@resource_templates) do |resource|
|
8
|
+
json.name resource.name
|
9
|
+
json.uriTemplate resource.uri_template
|
10
|
+
json.mimeType resource.mime_type
|
11
|
+
json.description resource.description
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
else
|
16
|
+
json.result do
|
17
|
+
json.array!(@resource_templates) do |resource|
|
18
|
+
json.name resource.name
|
19
|
+
json.uriTemplate resource.uri_template
|
20
|
+
json.mimeType resource.mime_type
|
21
|
+
json.description resource.description
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ActiveMcp
|
2
|
+
class Server
|
3
|
+
class Fetcher
|
4
|
+
def initialize(base_uri: nil, auth: nil)
|
5
|
+
@base_uri = base_uri
|
6
|
+
|
7
|
+
if auth
|
8
|
+
@auth_header = "#{auth[:type] == :bearer ? "Bearer" : "Basic"} #{auth[:token]}"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(params:)
|
13
|
+
return unless @base_uri
|
14
|
+
|
15
|
+
require "net/http"
|
16
|
+
|
17
|
+
unless @base_uri.is_a?(URI) || @base_uri.is_a?(String)
|
18
|
+
Server.log_error("Invalid URI type", StandardError.new("URI must be a String or URI object"))
|
19
|
+
return
|
20
|
+
end
|
21
|
+
|
22
|
+
begin
|
23
|
+
uri = URI.parse(@base_uri.to_s)
|
24
|
+
|
25
|
+
unless uri.scheme =~ /\Ahttps?\z/ && !uri.host.nil?
|
26
|
+
Server.log_error("Invalid URI", StandardError.new("URI must have a valid scheme and host"))
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
if defined?(Rails) && Rails.env.production? && uri.scheme != "https"
|
31
|
+
Server.log_error("HTTPS is required in production environment", StandardError.new("Non-HTTPS URI in production"))
|
32
|
+
return
|
33
|
+
end
|
34
|
+
rescue URI::InvalidURIError => e
|
35
|
+
Server.log_error("Invalid URI format", e)
|
36
|
+
return
|
37
|
+
end
|
38
|
+
|
39
|
+
request = Net::HTTP::Post.new(uri)
|
40
|
+
request.body = JSON.generate(params)
|
41
|
+
request["Content-Type"] = "application/json"
|
42
|
+
request["Authorization"] = @auth_header
|
43
|
+
|
44
|
+
begin
|
45
|
+
response = Net::HTTP.start(uri.hostname, uri.port) do |http|
|
46
|
+
http.request(request)
|
47
|
+
end
|
48
|
+
|
49
|
+
JSON.parse(response.body, symbolize_names: true)
|
50
|
+
rescue => e
|
51
|
+
Server.log_error("Error fetching resource_templates", e)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -17,10 +17,10 @@ module ActiveMcp
|
|
17
17
|
request = JSON.parse(message, symbolize_names: true)
|
18
18
|
handle_request(request)
|
19
19
|
rescue JSON::ParserError => e
|
20
|
-
log_error("JSON parse error", e)
|
20
|
+
Server.log_error("JSON parse error", e)
|
21
21
|
error_response(nil, ErrorCode::PARSE_ERROR, "Invalid JSON format")
|
22
22
|
rescue => e
|
23
|
-
log_error("Internal error during message processing", e)
|
23
|
+
Server.log_error("Internal error during message processing", e)
|
24
24
|
error_response(nil, ErrorCode::INTERNAL_ERROR, "An internal error occurred")
|
25
25
|
end
|
26
26
|
|
@@ -50,10 +50,12 @@ module ActiveMcp
|
|
50
50
|
handle_ping(request)
|
51
51
|
when Method::RESOURCES_LIST
|
52
52
|
handle_list_resources(request)
|
53
|
+
when Method::RESOURCES_TEMPLATES_LIST
|
54
|
+
handle_list_resource_templates(request)
|
53
55
|
when Method::TOOLS_LIST
|
54
56
|
handle_list_tools(request)
|
55
57
|
when Method::TOOLS_CALL
|
56
|
-
|
58
|
+
handle_call_tool(request)
|
57
59
|
when Method::RESOURCES_READ
|
58
60
|
handle_read_resource(request)
|
59
61
|
else
|
@@ -113,23 +115,63 @@ module ActiveMcp
|
|
113
115
|
end
|
114
116
|
|
115
117
|
def handle_list_resources(request)
|
116
|
-
success_response(
|
118
|
+
success_response(
|
119
|
+
request[:id],
|
120
|
+
{
|
121
|
+
resources: @server.fetch(
|
122
|
+
params: {
|
123
|
+
method: Method::RESOURCES_LIST,
|
124
|
+
arguments: {}
|
125
|
+
}
|
126
|
+
)[:result]
|
127
|
+
}
|
128
|
+
)
|
129
|
+
end
|
130
|
+
|
131
|
+
def handle_list_resource_templates(request)
|
132
|
+
success_response(
|
133
|
+
request[:id],
|
134
|
+
{
|
135
|
+
resourceTemplates: @server.fetch(
|
136
|
+
params: {
|
137
|
+
method: Method::RESOURCES_TEMPLATES_LIST,
|
138
|
+
arguments: {}
|
139
|
+
}
|
140
|
+
)[:result]
|
141
|
+
}
|
142
|
+
)
|
117
143
|
end
|
118
144
|
|
119
145
|
def handle_list_tools(request)
|
120
|
-
success_response(
|
146
|
+
success_response(
|
147
|
+
request[:id],
|
148
|
+
{
|
149
|
+
tools: @server.fetch(
|
150
|
+
params: {
|
151
|
+
method: Method::TOOLS_LIST,
|
152
|
+
arguments: {}
|
153
|
+
}
|
154
|
+
)[:result]
|
155
|
+
}
|
156
|
+
)
|
121
157
|
end
|
122
158
|
|
123
|
-
def
|
159
|
+
def handle_call_tool(request)
|
124
160
|
name = request.dig(:params, :name)
|
125
161
|
arguments = request.dig(:params, :arguments) || {}
|
126
162
|
|
127
163
|
begin
|
128
|
-
result = @server.
|
164
|
+
result = @server.fetch(
|
165
|
+
params: {
|
166
|
+
method: Method::TOOLS_CALL,
|
167
|
+
name:,
|
168
|
+
arguments:,
|
169
|
+
}
|
170
|
+
)
|
129
171
|
|
130
172
|
success_response(request[:id], result)
|
131
173
|
rescue => e
|
132
|
-
log_error("Error calling tool #{name}", e)
|
174
|
+
Server.log_error("Error calling tool #{name}", e)
|
133
175
|
error_response(request[:id], ErrorCode::INTERNAL_ERROR, "An error occurred while calling the tool")
|
134
176
|
end
|
135
177
|
end
|
@@ -137,11 +179,17 @@ module ActiveMcp
|
|
137
179
|
def handle_read_resource(request)
|
138
180
|
uri = request.dig(:params, :uri)
|
139
181
|
begin
|
140
|
-
result = @server.
|
182
|
+
result = @server.fetch(
|
183
|
+
params: {
|
184
|
+
method: Method::RESOURCES_READ,
|
185
|
+
uri:,
|
186
|
+
arguments: {},
|
187
|
+
}
|
188
|
+
)
|
141
189
|
|
142
190
|
success_response(request[:id], result)
|
143
191
|
rescue => e
|
144
|
-
|
192
|
+
Server.("Error reading resource #{uri}", e)
|
145
193
|
error_response(request[:id], ErrorCode::INTERNAL_ERROR, "An error occurred while reading the resource")
|
146
194
|
end
|
147
195
|
end
|
@@ -166,18 +214,6 @@ module ActiveMcp
|
|
166
214
|
response[:error][:data] = data if data
|
167
215
|
response
|
168
216
|
end
|
169
|
-
|
170
|
-
def log_error(message, error)
|
171
|
-
error_details = "#{message}: #{error.message}\n"
|
172
|
-
error_details += error.backtrace.join("\n") if error.backtrace
|
173
|
-
|
174
|
-
if defined?(Rails)
|
175
|
-
Rails.logger.error(error_details)
|
176
|
-
else
|
177
|
-
# Fresallback to standard error output if Rails is not available
|
178
|
-
$stderr.puts(error_details)
|
179
|
-
end
|
180
|
-
end
|
181
217
|
end
|
182
218
|
end
|
183
219
|
end
|
data/lib/active_mcp/server.rb
CHANGED
@@ -3,13 +3,12 @@ require "English"
|
|
3
3
|
require_relative "server/method"
|
4
4
|
require_relative "server/error_codes"
|
5
5
|
require_relative "server/stdio_connection"
|
6
|
-
require_relative "server/
|
7
|
-
require_relative "server/tool_manager"
|
6
|
+
require_relative "server/fetcher"
|
8
7
|
require_relative "server/protocol_handler"
|
9
8
|
|
10
9
|
module ActiveMcp
|
11
10
|
class Server
|
12
|
-
attr_reader :name, :version, :uri, :
|
11
|
+
attr_reader :name, :version, :uri, :protocol_handler, :fetcher
|
13
12
|
|
14
13
|
def initialize(
|
15
14
|
version: ActiveMcp::VERSION,
|
@@ -20,11 +19,23 @@ module ActiveMcp
|
|
20
19
|
@name = name
|
21
20
|
@version = version
|
22
21
|
@uri = uri
|
23
|
-
@
|
24
|
-
@tool_manager = ToolManager.new(uri: uri, auth:)
|
22
|
+
@fetcher = Fetcher.new(base_uri: uri, auth:)
|
25
23
|
@protocol_handler = ProtocolHandler.new(self)
|
26
|
-
|
27
|
-
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.log_error(message, error)
|
27
|
+
error_details = "#{message}: #{error.message}\n"
|
28
|
+
error_details += error.backtrace.join("\n") if error.backtrace
|
29
|
+
|
30
|
+
if defined?(Rails)
|
31
|
+
Rails.logger.error(error_details)
|
32
|
+
else
|
33
|
+
$stderr.puts(error_details)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def fetch(params:)
|
38
|
+
@fetcher.call(params:)
|
28
39
|
end
|
29
40
|
|
30
41
|
def start
|
data/lib/active_mcp/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_mcp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Moeki Kawakami
|
@@ -70,11 +70,12 @@ files:
|
|
70
70
|
- app/controllers/active_mcp/base_controller.rb
|
71
71
|
- app/controllers/concerns/active_mcp/request_handlable.rb
|
72
72
|
- app/controllers/concerns/active_mcp/resource_readable.rb
|
73
|
-
- app/
|
73
|
+
- app/controllers/concerns/active_mcp/tool_executable.rb
|
74
74
|
- app/views/active_mcp/cancelled.json.jbuilder
|
75
75
|
- app/views/active_mcp/initialize.json.jbuilder
|
76
76
|
- app/views/active_mcp/initialized.json.jbuilder
|
77
77
|
- app/views/active_mcp/no_method.json.jbuilder
|
78
|
+
- app/views/active_mcp/resource_templates_list.json.jbuilder
|
78
79
|
- app/views/active_mcp/resources_list.json.jbuilder
|
79
80
|
- app/views/active_mcp/resources_read.json.jbuilder
|
80
81
|
- app/views/active_mcp/tools_call.json.jbuilder
|
@@ -85,11 +86,10 @@ files:
|
|
85
86
|
- lib/active_mcp/engine.rb
|
86
87
|
- lib/active_mcp/server.rb
|
87
88
|
- lib/active_mcp/server/error_codes.rb
|
89
|
+
- lib/active_mcp/server/fetcher.rb
|
88
90
|
- lib/active_mcp/server/method.rb
|
89
91
|
- lib/active_mcp/server/protocol_handler.rb
|
90
|
-
- lib/active_mcp/server/resource_manager.rb
|
91
92
|
- lib/active_mcp/server/stdio_connection.rb
|
92
|
-
- lib/active_mcp/server/tool_manager.rb
|
93
93
|
- lib/active_mcp/tool.rb
|
94
94
|
- lib/active_mcp/version.rb
|
95
95
|
- lib/generators/active_mcp/install/install_generator.rb
|
@@ -1,150 +0,0 @@
|
|
1
|
-
require "json"
|
2
|
-
|
3
|
-
module ActiveMcp
|
4
|
-
class Server
|
5
|
-
class ResourceManager
|
6
|
-
attr_reader :resources
|
7
|
-
|
8
|
-
def initialize(uri: nil, auth: nil)
|
9
|
-
@resources = {}
|
10
|
-
@base_uri = uri
|
11
|
-
|
12
|
-
if auth
|
13
|
-
@auth_header = "#{auth[:type] == :bearer ? "Bearer" : "Basic"} #{auth[:token]}"
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def load_registered_resources
|
18
|
-
fetch_resources
|
19
|
-
end
|
20
|
-
|
21
|
-
def read_resource(uri)
|
22
|
-
require "net/http"
|
23
|
-
|
24
|
-
unless @base_uri.is_a?(URI) || @base_uri.is_a?(String)
|
25
|
-
log_error("Invalid URI type", StandardError.new("URI must be a String or URI object"))
|
26
|
-
return {
|
27
|
-
isError: true,
|
28
|
-
content: [{type: "text", text: "Invalid URI configuration"}]
|
29
|
-
}
|
30
|
-
end
|
31
|
-
|
32
|
-
begin
|
33
|
-
base_uri = URI.parse(@base_uri.to_s)
|
34
|
-
|
35
|
-
unless base_uri.scheme =~ /\Ahttps?\z/ && !base_uri.host.nil?
|
36
|
-
log_error("Invalid URI", StandardError.new("URI must have a valid scheme and host"))
|
37
|
-
return {
|
38
|
-
isError: true,
|
39
|
-
content: [{type: "text", text: "Invalid URI configuration"}]
|
40
|
-
}
|
41
|
-
end
|
42
|
-
|
43
|
-
if defined?(Rails) && Rails.env.production? && base_uri.scheme != "https"
|
44
|
-
return {
|
45
|
-
isError: true,
|
46
|
-
content: [{type: "text", text: "HTTPS is required in production environment"}]
|
47
|
-
}
|
48
|
-
end
|
49
|
-
rescue URI::InvalidURIError => e
|
50
|
-
log_error("Invalid URI format", e)
|
51
|
-
return {
|
52
|
-
isError: true,
|
53
|
-
content: [{type: "text", text: "Invalid URI format"}]
|
54
|
-
}
|
55
|
-
end
|
56
|
-
|
57
|
-
request = Net::HTTP::Post.new(base_uri)
|
58
|
-
request.body = JSON.generate({
|
59
|
-
method: Method::RESOURCES_READ,
|
60
|
-
uri:,
|
61
|
-
})
|
62
|
-
request["Content-Type"] = "application/json"
|
63
|
-
request["Authorization"] = @auth_header
|
64
|
-
|
65
|
-
begin
|
66
|
-
response = Net::HTTP.start(base_uri.hostname, base_uri.port) do |http|
|
67
|
-
http.request(request)
|
68
|
-
end
|
69
|
-
|
70
|
-
if response.code == "200"
|
71
|
-
JSON.parse(response.body, symbolize_names: true)
|
72
|
-
else
|
73
|
-
$stderr.puts(response.body)
|
74
|
-
{
|
75
|
-
isError: true,
|
76
|
-
contents: []
|
77
|
-
}
|
78
|
-
end
|
79
|
-
rescue => e
|
80
|
-
log_error("Error calling tool", e)
|
81
|
-
{
|
82
|
-
isError: true,
|
83
|
-
contents: []
|
84
|
-
}
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
private
|
89
|
-
|
90
|
-
def fetch_resources
|
91
|
-
return unless @base_uri
|
92
|
-
|
93
|
-
require "net/http"
|
94
|
-
|
95
|
-
unless @base_uri.is_a?(URI) || @base_uri.is_a?(String)
|
96
|
-
log_error("Invalid URI type", StandardError.new("URI must be a String or URI object"))
|
97
|
-
return
|
98
|
-
end
|
99
|
-
|
100
|
-
begin
|
101
|
-
uri = URI.parse(@base_uri.to_s)
|
102
|
-
|
103
|
-
unless uri.scheme =~ /\Ahttps?\z/ && !uri.host.nil?
|
104
|
-
log_error("Invalid URI", StandardError.new("URI must have a valid scheme and host"))
|
105
|
-
return
|
106
|
-
end
|
107
|
-
|
108
|
-
if defined?(Rails) && Rails.env.production? && uri.scheme != "https"
|
109
|
-
log_error("HTTPS is required in production environment", StandardError.new("Non-HTTPS URI in production"))
|
110
|
-
return
|
111
|
-
end
|
112
|
-
rescue URI::InvalidURIError => e
|
113
|
-
log_error("Invalid URI format", e)
|
114
|
-
return
|
115
|
-
end
|
116
|
-
|
117
|
-
request = Net::HTTP::Post.new(uri)
|
118
|
-
request.body = JSON.generate({
|
119
|
-
method: "resources/list",
|
120
|
-
arguments: "{}"
|
121
|
-
})
|
122
|
-
request["Content-Type"] = "application/json"
|
123
|
-
request["Authorization"] = @auth_header
|
124
|
-
|
125
|
-
begin
|
126
|
-
response = Net::HTTP.start(uri.hostname, uri.port) do |http|
|
127
|
-
http.request(request)
|
128
|
-
end
|
129
|
-
|
130
|
-
result = JSON.parse(response.body, symbolize_names: true)
|
131
|
-
@resources = result[:result]
|
132
|
-
rescue => e
|
133
|
-
log_error("Error fetching resources", e)
|
134
|
-
@resources = []
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
def log_error(message, error)
|
139
|
-
error_details = "#{message}: #{error.message}\n"
|
140
|
-
error_details += error.backtrace.join("\n") if error.backtrace
|
141
|
-
|
142
|
-
if defined?(Rails)
|
143
|
-
Rails.logger.error(error_details)
|
144
|
-
else
|
145
|
-
$stderr.puts(error_details)
|
146
|
-
end
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
150
|
-
end
|
@@ -1,163 +0,0 @@
|
|
1
|
-
require "json"
|
2
|
-
|
3
|
-
module ActiveMcp
|
4
|
-
class Server
|
5
|
-
class ToolManager
|
6
|
-
attr_reader :tools
|
7
|
-
|
8
|
-
def initialize(uri: nil, auth: nil)
|
9
|
-
@tools = {}
|
10
|
-
@uri = uri
|
11
|
-
|
12
|
-
if auth
|
13
|
-
@auth_header = "#{auth[:type] == :bearer ? "Bearer" : "Basic"} #{auth[:token]}"
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def call_tool(name, arguments = {})
|
18
|
-
tool_info = @tools.find { _1[:name] == name }
|
19
|
-
|
20
|
-
unless tool_info
|
21
|
-
return {
|
22
|
-
isError: true,
|
23
|
-
content: [{type: "text", text: "Tool not found: #{name}"}]
|
24
|
-
}
|
25
|
-
end
|
26
|
-
|
27
|
-
invoke_tool(name, arguments)
|
28
|
-
end
|
29
|
-
|
30
|
-
def load_registered_tools
|
31
|
-
fetch_tools
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
def invoke_tool(name, arguments)
|
37
|
-
require "net/http"
|
38
|
-
|
39
|
-
unless @uri.is_a?(URI) || @uri.is_a?(String)
|
40
|
-
log_error("Invalid URI type", StandardError.new("URI must be a String or URI object"))
|
41
|
-
return {
|
42
|
-
isError: true,
|
43
|
-
content: [{type: "text", text: "Invalid URI configuration"}]
|
44
|
-
}
|
45
|
-
end
|
46
|
-
|
47
|
-
begin
|
48
|
-
uri = URI.parse(@uri.to_s)
|
49
|
-
|
50
|
-
unless uri.scheme =~ /\Ahttps?\z/ && !uri.host.nil?
|
51
|
-
log_error("Invalid URI", StandardError.new("URI must have a valid scheme and host"))
|
52
|
-
return {
|
53
|
-
isError: true,
|
54
|
-
content: [{type: "text", text: "Invalid URI configuration"}]
|
55
|
-
}
|
56
|
-
end
|
57
|
-
|
58
|
-
if defined?(Rails) && Rails.env.production? && uri.scheme != "https"
|
59
|
-
return {
|
60
|
-
isError: true,
|
61
|
-
content: [{type: "text", text: "HTTPS is required in production environment"}]
|
62
|
-
}
|
63
|
-
end
|
64
|
-
rescue URI::InvalidURIError => e
|
65
|
-
log_error("Invalid URI format", e)
|
66
|
-
return {
|
67
|
-
isError: true,
|
68
|
-
content: [{type: "text", text: "Invalid URI format"}]
|
69
|
-
}
|
70
|
-
end
|
71
|
-
|
72
|
-
request = Net::HTTP::Post.new(uri)
|
73
|
-
request.body = JSON.generate({
|
74
|
-
method: "tools/call",
|
75
|
-
name:,
|
76
|
-
arguments: arguments
|
77
|
-
})
|
78
|
-
request["Content-Type"] = "application/json"
|
79
|
-
request["Authorization"] = @auth_header
|
80
|
-
|
81
|
-
begin
|
82
|
-
response = Net::HTTP.start(uri.hostname, uri.port) do |http|
|
83
|
-
http.request(request)
|
84
|
-
end
|
85
|
-
|
86
|
-
if response.code == "200"
|
87
|
-
JSON.parse(response.body, symbolize_names: true)
|
88
|
-
else
|
89
|
-
{
|
90
|
-
isError: true,
|
91
|
-
content: [{type: "text", text: "HTTP Error: #{response.code}"}]
|
92
|
-
}
|
93
|
-
end
|
94
|
-
rescue => e
|
95
|
-
log_error("Error calling tool", e)
|
96
|
-
{
|
97
|
-
isError: true,
|
98
|
-
content: [{type: "text", text: "Error calling tool"}]
|
99
|
-
}
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def fetch_tools
|
104
|
-
return unless @uri
|
105
|
-
|
106
|
-
require "net/http"
|
107
|
-
|
108
|
-
unless @uri.is_a?(URI) || @uri.is_a?(String)
|
109
|
-
log_error("Invalid URI type", StandardError.new("URI must be a String or URI object"))
|
110
|
-
return
|
111
|
-
end
|
112
|
-
|
113
|
-
begin
|
114
|
-
uri = URI.parse(@uri.to_s)
|
115
|
-
|
116
|
-
unless uri.scheme =~ /\Ahttps?\z/ && !uri.host.nil?
|
117
|
-
log_error("Invalid URI", StandardError.new("URI must have a valid scheme and host"))
|
118
|
-
return
|
119
|
-
end
|
120
|
-
|
121
|
-
if defined?(Rails) && Rails.env.production? && uri.scheme != "https"
|
122
|
-
log_error("HTTPS is required in production environment", StandardError.new("Non-HTTPS URI in production"))
|
123
|
-
return
|
124
|
-
end
|
125
|
-
rescue URI::InvalidURIError => e
|
126
|
-
log_error("Invalid URI format", e)
|
127
|
-
return
|
128
|
-
end
|
129
|
-
|
130
|
-
request = Net::HTTP::Post.new(uri)
|
131
|
-
request.body = JSON.generate({
|
132
|
-
method: "tools/list",
|
133
|
-
arguments: "{}"
|
134
|
-
})
|
135
|
-
request["Content-Type"] = "application/json"
|
136
|
-
request["Authorization"] = @auth_header
|
137
|
-
|
138
|
-
begin
|
139
|
-
response = Net::HTTP.start(uri.hostname, uri.port) do |http|
|
140
|
-
http.request(request)
|
141
|
-
end
|
142
|
-
|
143
|
-
result = JSON.parse(response.body, symbolize_names: true)
|
144
|
-
@tools = result[:result]
|
145
|
-
rescue => e
|
146
|
-
log_error("Error fetching tools", e)
|
147
|
-
@tools = []
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
def log_error(message, error)
|
152
|
-
error_details = "#{message}: #{error.message}\n"
|
153
|
-
error_details += error.backtrace.join("\n") if error.backtrace
|
154
|
-
|
155
|
-
if defined?(Rails)
|
156
|
-
Rails.logger.error(error_details)
|
157
|
-
else
|
158
|
-
$stderr.puts(error_details)
|
159
|
-
end
|
160
|
-
end
|
161
|
-
end
|
162
|
-
end
|
163
|
-
end
|