active_mcp 0.3.0 → 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: 051dd51506aaadb0c6bf460f1f001049b5d70dc950a060a663d2256050224de7
4
- data.tar.gz: e70d63d1618f882dd9d459de896a5ba086e29cbef9acbdf897f800a76266850e
3
+ metadata.gz: 862fda276ef5442ec0c9ee381134e33ce27b1dd797d386db7db95defc29fa1ee
4
+ data.tar.gz: 2d7892d5eceb1acc3643451e3786cc208cfa1fea1e4b440b05b6ec5a5c440ac8
5
5
  SHA512:
6
- metadata.gz: 7b5c6aaa00d5f9138d2186f2ea4c988f621f5f231674f4c01eeda64418187e0502999b1385c9417824bd99b888a9252f2833c5f5f42026e8943ce07d8a8952a4
7
- data.tar.gz: 31d7ea0b9f9cd7b6385a30ced3a9caf14d2105f366d614c1ae8fc5c227f617c547f1f86c63084e4816265cb838ed8e089a4997e9cd1d3e757f8304cd197717a7
6
+ metadata.gz: b14998a48988aef825e41603d34a3ec571df2ffb5dff804494ecfe137f4fadc138d55b462580dca5097341bc098fab890d1fe8456a90c29ee25b64a6337cf8d3
7
+ data.tar.gz: ab594bcf8c596d1f6f47c93f3a1c14163113b2767fb2dd9fb64cb9c92d97799144819d0b8870242562e02069c19fcee3eb0cde660b2c42c64f2783a35a300a4f
data/README.md CHANGED
@@ -72,7 +72,7 @@ end
72
72
 
73
73
  #### with streamable HTTP
74
74
 
75
- Set MCP destination as `https:your-app.example.com/mcp`
75
+ Set MCP destination to `https:your-app.example.com/mcp`
76
76
 
77
77
  #### with independent MCP Server
78
78
 
@@ -4,65 +4,56 @@ 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
- render_initialize
37
+ result = Response::Initialize.call(id: params[:id])
13
38
  when Method::INITIALIZED
14
- render json: {
15
- jsonrpc: "2.0",
16
- method: "notifications/initialized"
17
- }
39
+ result = Response::Initialized.call
18
40
  when Method::CANCELLED
19
- render json: {
20
- jsonrpc: "2.0",
21
- method: "notifications/cancelled"
22
- }
41
+ result = Response::Cancelled.call
23
42
  when Method::TOOLS_LIST
24
- if params[:jsonrpc]
25
- render_tools_list_as_jsonrpc
26
- else
27
- render_tools_list
28
- end
43
+ result = Response::ToolsList::Jsonrpc.call(
44
+ id: params[:id],
45
+ tools: Response::Tools.to_hash(auth_info: @auth_info)
46
+ )
29
47
  when Method::TOOLS_CALL
30
- if params[:jsonrpc]
31
- call_tool_as_jsonrpc
32
- else
33
- render json: call_tool(params)
34
- end
48
+ result = Response::ToolsCall::Jsonrpc.call(id: params[:id], params:, auth_info: @auth_info)
35
49
  else
36
- render json: {error: "Method not found: #{params[:method]}"}, status: 404
50
+ result = Response::NoMethod.call
37
51
  end
38
- end
39
52
 
40
- private
41
-
42
- def render_tools_list_as_jsonrpc
43
- render json: {
44
- jsonrpc: "2.0",
45
- id: params[:id],
46
- result: {tools:}
47
- }
53
+ render json: result, status: 200
48
54
  end
49
55
 
50
- def call_tool_as_jsonrpc
51
- render json: {
52
- jsonrpc: "2.0",
53
- id: params[:id],
54
- result: {
55
- content: [
56
- {
57
- type: "text",
58
- text: call_tool(params[:params])[:result]
59
- }
60
- ]
61
- }
62
- }
63
- end
64
-
65
- def process_authentication
56
+ def authenticate
66
57
  auth_header = request.headers["Authorization"]
67
58
  if auth_header.present?
68
59
  @auth_info = {
@@ -78,90 +69,5 @@ module ActiveMcp
78
69
  }
79
70
  end
80
71
  end
81
-
82
- def render_initialize
83
- render json: {
84
- jsonrpc: "2.0",
85
- id: params[:id],
86
- result: {
87
- protocolVersion: "2024-11-05",
88
- capabilities: {
89
- logging: {},
90
- capabilities: {
91
- resources: {
92
- subscribe: false,
93
- listChanged: false
94
- },
95
- tools: {
96
- listChanged: false
97
- }
98
- },
99
- },
100
- serverInfo: {
101
- name: ActiveMcp::Configuration.config.server_name,
102
- version: ActiveMcp::Configuration.config.server_version
103
- }
104
- }
105
- }
106
- end
107
-
108
- def render_tools_list
109
- render json: {result: tools}
110
- end
111
-
112
- def tools
113
- Tool.registered_tools.select do |tool_class|
114
- tool_class.authorized?(@auth_info)
115
- end.map do |tool_class|
116
- {
117
- name: tool_class.tool_name,
118
- description: tool_class.desc,
119
- inputSchema: tool_class.schema
120
- }
121
- end
122
- end
123
-
124
- def call_tool(params)
125
- tool_name = params[:name]
126
-
127
- unless tool_name
128
- return {error: "Invalid params: missing tool name"}
129
- end
130
-
131
- tool_class = Tool.registered_tools.find do |tc|
132
- tc.tool_name == tool_name
133
- end
134
-
135
- unless tool_class
136
- return {error: "Tool not found: #{tool_name}"}
137
- end
138
-
139
- unless tool_class.authorized?(@auth_info)
140
- return {error: "Unauthorized: Access to tool '#{tool_name}' denied"}
141
- end
142
-
143
- if params[:arguments].is_a?(Hash)
144
- arguments = params[:arguments].symbolize_keys
145
- else
146
- arguments = params[:arguments].permit!.to_hash.symbolize_keys.transform_values { _1.match(/^\d+$/) ? _1.to_i : _1 }
147
- end
148
-
149
- tool = tool_class.new
150
- validation_result = tool.validate_arguments(arguments)
151
-
152
- if validation_result.is_a?(Hash) && validation_result[:error]
153
- return {result: validation_result[:error]}
154
- end
155
-
156
- begin
157
- arguments[:auth_info] = @auth_info if @auth_info.present?
158
-
159
- result = tool.call(**arguments.symbolize_keys)
160
-
161
- return {result: result}
162
- rescue => e
163
- return {error: "Error: #{e.message}"}
164
- end
165
- end
166
72
  end
167
73
  end
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,31 @@
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
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,17 @@
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
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,16 @@
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
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,15 @@
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
@@ -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.0"
2
+ VERSION = "0.3.2"
3
3
  end
data/lib/active_mcp.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "active_mcp/version"
4
- require_relative "active_mcp/config"
4
+ require_relative "active_mcp/configuration"
5
5
  require_relative "active_mcp/tool"
6
6
  require_relative "active_mcp/server"
7
7
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
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
- date: 2025-04-05 00:00:00.000000000 Z
10
+ date: 2025-04-06 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -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: []
@@ -54,9 +54,19 @@ files:
54
54
  - README.md
55
55
  - Rakefile
56
56
  - 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
66
+ - app/models/active_mcp/tool_executor.rb
57
67
  - config/routes.rb
58
68
  - lib/active_mcp.rb
59
- - lib/active_mcp/config.rb
69
+ - lib/active_mcp/configuration.rb
60
70
  - lib/active_mcp/engine.rb
61
71
  - lib/active_mcp/server.rb
62
72
  - lib/active_mcp/server/error_codes.rb
@@ -68,7 +78,6 @@ files:
68
78
  - lib/active_mcp/version.rb
69
79
  - lib/generators/active_mcp/install/install_generator.rb
70
80
  - lib/generators/active_mcp/install/templates/initializer.rb
71
- - lib/generators/active_mcp/install/templates/mcp_server.rb
72
81
  - lib/generators/active_mcp/tool/templates/tool.rb.erb
73
82
  - lib/generators/active_mcp/tool/tool_generator.rb
74
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
File without changes