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.
@@ -2,20 +2,17 @@ module ActiveMcp
2
2
  class Engine < ::Rails::Engine
3
3
  isolate_namespace ActiveMcp
4
4
 
5
- initializer "active_mcp.eager_load_tools" do |app|
6
- tools_path = Rails.root.join("app", "tools")
7
- if Dir.exist?(tools_path)
8
- Dir[tools_path.join("*.rb")].sort.each do |file|
9
- require_dependency file
10
- end
11
- end
12
- end
13
-
14
- initializer "active_mcp.eager_load_resources" do |app|
15
- tools_path = Rails.root.join("app", "resources")
16
- if Dir.exist?(tools_path)
17
- Dir[tools_path.join("*.rb")].sort.each do |file|
18
- require_dependency file
5
+ initializer "active_mcp.eager_load" do |app|
6
+ [
7
+ Rails.root.join("app", "mcp", "tools"),
8
+ Rails.root.join("app", "mcp", "resources"),
9
+ Rails.root.join("app", "mcp", "resource_templates"),
10
+ Rails.root.join("app", "mcp", "schemas")
11
+ ].each do |tools_path|
12
+ if Dir.exist?(tools_path)
13
+ Dir[tools_path.join("*.rb")].sort.each do |file|
14
+ require_dependency file
15
+ end
19
16
  end
20
17
  end
21
18
  end
@@ -0,0 +1,46 @@
1
+ module ActiveMcp
2
+ module Schema
3
+ class Base
4
+ class << self
5
+ attr_reader :resources, :resource_templates, :tools
6
+
7
+ def resource(klass)
8
+ @resources ||= []
9
+ @resources << klass
10
+ end
11
+
12
+ def resource_template(klass)
13
+ @resource_templates ||= []
14
+ @resource_templates << klass
15
+ end
16
+
17
+ def tool(klass)
18
+ @tools ||= []
19
+ @tools << klass
20
+ end
21
+ end
22
+
23
+ def initialize(context: {})
24
+ @context = context
25
+ end
26
+
27
+ def resources
28
+ self.class.resources.filter do |resource|
29
+ !resource.respond_to?(:visible?) || resource.visible?(context: @context)
30
+ end
31
+ end
32
+
33
+ def resource_templates
34
+ self.class.resource_templates.filter do |tool_resource|
35
+ !tool_resource.respond_to?(:visible?) || tool_resource.visible?(context: @context)
36
+ end
37
+ end
38
+
39
+ def tools
40
+ self.class.tools.filter do |tool|
41
+ !tool.respond_to?(:visible?) || tool.visible?(context: @context)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ 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
@@ -0,0 +1,52 @@
1
+ require "json-schema"
2
+
3
+ module ActiveMcp
4
+ module Tool
5
+ class Base
6
+ class << self
7
+ attr_reader :schema
8
+
9
+ def argument(name, type, required: false, description: nil)
10
+ @schema ||= default_schema
11
+
12
+ @schema["properties"][name.to_s] = {"type" => type.to_s}
13
+ @schema["properties"][name.to_s]["description"] = description if description
14
+ @schema["required"] << name.to_s if required
15
+ end
16
+
17
+ def default_schema
18
+ {
19
+ "type" => "object",
20
+ "properties" => {},
21
+ "required" => []
22
+ }
23
+ end
24
+ end
25
+
26
+ def initialize
27
+ end
28
+
29
+ def name
30
+ end
31
+
32
+ def description
33
+ end
34
+
35
+ def visible?(context: {})
36
+ true
37
+ end
38
+
39
+ def call(context: {}, **args)
40
+ raise NotImplementedError, "#{self.class.name}#call must be implemented"
41
+ end
42
+
43
+ def validate_arguments(args)
44
+ return true unless self.class.schema
45
+
46
+ JSON::Validator.validate!(self.class.schema, args)
47
+ rescue JSON::Schema::ValidationError => e
48
+ {error: e.message}
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,3 +1,3 @@
1
1
  module ActiveMcp
2
- VERSION = "0.5.1"
2
+ VERSION = "0.7.0"
3
3
  end
data/lib/active_mcp.rb CHANGED
@@ -3,11 +3,13 @@
3
3
  require "jbuilder"
4
4
  require_relative "active_mcp/version"
5
5
  require_relative "active_mcp/configuration"
6
- require_relative "active_mcp/tool"
6
+ require_relative "active_mcp/schema/base"
7
+ require_relative "active_mcp/tool/base"
7
8
  require_relative "active_mcp/server"
8
9
 
9
10
  if defined? ::Rails
10
11
  require_relative "active_mcp/engine"
12
+ require_relative "active_mcp/controller/base"
11
13
  end
12
14
 
13
15
  module ActiveMcp
@@ -2,16 +2,12 @@ module ActiveMcp
2
2
  module Generators
3
3
  class InstallGenerator < Rails::Generators::Base
4
4
  source_root File.expand_path('templates', __dir__)
5
-
6
- desc "Creates an Active MCP initializer and mounts the engine in your routes"
7
-
5
+
6
+ desc "Creates an Active MCP initializer"
7
+
8
8
  def create_initializer_file
9
9
  template "initializer.rb", "config/initializers/active_mcp.rb"
10
10
  end
11
-
12
- def update_routes
13
- route "mount ActiveMcp::Engine, at: '/mcp'"
14
- end
15
11
  end
16
12
  end
17
13
  end
@@ -4,7 +4,7 @@ module ActiveMcp
4
4
  source_root File.expand_path("templates", __dir__)
5
5
 
6
6
  def create_resource_file
7
- template "resource.rb.erb", File.join("app/resources", "#{file_name}_resource.rb")
7
+ template "resource.rb.erb", File.join("app/mcp/resources", "#{file_name}_resource.rb")
8
8
  end
9
9
 
10
10
  private
@@ -1,12 +1,4 @@
1
1
  class <%= class_name %>
2
- def initialize(auth_info:)
3
- @auth_info = auth_info
4
-
5
- # Authentication information can be accessed via @auth_info parameter
6
- # @auth_info = { type: :bearer, token: "xxx", header: "Bearer xxx" }
7
- # or { type: :basic, token: "base64encoded", header: "Basic base64encoded" }
8
- end
9
-
10
2
  def name
11
3
  "<%= file_name %>"
12
4
  end
@@ -25,16 +17,16 @@ class <%= class_name %>
25
17
 
26
18
  # Uncomment and modify this method to implement authorization control
27
19
  # This controls who can see and use this tool
28
- # def visible?
20
+ # def visible?(context: {})
29
21
  # # Example: require authentication
30
- # # return false unless @auth_info
22
+ # # return false unless context
31
23
  #
32
24
  # # Example: require a specific authentication type
33
- # # return false unless @auth_info[:type] == :bearer
25
+ # # return false unless context[:auth_info][:type] == :bearer
34
26
  #
35
27
  # # Example: check for admin permissions
36
28
  # # admin_tokens = ["admin-token"]
37
- # # return admin_tokens.include?(@auth_info[:token])
29
+ # # return admin_tokens.include?(context[:auth_info][:token])
38
30
  #
39
31
  # # Default: allow all access
40
32
  # true
@@ -1,5 +1,11 @@
1
- class <%= class_name %> < ActiveMcp::Tool
2
- description "<%= file_name.humanize %>"
1
+ class <%= class_name %> < ActiveMcp::Tool::Base
2
+ def name
3
+ "<%= file_name.humanize %>"
4
+ end
5
+
6
+ def description
7
+ "<%= file_name.humanize %>"
8
+ end
3
9
 
4
10
  argument :param1, :string, required: true, description: "First parameter description"
5
11
  argument :param2, :string, required: false, description: "Second parameter description"
@@ -7,25 +13,25 @@ class <%= class_name %> < ActiveMcp::Tool
7
13
 
8
14
  # Uncomment and modify this method to implement authorization control
9
15
  # This controls who can see and use this tool
10
- # def self.visible?(auth_info)
16
+ # def visible?(context: {})
11
17
  # # Example: require authentication
12
- # # return false unless auth_info
18
+ # # return false unless context
13
19
  #
14
20
  # # Example: require a specific authentication type
15
- # # return false unless auth_info[:type] == :bearer
21
+ # # return false unless context[:auth_info][:type] == :bearer
16
22
  #
17
23
  # # Example: check for admin permissions
18
24
  # # admin_tokens = ["admin-token"]
19
- # # return admin_tokens.include?(auth_info[:token])
25
+ # # return admin_tokens.include?(context[:auth_info][:token])
20
26
  #
21
27
  # # Default: allow all access
22
28
  # true
23
29
  # end
24
30
 
25
- def call(param1:, param2: nil, auth_info: nil, **args)
31
+ def call(param1:, param2: nil, context: {})
26
32
  # Authentication information can be accessed via _auth_info parameter
27
- # auth_info = { type: :bearer, token: "xxx", header: "Bearer xxx" }
28
- # or { type: :basic, token: "base64encoded", header: "Basic base64encoded" }
33
+ # context = { auth_info: { type: :bearer, token: "xxx", header: "Bearer xxx" } }
34
+ # or { auth_info: { type: :basic, token: "base64encoded", header: "Basic base64encoded" } }
29
35
 
30
36
  # Implement tool logic here
31
37
 
@@ -4,7 +4,7 @@ module ActiveMcp
4
4
  source_root File.expand_path("templates", __dir__)
5
5
 
6
6
  def create_tool_file
7
- template "tool.rb.erb", File.join("app/tools", "#{file_name}_tool.rb")
7
+ template "tool.rb.erb", File.join("app/mcp/tools", "#{file_name}_tool.rb")
8
8
  end
9
9
 
10
10
  private
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.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Moeki Kawakami
@@ -68,6 +68,7 @@ files:
68
68
  - README.md
69
69
  - Rakefile
70
70
  - app/controllers/active_mcp/base_controller.rb
71
+ - app/controllers/concerns/active_mcp/authenticatable.rb
71
72
  - app/controllers/concerns/active_mcp/request_handlable.rb
72
73
  - app/controllers/concerns/active_mcp/resource_readable.rb
73
74
  - app/controllers/concerns/active_mcp/tool_executable.rb
@@ -75,6 +76,7 @@ files:
75
76
  - app/views/active_mcp/initialize.json.jbuilder
76
77
  - app/views/active_mcp/initialized.json.jbuilder
77
78
  - app/views/active_mcp/no_method.json.jbuilder
79
+ - app/views/active_mcp/resource_templates_list.json.jbuilder
78
80
  - app/views/active_mcp/resources_list.json.jbuilder
79
81
  - app/views/active_mcp/resources_read.json.jbuilder
80
82
  - app/views/active_mcp/tools_call.json.jbuilder
@@ -82,15 +84,16 @@ files:
82
84
  - config/routes.rb
83
85
  - lib/active_mcp.rb
84
86
  - lib/active_mcp/configuration.rb
87
+ - lib/active_mcp/controller/base.rb
85
88
  - lib/active_mcp/engine.rb
89
+ - lib/active_mcp/schema/base.rb
86
90
  - lib/active_mcp/server.rb
87
91
  - lib/active_mcp/server/error_codes.rb
92
+ - lib/active_mcp/server/fetcher.rb
88
93
  - lib/active_mcp/server/method.rb
89
94
  - lib/active_mcp/server/protocol_handler.rb
90
- - lib/active_mcp/server/resource_manager.rb
91
95
  - lib/active_mcp/server/stdio_connection.rb
92
- - lib/active_mcp/server/tool_manager.rb
93
- - lib/active_mcp/tool.rb
96
+ - lib/active_mcp/tool/base.rb
94
97
  - lib/active_mcp/version.rb
95
98
  - lib/generators/active_mcp/install/install_generator.rb
96
99
  - lib/generators/active_mcp/install/templates/initializer.rb