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 +4 -4
- data/README.md +87 -86
- data/app/controllers/active_mcp/base_controller.rb +27 -3
- data/app/controllers/concerns/active_mcp/authenticatable.rb +29 -0
- data/app/controllers/concerns/active_mcp/request_handlable.rb +12 -46
- data/app/controllers/concerns/active_mcp/resource_readable.rb +3 -11
- data/app/controllers/concerns/active_mcp/tool_executable.rb +6 -9
- data/app/views/active_mcp/tools_list.json.jbuilder +14 -2
- data/lib/active_mcp/controller/base.rb +7 -0
- data/lib/active_mcp/engine.rb +11 -14
- data/lib/active_mcp/schema/base.rb +46 -0
- data/lib/active_mcp/tool/base.rb +52 -0
- data/lib/active_mcp/version.rb +1 -1
- data/lib/active_mcp.rb +3 -1
- data/lib/generators/active_mcp/install/install_generator.rb +3 -7
- data/lib/generators/active_mcp/resource/resource_generator.rb +1 -1
- data/lib/generators/active_mcp/resource/templates/resource.rb.erb +4 -12
- data/lib/generators/active_mcp/tool/templates/tool.rb.erb +15 -9
- data/lib/generators/active_mcp/tool/tool_generator.rb +1 -1
- metadata +5 -2
- data/lib/active_mcp/tool.rb +0 -82
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c9367d905ac3b7dfa87f27063c41f24f1ed93de9d88a5fc009010ed35bfafa85
|
4
|
+
data.tar.gz: 7b1ed7d1f79ad9487c92a4e0595727cc60125c3584ae7029be8d3e86f9c2537b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
-
|
89
|
+
```bash
|
90
|
+
$ rails generate active_mcp:tool create_note
|
91
|
+
```
|
97
92
|
|
98
|
-
|
93
|
+
```ruby
|
94
|
+
class CreateNoteTool < ActiveMcp::Tool::Base
|
95
|
+
def name
|
96
|
+
"Create Note"
|
97
|
+
end
|
99
98
|
|
100
|
-
|
99
|
+
def description
|
100
|
+
"Create Note"
|
101
|
+
end
|
101
102
|
|
102
|
-
|
103
|
+
argument :title, :string, required: true
|
104
|
+
argument :content, :string, required: true
|
103
105
|
|
104
|
-
|
105
|
-
|
106
|
-
mount ActiveMcp::Engine, at: "/mcp"
|
106
|
+
def call(title:, content:, context:)
|
107
|
+
note = Note.create(title: title, content: content)
|
107
108
|
|
108
|
-
|
109
|
+
"Created note with ID: #{note.id}"
|
110
|
+
end
|
109
111
|
end
|
110
112
|
```
|
111
113
|
|
112
|
-
|
114
|
+
3. Create schema for your application:
|
113
115
|
|
114
116
|
```ruby
|
115
|
-
class
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
argument :content, :string, required: true
|
117
|
+
class MySchema < ActiveMcp::Schema::Base
|
118
|
+
tool CreateNoteTool.new
|
119
|
+
end
|
120
|
+
```
|
120
121
|
|
121
|
-
|
122
|
-
note = Note.create(title: title, content: content)
|
122
|
+
4. Create controller ans set up routing:
|
123
123
|
|
124
|
-
|
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
|
-
|
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
|
-
|
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
|
260
|
-
return false unless
|
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
|
-
|
287
|
+
context[:auth_info] == "admin-token" || User.find_by_token(context[:auth_info])&.admin?
|
265
288
|
end
|
266
289
|
|
267
|
-
def call(command:,
|
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:,
|
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
|
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
|
355
|
-
|
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
|
-
"
|
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
|
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
|
-
|
446
|
-
|
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
|
-
|
473
|
-
|
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:,
|
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 =
|
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 =
|
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:,
|
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 =
|
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
|
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 =
|
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:,
|
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 =
|
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 =
|
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
|
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
|
95
|
-
|
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
|
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 =
|
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:,
|
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
|
-
|
29
|
-
tc.
|
28
|
+
tool = schema.tools.find do |tc|
|
29
|
+
tc.name == tool_name
|
30
30
|
end
|
31
31
|
|
32
|
-
unless
|
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
|
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
|
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
|
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
|
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
|
data/lib/active_mcp/engine.rb
CHANGED
@@ -2,20 +2,17 @@ module ActiveMcp
|
|
2
2
|
class Engine < ::Rails::Engine
|
3
3
|
isolate_namespace ActiveMcp
|
4
4
|
|
5
|
-
initializer "active_mcp.
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
data/lib/active_mcp/version.rb
CHANGED
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/
|
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
|
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
|
22
|
+
# # return false unless context
|
31
23
|
#
|
32
24
|
# # Example: require a specific authentication type
|
33
|
-
# # return false unless
|
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?(
|
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
|
-
|
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
|
16
|
+
# def visible?(context: {})
|
11
17
|
# # Example: require authentication
|
12
|
-
# # return false unless
|
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,
|
31
|
+
def call(param1:, param2: nil, context: {})
|
26
32
|
# Authentication information can be accessed via _auth_info parameter
|
27
|
-
#
|
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
|
|
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.
|
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
|
data/lib/active_mcp/tool.rb
DELETED
@@ -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
|