active_mcp 0.6.0 → 0.8.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: 664064f2b6638585937706546194313e1b9beddcec7952bde94b34b39e39f851
4
+ data.tar.gz: d97d6e74d9a1235fa99f7362e0c38102069705b7c6ccb8f45d292ed6f45f08f3
5
5
  SHA512:
6
- metadata.gz: b20d1f9baf3a7b48cab071135ca15fc182fb3d1cdc3570bcd0b0fb337906dd9e5fa0dc1f646823b281a9600911e3ef9ef7d5cf67eaab80a19a2cad27304f04bb
7
- data.tar.gz: 5036f8e8b6056255e81b693cb5fc436d78bf04b8f0546c186110eb4774e6b7b7fe36d4e755c30e84884cadb75a4455af469fbe27d869b502a5ee5d0c79c9d167
6
+ metadata.gz: bd746883f2345681dafaf3ba99d1925c11eb8bf0a2ee2dc25fd17d4f1c3906b371f961a64298edc998cc58b6c1424a55efdfbbd0195c5e6e7a21dd160376821d
7
+ data.tar.gz: dd3ccaa91bd1f807d6f2ea7fc9dc03dc363b30b4bcad3335448f34d978c70f98d63b0ac4641455e9600639d2b88c94ca6c47cd945c8a82edecc5577b4ecbe65c
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"
@@ -316,8 +339,8 @@ MCP Resources allow you to share data and files with AI assistants. Resources ha
316
339
  Resources are Ruby classes `**Resource`:
317
340
 
318
341
  ```ruby
319
- class UserResource
320
- def initialize(id:, auth_info: nil)
342
+ class UserResource < ActiveMcp::Resource::Base
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
  ```
@@ -378,19 +401,21 @@ end
378
401
  2. **Binary Content** - Use the `blob` method to return binary files:
379
402
 
380
403
  ```ruby
381
- class ImageResource
404
+ class ImageResource < ActiveMcp::Resource::Base
405
+ class << self
406
+ def mime_type
407
+ "image/png"
408
+ end
409
+ end
410
+
382
411
  def name
383
- "image"
412
+ "Profile Image"
384
413
  end
385
414
 
386
415
  def uri
387
416
  "data://localhost/image"
388
417
  end
389
418
 
390
- def mime_type
391
- "image/png"
392
- end
393
-
394
419
  def description
395
420
  "Profile image"
396
421
  end
@@ -405,12 +430,12 @@ end
405
430
  Resources can be protected using the same authorization mechanism as tools:
406
431
 
407
432
  ```ruby
408
- def visible?
409
- return false unless auth_info
410
- return false unless auth_info[:type] == :bearer
433
+ def visible?(context: {})
434
+ return false unless context
435
+ return false unless context[:auth_info][:type] == :bearer
411
436
 
412
437
  # Check if the token belongs to an admin
413
- User.find_by_token(auth_info[:token])&.admin?
438
+ User.find_by_token(context[:auth_info][:token])&.admin?
414
439
  end
415
440
  ```
416
441
 
@@ -423,54 +448,60 @@ MCP Resource Teamplates allow you to define template of resources.
423
448
  Resources are Ruby classes `**ResourceTemplates`:
424
449
 
425
450
  ```ruby
426
- class UserResourceTemplate
427
- def name
428
- "Users"
429
- end
451
+ class UserResource < ActiveMcp::Resource::Base
452
+ class << self
453
+ def name
454
+ "Users"
455
+ end
430
456
 
431
- def uri_template
432
- "data://localhost/users/{id}"
433
- end
457
+ def uri_template
458
+ "data://localhost/users/{id}"
459
+ end
434
460
 
435
- def mime_type
436
- "application/json"
437
- end
461
+ def mime_type
462
+ "application/json"
463
+ end
438
464
 
439
- def description
440
- "This is a test."
465
+ def description
466
+ "This is a test."
467
+ end
468
+
469
+ def visible?(context:)
470
+ # Your logic...
471
+ end
441
472
  end
442
- end
443
- ```
444
473
 
445
- ```ruby
446
- class McpController < ActiveMcp::BaseController
447
- private
474
+ argument :id, ->(value) do
475
+ User.all.pluck(:id).filter { _1.match(value) }
476
+ end
448
477
 
449
- def resource_templates_list
450
- [
451
- UserResourceTemplate.new
452
- ]
478
+ def initialize(id:)
479
+ @user = User.find(id)
453
480
  end
454
- end
455
- ```
456
481
 
457
- ## ⚙️ Advanced Configuration
482
+ def name
483
+ @user.name
484
+ end
458
485
 
459
- ### Custom Controller
486
+ def description
487
+ @user.profile
488
+ end
460
489
 
461
- Create a custom controller for advanced needs:
490
+ def uri
491
+ "data://localhost/users/#{@user.name}"
492
+ end
462
493
 
463
- ```ruby
464
- class CustomMcpController < ActiveMcp::BaseController
465
- # Custom MCP handling logic
494
+ def text
495
+ { name: @user.name }
496
+ end
466
497
  end
467
498
  ```
468
499
 
469
- Update routes:
470
-
471
500
  ```ruby
472
- Rails.application.routes.draw do
473
- post "/mcp", to: "custom_mcp#index"
501
+ class MySchema < ActiveMcp::Schema::Base
502
+ User.all.each do |user|
503
+ resource UserResource.new(id: user.id)
504
+ end
474
505
  end
475
506
  ```
476
507
 
@@ -482,12 +513,12 @@ Create dedicated tool classes for each model or operation instead of generic too
482
513
 
483
514
  ```ruby
484
515
  # ✅ GOOD: Specific tool for a single purpose
485
- class SearchUsersTool < ActiveMcp::Tool
516
+ class SearchUsersTool < ActiveMcp::Tool::Base
486
517
  # ...specific implementation
487
518
  end
488
519
 
489
520
  # ❌ BAD: Generic tool that dynamically loads models
490
- class GenericSearchTool < ActiveMcp::Tool
521
+ class GenericSearchTool < ActiveMcp::Tool::Base
491
522
  # Avoid this pattern - security and maintainability issues
492
523
  end
493
524
  ```
@@ -497,7 +528,7 @@ end
497
528
  Always validate and sanitize inputs in your tool implementations:
498
529
 
499
530
  ```ruby
500
- def call(user_id:, **args)
531
+ def call(user_id:, **args, context: {})
501
532
  # Validate input
502
533
  unless user_id.is_a?(Integer) || user_id.to_s.match?(/^\d+$/)
503
534
  raise "Invalid user ID format"
@@ -514,7 +545,7 @@ end
514
545
  Return structured responses that are easy for AI to parse:
515
546
 
516
547
  ```ruby
517
- def call(query:, **args)
548
+ def call(query:, context: {})
518
549
  results = User.search(query)
519
550
 
520
551
  {
@@ -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,25 +21,30 @@ 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
43
+ when Method::COMPLETION_COMPLETE
44
+ type = params.dig(:params, :ref, :type)
45
+ @completion = ActiveMcp::Completion.new.complete(params: params[:params], context:, refs: type === "ref/resource" ? schema.resource_templates : [])
46
+ @format = :jsonrpc
47
+ render "active_mcp/completion_complete", formats: :json
58
48
  else
59
49
  @format = :jsonrpc
60
50
  render 'active_mcp/no_method', formats: :json
@@ -62,54 +52,40 @@ module ActiveMcp
62
52
  end
63
53
 
64
54
  def handle_mcp_server_request
65
- @auth_info = auth_info
66
-
67
55
  case params[:method]
68
56
  when Method::RESOURCES_LIST
69
- @resources = resources_list
57
+ @resources = schema.resources
70
58
  @format = :json
71
59
  render 'active_mcp/resources_list', formats: :json
72
60
  when Method::RESOURCES_READ
73
- @resource = read_resource(params:, auth_info:)
61
+ @resource = read_resource(params:, context:)
74
62
  @format = :json
75
63
  render 'active_mcp/resources_read', formats: :json
76
64
  when Method::RESOURCES_TEMPLATES_LIST
77
- @resource_templates = resource_templates_list
65
+ @resource_templates = schema.resource_templates
78
66
  @format = :json
79
67
  render 'active_mcp/resource_templates_list', formats: :json
80
68
  when Method::TOOLS_LIST
81
- @tools = ActiveMcp::Tool.authorized_tools(auth_info)
69
+ @tools = schema.tools
82
70
  @format = :json
83
71
  render 'active_mcp/tools_list', formats: :json
84
72
  when Method::TOOLS_CALL
85
- @tool_result = execute_tool(params: params, auth_info: auth_info)
73
+ @tool_result = execute_tool(params:, context:)
86
74
  @format = :json
87
75
  render 'active_mcp/tools_call', formats: :json
76
+ when Method::COMPLETION_COMPLETE
77
+ type = params.dig(:params, :ref, :type)
78
+ @completion = ActiveMcp::Completion.new.complete(params: params[:params], context:, refs: type == "ref/resource" ? schema.resource_templates : [])
79
+ @format = :json
80
+ render "active_mcp/completion_complete", formats: :json
88
81
  else
89
82
  @format = :json
90
83
  render 'active_mcp/no_method', formats: :json
91
84
  end
92
85
  end
93
86
 
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
87
+ def context
88
+ @context ||= {}
113
89
  end
114
90
  end
115
91
  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: []
@@ -50,7 +42,7 @@ module ActiveMcp
50
42
  contents: [
51
43
  {
52
44
  uri:,
53
- mimeType: resource.mime_type,
45
+ mimeType: resource.class.mime_type,
54
46
  text: formatted(content)
55
47
  }
56
48
  ]
@@ -60,7 +52,7 @@ module ActiveMcp
60
52
  contents: [
61
53
  {
62
54
  uri:,
63
- mimeType: resource.mime_type,
55
+ mimeType: resource.class.mime_type,
64
56
  blob: Base64.strict_encode64(content)
65
57
  }
66
58
  ]
@@ -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
  }
@@ -0,0 +1,16 @@
1
+ json.jsonrpc ActiveMcp::JSON_RPC_VERSION if @format == :jsonrpc
2
+ json.id @id if @format == :jsonrpc && @id.present?
3
+
4
+ if @format == :jsonrpc
5
+ json.result do
6
+ json.completion do
7
+ json.values @completion[:values]
8
+ json.total @completion[:total]
9
+ end
10
+ end
11
+ else
12
+ json.result do
13
+ json.values @completion[:values]
14
+ json.total @completion[:total]
15
+ end
16
+ end
@@ -7,7 +7,7 @@ if @format == :jsonrpc
7
7
  json.array!(@resources) do |resource|
8
8
  json.name resource.name
9
9
  json.uri resource.uri
10
- json.mimeType resource.mime_type
10
+ json.mimeType resource.class.mime_type
11
11
  json.description resource.description
12
12
  end
13
13
  end
@@ -17,7 +17,7 @@ else
17
17
  json.array!(@resources) do |resource|
18
18
  json.name resource.name
19
19
  json.uri resource.uri
20
- json.mimeType resource.mime_type
20
+ json.mimeType resource.class.mime_type
21
21
  json.description resource.description
22
22
  end
23
23
  end
@@ -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,18 @@
1
+ module ActiveMcp
2
+ class Completion
3
+ def complete(params: {}, context: {}, refs: [])
4
+ ref_name = params.dig(:ref, :name)
5
+ uri_template = params.dig(:ref, :uri)
6
+ arg_name = params.dig(:argument, :name)
7
+ value = params.dig(:argument, :value)
8
+
9
+ if uri_template
10
+ resource_class = refs.find { _1.uri_template == uri_template }
11
+ values = resource_class.arguments[arg_name.to_sym].call(value)
12
+ { values:, total: values.length }
13
+ elsif ref_name
14
+ { values: [], total: 0 }
15
+ end
16
+ end
17
+ end
18
+ 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,29 @@
1
+ require "json-schema"
2
+
3
+ module ActiveMcp
4
+ module Resource
5
+ class Base
6
+ class << self
7
+ attr_reader :schema, :arguments
8
+
9
+ def argument(name, complete)
10
+ @arguments = {}
11
+ @arguments[name] = complete
12
+ end
13
+ end
14
+
15
+ def initialize
16
+ end
17
+
18
+ def name
19
+ end
20
+
21
+ def description
22
+ end
23
+
24
+ def visible?(context: {})
25
+ true
26
+ end
27
+ end
28
+ end
29
+ 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
+
11
+ if klass.class.respond_to?(:uri_template)
12
+ @resource_templates ||= []
13
+ @resource_templates << klass.class unless klass.class.in?(@resource_templates)
14
+ end
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 |template|
35
+ !template.respond_to?(:visible?) || template.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
@@ -11,5 +11,6 @@ module ActiveMcp
11
11
  RESOURCES_LIST = "resources/list"
12
12
  RESOURCES_READ = "resources/read"
13
13
  RESOURCES_TEMPLATES_LIST = "resources/templates/list"
14
+ COMPLETION_COMPLETE = "completion/complete"
14
15
  end
15
16
  end
@@ -58,6 +58,8 @@ module ActiveMcp
58
58
  handle_call_tool(request)
59
59
  when Method::RESOURCES_READ
60
60
  handle_read_resource(request)
61
+ when Method::COMPLETION_COMPLETE
62
+ handle_complete(request)
61
63
  else
62
64
  error_response(request[:id], ErrorCode::METHOD_NOT_FOUND, "Unknown method: #{request[:method]}")
63
65
  end
@@ -194,6 +196,21 @@ module ActiveMcp
194
196
  end
195
197
  end
196
198
 
199
+ def handle_complete(request)
200
+ begin
201
+ result = @server.fetch(
202
+ params: {
203
+ method: Method::COMPLETION_COMPLETE,
204
+ params: request[:params],
205
+ }
206
+ )
207
+ success_response(request[:id], { completion: result[:result] })
208
+ rescue => e
209
+ Server.("Error reading resource #{uri}", e)
210
+ error_response(request[:id], ErrorCode::INTERNAL_ERROR, "An error occurred while reading the resource")
211
+ end
212
+ end
213
+
197
214
  def success_response(id, result)
198
215
  {
199
216
  jsonrpc: JSON_RPC_VERSION,
@@ -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.8.0"
3
3
  end
data/lib/active_mcp.rb CHANGED
@@ -3,11 +3,15 @@
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"
8
+ require_relative "active_mcp/resource/base"
7
9
  require_relative "active_mcp/server"
10
+ require_relative "active_mcp/completion"
8
11
 
9
12
  if defined? ::Rails
10
13
  require_relative "active_mcp/engine"
14
+ require_relative "active_mcp/controller/base"
11
15
  end
12
16
 
13
17
  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,13 +1,13 @@
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.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Moeki Kawakami
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-07 00:00:00.000000000 Z
10
+ date: 2025-04-08 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -68,10 +68,12 @@ 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
74
75
  - app/views/active_mcp/cancelled.json.jbuilder
76
+ - app/views/active_mcp/completion_complete.json.jbuilder
75
77
  - app/views/active_mcp/initialize.json.jbuilder
76
78
  - app/views/active_mcp/initialized.json.jbuilder
77
79
  - app/views/active_mcp/no_method.json.jbuilder
@@ -82,15 +84,19 @@ files:
82
84
  - app/views/active_mcp/tools_list.json.jbuilder
83
85
  - config/routes.rb
84
86
  - lib/active_mcp.rb
87
+ - lib/active_mcp/completion.rb
85
88
  - lib/active_mcp/configuration.rb
89
+ - lib/active_mcp/controller/base.rb
86
90
  - lib/active_mcp/engine.rb
91
+ - lib/active_mcp/resource/base.rb
92
+ - lib/active_mcp/schema/base.rb
87
93
  - lib/active_mcp/server.rb
88
94
  - lib/active_mcp/server/error_codes.rb
89
95
  - lib/active_mcp/server/fetcher.rb
90
96
  - lib/active_mcp/server/method.rb
91
97
  - lib/active_mcp/server/protocol_handler.rb
92
98
  - lib/active_mcp/server/stdio_connection.rb
93
- - lib/active_mcp/tool.rb
99
+ - lib/active_mcp/tool/base.rb
94
100
  - lib/active_mcp/version.rb
95
101
  - lib/generators/active_mcp/install/install_generator.rb
96
102
  - 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