active_mcp 0.2.1 → 0.3.1

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: f94cddd4a196e9f5d1b0501457cb9d318fe1a32cfb997391f0b428f42b850813
4
- data.tar.gz: de38f78d65269af6441c9f375ebca7ae2372acbee26c87ac04d68815a844da07
3
+ metadata.gz: 791510bf59750086b139e1688d8cfd7367c635a359f2dfe2678756e612a20816
4
+ data.tar.gz: 320cb00fa8a75622a667260477c479fd1c9df653e54af85238dc77384923b0a9
5
5
  SHA512:
6
- metadata.gz: 17f8847a39433c08b61838f416dfa6f3e0aecce6daf6ef8051dffdb5e71ac58f191c7654080570f18aae88b002219efd06a260d3afd26d7d1724a5758abf8746
7
- data.tar.gz: afc770724f80df1f92929ab69c88577a6a0e974ad0647acf2dbdf677c7d6dc5b0a01f8792d53ef7eac818a72e11346769f13cd64f6651cc6375ba1bb544c8099
6
+ metadata.gz: d65ba7458c1718d59d8b718eb8422192a1b520e72f877427feb4bdcb9057f9350718ebc8e4687eb7cc05f447962d728949cef94ed2c90937d82deb5ea3fc0727
7
+ data.tar.gz: 54ed82f6c351d9e91e4e4108ed27767564c84d7d0bfc65a101598c0cdc5b879c70aa1b926d2e5c60a8181372a88ad6e9049a7232242726607ae65f58b9585816
data/README.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  A Ruby on Rails engine that provides [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) capabilities to Rails applications. This gem allows you to easily create and expose MCP-compatible tools from your Rails application.
4
4
 
5
- ![Active MCP](./docs/active_mcp.png)
6
-
7
5
  ## Installation
8
6
 
9
7
  Add this line to your application's Gemfile:
@@ -26,6 +24,25 @@ $ gem install active_mcp
26
24
 
27
25
  ## Setup
28
26
 
27
+ ### Using the Install Generator (Recommended)
28
+
29
+ The easiest way to set up Active MCP in your Rails application is to use the install generator:
30
+
31
+ ```bash
32
+ $ rails generate active_mcp:install
33
+ ```
34
+
35
+ This generator will:
36
+
37
+ 1. Create a configuration initializer at `config/initializers/active_mcp.rb`
38
+ 2. Mount the ActiveMcp engine in your routes
39
+
40
+ After running the generator, follow the displayed instructions to create and configure your MCP tools.
41
+
42
+ ### Manual Setup
43
+
44
+ If you prefer to set up manually:
45
+
29
46
  1. Mount the ActiveMcp engine in your `config/routes.rb`:
30
47
 
31
48
  ```ruby
@@ -53,7 +70,13 @@ class CreateNoteTool < ActiveMcp::Tool
53
70
  end
54
71
  ```
55
72
 
56
- 3. Start the MCP server:
73
+ #### with streamable HTTP
74
+
75
+ Set MCP destination to `https:your-app.example.com/mcp`
76
+
77
+ #### with independent MCP Server
78
+
79
+ Start the MCP server:
57
80
 
58
81
  ```ruby
59
82
  # server.rb
@@ -64,7 +87,7 @@ server = ActiveMcp::Server.new(
64
87
  server.start
65
88
  ```
66
89
 
67
- 4. Set up MCP Client
90
+ Set up MCP Client
68
91
 
69
92
  ```json
70
93
  {
@@ -79,14 +102,28 @@ server.start
79
102
 
80
103
  ## Rails Generators
81
104
 
82
- MCP Rails provides generators to help you quickly create new MCP tools:
105
+ Active MCP provides generators to help you quickly set up and extend your MCP integration:
106
+
107
+ ### Install Generator
108
+
109
+ Initialize Active MCP in your Rails application:
110
+
111
+ ```bash
112
+ $ rails generate active_mcp:install
113
+ ```
114
+
115
+ This sets up all necessary configuration files and mounts the MCP engine in your routes.
116
+
117
+ ### Tool Generator
118
+
119
+ Create new MCP tools quickly:
83
120
 
84
121
  ```bash
85
122
  # Generate a new MCP tool
86
123
  $ rails generate active_mcp:tool search_users
87
124
  ```
88
125
 
89
- This creates a new tool file at `app/models/tools/search_users_tool.rb` with the following starter code:
126
+ This creates a new tool file at `app/tools/search_users_tool.rb` with the following starter code:
90
127
 
91
128
  ```ruby
92
129
  class SearchUsersTool < ActiveMcp::Tool
@@ -154,7 +191,7 @@ class AdminOnlyTool < ActiveMcp::Tool
154
191
  def self.authorized?(auth_info)
155
192
  return false unless auth_info
156
193
  return false unless auth_info[:type] == :bearer
157
-
194
+
158
195
  # Check if the token belongs to an admin
159
196
  auth_info[:token] == "admin-token" || User.find_by_token(auth_info[:token])&.admin?
160
197
  end
@@ -166,6 +203,7 @@ end
166
203
  ```
167
204
 
168
205
  When a user makes a request to the MCP server:
206
+
169
207
  1. Only tools that return `true` from their `authorized?` method will be included in the tools list
170
208
  2. Users can only call tools that they're authorized to use
171
209
  3. Unauthorized access attempts will return a 403 Forbidden response
@@ -8,13 +8,30 @@ module ActiveMcp
8
8
 
9
9
  def index
10
10
  case params[:method]
11
+ when Method::INITIALIZE
12
+ result = Response::Initialize.call(id: params[:id])
13
+ when Method::INITIALIZED
14
+ result = Response::Initialized.call
15
+ when Method::CANCELLED
16
+ result = Response::Cancelled.call
11
17
  when Method::TOOLS_LIST
12
- render_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
13
24
  when Method::TOOLS_CALL
14
- call_tool(params)
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
15
30
  else
16
- render json: {error: "Method not found: #{params[:method]}"}, status: 404
31
+ result = Response::NoMethod.call
17
32
  end
33
+
34
+ render json: result[:body], status: result[:status]
18
35
  end
19
36
 
20
37
  private
@@ -35,63 +52,5 @@ module ActiveMcp
35
52
  }
36
53
  end
37
54
  end
38
-
39
- def render_tools_list
40
- # 認可チェックを含めてツールリストをフィルタリング
41
- tools = Tool.registered_tools.select do |tool_class|
42
- # 認可チェック - ツールがauthorized?メソッドでtrueを返すもののみを選択
43
- tool_class.authorized?(@auth_info)
44
- end.map do |tool_class|
45
- {
46
- name: tool_class.tool_name,
47
- description: tool_class.desc,
48
- inputSchema: tool_class.schema
49
- }
50
- end
51
-
52
- render json: {result: tools}
53
- end
54
-
55
- def call_tool(params)
56
- tool_name = params[:name]
57
- arguments = JSON.parse(params[:arguments] || "{}")
58
-
59
- unless tool_name
60
- render json: {error: "Invalid params: missing tool name"}, status: 422
61
- return
62
- end
63
-
64
- tool_class = Tool.registered_tools.find do |tc|
65
- tc.tool_name == tool_name
66
- end
67
-
68
- unless tool_class
69
- render json: {error: "Tool not found: #{tool_name}"}, status: 404
70
- return
71
- end
72
-
73
- # 認可チェック
74
- unless tool_class.authorized?(@auth_info)
75
- render json: {error: "Unauthorized: Access to tool '#{tool_name}' denied"}, status: 403
76
- return
77
- end
78
-
79
- tool = tool_class.new
80
- validation_result = tool.validate_arguments(arguments)
81
-
82
- if validation_result.is_a?(Hash) && validation_result[:error]
83
- render json: {result: validation_result[:error]}
84
- return
85
- end
86
-
87
- begin
88
- arguments[:auth_info] = @auth_info if @auth_info.present?
89
-
90
- result = tool.call(**arguments.symbolize_keys)
91
- render json: {result: result}
92
- rescue => e
93
- render json: {error: "Error: #{e.message}"}
94
- end
95
- end
96
55
  end
97
56
  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,34 @@
1
+ module ActiveMcp
2
+ module Response
3
+ class Initialize
4
+ def self.to_hash(id:, name:, version:)
5
+ {
6
+ body: {
7
+ jsonrpc: JSON_RPC_VERSION,
8
+ id:,
9
+ result: {
10
+ protocolVersion: PROTOCOL_VERSION,
11
+ capabilities: {
12
+ logging: {},
13
+ capabilities: {
14
+ resources: {
15
+ subscribe: false,
16
+ listChanged: false
17
+ },
18
+ tools: {
19
+ listChanged: false
20
+ }
21
+ },
22
+ },
23
+ serverInfo: {
24
+ name: ActiveMcp.config.server_name,
25
+ version: ActiveMcp.config.server_version
26
+ }
27
+ }
28
+ },
29
+ status: 200
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveMcp
2
+ module Response
3
+ class Initialized
4
+ def self.to_hash
5
+ {
6
+ body: {
7
+ jsonrpc: JSON_RPC_VERSION,
8
+ method: Method::INITIALIZED
9
+ },
10
+ status: 200
11
+ }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module ActiveMcp
2
+ module Response
3
+ class NoMethod
4
+ def self.call
5
+ {
6
+ body: { error: "Method not found" },
7
+ status: 404
8
+ }
9
+ end
10
+ end
11
+ end
12
+ 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,66 @@
1
+ module ActiveMcp
2
+ module Response
3
+ module ToolsCall
4
+ class Json
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
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,26 @@
1
+ module ActiveMcp
2
+ module Response
3
+ module ToolsCall
4
+ class Jsonrpc
5
+ def self.call(id:, params:, auth_info:)
6
+ result = Json.call(params: params[:params], auth_info:)
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]
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ module ActiveMcp
2
+ module Response
3
+ module ToolsList
4
+ class Json
5
+ def self.call(tools:)
6
+ {
7
+ body: { result: tools},
8
+ status: 200
9
+ }
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ module ActiveMcp
2
+ module Response
3
+ module ToolsList
4
+ class Jsonrpc
5
+ def self.call(id:, tools:)
6
+ {
7
+ body: {
8
+ jsonrpc: JSON_RPC_VERSION,
9
+ id:,
10
+ result: {tools:}
11
+ },
12
+ status: 200
13
+ }
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveMcp
4
+ class Configuration
5
+ attr_accessor :server_name, :server_version
6
+
7
+ def initialize
8
+ @server_name = "MCP Server"
9
+ @server_version = "1.0.0"
10
+ end
11
+ end
12
+
13
+ class << self
14
+ def configure
15
+ yield config
16
+ end
17
+
18
+ def config
19
+ @config ||= Configuration.new
20
+ end
21
+ end
22
+ end
@@ -4,6 +4,7 @@ module ActiveMcp
4
4
  module Method
5
5
  INITIALIZE = "initialize"
6
6
  INITIALIZED = "notifications/initialized"
7
+ CANCELLED = "notifications/cancelled"
7
8
  PING = "ping"
8
9
  TOOLS_LIST = "tools/list"
9
10
  TOOLS_CALL = "tools/call"
@@ -173,7 +173,7 @@ module ActiveMcp
173
173
  if defined?(Rails)
174
174
  Rails.logger.error(error_details)
175
175
  else
176
- # Fallback to standard error output if Rails is not available
176
+ # Fresallback to standard error output if Rails is not available
177
177
  $stderr.puts(error_details)
178
178
  end
179
179
  end
@@ -76,7 +76,7 @@ module ActiveMcp
76
76
  request.body = JSON.generate({
77
77
  method: "tools/call",
78
78
  name:,
79
- arguments: arguments.to_json
79
+ arguments: arguments
80
80
  })
81
81
  request["Content-Type"] = "application/json"
82
82
  request["Authorization"] = @auth_header
@@ -1,3 +1,3 @@
1
1
  module ActiveMcp
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.1"
3
3
  end
data/lib/active_mcp.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "active_mcp/version"
4
+ require_relative "active_mcp/configuration"
4
5
  require_relative "active_mcp/tool"
5
6
  require_relative "active_mcp/server"
6
7
 
@@ -0,0 +1,17 @@
1
+ module ActiveMcp
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path('templates', __dir__)
5
+
6
+ desc "Creates an Active MCP initializer and mounts the engine in your routes"
7
+
8
+ def create_initializer_file
9
+ template "initializer.rb", "config/initializers/active_mcp.rb"
10
+ end
11
+
12
+ def update_routes
13
+ route "mount ActiveMcp::Engine, at: '/mcp'"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # Active MCP configuration
2
+ ActiveMcp.configure do |config|
3
+ config.server_name = 'MCP Server'
4
+ config.server_version = '1.0.0'
5
+ end
@@ -0,0 +1,31 @@
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
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.2.1
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Your Name
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-04 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
@@ -54,8 +54,18 @@ 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
57
66
  - config/routes.rb
58
67
  - lib/active_mcp.rb
68
+ - lib/active_mcp/configuration.rb
59
69
  - lib/active_mcp/engine.rb
60
70
  - lib/active_mcp/server.rb
61
71
  - lib/active_mcp/server/error_codes.rb
@@ -65,6 +75,9 @@ files:
65
75
  - lib/active_mcp/server/tool_manager.rb
66
76
  - lib/active_mcp/tool.rb
67
77
  - lib/active_mcp/version.rb
78
+ - lib/generators/active_mcp/install/install_generator.rb
79
+ - lib/generators/active_mcp/install/templates/initializer.rb
80
+ - lib/generators/active_mcp/install/templates/mcp_server.rb
68
81
  - lib/generators/active_mcp/tool/templates/tool.rb.erb
69
82
  - lib/generators/active_mcp/tool/tool_generator.rb
70
83
  homepage: https://github.com/moekiorg/active_mcp