active_mcp 0.6.0 → 0.7.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: 33e83423b0e7e4bdf722b5473ec742e92efe3af946270f3fc1edf2ee9806c8d2
4
- data.tar.gz: a9fe38a30a53fbdc4810b2c5d08884730f71cd4ace372188c69916f6db61742a
3
+ metadata.gz: c9367d905ac3b7dfa87f27063c41f24f1ed93de9d88a5fc009010ed35bfafa85
4
+ data.tar.gz: 7b1ed7d1f79ad9487c92a4e0595727cc60125c3584ae7029be8d3e86f9c2537b
5
5
  SHA512:
6
- metadata.gz: b20d1f9baf3a7b48cab071135ca15fc182fb3d1cdc3570bcd0b0fb337906dd9e5fa0dc1f646823b281a9600911e3ef9ef7d5cf67eaab80a19a2cad27304f04bb
7
- data.tar.gz: 5036f8e8b6056255e81b693cb5fc436d78bf04b8f0546c186110eb4774e6b7b7fe36d4e755c30e84884cadb75a4455af469fbe27d869b502a5ee5d0c79c9d167
6
+ metadata.gz: 5f429c060c395a06e9e146c68ea859a1750a00add2b28ba5fe3a968b20b8b335fcc3908a1f89b63aeaf24f092942af9edd4979eec2a3b380d431519ee35623d4
7
+ data.tar.gz: 1a0bff75ee866fbba70a5549ee867f6260cd8e925a592b9a044b3769a91ba9955487e3798647c7fa6b356e4c9cbfbd3610550a713aa4cee97dfff896c7514c15
data/README.md CHANGED
@@ -17,8 +17,6 @@ A Ruby on Rails engine for the [Model Context Protocol (MCP)](https://modelconte
17
17
  - [✨ Features](#-features)
18
18
  - [📦 Installation](#-installation)
19
19
  - [🚀 Setup](#-setup)
20
- - [Using the Install Generator (Recommended)](#using-the-install-generator-recommended)
21
- - [Manual Setup](#manual-setup)
22
20
  - [🔌 MCP Connection Methods](#-mcp-connection-methods)
23
21
  - [1. Direct HTTP Connection](#1-direct-http-connection)
24
22
  - [2. Standalone MCP Server](#2-standalone-mcp-server)
@@ -38,8 +36,6 @@ A Ruby on Rails engine for the [Model Context Protocol (MCP)](https://modelconte
38
36
  - [Resource Types](#resource-types)
39
37
  - [📦 MCP Resource Templates](#-mcp-resource-templates)
40
38
  - [Creating Resource Templates](#creating-resource-templates)
41
- - [⚙️ Advanced Configuration](#️-advanced-configuration)
42
- - [Custom Controller](#custom-controller)
43
39
  - [💡 Best Practices](#-best-practices)
44
40
  - [1. Create Specific Tool Classes](#1-create-specific-tool-classes)
45
41
  - [2. Validate and Sanitize Inputs](#2-validate-and-sanitize-inputs)
@@ -78,7 +74,7 @@ $ gem install active_mcp
78
74
 
79
75
  ## 🚀 Setup
80
76
 
81
- ### Using the Install Generator (Recommended)
77
+ 1. Initialize
82
78
 
83
79
  The easiest way to set up Active MCP in your Rails application is to use the install generator:
84
80
 
@@ -86,46 +82,61 @@ The easiest way to set up Active MCP in your Rails application is to use the ins
86
82
  $ rails generate active_mcp:install
87
83
  ```
88
84
 
89
- This generator will:
85
+ This generator will create a configuration initializer at `config/initializers/active_mcp.rb`
90
86
 
91
- 1. Create a configuration initializer at `config/initializers/active_mcp.rb`
92
- 2. Mount the ActiveMcp engine in your routes
93
- 3. Create an MCP server script at `script/mcp_server.rb`
94
- 4. Show instructions for next steps
87
+ 2. Create a tool by inheriting from `ActiveMcp::Tool::Base`:
95
88
 
96
- After running the generator, follow the displayed instructions to create and configure your MCP tools.
89
+ ```bash
90
+ $ rails generate active_mcp:tool create_note
91
+ ```
97
92
 
98
- ### Manual Setup
93
+ ```ruby
94
+ class CreateNoteTool < ActiveMcp::Tool::Base
95
+ def name
96
+ "Create Note"
97
+ end
99
98
 
100
- If you prefer to set up manually:
99
+ def description
100
+ "Create Note"
101
+ end
101
102
 
102
- 1. Mount the ActiveMcp engine in your `config/routes.rb`:
103
+ argument :title, :string, required: true
104
+ argument :content, :string, required: true
103
105
 
104
- ```ruby
105
- Rails.application.routes.draw do
106
- mount ActiveMcp::Engine, at: "/mcp"
106
+ def call(title:, content:, context:)
107
+ note = Note.create(title: title, content: content)
107
108
 
108
- # Your other routes
109
+ "Created note with ID: #{note.id}"
110
+ end
109
111
  end
110
112
  ```
111
113
 
112
- 2. Create a tool by inheriting from `ActiveMcp::Tool`:
114
+ 3. Create schema for your application:
113
115
 
114
116
  ```ruby
115
- class CreateNoteTool < ActiveMcp::Tool
116
- description "Create Note"
117
-
118
- argument :title, :string, required: true
119
- argument :content, :string, required: true
117
+ class MySchema < ActiveMcp::Schema::Base
118
+ tool CreateNoteTool.new
119
+ end
120
+ ```
120
121
 
121
- def call(title:, content:)
122
- note = Note.create(title: title, content: content)
122
+ 4. Create controller ans set up routing:
123
123
 
124
- "Created note with ID: #{note.id}"
124
+ ```ruby
125
+ class MyMcpController < ActiveMcp::Controller::Base
126
+ def schema
127
+ MySchema.new(context:)
125
128
  end
126
129
  end
127
130
  ```
128
131
 
132
+ ```ruby
133
+ Rails.application.routes.draw do
134
+ post "/mcp", to: "my_mcp#index"
135
+
136
+ # Your other routes
137
+ end
138
+ ```
139
+
129
140
  ## 🔌 MCP Connection Methods
130
141
 
131
142
  Active MCP supports two connection methods:
@@ -184,7 +195,7 @@ Create new MCP tools quickly:
184
195
  $ rails generate active_mcp:tool search_users
185
196
  ```
186
197
 
187
- This creates a new tool file at `app/tools/search_users_tool.rb` with ready-to-customize starter code.
198
+ This creates a new tool file at `app/mcp/tools/search_users_tool.rb` with ready-to-customize starter code.
188
199
 
189
200
  ### Resource Generator
190
201
 
@@ -194,21 +205,27 @@ Generate new MCP resources to share data with AI:
194
205
  $ rails generate active_mcp:resource profile_image
195
206
  ```
196
207
 
197
- This creates a new resource file at `app/resources/profile_image_resource.rb` that you can customize to provide various types of content to AI assistants.
208
+ This creates a new resource file at `app/mcp/resources/profile_image_resource.rb` that you can customize to provide various types of content to AI assistants.
198
209
 
199
210
  ## 🧰 Creating MCP Tools
200
211
 
201
- MCP tools are Ruby classes that inherit from `ActiveMcp::Tool` and define an interface for AI to interact with your application:
212
+ MCP tools are Ruby classes that inherit from `ActiveMcp::Tool::Base` and define an interface for AI to interact with your application:
202
213
 
203
214
  ```ruby
204
- class SearchUsersTool < ActiveMcp::Tool
205
- description 'Search users by criteria'
215
+ class SearchUsersTool < ActiveMcp::Tool::Base
216
+ def name
217
+ "Search Users"
218
+ end
219
+
220
+ def description
221
+ 'Search users by criteria'
222
+ end
206
223
 
207
224
  argument :email, :string, required: false, description: 'Email to search for'
208
225
  argument :name, :string, required: false, description: 'Name to search for'
209
226
  argument :limit, :integer, required: false, description: 'Maximum number of records to return'
210
227
 
211
- def call(email: nil, name: nil, limit: 10)
228
+ def call(email: nil, name: nil, limit: 10, context: {})
212
229
  criteria = {}
213
230
  criteria[:email] = email if email.present?
214
231
  criteria[:name] = name if name.present?
@@ -250,21 +267,27 @@ Supported types:
250
267
  Control access to tools by overriding the `visible?` class method:
251
268
 
252
269
  ```ruby
253
- class AdminOnlyTool < ActiveMcp::Tool
254
- description "Admin-only tool"
270
+ class AdminOnlyTool < ActiveMcp::Tool::Base
271
+ def name
272
+ "Admin-only tool"
273
+ end
274
+
275
+ def description
276
+ "Admin-only tool"
277
+ end
255
278
 
256
279
  argument :command, :string, required: true, description: "Admin command"
257
280
 
258
281
  # Only allow admins to access this tool
259
- def self.visible?(auth_info)
260
- return false unless auth_info
261
- return false unless auth_info[:type] == :bearer
282
+ def visible?(context:)
283
+ return false unless context
284
+ return false unless context[:auth_info][:type] == :bearer
262
285
 
263
286
  # Check if the token belongs to an admin
264
- auth_info[:token] == "admin-token" || User.find_by_token(auth_info[:token])&.admin?
287
+ context[:auth_info] == "admin-token" || User.find_by_token(context[:auth_info])&.admin?
265
288
  end
266
289
 
267
- def call(command:, auth_info: nil)
290
+ def call(command:, context: {})
268
291
  # Tool implementation
269
292
  end
270
293
  end
@@ -289,14 +312,14 @@ server.start
289
312
  #### 2. Token Verification in Tools
290
313
 
291
314
  ```ruby
292
- def call(resource_id:, auth_info: nil, **args)
315
+ def call(resource_id:, context: {})
293
316
  # Check if authentication is provided
294
- unless auth_info.present?
317
+ unless context[:auth_info].present?
295
318
  raise "Authentication required"
296
319
  end
297
320
 
298
321
  # Verify the token
299
- user = User.authenticate_with_token(auth_info[:token])
322
+ user = User.authenticate_with_token(context[:auth_info][:token])
300
323
 
301
324
  unless user
302
325
  raise "Invalid authentication token"
@@ -317,7 +340,7 @@ Resources are Ruby classes `**Resource`:
317
340
 
318
341
  ```ruby
319
342
  class UserResource
320
- def initialize(id:, auth_info: nil)
343
+ def initialize(id:)
321
344
  @user = User.find(id)
322
345
  @auth_info = auth_info
323
346
  end
@@ -338,6 +361,10 @@ class UserResource
338
361
  @user.profile
339
362
  end
340
363
 
364
+ def visible?(context:)
365
+ # Your logic...
366
+ end
367
+
341
368
  def text
342
369
  # Return JSON data
343
370
  {
@@ -351,13 +378,9 @@ end
351
378
  ```
352
379
 
353
380
  ```ruby
354
- class McpController < ActiveMcp::BaseController
355
- private
356
-
357
- def resource_list
358
- User.all.map do |user|
359
- UserResource.new(id: user.id)
360
- end
381
+ class MySchema < ActiveMcp::Schema::Base
382
+ User.all.each do |user|
383
+ resource UserResource.new(id: user.id)
361
384
  end
362
385
  end
363
386
  ```
@@ -380,7 +403,7 @@ end
380
403
  ```ruby
381
404
  class ImageResource
382
405
  def name
383
- "image"
406
+ "Profile Image"
384
407
  end
385
408
 
386
409
  def uri
@@ -405,12 +428,12 @@ end
405
428
  Resources can be protected using the same authorization mechanism as tools:
406
429
 
407
430
  ```ruby
408
- def visible?
409
- return false unless auth_info
410
- return false unless auth_info[:type] == :bearer
431
+ def visible?(context: {})
432
+ return false unless context
433
+ return false unless context[:auth_info][:type] == :bearer
411
434
 
412
435
  # Check if the token belongs to an admin
413
- User.find_by_token(auth_info[:token])&.admin?
436
+ User.find_by_token(context[:auth_info][:token])&.admin?
414
437
  end
415
438
  ```
416
439
 
@@ -439,38 +462,16 @@ class UserResourceTemplate
439
462
  def description
440
463
  "This is a test."
441
464
  end
442
- end
443
- ```
444
465
 
445
- ```ruby
446
- class McpController < ActiveMcp::BaseController
447
- private
448
-
449
- def resource_templates_list
450
- [
451
- UserResourceTemplate.new
452
- ]
466
+ def visible?(context:)
467
+ # Your logic...
453
468
  end
454
469
  end
455
470
  ```
456
471
 
457
- ## ⚙️ Advanced Configuration
458
-
459
- ### Custom Controller
460
-
461
- Create a custom controller for advanced needs:
462
-
463
- ```ruby
464
- class CustomMcpController < ActiveMcp::BaseController
465
- # Custom MCP handling logic
466
- end
467
- ```
468
-
469
- Update routes:
470
-
471
472
  ```ruby
472
- Rails.application.routes.draw do
473
- post "/mcp", to: "custom_mcp#index"
473
+ class MySchema < ActiveMcp::Schema::Base
474
+ resource_template UserResourceTemplate.new
474
475
  end
475
476
  ```
476
477
 
@@ -482,12 +483,12 @@ Create dedicated tool classes for each model or operation instead of generic too
482
483
 
483
484
  ```ruby
484
485
  # ✅ GOOD: Specific tool for a single purpose
485
- class SearchUsersTool < ActiveMcp::Tool
486
+ class SearchUsersTool < ActiveMcp::Tool::Base
486
487
  # ...specific implementation
487
488
  end
488
489
 
489
490
  # ❌ BAD: Generic tool that dynamically loads models
490
- class GenericSearchTool < ActiveMcp::Tool
491
+ class GenericSearchTool < ActiveMcp::Tool::Base
491
492
  # Avoid this pattern - security and maintainability issues
492
493
  end
493
494
  ```
@@ -497,7 +498,7 @@ end
497
498
  Always validate and sanitize inputs in your tool implementations:
498
499
 
499
500
  ```ruby
500
- def call(user_id:, **args)
501
+ def call(user_id:, **args, context: {})
501
502
  # Validate input
502
503
  unless user_id.is_a?(Integer) || user_id.to_s.match?(/^\d+$/)
503
504
  raise "Invalid user ID format"
@@ -514,7 +515,7 @@ end
514
515
  Return structured responses that are easy for AI to parse:
515
516
 
516
517
  ```ruby
517
- def call(query:, **args)
518
+ def call(query:, context: {})
518
519
  results = User.search(query)
519
520
 
520
521
  {
@@ -1,9 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../concerns/active_mcp/authenticatable"
4
+ require_relative "../concerns/active_mcp/request_handlable"
5
+ require_relative "../concerns/active_mcp/resource_readable"
6
+ require_relative "../concerns/active_mcp/tool_executable"
7
+
3
8
  module ActiveMcp
4
9
  class BaseController < ActionController::Base
5
- include RequestHandlable
6
- include ResourceReadable
7
- include ToolExecutable
10
+ include ::ActiveMcp::RequestHandlable
11
+ include ::ActiveMcp::ResourceReadable
12
+ include ::ActiveMcp::ToolExecutable
13
+ include ::ActiveMcp::Authenticatable
14
+
15
+ protect_from_forgery with: :null_session
16
+ skip_before_action :verify_authenticity_token
17
+ before_action :authenticate, only: [:index]
18
+
19
+ def index
20
+ if json_rpc_request?
21
+ handle_mcp_client_request
22
+ else
23
+ handle_mcp_server_request
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def schema
30
+ nil
31
+ end
8
32
  end
9
33
  end
@@ -0,0 +1,29 @@
1
+ module ActiveMcp
2
+ module Authenticatable
3
+ extend ActiveSupport::Concern
4
+
5
+ private
6
+
7
+ def authenticate
8
+ auth_header = request.headers["Authorization"]
9
+ if auth_header.present?
10
+ @context ||= {}
11
+ @context[:auth_info] = {
12
+ header: auth_header,
13
+ type: if auth_header.start_with?("Bearer ")
14
+ :bearer
15
+ elsif auth_header.start_with?("Basic ")
16
+ :basic
17
+ else
18
+ :unknown
19
+ end,
20
+ token: auth_header.split(" ").last
21
+ }
22
+ end
23
+ end
24
+
25
+ def context
26
+ @context
27
+ end
28
+ end
29
+ end
@@ -4,20 +4,6 @@ module ActiveMcp
4
4
  module RequestHandlable
5
5
  extend ActiveSupport::Concern
6
6
 
7
- included do
8
- protect_from_forgery with: :null_session
9
- skip_before_action :verify_authenticity_token
10
- before_action :authenticate, only: [:index]
11
- end
12
-
13
- def index
14
- if json_rpc_request?
15
- handle_mcp_client_request
16
- else
17
- handle_mcp_server_request
18
- end
19
- end
20
-
21
7
  private
22
8
 
23
9
  def json_rpc_request?
@@ -26,7 +12,6 @@ module ActiveMcp
26
12
 
27
13
  def handle_mcp_client_request
28
14
  @id = params[:id]
29
- @auth_info = auth_info
30
15
 
31
16
  case params[:method]
32
17
  when Method::INITIALIZE
@@ -36,23 +21,23 @@ module ActiveMcp
36
21
  when Method::CANCELLED
37
22
  render 'active_mcp/cancelled', formats: :json
38
23
  when Method::RESOURCES_LIST
39
- @resources = resources_list
24
+ @resources = schema.resources
40
25
  @format = :jsonrpc
41
26
  render 'active_mcp/resources_list', formats: :json
42
27
  when Method::RESOURCES_TEMPLATES_LIST
43
- @resource_templates = resource_templates_list
28
+ @resource_templates = schema.resource_templates
44
29
  @format = :jsonrpc
45
30
  render 'active_mcp/resource_templates_list', formats: :json
46
31
  when Method::RESOURCES_READ
47
- @resource = read_resource(params:, auth_info:)
32
+ @resource = read_resource(params:, context:)
48
33
  @format = :jsonrpc
49
34
  render 'active_mcp/resources_read', formats: :json
50
35
  when Method::TOOLS_LIST
51
- @tools = ActiveMcp::Tool.authorized_tools(auth_info)
36
+ @tools = schema.tools
52
37
  @format = :jsonrpc
53
38
  render 'active_mcp/tools_list', formats: :json
54
39
  when Method::TOOLS_CALL
55
- @tool_result = execute_tool(params: params, auth_info: auth_info)
40
+ @tool_result = execute_tool(params:, context:)
56
41
  @format = :jsonrpc
57
42
  render 'active_mcp/tools_call', formats: :json
58
43
  else
@@ -62,27 +47,25 @@ module ActiveMcp
62
47
  end
63
48
 
64
49
  def handle_mcp_server_request
65
- @auth_info = auth_info
66
-
67
50
  case params[:method]
68
51
  when Method::RESOURCES_LIST
69
- @resources = resources_list
52
+ @resources = schema.resources
70
53
  @format = :json
71
54
  render 'active_mcp/resources_list', formats: :json
72
55
  when Method::RESOURCES_READ
73
- @resource = read_resource(params:, auth_info:)
56
+ @resource = read_resource(params:, context:)
74
57
  @format = :json
75
58
  render 'active_mcp/resources_read', formats: :json
76
59
  when Method::RESOURCES_TEMPLATES_LIST
77
- @resource_templates = resource_templates_list
60
+ @resource_templates = schema.resource_templates
78
61
  @format = :json
79
62
  render 'active_mcp/resource_templates_list', formats: :json
80
63
  when Method::TOOLS_LIST
81
- @tools = ActiveMcp::Tool.authorized_tools(auth_info)
64
+ @tools = schema.tools
82
65
  @format = :json
83
66
  render 'active_mcp/tools_list', formats: :json
84
67
  when Method::TOOLS_CALL
85
- @tool_result = execute_tool(params: params, auth_info: auth_info)
68
+ @tool_result = execute_tool(params:, context:)
86
69
  @format = :json
87
70
  render 'active_mcp/tools_call', formats: :json
88
71
  else
@@ -91,25 +74,8 @@ module ActiveMcp
91
74
  end
92
75
  end
93
76
 
94
- def authenticate
95
- auth_header = request.headers["Authorization"]
96
- if auth_header.present?
97
- @auth_info = {
98
- header: auth_header,
99
- type: if auth_header.start_with?("Bearer ")
100
- :bearer
101
- elsif auth_header.start_with?("Basic ")
102
- :basic
103
- else
104
- :unknown
105
- end,
106
- token: auth_header.split(" ").last
107
- }
108
- end
109
- end
110
-
111
- def auth_info
112
- @auth_info
77
+ def context
78
+ @context ||= {}
113
79
  end
114
80
  end
115
81
  end
@@ -4,15 +4,7 @@ module ActiveMcp
4
4
 
5
5
  private
6
6
 
7
- def resource_templates_list
8
- []
9
- end
10
-
11
- def resources_list
12
- []
13
- end
14
-
15
- def read_resource(params:, auth_info:)
7
+ def read_resource(params:, context:)
16
8
  if params[:jsonrpc].present?
17
9
  uri = params[:params][:uri]
18
10
  else
@@ -26,7 +18,7 @@ module ActiveMcp
26
18
  }
27
19
  end
28
20
 
29
- resource = resources_list.find do |r|
21
+ resource = schema.resources.find do |r|
30
22
  r.uri == uri
31
23
  end
32
24
 
@@ -37,7 +29,7 @@ module ActiveMcp
37
29
  }
38
30
  end
39
31
 
40
- if resource.respond_to?(:visible?) && !resource.visible?
32
+ if resource.respond_to?(:visible?) && !resource.visible?(context:)
41
33
  return {
42
34
  isError: true,
43
35
  contents: []
@@ -4,7 +4,7 @@ module ActiveMcp
4
4
 
5
5
  private
6
6
 
7
- def execute_tool(params:, auth_info:)
7
+ def execute_tool(params:, context: {})
8
8
  if params[:jsonrpc].present?
9
9
  tool_name = params[:params][:name]
10
10
  tool_params = params[:params][:arguments]
@@ -25,11 +25,11 @@ module ActiveMcp
25
25
  }
26
26
  end
27
27
 
28
- tool_class = Tool.registered_tools.find do |tc|
29
- tc.tool_name == tool_name
28
+ tool = schema.tools.find do |tc|
29
+ tc.name == tool_name
30
30
  end
31
31
 
32
- unless tool_class
32
+ unless tool
33
33
  return {
34
34
  isError: true,
35
35
  content: [
@@ -41,7 +41,7 @@ module ActiveMcp
41
41
  }
42
42
  end
43
43
 
44
- unless tool_class.visible?(auth_info)
44
+ unless tool.visible?(context:)
45
45
  return {
46
46
  isError: true,
47
47
  content: [
@@ -69,7 +69,6 @@ module ActiveMcp
69
69
  end
70
70
  end
71
71
 
72
- tool = tool_class.new
73
72
  validation_result = tool.validate_arguments(arguments)
74
73
 
75
74
  if validation_result.is_a?(Hash) && validation_result[:error]
@@ -86,13 +85,11 @@ module ActiveMcp
86
85
 
87
86
  # Execute the tool
88
87
  begin
89
- arguments[:auth_info] = auth_info if auth_info.present?
90
-
91
88
  return {
92
89
  content: [
93
90
  {
94
91
  type: "text",
95
- text: formatted(tool.call(**arguments.symbolize_keys))
92
+ text: formatted(tool.call(**arguments, context:))
96
93
  }
97
94
  ]
98
95
  }
@@ -3,8 +3,20 @@ json.id @id if @format == :jsonrpc && @id.present?
3
3
 
4
4
  if @format == :jsonrpc
5
5
  json.result do
6
- json.tools @tools
6
+ json.tools do
7
+ json.array!(@tools) do |tool|
8
+ json.name tool.name
9
+ json.description tool.description
10
+ json.inputSchema tool.class.schema
11
+ end
12
+ end
7
13
  end
8
14
  else
9
- json.result @tools
15
+ json.result do
16
+ json.array!(@tools) do |tool|
17
+ json.name tool.name
18
+ json.description tool.description
19
+ json.inputSchema tool.class.schema
20
+ end
21
+ end
10
22
  end
@@ -0,0 +1,7 @@
1
+ require_relative "../../../app/controllers/active_mcp/base_controller"
2
+
3
+ module ActiveMcp
4
+ module Controller
5
+ Base = ActiveMcp::BaseController
6
+ end
7
+ end
@@ -2,20 +2,17 @@ module ActiveMcp
2
2
  class Engine < ::Rails::Engine
3
3
  isolate_namespace ActiveMcp
4
4
 
5
- initializer "active_mcp.eager_load_tools" do |app|
6
- tools_path = Rails.root.join("app", "tools")
7
- if Dir.exist?(tools_path)
8
- Dir[tools_path.join("*.rb")].sort.each do |file|
9
- require_dependency file
10
- end
11
- end
12
- end
13
-
14
- initializer "active_mcp.eager_load_resources" do |app|
15
- tools_path = Rails.root.join("app", "resources")
16
- if Dir.exist?(tools_path)
17
- Dir[tools_path.join("*.rb")].sort.each do |file|
18
- require_dependency file
5
+ initializer "active_mcp.eager_load" do |app|
6
+ [
7
+ Rails.root.join("app", "mcp", "tools"),
8
+ Rails.root.join("app", "mcp", "resources"),
9
+ Rails.root.join("app", "mcp", "resource_templates"),
10
+ Rails.root.join("app", "mcp", "schemas")
11
+ ].each do |tools_path|
12
+ if Dir.exist?(tools_path)
13
+ Dir[tools_path.join("*.rb")].sort.each do |file|
14
+ require_dependency file
15
+ end
19
16
  end
20
17
  end
21
18
  end
@@ -0,0 +1,46 @@
1
+ module ActiveMcp
2
+ module Schema
3
+ class Base
4
+ class << self
5
+ attr_reader :resources, :resource_templates, :tools
6
+
7
+ def resource(klass)
8
+ @resources ||= []
9
+ @resources << klass
10
+ end
11
+
12
+ def resource_template(klass)
13
+ @resource_templates ||= []
14
+ @resource_templates << klass
15
+ end
16
+
17
+ def tool(klass)
18
+ @tools ||= []
19
+ @tools << klass
20
+ end
21
+ end
22
+
23
+ def initialize(context: {})
24
+ @context = context
25
+ end
26
+
27
+ def resources
28
+ self.class.resources.filter do |resource|
29
+ !resource.respond_to?(:visible?) || resource.visible?(context: @context)
30
+ end
31
+ end
32
+
33
+ def resource_templates
34
+ self.class.resource_templates.filter do |tool_resource|
35
+ !tool_resource.respond_to?(:visible?) || tool_resource.visible?(context: @context)
36
+ end
37
+ end
38
+
39
+ def tools
40
+ self.class.tools.filter do |tool|
41
+ !tool.respond_to?(:visible?) || tool.visible?(context: @context)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,52 @@
1
+ require "json-schema"
2
+
3
+ module ActiveMcp
4
+ module Tool
5
+ class Base
6
+ class << self
7
+ attr_reader :schema
8
+
9
+ def argument(name, type, required: false, description: nil)
10
+ @schema ||= default_schema
11
+
12
+ @schema["properties"][name.to_s] = {"type" => type.to_s}
13
+ @schema["properties"][name.to_s]["description"] = description if description
14
+ @schema["required"] << name.to_s if required
15
+ end
16
+
17
+ def default_schema
18
+ {
19
+ "type" => "object",
20
+ "properties" => {},
21
+ "required" => []
22
+ }
23
+ end
24
+ end
25
+
26
+ def initialize
27
+ end
28
+
29
+ def name
30
+ end
31
+
32
+ def description
33
+ end
34
+
35
+ def visible?(context: {})
36
+ true
37
+ end
38
+
39
+ def call(context: {}, **args)
40
+ raise NotImplementedError, "#{self.class.name}#call must be implemented"
41
+ end
42
+
43
+ def validate_arguments(args)
44
+ return true unless self.class.schema
45
+
46
+ JSON::Validator.validate!(self.class.schema, args)
47
+ rescue JSON::Schema::ValidationError => e
48
+ {error: e.message}
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,3 +1,3 @@
1
1
  module ActiveMcp
2
- VERSION = "0.6.0"
2
+ VERSION = "0.7.0"
3
3
  end
data/lib/active_mcp.rb CHANGED
@@ -3,11 +3,13 @@
3
3
  require "jbuilder"
4
4
  require_relative "active_mcp/version"
5
5
  require_relative "active_mcp/configuration"
6
- require_relative "active_mcp/tool"
6
+ require_relative "active_mcp/schema/base"
7
+ require_relative "active_mcp/tool/base"
7
8
  require_relative "active_mcp/server"
8
9
 
9
10
  if defined? ::Rails
10
11
  require_relative "active_mcp/engine"
12
+ require_relative "active_mcp/controller/base"
11
13
  end
12
14
 
13
15
  module ActiveMcp
@@ -2,16 +2,12 @@ module ActiveMcp
2
2
  module Generators
3
3
  class InstallGenerator < Rails::Generators::Base
4
4
  source_root File.expand_path('templates', __dir__)
5
-
6
- desc "Creates an Active MCP initializer and mounts the engine in your routes"
7
-
5
+
6
+ desc "Creates an Active MCP initializer"
7
+
8
8
  def create_initializer_file
9
9
  template "initializer.rb", "config/initializers/active_mcp.rb"
10
10
  end
11
-
12
- def update_routes
13
- route "mount ActiveMcp::Engine, at: '/mcp'"
14
- end
15
11
  end
16
12
  end
17
13
  end
@@ -4,7 +4,7 @@ module ActiveMcp
4
4
  source_root File.expand_path("templates", __dir__)
5
5
 
6
6
  def create_resource_file
7
- template "resource.rb.erb", File.join("app/resources", "#{file_name}_resource.rb")
7
+ template "resource.rb.erb", File.join("app/mcp/resources", "#{file_name}_resource.rb")
8
8
  end
9
9
 
10
10
  private
@@ -1,12 +1,4 @@
1
1
  class <%= class_name %>
2
- def initialize(auth_info:)
3
- @auth_info = auth_info
4
-
5
- # Authentication information can be accessed via @auth_info parameter
6
- # @auth_info = { type: :bearer, token: "xxx", header: "Bearer xxx" }
7
- # or { type: :basic, token: "base64encoded", header: "Basic base64encoded" }
8
- end
9
-
10
2
  def name
11
3
  "<%= file_name %>"
12
4
  end
@@ -25,16 +17,16 @@ class <%= class_name %>
25
17
 
26
18
  # Uncomment and modify this method to implement authorization control
27
19
  # This controls who can see and use this tool
28
- # def visible?
20
+ # def visible?(context: {})
29
21
  # # Example: require authentication
30
- # # return false unless @auth_info
22
+ # # return false unless context
31
23
  #
32
24
  # # Example: require a specific authentication type
33
- # # return false unless @auth_info[:type] == :bearer
25
+ # # return false unless context[:auth_info][:type] == :bearer
34
26
  #
35
27
  # # Example: check for admin permissions
36
28
  # # admin_tokens = ["admin-token"]
37
- # # return admin_tokens.include?(@auth_info[:token])
29
+ # # return admin_tokens.include?(context[:auth_info][:token])
38
30
  #
39
31
  # # Default: allow all access
40
32
  # true
@@ -1,5 +1,11 @@
1
- class <%= class_name %> < ActiveMcp::Tool
2
- description "<%= file_name.humanize %>"
1
+ class <%= class_name %> < ActiveMcp::Tool::Base
2
+ def name
3
+ "<%= file_name.humanize %>"
4
+ end
5
+
6
+ def description
7
+ "<%= file_name.humanize %>"
8
+ end
3
9
 
4
10
  argument :param1, :string, required: true, description: "First parameter description"
5
11
  argument :param2, :string, required: false, description: "Second parameter description"
@@ -7,25 +13,25 @@ class <%= class_name %> < ActiveMcp::Tool
7
13
 
8
14
  # Uncomment and modify this method to implement authorization control
9
15
  # This controls who can see and use this tool
10
- # def self.visible?(auth_info)
16
+ # def visible?(context: {})
11
17
  # # Example: require authentication
12
- # # return false unless auth_info
18
+ # # return false unless context
13
19
  #
14
20
  # # Example: require a specific authentication type
15
- # # return false unless auth_info[:type] == :bearer
21
+ # # return false unless context[:auth_info][:type] == :bearer
16
22
  #
17
23
  # # Example: check for admin permissions
18
24
  # # admin_tokens = ["admin-token"]
19
- # # return admin_tokens.include?(auth_info[:token])
25
+ # # return admin_tokens.include?(context[:auth_info][:token])
20
26
  #
21
27
  # # Default: allow all access
22
28
  # true
23
29
  # end
24
30
 
25
- def call(param1:, param2: nil, auth_info: nil, **args)
31
+ def call(param1:, param2: nil, context: {})
26
32
  # Authentication information can be accessed via _auth_info parameter
27
- # auth_info = { type: :bearer, token: "xxx", header: "Bearer xxx" }
28
- # or { type: :basic, token: "base64encoded", header: "Basic base64encoded" }
33
+ # context = { auth_info: { type: :bearer, token: "xxx", header: "Bearer xxx" } }
34
+ # or { auth_info: { type: :basic, token: "base64encoded", header: "Basic base64encoded" } }
29
35
 
30
36
  # Implement tool logic here
31
37
 
@@ -4,7 +4,7 @@ module ActiveMcp
4
4
  source_root File.expand_path("templates", __dir__)
5
5
 
6
6
  def create_tool_file
7
- template "tool.rb.erb", File.join("app/tools", "#{file_name}_tool.rb")
7
+ template "tool.rb.erb", File.join("app/mcp/tools", "#{file_name}_tool.rb")
8
8
  end
9
9
 
10
10
  private
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Moeki Kawakami
@@ -68,6 +68,7 @@ files:
68
68
  - README.md
69
69
  - Rakefile
70
70
  - app/controllers/active_mcp/base_controller.rb
71
+ - app/controllers/concerns/active_mcp/authenticatable.rb
71
72
  - app/controllers/concerns/active_mcp/request_handlable.rb
72
73
  - app/controllers/concerns/active_mcp/resource_readable.rb
73
74
  - app/controllers/concerns/active_mcp/tool_executable.rb
@@ -83,14 +84,16 @@ files:
83
84
  - config/routes.rb
84
85
  - lib/active_mcp.rb
85
86
  - lib/active_mcp/configuration.rb
87
+ - lib/active_mcp/controller/base.rb
86
88
  - lib/active_mcp/engine.rb
89
+ - lib/active_mcp/schema/base.rb
87
90
  - lib/active_mcp/server.rb
88
91
  - lib/active_mcp/server/error_codes.rb
89
92
  - lib/active_mcp/server/fetcher.rb
90
93
  - lib/active_mcp/server/method.rb
91
94
  - lib/active_mcp/server/protocol_handler.rb
92
95
  - lib/active_mcp/server/stdio_connection.rb
93
- - lib/active_mcp/tool.rb
96
+ - lib/active_mcp/tool/base.rb
94
97
  - lib/active_mcp/version.rb
95
98
  - lib/generators/active_mcp/install/install_generator.rb
96
99
  - lib/generators/active_mcp/install/templates/initializer.rb
@@ -1,82 +0,0 @@
1
- require "json-schema"
2
-
3
- module ActiveMcp
4
- class Tool
5
- class << self
6
- attr_reader :desc, :schema
7
-
8
- def tool_name
9
- name ? name.underscore.sub(/_tool$/, "") : ""
10
- end
11
-
12
- def description(value)
13
- @desc = value
14
- end
15
-
16
- def property(name, type, required: false, description: nil)
17
- @schema ||= default_schema
18
-
19
- @schema["properties"][name.to_s] = {"type" => type.to_s}
20
- @schema["properties"][name.to_s]["description"] = description if description
21
- @schema["required"] << name.to_s if required
22
- end
23
-
24
- def argument(...)
25
- property(...)
26
- end
27
-
28
- def registered_tools
29
- @registered_tools ||= []
30
- end
31
-
32
- attr_writer :registered_tools
33
-
34
- def inherited(subclass)
35
- registered_tools << subclass
36
- end
37
-
38
- def visible?(auth_info)
39
- if respond_to?(:authorized?)
40
- authorized?(auth_info)
41
- else
42
- true
43
- end
44
- end
45
-
46
- def authorized_tools(auth_info = nil)
47
- registered_tools.select do |tool_class|
48
- tool_class.visible?(auth_info)
49
- end.map do |tool_class|
50
- {
51
- name: tool_class.tool_name,
52
- description: tool_class.desc,
53
- inputSchema: tool_class.schema || default_schema
54
- }
55
- end
56
- end
57
-
58
- def default_schema
59
- {
60
- "type" => "object",
61
- "properties" => {},
62
- "required" => []
63
- }
64
- end
65
- end
66
-
67
- def initialize
68
- end
69
-
70
- def call(**args)
71
- raise NotImplementedError, "#{self.class.name}#call must be implemented"
72
- end
73
-
74
- def validate_arguments(args)
75
- return true unless self.class.schema
76
-
77
- JSON::Validator.validate!(self.class.schema, args)
78
- rescue JSON::Schema::ValidationError => e
79
- {error: e.message}
80
- end
81
- end
82
- end