active_mcp 0.3.2 → 0.3.4

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: 862fda276ef5442ec0c9ee381134e33ce27b1dd797d386db7db95defc29fa1ee
4
- data.tar.gz: 2d7892d5eceb1acc3643451e3786cc208cfa1fea1e4b440b05b6ec5a5c440ac8
3
+ metadata.gz: 8c67705b1452f86930b120080e883a10cdb4d231bf2a8a533621adb55b3099c6
4
+ data.tar.gz: 62d30c1eba1c08c25b6c0e8a74337e3c735a3d14fe85c8f81a19505a0db248cd
5
5
  SHA512:
6
- metadata.gz: b14998a48988aef825e41603d34a3ec571df2ffb5dff804494ecfe137f4fadc138d55b462580dca5097341bc098fab890d1fe8456a90c29ee25b64a6337cf8d3
7
- data.tar.gz: ab594bcf8c596d1f6f47c93f3a1c14163113b2767fb2dd9fb64cb9c92d97799144819d0b8870242562e02069c19fcee3eb0cde660b2c42c64f2783a35a300a4f
6
+ metadata.gz: ce288f544ec46e5a5c44da9651c344fa985e17b730dbd463230cb86143f1bb8ddd2992c64219ee3eacb9dc3264820ae1b92d4c751938dd01dcac5ac3b2a6d2e0
7
+ data.tar.gz: 9709e2ce17bbe2f86fd3e15a3ecee1748a7708b69b5ecf79843cd94a342fabb1009563d2651d0d8377f0335c663e9d9e26b3017587d33d1fdb3ab7e580577aca
data/README.md CHANGED
@@ -59,8 +59,8 @@ end
59
59
  class CreateNoteTool < ActiveMcp::Tool
60
60
  description "Create Note!!"
61
61
 
62
- property :title, :string
63
- property :content, :string
62
+ argument :title, :string
63
+ argument :content, :string
64
64
 
65
65
  def call(title:, content:)
66
66
  Note.create(title:, content:)
@@ -129,8 +129,8 @@ This creates a new tool file at `app/tools/search_users_tool.rb` with the follow
129
129
  class SearchUsersTool < ActiveMcp::Tool
130
130
  description 'Search users'
131
131
 
132
- property :param1, :string, required: true, description: 'First parameter description'
133
- property :param2, :string, required: false, description: 'Second parameter description'
132
+ argument :param1, :string, required: true, description: 'First parameter description'
133
+ argument :param2, :string, required: false, description: 'Second parameter description'
134
134
  # Add more parameters as needed
135
135
 
136
136
  def call(param1:, param2: nil, auth_info: nil, **args)
@@ -147,10 +147,10 @@ You can then customize the generated tool to fit your needs.
147
147
  ## Input Schema
148
148
 
149
149
  ```ruby
150
- property :name, :string, required: true, description: 'User name'
151
- property :age, :integer, required: false, description: 'User age'
152
- property :addresses, :array, required: false, description: 'User addresses'
153
- property :preferences, :object, required: false, description: 'User preferences'
150
+ argument :name, :string, required: true, description: 'User name'
151
+ argument :age, :integer, required: false, description: 'User age'
152
+ argument :addresses, :array, required: false, description: 'User addresses'
153
+ argument :preferences, :object, required: false, description: 'User preferences'
154
154
  ```
155
155
 
156
156
  Supported types include:
@@ -179,16 +179,16 @@ ActiveMcp supports both authentication (verifying who a user is) and authorizati
179
179
 
180
180
  ### Authorization for Tools
181
181
 
182
- You can control which tools are visible and accessible to different users by overriding the `authorized?` class method:
182
+ You can control which tools are visible and accessible to different users by overriding the `visible?` class method:
183
183
 
184
184
  ```ruby
185
185
  class AdminOnlyTool < ActiveMcp::Tool
186
186
  description "This tool is only accessible by admins"
187
187
 
188
- property :command, :string, required: true, description: "Admin command to execute"
188
+ argument :command, :string, required: true, description: "Admin command to execute"
189
189
 
190
190
  # Define authorization logic - only admin tokens can access this tool
191
- def self.authorized?(auth_info)
191
+ def self.visible?(auth_info)
192
192
  return false unless auth_info
193
193
  return false unless auth_info[:type] == :bearer
194
194
 
@@ -274,7 +274,7 @@ Authentication information is automatically passed to your tools through the `au
274
274
  class SecuredDataTool < ActiveMcp::Tool
275
275
  description 'Access secured data'
276
276
 
277
- property :resource_id, :string, required: true, description: 'ID of the resource to access'
277
+ argument :resource_id, :string, required: true, description: 'ID of the resource to access'
278
278
 
279
279
  def call(resource_id:, auth_info: nil, **args)
280
280
  # Check if auth info exists
@@ -345,9 +345,9 @@ For example, instead of creating a generic search tool, create specific search t
345
345
  class SearchUsersTool < ActiveMcp::Tool
346
346
  description 'Search users by criteria'
347
347
 
348
- property :email, :string, required: false, description: 'Email to search for'
349
- property :name, :string, required: false, description: 'Name to search for'
350
- property :limit, :integer, required: false, description: 'Maximum number of records to return'
348
+ argument :email, :string, required: false, description: 'Email to search for'
349
+ argument :name, :string, required: false, description: 'Name to search for'
350
+ argument :limit, :integer, required: false, description: 'Maximum number of records to return'
351
351
 
352
352
  def call(email: nil, name: nil, limit: 10)
353
353
  criteria = {}
@@ -367,9 +367,9 @@ end
367
367
  class SearchPostsTool < ActiveMcp::Tool
368
368
  description 'Search posts by criteria'
369
369
 
370
- property :title, :string, required: false, description: 'Title to search for'
371
- property :author_id, :integer, required: false, description: 'Author ID to filter by'
372
- property :limit, :integer, required: false, description: 'Maximum number of records to return'
370
+ argument :title, :string, required: false, description: 'Title to search for'
371
+ argument :author_id, :integer, required: false, description: 'Author ID to filter by'
372
+ argument :limit, :integer, required: false, description: 'Maximum number of records to return'
373
373
 
374
374
  def call(title: nil, author_id: nil, limit: 10)
375
375
  criteria = {}
@@ -2,72 +2,6 @@
2
2
 
3
3
  module ActiveMcp
4
4
  class BaseController < ActionController::Base
5
- protect_from_forgery with: :null_session
6
- skip_before_action :verify_authenticity_token
7
- before_action :authenticate, only: [:index]
8
-
9
- def index
10
- if params[:jsonrpc]
11
- process_request_from_mcp_client
12
- else
13
- process_request_from_mcp_server
14
- end
15
- end
16
-
17
- private
18
-
19
- def process_request_from_mcp_server
20
- case params[:method]
21
- when Method::TOOLS_LIST
22
- result = Response::ToolsList::Json.call(
23
- tools: Response::Tools.to_hash(auth_info: @auth_info)
24
- )
25
- when Method::TOOLS_CALL
26
- result = Response::ToolsCall::Json.call(params:, auth_info: @auth_info)
27
- else
28
- result = Response::NoMethod.call
29
- end
30
-
31
- render json: result, status: 200
32
- end
33
-
34
- def process_request_from_mcp_client
35
- case params[:method]
36
- when Method::INITIALIZE
37
- result = Response::Initialize.call(id: params[:id])
38
- when Method::INITIALIZED
39
- result = Response::Initialized.call
40
- when Method::CANCELLED
41
- result = Response::Cancelled.call
42
- when Method::TOOLS_LIST
43
- result = Response::ToolsList::Jsonrpc.call(
44
- id: params[:id],
45
- tools: Response::Tools.to_hash(auth_info: @auth_info)
46
- )
47
- when Method::TOOLS_CALL
48
- result = Response::ToolsCall::Jsonrpc.call(id: params[:id], params:, auth_info: @auth_info)
49
- else
50
- result = Response::NoMethod.call
51
- end
52
-
53
- render json: result, status: 200
54
- end
55
-
56
- def authenticate
57
- auth_header = request.headers["Authorization"]
58
- if auth_header.present?
59
- @auth_info = {
60
- header: auth_header,
61
- type: if auth_header.start_with?("Bearer ")
62
- :bearer
63
- elsif auth_header.start_with?("Basic ")
64
- :basic
65
- else
66
- :unknown
67
- end,
68
- token: auth_header.split(" ").last
69
- }
70
- end
71
- end
5
+ include RequestHandler
72
6
  end
73
7
  end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveMcp
4
+ module RequestHandler
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ protect_from_forgery with: :null_session
9
+ skip_before_action :verify_authenticity_token
10
+ before_action :authenticate, only: [:index]
11
+ end
12
+
13
+ def index
14
+ if json_rpc_request?
15
+ handle_mcp_client_request
16
+ else
17
+ handle_mcp_server_request
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def json_rpc_request?
24
+ params[:jsonrpc].present?
25
+ end
26
+
27
+ def handle_mcp_client_request
28
+ @id = params[:id]
29
+ @auth_info = auth_info
30
+
31
+ case params[:method]
32
+ when Method::INITIALIZE
33
+ render 'active_mcp/initialize', formats: :json
34
+ when Method::INITIALIZED
35
+ render 'active_mcp/initialized', formats: :json
36
+ when Method::CANCELLED
37
+ render 'active_mcp/cancelled', formats: :json
38
+ when Method::TOOLS_LIST
39
+ @tools = ActiveMcp::Tool.authorized_tools(auth_info)
40
+ @format = :jsonrpc
41
+ render 'active_mcp/tools_list', formats: :json
42
+ when Method::TOOLS_CALL
43
+ @tool_result = ActiveMcp::ToolExecutor.execute(params: params, auth_info: auth_info)
44
+ @format = :jsonrpc
45
+ render 'active_mcp/tools_call', formats: :json
46
+ else
47
+ @format = :jsonrpc
48
+ render 'active_mcp/no_method', formats: :json
49
+ end
50
+ end
51
+
52
+ def handle_mcp_server_request
53
+ @auth_info = auth_info
54
+
55
+ case params[:method]
56
+ when Method::TOOLS_LIST
57
+ @tools = ActiveMcp::Tool.authorized_tools(auth_info)
58
+ @format = :json
59
+ render 'active_mcp/tools_list', formats: :json
60
+ when Method::TOOLS_CALL
61
+ @tool_result = ActiveMcp::ToolExecutor.execute(params: params, auth_info: auth_info)
62
+ @format = :json
63
+ render 'active_mcp/tools_call', formats: :json
64
+ else
65
+ @format = :json
66
+ render 'active_mcp/no_method', formats: :json
67
+ end
68
+ end
69
+
70
+ def authenticate
71
+ auth_header = request.headers["Authorization"]
72
+ if auth_header.present?
73
+ @auth_info = {
74
+ header: auth_header,
75
+ type: if auth_header.start_with?("Bearer ")
76
+ :bearer
77
+ elsif auth_header.start_with?("Basic ")
78
+ :basic
79
+ else
80
+ :unknown
81
+ end,
82
+ token: auth_header.split(" ").last
83
+ }
84
+ end
85
+ end
86
+
87
+ def auth_info
88
+ @auth_info
89
+ end
90
+ end
91
+ end
@@ -1,8 +1,14 @@
1
1
  module ActiveMcp
2
- module ToolExecutor
3
- def self.call(params:, auth_info:)
4
- tool_name = params[:name]
5
-
2
+ class ToolExecutor
3
+ def self.execute(params:, auth_info:)
4
+ if params[:jsonrpc].present?
5
+ tool_name = params[:params][:name]
6
+ tool_params = params[:params][:arguments]
7
+ else
8
+ tool_name = params[:name]
9
+ tool_params = params[:arguments]
10
+ end
11
+
6
12
  unless tool_name
7
13
  return {
8
14
  isError: true,
@@ -18,7 +24,7 @@ module ActiveMcp
18
24
  tool_class = Tool.registered_tools.find do |tc|
19
25
  tc.tool_name == tool_name
20
26
  end
21
-
27
+
22
28
  unless tool_class
23
29
  return {
24
30
  isError: true,
@@ -30,8 +36,8 @@ module ActiveMcp
30
36
  ]
31
37
  }
32
38
  end
33
-
34
- unless tool_class.authorized?(auth_info)
39
+
40
+ unless tool_class.visible?(auth_info)
35
41
  return {
36
42
  isError: true,
37
43
  content: [
@@ -43,12 +49,16 @@ module ActiveMcp
43
49
  }
44
50
  end
45
51
 
46
- arguments = params[:arguments].permit!.to_hash.symbolize_keys.transform_values do |value|
47
- if !value.is_a?(String)
48
- value
49
- else
50
- value.match(/^\d+$/) ? value.to_i : value
52
+ if tool_params
53
+ arguments = tool_params.permit!.to_hash.symbolize_keys.transform_values do |value|
54
+ if !value.is_a?(String)
55
+ value
56
+ else
57
+ value.match(/^\d+$/) ? value.to_i : value
58
+ end
51
59
  end
60
+ else
61
+ arguments = {}
52
62
  end
53
63
 
54
64
  tool = tool_class.new
@@ -65,10 +75,11 @@ module ActiveMcp
65
75
  ]
66
76
  }
67
77
  end
68
-
78
+
79
+ # Execute the tool
69
80
  begin
70
81
  arguments[:auth_info] = auth_info if auth_info.present?
71
-
82
+
72
83
  return {
73
84
  content: [
74
85
  {
@@ -89,7 +100,7 @@ module ActiveMcp
89
100
  }
90
101
  end
91
102
  end
92
-
103
+
93
104
  def self.formatted(object)
94
105
  case object
95
106
  when String
@@ -0,0 +1,2 @@
1
+ json.jsonrpc ActiveMcp::JSON_RPC_VERSION
2
+ json.result true
@@ -0,0 +1,21 @@
1
+ json.jsonrpc ActiveMcp::JSON_RPC_VERSION
2
+ json.id @id
3
+ json.result do
4
+ json.protocolVersion ActiveMcp::PROTOCOL_VERSION
5
+ json.capabilities do
6
+ json.logging Hash.new
7
+ json.capabilities do
8
+ json.resources do
9
+ json.subscribe false
10
+ json.listChanged false
11
+ end
12
+ json.tools do
13
+ json.listChanged false
14
+ end
15
+ end
16
+ end
17
+ json.serverInfo do
18
+ json.name ActiveMcp.config.respond_to?(:server_name) ? ActiveMcp.config.server_name : "Active MCP Server"
19
+ json.version ActiveMcp.config.respond_to?(:server_version) ? ActiveMcp.config.server_version : ActiveMcp::VERSION
20
+ end
21
+ end
@@ -0,0 +1,2 @@
1
+ json.jsonrpc ActiveMcp::JSON_RPC_VERSION
2
+ json.method ActiveMcp::Method::INITIALIZED
@@ -0,0 +1,4 @@
1
+ json.jsonrpc ActiveMcp::JSON_RPC_VERSION if @format == :jsonrpc
2
+ json.id @id if @format == :jsonrpc && @id.present?
3
+
4
+ json.error "Method not found"
@@ -0,0 +1,9 @@
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 @tool_result
6
+ else
7
+ json.isError @tool_result[:isError] if @tool_result[:isError]
8
+ json.content @tool_result[:content]
9
+ end
@@ -0,0 +1,10 @@
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.tools @tools
7
+ end
8
+ else
9
+ json.result @tools
10
+ end
@@ -10,5 +10,12 @@ module ActiveMcp
10
10
  end
11
11
  end
12
12
  end
13
+
14
+ initializer "active_mcp.configure_jbuilder" do |app|
15
+ if Rails.env.development?
16
+ Jbuilder.key_format camelize: :lower
17
+ Jbuilder.prettify if Jbuilder.respond_to?(:prettify)
18
+ end
19
+ end
13
20
  end
14
21
  end
@@ -25,6 +25,10 @@ module ActiveMcp
25
25
  @schema["required"] << name.to_s if required
26
26
  end
27
27
 
28
+ def argument(...)
29
+ property(...)
30
+ end
31
+
28
32
  def registered_tools
29
33
  @registered_tools ||= []
30
34
  end
@@ -35,8 +39,24 @@ module ActiveMcp
35
39
  registered_tools << subclass
36
40
  end
37
41
 
38
- def authorized?(auth_info)
39
- true
42
+ def visible?(auth_info)
43
+ if respond_to?(:authorized?)
44
+ authorized?(auth_info)
45
+ else
46
+ true
47
+ end
48
+ end
49
+
50
+ def authorized_tools(auth_info = nil)
51
+ registered_tools.select do |tool_class|
52
+ tool_class.visible?(auth_info)
53
+ end.map do |tool_class|
54
+ {
55
+ name: tool_class.tool_name,
56
+ description: tool_class.desc,
57
+ inputSchema: tool_class.schema
58
+ }
59
+ end
40
60
  end
41
61
  end
42
62
 
@@ -1,3 +1,3 @@
1
1
  module ActiveMcp
2
- VERSION = "0.3.2"
2
+ VERSION = "0.3.4"
3
3
  end
@@ -1,13 +1,13 @@
1
1
  class <%= class_name %> < ActiveMcp::Tool
2
2
  description "<%= file_name.humanize %>"
3
3
 
4
- property :param1, :string, required: true, description: "First parameter description"
5
- property :param2, :string, required: false, description: "Second parameter description"
4
+ argument :param1, :string, required: true, description: "First parameter description"
5
+ argument :param2, :string, required: false, description: "Second parameter description"
6
6
  # Add more parameters as needed
7
7
 
8
8
  # Uncomment and modify this method to implement authorization control
9
9
  # This controls who can see and use this tool
10
- # def self.authorized?(auth_info)
10
+ # def self.visible?(auth_info)
11
11
  # # Example: require authentication
12
12
  # # return false unless auth_info
13
13
  #
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.3.2
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Moeki Kawakami
@@ -43,6 +43,20 @@ dependencies:
43
43
  - - ">="
44
44
  - !ruby/object:Gem::Version
45
45
  version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: jbuilder
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '2.7'
53
+ type: :runtime
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '2.7'
46
60
  description: A Rails engine that provides MCP capabilities to your Rails application
47
61
  email:
48
62
  - hi@moeki.org
@@ -54,16 +68,14 @@ files:
54
68
  - README.md
55
69
  - Rakefile
56
70
  - app/controllers/active_mcp/base_controller.rb
57
- - app/models/active_mcp/response/cancelled.rb
58
- - app/models/active_mcp/response/initialize.rb
59
- - app/models/active_mcp/response/initialized.rb
60
- - app/models/active_mcp/response/no_method.rb
61
- - app/models/active_mcp/response/tools.rb
62
- - app/models/active_mcp/response/tools_call/json.rb
63
- - app/models/active_mcp/response/tools_call/jsonrpc.rb
64
- - app/models/active_mcp/response/tools_list/json.rb
65
- - app/models/active_mcp/response/tools_list/jsonrpc.rb
71
+ - app/controllers/concerns/active_mcp/request_handler.rb
66
72
  - app/models/active_mcp/tool_executor.rb
73
+ - app/views/active_mcp/cancelled.json.jbuilder
74
+ - app/views/active_mcp/initialize.json.jbuilder
75
+ - app/views/active_mcp/initialized.json.jbuilder
76
+ - app/views/active_mcp/no_method.json.jbuilder
77
+ - app/views/active_mcp/tools_call.json.jbuilder
78
+ - app/views/active_mcp/tools_list.json.jbuilder
67
79
  - config/routes.rb
68
80
  - lib/active_mcp.rb
69
81
  - lib/active_mcp/configuration.rb
@@ -1,12 +0,0 @@
1
- module ActiveMcp
2
- module Response
3
- class Cancelled
4
- def self.call
5
- {
6
- jsonrpc: JSON_RPC_VERSION,
7
- method: Method::CANCELLED
8
- }
9
- end
10
- end
11
- end
12
- end
@@ -1,31 +0,0 @@
1
- module ActiveMcp
2
- module Response
3
- class Initialize
4
- def self.call(id:)
5
- {
6
- jsonrpc: JSON_RPC_VERSION,
7
- id:,
8
- result: {
9
- protocolVersion: PROTOCOL_VERSION,
10
- capabilities: {
11
- logging: {},
12
- capabilities: {
13
- resources: {
14
- subscribe: false,
15
- listChanged: false
16
- },
17
- tools: {
18
- listChanged: false
19
- }
20
- },
21
- },
22
- serverInfo: {
23
- name: ActiveMcp.config.server_name,
24
- version: ActiveMcp.config.server_version
25
- }
26
- }
27
- }
28
- end
29
- end
30
- end
31
- end
@@ -1,12 +0,0 @@
1
- module ActiveMcp
2
- module Response
3
- class Initialized
4
- def self.call
5
- {
6
- jsonrpc: JSON_RPC_VERSION,
7
- method: Method::INITIALIZED
8
- }
9
- end
10
- end
11
- end
12
- end
@@ -1,11 +0,0 @@
1
- module ActiveMcp
2
- module Response
3
- class NoMethod
4
- def self.call
5
- {
6
- error: "Method not found"
7
- }
8
- end
9
- end
10
- end
11
- end
@@ -1,17 +0,0 @@
1
- module ActiveMcp
2
- module Response
3
- class Tools
4
- def self.to_hash(auth_info:)
5
- Tool.registered_tools.select do |tool_class|
6
- tool_class.authorized?(auth_info)
7
- end.map do |tool_class|
8
- {
9
- name: tool_class.tool_name,
10
- description: tool_class.desc,
11
- inputSchema: tool_class.schema
12
- }
13
- end
14
- end
15
- end
16
- end
17
- end
@@ -1,11 +0,0 @@
1
- module ActiveMcp
2
- module Response
3
- module ToolsCall
4
- class Json
5
- def self.call(params:, auth_info:)
6
- ActiveMcp::ToolExecutor.call(params:, auth_info:)
7
- end
8
- end
9
- end
10
- end
11
- end
@@ -1,16 +0,0 @@
1
- module ActiveMcp
2
- module Response
3
- module ToolsCall
4
- class Jsonrpc
5
- def self.call(id:, params:, auth_info:)
6
- result = ActiveMcp::ToolExecutor.call(params: params[:params], auth_info:)
7
- {
8
- jsonrpc: JSON_RPC_VERSION,
9
- id:,
10
- result:
11
- }
12
- end
13
- end
14
- end
15
- end
16
- end
@@ -1,13 +0,0 @@
1
- module ActiveMcp
2
- module Response
3
- module ToolsList
4
- class Json
5
- def self.call(tools:)
6
- {
7
- result: tools
8
- }
9
- end
10
- end
11
- end
12
- end
13
- end
@@ -1,15 +0,0 @@
1
- module ActiveMcp
2
- module Response
3
- module ToolsList
4
- class Jsonrpc
5
- def self.call(id:, tools:)
6
- {
7
- jsonrpc: JSON_RPC_VERSION,
8
- id:,
9
- result: {tools:}
10
- }
11
- end
12
- end
13
- end
14
- end
15
- end