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 +4 -4
- data/app/controllers/active_mcp/base_controller.rb +33 -16
- data/app/models/active_mcp/response/initialize.rb +18 -21
- data/app/models/active_mcp/response/initialized.rb +3 -6
- data/app/models/active_mcp/response/no_method.rb +1 -2
- data/app/models/active_mcp/response/tools_call/json.rb +1 -56
- data/app/models/active_mcp/response/tools_call/jsonrpc.rb +4 -14
- data/app/models/active_mcp/response/tools_list/json.rb +1 -2
- data/app/models/active_mcp/response/tools_list/jsonrpc.rb +3 -6
- data/app/models/active_mcp/tool_executor.rb +104 -0
- data/lib/active_mcp/server/tool_manager.rb +1 -28
- data/lib/active_mcp/version.rb +1 -1
- metadata +4 -4
- data/lib/generators/active_mcp/install/templates/mcp_server.rb +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 862fda276ef5442ec0c9ee381134e33ce27b1dd797d386db7db95defc29fa1ee
|
4
|
+
data.tar.gz: 2d7892d5eceb1acc3643451e3786cc208cfa1fea1e4b440b05b6ec5a5c440ac8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 :
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
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
|
53
|
+
render json: result, status: 200
|
35
54
|
end
|
36
55
|
|
37
|
-
|
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.
|
4
|
+
def self.call(id:)
|
5
5
|
{
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
jsonrpc: JSON_RPC_VERSION,
|
7
|
+
id:,
|
8
|
+
result: {
|
9
|
+
protocolVersion: PROTOCOL_VERSION,
|
10
|
+
capabilities: {
|
11
|
+
logging: {},
|
11
12
|
capabilities: {
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
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.
|
4
|
+
def self.call
|
5
5
|
{
|
6
|
-
|
7
|
-
|
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,62 +3,7 @@ module ActiveMcp
|
|
3
3
|
module ToolsCall
|
4
4
|
class Json
|
5
5
|
def self.call(params:, auth_info:)
|
6
|
-
|
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 =
|
6
|
+
result = ActiveMcp::ToolExecutor.call(params: params[:params], auth_info:)
|
7
7
|
{
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
@@ -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
|
-
|
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
|
data/lib/active_mcp/version.rb
CHANGED
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.
|
4
|
+
version: 0.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
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
|
-
-
|
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
|