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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1891356bb19d256dcbd11f7d441fb9c5f668fc9cc0b115ea17eec44aef99ee54
4
- data.tar.gz: 8031f7e13bfd5a837cf84ff3ca069bd5c56b46901e7e04c861d6c15eabcf2c94
3
+ metadata.gz: 33e83423b0e7e4bdf722b5473ec742e92efe3af946270f3fc1edf2ee9806c8d2
4
+ data.tar.gz: a9fe38a30a53fbdc4810b2c5d08884730f71cd4ace372188c69916f6db61742a
5
5
  SHA512:
6
- metadata.gz: 20438f69b5a08fa089187a1eb11c0f0e8ccc5525809439e45da36105411b7c62187a981f0a73deda52fdb292ede1ffc429aaa26d0a21f2a5a369b9601bd2306d
7
- data.tar.gz: cf9c750474af01cc72c1a8480c568c36a29d271b7c7a9f5a2bf5ecdec8cc42e1fc0fe70493ae6e9a82d3f431445aca068a881fc89c71c6181f976384c8b0e2ca
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 that inherit from `ActiveMcp::Resource`:
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
@@ -4,5 +4,6 @@ module ActiveMcp
4
4
  class BaseController < ActionController::Base
5
5
  include RequestHandlable
6
6
  include ResourceReadable
7
+ include ToolExecutable
7
8
  end
8
9
  end
@@ -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 = ActiveMcp::ToolExecutor.execute(params: params, auth_info: auth_info)
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 = ActiveMcp::ToolExecutor.execute(params: params, auth_info: auth_info)
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
  {
@@ -1,6 +1,10 @@
1
1
  module ActiveMcp
2
- class ToolExecutor
3
- def self.execute(params:, auth_info:)
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 self.formatted(object)
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
@@ -10,6 +10,6 @@ module ActiveMcp
10
10
  TOOLS_CALL = "tools/call"
11
11
  RESOURCES_LIST = "resources/list"
12
12
  RESOURCES_READ = "resources/read"
13
- RESOURCES_TEMPLATES_LIST = "resources/list"
13
+ RESOURCES_TEMPLATES_LIST = "resources/templates/list"
14
14
  end
15
15
  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
- handle_use_tool(request)
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(request[:id], {resources: @server.resource_manager.resources})
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(request[:id], {tools: @server.tool_manager.tools})
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 handle_use_tool(request)
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.tool_manager.call_tool(name, arguments)
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.resource_manager.read_resource(uri)
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
- log_error("Error reading resource #{uri}", e)
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
@@ -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/resource_manager"
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, :tool_manager, :protocol_handler, :resource_manager
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
- @resource_manager = ResourceManager.new(uri:, auth:)
24
- @tool_manager = ToolManager.new(uri: uri, auth:)
22
+ @fetcher = Fetcher.new(base_uri: uri, auth:)
25
23
  @protocol_handler = ProtocolHandler.new(self)
26
- @tool_manager.load_registered_tools
27
- @resource_manager.load_registered_resources
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
@@ -1,3 +1,3 @@
1
1
  module ActiveMcp
2
- VERSION = "0.5.0"
2
+ VERSION = "0.6.0"
3
3
  end
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.5.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/models/active_mcp/tool_executor.rb
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