active_mcp 0.2.0 → 0.3.0

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: 00ecbb60485be256f7d033221a79ff3cae7d113bbd1876e5b6f191f2aeeff393
4
- data.tar.gz: a7d32577afebf477e230adbac59116f8f3b92331aeac111a9c129bfac83b132b
3
+ metadata.gz: 051dd51506aaadb0c6bf460f1f001049b5d70dc950a060a663d2256050224de7
4
+ data.tar.gz: e70d63d1618f882dd9d459de896a5ba086e29cbef9acbdf897f800a76266850e
5
5
  SHA512:
6
- metadata.gz: c94ad9b53bbfc8e9e6104f0e8ae76dbc9b1b54c1f153e55eab3c2be02c6f5f54ae40fec89c4b55a39505d1a8d9e42e1bb2d3c65868bd6c711b0fbad72e4d069d
7
- data.tar.gz: 7ba6697d22894f9c5166623ae176319885d4fc050c462c8ccaf18322b119b53ec858d20de75fc91feacc87554037de2e7157cae7a4b32dd55d1a5da03ba17491
6
+ metadata.gz: 7b5c6aaa00d5f9138d2186f2ea4c988f621f5f231674f4c01eeda64418187e0502999b1385c9417824bd99b888a9252f2833c5f5f42026e8943ce07d8a8952a4
7
+ data.tar.gz: 31d7ea0b9f9cd7b6385a30ced3a9caf14d2105f366d614c1ae8fc5c227f617c547f1f86c63084e4816265cb838ed8e089a4997e9cd1d3e757f8304cd197717a7
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 as `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
@@ -136,7 +173,44 @@ http://your-app.example.com/mcp
136
173
 
137
174
  Clients will discover the available tools and their input schemas automatically through the MCP protocol.
138
175
 
139
- ## Authentication Flow
176
+ ## Authorization & Authentication
177
+
178
+ ActiveMcp supports both authentication (verifying who a user is) and authorization (controlling what resources they can access).
179
+
180
+ ### Authorization for Tools
181
+
182
+ You can control which tools are visible and accessible to different users by overriding the `authorized?` class method:
183
+
184
+ ```ruby
185
+ class AdminOnlyTool < ActiveMcp::Tool
186
+ description "This tool is only accessible by admins"
187
+
188
+ property :command, :string, required: true, description: "Admin command to execute"
189
+
190
+ # Define authorization logic - only admin tokens can access this tool
191
+ def self.authorized?(auth_info)
192
+ return false unless auth_info
193
+ return false unless auth_info[:type] == :bearer
194
+
195
+ # Check if the token belongs to an admin
196
+ auth_info[:token] == "admin-token" || User.find_by_token(auth_info[:token])&.admin?
197
+ end
198
+
199
+ def call(command:, auth_info: nil)
200
+ # Tool implementation
201
+ end
202
+ end
203
+ ```
204
+
205
+ When a user makes a request to the MCP server:
206
+
207
+ 1. Only tools that return `true` from their `authorized?` method will be included in the tools list
208
+ 2. Users can only call tools that they're authorized to use
209
+ 3. Unauthorized access attempts will return a 403 Forbidden response
210
+
211
+ This makes it easy to create role-based access control for your MCP tools.
212
+
213
+ ### Authentication Flow
140
214
 
141
215
  ActiveMcp supports receiving authentication credentials from MCP clients and forwarding them to your Rails application. There are two ways to handle authentication:
142
216
 
@@ -8,10 +8,30 @@ module ActiveMcp
8
8
 
9
9
  def index
10
10
  case params[:method]
11
+ when Method::INITIALIZE
12
+ render_initialize
13
+ when Method::INITIALIZED
14
+ render json: {
15
+ jsonrpc: "2.0",
16
+ method: "notifications/initialized"
17
+ }
18
+ when Method::CANCELLED
19
+ render json: {
20
+ jsonrpc: "2.0",
21
+ method: "notifications/cancelled"
22
+ }
11
23
  when Method::TOOLS_LIST
12
- render_tools_list
24
+ if params[:jsonrpc]
25
+ render_tools_list_as_jsonrpc
26
+ else
27
+ render_tools_list
28
+ end
13
29
  when Method::TOOLS_CALL
14
- call_tool(params)
30
+ if params[:jsonrpc]
31
+ call_tool_as_jsonrpc
32
+ else
33
+ render json: call_tool(params)
34
+ end
15
35
  else
16
36
  render json: {error: "Method not found: #{params[:method]}"}, status: 404
17
37
  end
@@ -19,6 +39,29 @@ module ActiveMcp
19
39
 
20
40
  private
21
41
 
42
+ def render_tools_list_as_jsonrpc
43
+ render json: {
44
+ jsonrpc: "2.0",
45
+ id: params[:id],
46
+ result: {tools:}
47
+ }
48
+ end
49
+
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
+
22
65
  def process_authentication
23
66
  auth_header = request.headers["Authorization"]
24
67
  if auth_header.present?
@@ -36,25 +79,53 @@ module ActiveMcp
36
79
  end
37
80
  end
38
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
+
39
108
  def render_tools_list
40
- tools = Tool.registered_tools.map do |tool_class|
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|
41
116
  {
42
117
  name: tool_class.tool_name,
43
118
  description: tool_class.desc,
44
119
  inputSchema: tool_class.schema
45
120
  }
46
121
  end
47
-
48
- render json: {result: tools}
49
122
  end
50
123
 
51
124
  def call_tool(params)
52
125
  tool_name = params[:name]
53
- arguments = JSON.parse(params[:arguments] || "{}")
54
126
 
55
127
  unless tool_name
56
- render json: {error: "Invalid params: missing tool name"}, status: 422
57
- return
128
+ return {error: "Invalid params: missing tool name"}
58
129
  end
59
130
 
60
131
  tool_class = Tool.registered_tools.find do |tc|
@@ -62,25 +133,34 @@ module ActiveMcp
62
133
  end
63
134
 
64
135
  unless tool_class
65
- render json: {error: "Tool not found: #{tool_name}"}, status: 404
66
- return
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 }
67
147
  end
68
148
 
69
149
  tool = tool_class.new
70
150
  validation_result = tool.validate_arguments(arguments)
71
151
 
72
152
  if validation_result.is_a?(Hash) && validation_result[:error]
73
- render json: {result: validation_result[:error]}
74
- return
153
+ return {result: validation_result[:error]}
75
154
  end
76
155
 
77
156
  begin
78
157
  arguments[:auth_info] = @auth_info if @auth_info.present?
79
158
 
80
159
  result = tool.call(**arguments.symbolize_keys)
81
- render json: {result: result}
160
+
161
+ return {result: result}
82
162
  rescue => e
83
- render json: {error: "Error: #{e.message}"}
163
+ return {error: "Error: #{e.message}"}
84
164
  end
85
165
  end
86
166
  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
@@ -34,6 +34,10 @@ module ActiveMcp
34
34
  def inherited(subclass)
35
35
  registered_tools << subclass
36
36
  end
37
+
38
+ def authorized?(auth_info)
39
+ true
40
+ end
37
41
  end
38
42
 
39
43
  def initialize
@@ -1,3 +1,3 @@
1
1
  module ActiveMcp
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
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/config"
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
@@ -5,6 +5,23 @@ class <%= class_name %> < ActiveMcp::Tool
5
5
  property :param2, :string, required: false, description: "Second parameter description"
6
6
  # Add more parameters as needed
7
7
 
8
+ # Uncomment and modify this method to implement authorization control
9
+ # This controls who can see and use this tool
10
+ # def self.authorized?(auth_info)
11
+ # # Example: require authentication
12
+ # # return false unless auth_info
13
+ #
14
+ # # Example: require a specific authentication type
15
+ # # return false unless auth_info[:type] == :bearer
16
+ #
17
+ # # Example: check for admin permissions
18
+ # # admin_tokens = ["admin-token"]
19
+ # # return admin_tokens.include?(auth_info[:token])
20
+ #
21
+ # # Default: allow all access
22
+ # true
23
+ # end
24
+
8
25
  def call(param1:, param2: nil, auth_info: nil, **args)
9
26
  # Authentication information can be accessed via _auth_info parameter
10
27
  # auth_info = { type: :bearer, token: "xxx", header: "Bearer xxx" }
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.0
4
+ version: 0.3.0
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-05 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -56,6 +56,7 @@ files:
56
56
  - app/controllers/active_mcp/base_controller.rb
57
57
  - config/routes.rb
58
58
  - lib/active_mcp.rb
59
+ - lib/active_mcp/config.rb
59
60
  - lib/active_mcp/engine.rb
60
61
  - lib/active_mcp/server.rb
61
62
  - lib/active_mcp/server/error_codes.rb
@@ -65,6 +66,9 @@ files:
65
66
  - lib/active_mcp/server/tool_manager.rb
66
67
  - lib/active_mcp/tool.rb
67
68
  - lib/active_mcp/version.rb
69
+ - lib/generators/active_mcp/install/install_generator.rb
70
+ - lib/generators/active_mcp/install/templates/initializer.rb
71
+ - lib/generators/active_mcp/install/templates/mcp_server.rb
68
72
  - lib/generators/active_mcp/tool/templates/tool.rb.erb
69
73
  - lib/generators/active_mcp/tool/tool_generator.rb
70
74
  homepage: https://github.com/moekiorg/active_mcp