active_mcp 0.3.1 → 0.3.2

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: 791510bf59750086b139e1688d8cfd7367c635a359f2dfe2678756e612a20816
4
- data.tar.gz: 320cb00fa8a75622a667260477c479fd1c9df653e54af85238dc77384923b0a9
3
+ metadata.gz: 862fda276ef5442ec0c9ee381134e33ce27b1dd797d386db7db95defc29fa1ee
4
+ data.tar.gz: 2d7892d5eceb1acc3643451e3786cc208cfa1fea1e4b440b05b6ec5a5c440ac8
5
5
  SHA512:
6
- metadata.gz: d65ba7458c1718d59d8b718eb8422192a1b520e72f877427feb4bdcb9057f9350718ebc8e4687eb7cc05f447962d728949cef94ed2c90937d82deb5ea3fc0727
7
- data.tar.gz: 54ed82f6c351d9e91e4e4108ed27767564c84d7d0bfc65a101598c0cdc5b879c70aa1b926d2e5c60a8181372a88ad6e9049a7232242726607ae65f58b9585816
6
+ metadata.gz: b14998a48988aef825e41603d34a3ec571df2ffb5dff804494ecfe137f4fadc138d55b462580dca5097341bc098fab890d1fe8456a90c29ee25b64a6337cf8d3
7
+ data.tar.gz: ab594bcf8c596d1f6f47c93f3a1c14163113b2767fb2dd9fb64cb9c92d97799144819d0b8870242562e02069c19fcee3eb0cde660b2c42c64f2783a35a300a4f
@@ -4,9 +4,34 @@ module ActiveMcp
4
4
  class BaseController < ActionController::Base
5
5
  protect_from_forgery with: :null_session
6
6
  skip_before_action :verify_authenticity_token
7
- before_action :process_authentication, only: [:index]
7
+ before_action :authenticate, only: [:index]
8
8
 
9
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
10
35
  case params[:method]
11
36
  when Method::INITIALIZE
12
37
  result = Response::Initialize.call(id: params[:id])
@@ -15,28 +40,20 @@ module ActiveMcp
15
40
  when Method::CANCELLED
16
41
  result = Response::Cancelled.call
17
42
  when Method::TOOLS_LIST
18
- tools = Response::Tools.to_hash(auth_info: @auth_info)
19
- if params[:jsonrpc]
20
- result = Response::ToolsList::Jsonrpc.call(id: params[:id], tools:)
21
- else
22
- result = Response::ToolsList::Json.call(tools:)
23
- end
43
+ result = Response::ToolsList::Jsonrpc.call(
44
+ id: params[:id],
45
+ tools: Response::Tools.to_hash(auth_info: @auth_info)
46
+ )
24
47
  when Method::TOOLS_CALL
25
- if params[:jsonrpc]
26
- result = Response::ToolsCall::Jsonrpc.call(id: params[:id], params:, auth_info: @auth_info)
27
- else
28
- result = Response::ToolsCall::Json.call(params:, auth_info: @auth_info)
29
- end
48
+ result = Response::ToolsCall::Jsonrpc.call(id: params[:id], params:, auth_info: @auth_info)
30
49
  else
31
50
  result = Response::NoMethod.call
32
51
  end
33
52
 
34
- render json: result[:body], status: result[:status]
53
+ render json: result, status: 200
35
54
  end
36
55
 
37
- private
38
-
39
- def process_authentication
56
+ def authenticate
40
57
  auth_header = request.headers["Authorization"]
41
58
  if auth_header.present?
42
59
  @auth_info = {
@@ -1,32 +1,29 @@
1
1
  module ActiveMcp
2
2
  module Response
3
3
  class Initialize
4
- def self.to_hash(id:, name:, version:)
4
+ def self.call(id:)
5
5
  {
6
- body: {
7
- jsonrpc: JSON_RPC_VERSION,
8
- id:,
9
- result: {
10
- protocolVersion: PROTOCOL_VERSION,
6
+ jsonrpc: JSON_RPC_VERSION,
7
+ id:,
8
+ result: {
9
+ protocolVersion: PROTOCOL_VERSION,
10
+ capabilities: {
11
+ logging: {},
11
12
  capabilities: {
12
- logging: {},
13
- capabilities: {
14
- resources: {
15
- subscribe: false,
16
- listChanged: false
17
- },
18
- tools: {
19
- listChanged: false
20
- }
13
+ resources: {
14
+ subscribe: false,
15
+ listChanged: false
21
16
  },
17
+ tools: {
18
+ listChanged: false
19
+ }
22
20
  },
23
- serverInfo: {
24
- name: ActiveMcp.config.server_name,
25
- version: ActiveMcp.config.server_version
26
- }
21
+ },
22
+ serverInfo: {
23
+ name: ActiveMcp.config.server_name,
24
+ version: ActiveMcp.config.server_version
27
25
  }
28
- },
29
- status: 200
26
+ }
30
27
  }
31
28
  end
32
29
  end
@@ -1,13 +1,10 @@
1
1
  module ActiveMcp
2
2
  module Response
3
3
  class Initialized
4
- def self.to_hash
4
+ def self.call
5
5
  {
6
- body: {
7
- jsonrpc: JSON_RPC_VERSION,
8
- method: Method::INITIALIZED
9
- },
10
- status: 200
6
+ jsonrpc: JSON_RPC_VERSION,
7
+ method: Method::INITIALIZED
11
8
  }
12
9
  end
13
10
  end
@@ -3,8 +3,7 @@ module ActiveMcp
3
3
  class NoMethod
4
4
  def self.call
5
5
  {
6
- body: { error: "Method not found" },
7
- status: 404
6
+ error: "Method not found"
8
7
  }
9
8
  end
10
9
  end
@@ -3,62 +3,7 @@ module ActiveMcp
3
3
  module ToolsCall
4
4
  class Json
5
5
  def self.call(params:, auth_info:)
6
- tool_name = params[:name]
7
-
8
- unless tool_name
9
- return {
10
- body: {error: "Invalid params: missing tool name"},
11
- status: 400
12
- }
13
- end
14
-
15
- tool_class = Tool.registered_tools.find do |tc|
16
- tc.tool_name == tool_name
17
- end
18
-
19
- unless tool_class
20
- return {
21
- body: {error: "Tool not found: #{tool_name}"},
22
- status: 404
23
- }
24
- end
25
-
26
- unless tool_class.authorized?(auth_info)
27
- return {
28
- body: {error: "Unauthorized: Access to tool '#{tool_name}' denied"},
29
- status: 401
30
- }
31
- end
32
-
33
- arguments = params[:arguments].permit!.to_hash.symbolize_keys.transform_values { _1.match(/^\d+$/) ? _1.to_i : _1 }
34
-
35
- p arguments
36
-
37
- tool = tool_class.new
38
- validation_result = tool.validate_arguments(arguments)
39
-
40
- if validation_result.is_a?(Hash) && validation_result[:error]
41
- return {
42
- body: {result: validation_result[:error]},
43
- status: 400
44
- }
45
- end
46
-
47
- begin
48
- arguments[:auth_info] = auth_info if auth_info.present?
49
-
50
- result = tool.call(**arguments.symbolize_keys)
51
-
52
- return {
53
- body: {result: result},
54
- status: 200,
55
- }
56
- rescue => e
57
- return {
58
- body: {error: "Error: #{e.message}"},
59
- status: 500
60
- }
61
- end
6
+ ActiveMcp::ToolExecutor.call(params:, auth_info:)
62
7
  end
63
8
  end
64
9
  end
@@ -3,21 +3,11 @@ module ActiveMcp
3
3
  module ToolsCall
4
4
  class Jsonrpc
5
5
  def self.call(id:, params:, auth_info:)
6
- result = Json.call(params: params[:params], auth_info:)
6
+ result = ActiveMcp::ToolExecutor.call(params: params[:params], auth_info:)
7
7
  {
8
- body: {
9
- jsonrpc: JSON_RPC_VERSION,
10
- id:,
11
- result: {
12
- content: [
13
- {
14
- type: "text",
15
- text: result[:body][:result]
16
- }
17
- ]
18
- },
19
- },
20
- status: result[:status]
8
+ jsonrpc: JSON_RPC_VERSION,
9
+ id:,
10
+ result:
21
11
  }
22
12
  end
23
13
  end
@@ -4,8 +4,7 @@ module ActiveMcp
4
4
  class Json
5
5
  def self.call(tools:)
6
6
  {
7
- body: { result: tools},
8
- status: 200
7
+ result: tools
9
8
  }
10
9
  end
11
10
  end
@@ -4,12 +4,9 @@ module ActiveMcp
4
4
  class Jsonrpc
5
5
  def self.call(id:, tools:)
6
6
  {
7
- body: {
8
- jsonrpc: JSON_RPC_VERSION,
9
- id:,
10
- result: {tools:}
11
- },
12
- status: 200
7
+ jsonrpc: JSON_RPC_VERSION,
8
+ id:,
9
+ result: {tools:}
13
10
  }
14
11
  end
15
12
  end
@@ -0,0 +1,104 @@
1
+ module ActiveMcp
2
+ module ToolExecutor
3
+ def self.call(params:, auth_info:)
4
+ tool_name = params[:name]
5
+
6
+ unless tool_name
7
+ return {
8
+ isError: true,
9
+ content: [
10
+ {
11
+ type: "text",
12
+ text: "Invalid params: missing tool name",
13
+ }
14
+ ]
15
+ }
16
+ end
17
+
18
+ tool_class = Tool.registered_tools.find do |tc|
19
+ tc.tool_name == tool_name
20
+ end
21
+
22
+ unless tool_class
23
+ return {
24
+ isError: true,
25
+ content: [
26
+ {
27
+ type: "text",
28
+ text: "Tool not found: #{tool_name}",
29
+ }
30
+ ]
31
+ }
32
+ end
33
+
34
+ unless tool_class.authorized?(auth_info)
35
+ return {
36
+ isError: true,
37
+ content: [
38
+ {
39
+ type: "text",
40
+ text: "Unauthorized: Access to tool '#{tool_name}' denied",
41
+ }
42
+ ]
43
+ }
44
+ end
45
+
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
51
+ end
52
+ end
53
+
54
+ tool = tool_class.new
55
+ validation_result = tool.validate_arguments(arguments)
56
+
57
+ if validation_result.is_a?(Hash) && validation_result[:error]
58
+ return {
59
+ isError: true,
60
+ content: [
61
+ {
62
+ type: "text",
63
+ text: validation_result[:error],
64
+ }
65
+ ]
66
+ }
67
+ end
68
+
69
+ begin
70
+ arguments[:auth_info] = auth_info if auth_info.present?
71
+
72
+ return {
73
+ content: [
74
+ {
75
+ type: "text",
76
+ text: formatted(tool.call(**arguments.symbolize_keys))
77
+ }
78
+ ]
79
+ }
80
+ rescue => e
81
+ return {
82
+ isError: true,
83
+ content: [
84
+ {
85
+ type: "text",
86
+ text: "Error: #{e.message}",
87
+ }
88
+ ]
89
+ }
90
+ end
91
+ end
92
+
93
+ def self.formatted(object)
94
+ case object
95
+ when String
96
+ object
97
+ when Hash
98
+ object.to_json
99
+ else
100
+ object.to_s
101
+ end
102
+ end
103
+ end
104
+ end
@@ -36,7 +36,6 @@ module ActiveMcp
36
36
  def invoke_tool(name, arguments)
37
37
  require "net/http"
38
38
 
39
- # URIの検証
40
39
  unless @uri.is_a?(URI) || @uri.is_a?(String)
41
40
  log_error("Invalid URI type", StandardError.new("URI must be a String or URI object"))
42
41
  return {
@@ -48,7 +47,6 @@ module ActiveMcp
48
47
  begin
49
48
  uri = URI.parse(@uri.to_s)
50
49
 
51
- # 有効なスキームとホストの検証
52
50
  unless uri.scheme =~ /\Ahttps?\z/ && !uri.host.nil?
53
51
  log_error("Invalid URI", StandardError.new("URI must have a valid scheme and host"))
54
52
  return {
@@ -57,7 +55,6 @@ module ActiveMcp
57
55
  }
58
56
  end
59
57
 
60
- # 本番環境ではHTTPSを強制
61
58
  if defined?(Rails) && Rails.env.production? && uri.scheme != "https"
62
59
  return {
63
60
  isError: true,
@@ -87,15 +84,7 @@ module ActiveMcp
87
84
  end
88
85
 
89
86
  if response.code == "200"
90
- body = JSON.parse(response.body, symbolize_names: true)
91
- if body[:error]
92
- {
93
- isError: true,
94
- content: [{type: "text", text: body[:error]}]
95
- }
96
- else
97
- format_result(body[:result])
98
- end
87
+ JSON.parse(response.body, symbolize_names: true)
99
88
  else
100
89
  {
101
90
  isError: true,
@@ -103,7 +92,6 @@ module ActiveMcp
103
92
  }
104
93
  end
105
94
  rescue => e
106
- # ログに詳細を記録
107
95
  log_error("Error calling tool", e)
108
96
  {
109
97
  isError: true,
@@ -117,7 +105,6 @@ module ActiveMcp
117
105
 
118
106
  require "net/http"
119
107
 
120
- # URIの検証
121
108
  unless @uri.is_a?(URI) || @uri.is_a?(String)
122
109
  log_error("Invalid URI type", StandardError.new("URI must be a String or URI object"))
123
110
  return
@@ -126,13 +113,11 @@ module ActiveMcp
126
113
  begin
127
114
  uri = URI.parse(@uri.to_s)
128
115
 
129
- # 有効なスキームとホストの検証
130
116
  unless uri.scheme =~ /\Ahttps?\z/ && !uri.host.nil?
131
117
  log_error("Invalid URI", StandardError.new("URI must have a valid scheme and host"))
132
118
  return
133
119
  end
134
120
 
135
- # 本番環境ではHTTPSを強制
136
121
  if defined?(Rails) && Rails.env.production? && uri.scheme != "https"
137
122
  log_error("HTTPS is required in production environment", StandardError.new("Non-HTTPS URI in production"))
138
123
  return
@@ -162,17 +147,6 @@ module ActiveMcp
162
147
  @tools = []
163
148
  end
164
149
  end
165
-
166
- def format_result(result)
167
- case result
168
- when String
169
- {content: [{type: "text", text: result}]}
170
- when Hash
171
- {content: [{type: "text", text: result.to_json}]}
172
- else
173
- {content: [{type: "text", text: result.to_s}]}
174
- end
175
- end
176
150
 
177
151
  def log_error(message, error)
178
152
  error_details = "#{message}: #{error.message}\n"
@@ -181,7 +155,6 @@ module ActiveMcp
181
155
  if defined?(Rails)
182
156
  Rails.logger.error(error_details)
183
157
  else
184
- # Fallback to standard error output if Rails is not available
185
158
  $stderr.puts(error_details)
186
159
  end
187
160
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveMcp
2
- VERSION = "0.3.1"
2
+ VERSION = "0.3.2"
3
3
  end
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
- - Your Name
7
+ - Moeki Kawakami
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 2025-04-06 00:00:00.000000000 Z
@@ -45,7 +45,7 @@ dependencies:
45
45
  version: '0'
46
46
  description: A Rails engine that provides MCP capabilities to your Rails application
47
47
  email:
48
- - your.email@example.com
48
+ - hi@moeki.org
49
49
  executables: []
50
50
  extensions: []
51
51
  extra_rdoc_files: []
@@ -63,6 +63,7 @@ files:
63
63
  - app/models/active_mcp/response/tools_call/jsonrpc.rb
64
64
  - app/models/active_mcp/response/tools_list/json.rb
65
65
  - app/models/active_mcp/response/tools_list/jsonrpc.rb
66
+ - app/models/active_mcp/tool_executor.rb
66
67
  - config/routes.rb
67
68
  - lib/active_mcp.rb
68
69
  - lib/active_mcp/configuration.rb
@@ -77,7 +78,6 @@ files:
77
78
  - lib/active_mcp/version.rb
78
79
  - lib/generators/active_mcp/install/install_generator.rb
79
80
  - lib/generators/active_mcp/install/templates/initializer.rb
80
- - lib/generators/active_mcp/install/templates/mcp_server.rb
81
81
  - lib/generators/active_mcp/tool/templates/tool.rb.erb
82
82
  - lib/generators/active_mcp/tool/tool_generator.rb
83
83
  homepage: https://github.com/moekiorg/active_mcp
@@ -1,31 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # MCP Server script for Active MCP
3
- require 'active_mcp'
4
-
5
- # Load Rails application
6
- ENV['RAILS_ENV'] ||= 'development'
7
- require File.expand_path('../config/environment', __dir__)
8
-
9
- # Initialize MCP server
10
- server = ActiveMcp::Server.new(
11
- name: "<%= Rails.application.class.module_parent_name %> MCP Server",
12
- uri: '<%= Rails.application.config.action_controller.default_url_options&.fetch(:host, 'http://localhost:3000') %>/mcp'
13
- )
14
-
15
- # Optional authentication
16
- <% if ActiveMcp.config.auth_enabled %>
17
- server.auth = {
18
- type: :bearer,
19
- token: ENV['MCP_AUTH_TOKEN'] || '<%= ActiveMcp.config.auth_token %>'
20
- }
21
- <% end %>
22
-
23
- # Start the server
24
- puts "Starting MCP server for <%= Rails.application.class.module_parent_name %>..."
25
- puts "Connect to this server in MCP clients using the following configuration:"
26
- puts
27
- puts " Command: #{File.expand_path(__FILE__)}"
28
- puts " Args: []"
29
- puts
30
- puts "Press Ctrl+C to stop the server"
31
- server.start