active_mcp 0.5.1 → 0.7.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 +118 -63
- data/app/controllers/active_mcp/base_controller.rb +27 -3
- data/app/controllers/concerns/active_mcp/authenticatable.rb +29 -0
- data/app/controllers/concerns/active_mcp/request_handlable.rb +18 -44
- data/app/controllers/concerns/active_mcp/resource_readable.rb +3 -7
- data/app/controllers/concerns/active_mcp/tool_executable.rb +6 -9
- data/app/views/active_mcp/resource_templates_list.json.jbuilder +24 -0
- data/app/views/active_mcp/tools_list.json.jbuilder +14 -2
- data/lib/active_mcp/controller/base.rb +7 -0
- data/lib/active_mcp/engine.rb +11 -14
- data/lib/active_mcp/schema/base.rb +46 -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/tool/base.rb +52 -0
- data/lib/active_mcp/version.rb +1 -1
- data/lib/active_mcp.rb +3 -1
- data/lib/generators/active_mcp/install/install_generator.rb +3 -7
- data/lib/generators/active_mcp/resource/resource_generator.rb +1 -1
- data/lib/generators/active_mcp/resource/templates/resource.rb.erb +4 -12
- data/lib/generators/active_mcp/tool/templates/tool.rb.erb +15 -9
- data/lib/generators/active_mcp/tool/tool_generator.rb +1 -1
- metadata +7 -4
- data/lib/active_mcp/server/resource_manager.rb +0 -150
- data/lib/active_mcp/server/tool_manager.rb +0 -163
- data/lib/active_mcp/tool.rb +0 -82
@@ -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
|
data/lib/active_mcp/tool.rb
DELETED
@@ -1,82 +0,0 @@
|
|
1
|
-
require "json-schema"
|
2
|
-
|
3
|
-
module ActiveMcp
|
4
|
-
class Tool
|
5
|
-
class << self
|
6
|
-
attr_reader :desc, :schema
|
7
|
-
|
8
|
-
def tool_name
|
9
|
-
name ? name.underscore.sub(/_tool$/, "") : ""
|
10
|
-
end
|
11
|
-
|
12
|
-
def description(value)
|
13
|
-
@desc = value
|
14
|
-
end
|
15
|
-
|
16
|
-
def property(name, type, required: false, description: nil)
|
17
|
-
@schema ||= default_schema
|
18
|
-
|
19
|
-
@schema["properties"][name.to_s] = {"type" => type.to_s}
|
20
|
-
@schema["properties"][name.to_s]["description"] = description if description
|
21
|
-
@schema["required"] << name.to_s if required
|
22
|
-
end
|
23
|
-
|
24
|
-
def argument(...)
|
25
|
-
property(...)
|
26
|
-
end
|
27
|
-
|
28
|
-
def registered_tools
|
29
|
-
@registered_tools ||= []
|
30
|
-
end
|
31
|
-
|
32
|
-
attr_writer :registered_tools
|
33
|
-
|
34
|
-
def inherited(subclass)
|
35
|
-
registered_tools << subclass
|
36
|
-
end
|
37
|
-
|
38
|
-
def visible?(auth_info)
|
39
|
-
if respond_to?(:authorized?)
|
40
|
-
authorized?(auth_info)
|
41
|
-
else
|
42
|
-
true
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def authorized_tools(auth_info = nil)
|
47
|
-
registered_tools.select do |tool_class|
|
48
|
-
tool_class.visible?(auth_info)
|
49
|
-
end.map do |tool_class|
|
50
|
-
{
|
51
|
-
name: tool_class.tool_name,
|
52
|
-
description: tool_class.desc,
|
53
|
-
inputSchema: tool_class.schema || default_schema
|
54
|
-
}
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def default_schema
|
59
|
-
{
|
60
|
-
"type" => "object",
|
61
|
-
"properties" => {},
|
62
|
-
"required" => []
|
63
|
-
}
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def initialize
|
68
|
-
end
|
69
|
-
|
70
|
-
def call(**args)
|
71
|
-
raise NotImplementedError, "#{self.class.name}#call must be implemented"
|
72
|
-
end
|
73
|
-
|
74
|
-
def validate_arguments(args)
|
75
|
-
return true unless self.class.schema
|
76
|
-
|
77
|
-
JSON::Validator.validate!(self.class.schema, args)
|
78
|
-
rescue JSON::Schema::ValidationError => e
|
79
|
-
{error: e.message}
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|