active_mcp 0.7.0 → 0.9.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 +117 -22
- data/app/controllers/concerns/active_mcp/request_handlable.rb +26 -0
- data/app/controllers/concerns/active_mcp/resource_readable.rb +4 -15
- data/app/controllers/concerns/active_mcp/tool_executable.rb +1 -1
- data/app/views/active_mcp/completion_complete.json.jbuilder +16 -0
- data/app/views/active_mcp/prompts_get.json.jbuilder +12 -0
- data/app/views/active_mcp/prompts_list.json.jbuilder +22 -0
- data/app/views/active_mcp/resource_templates_list.json.jbuilder +2 -2
- data/app/views/active_mcp/resources_list.json.jbuilder +4 -4
- data/app/views/active_mcp/tools_list.json.jbuilder +2 -2
- data/lib/active_mcp/completion.rb +20 -0
- data/lib/active_mcp/engine.rb +1 -0
- data/lib/active_mcp/message/audio.rb +22 -0
- data/lib/active_mcp/message/image.rb +22 -0
- data/lib/active_mcp/message/resource.rb +24 -0
- data/lib/active_mcp/message/text.rb +20 -0
- data/lib/active_mcp/prompt/base.rb +37 -0
- data/lib/active_mcp/resource/base.rb +49 -0
- data/lib/active_mcp/schema/base.rb +20 -9
- data/lib/active_mcp/server/method.rb +3 -0
- data/lib/active_mcp/server/protocol_handler.rb +56 -18
- data/lib/active_mcp/tool/base.rb +1 -1
- data/lib/active_mcp/version.rb +1 -1
- data/lib/active_mcp.rb +7 -0
- data/lib/generators/active_mcp/resource/templates/resource.rb.erb +1 -1
- data/lib/generators/active_mcp/tool/templates/tool.rb.erb +1 -1
- metadata +12 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5e4d7ce1740274e7a16c0f4b42f0d1285997c068ed416e166313076f7629ddc1
|
4
|
+
data.tar.gz: 3ff81f15c762fd1e8393afc2f29c1ef462bc4a6b3dcf544b826b01a5c6e05e3f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86bf41931c683bd3a462521fd8b95d379aeb901dc1ae4087a913edd7ca302bf5d018a37b49e4f3da043cc1e1133752e352b407804fe9dbd3d5c4fc2a3720071c
|
7
|
+
data.tar.gz: 6d91c11ebf3b8f8485d9a0acc13f75b64965c68fd5443f512d818e57f755989c5d744c5d76b1650f24bcac0d681643de881ef18a15997cb46cc9f37269a36c79
|
data/README.md
CHANGED
@@ -36,6 +36,8 @@ A Ruby on Rails engine for the [Model Context Protocol (MCP)](https://modelconte
|
|
36
36
|
- [Resource Types](#resource-types)
|
37
37
|
- [📦 MCP Resource Templates](#-mcp-resource-templates)
|
38
38
|
- [Creating Resource Templates](#creating-resource-templates)
|
39
|
+
- [💬 MCP Prompts](#-mcp-prompts)
|
40
|
+
- [Creating Prompt](#creating-prompt)
|
39
41
|
- [💡 Best Practices](#-best-practices)
|
40
42
|
- [1. Create Specific Tool Classes](#1-create-specific-tool-classes)
|
41
43
|
- [2. Validate and Sanitize Inputs](#2-validate-and-sanitize-inputs)
|
@@ -92,7 +94,7 @@ $ rails generate active_mcp:tool create_note
|
|
92
94
|
|
93
95
|
```ruby
|
94
96
|
class CreateNoteTool < ActiveMcp::Tool::Base
|
95
|
-
def
|
97
|
+
def tool_name
|
96
98
|
"Create Note"
|
97
99
|
end
|
98
100
|
|
@@ -213,7 +215,7 @@ MCP tools are Ruby classes that inherit from `ActiveMcp::Tool::Base` and define
|
|
213
215
|
|
214
216
|
```ruby
|
215
217
|
class SearchUsersTool < ActiveMcp::Tool::Base
|
216
|
-
def
|
218
|
+
def tool_name
|
217
219
|
"Search Users"
|
218
220
|
end
|
219
221
|
|
@@ -268,7 +270,7 @@ Control access to tools by overriding the `visible?` class method:
|
|
268
270
|
|
269
271
|
```ruby
|
270
272
|
class AdminOnlyTool < ActiveMcp::Tool::Base
|
271
|
-
def
|
273
|
+
def tool_name
|
272
274
|
"Admin-only tool"
|
273
275
|
end
|
274
276
|
|
@@ -339,13 +341,13 @@ MCP Resources allow you to share data and files with AI assistants. Resources ha
|
|
339
341
|
Resources are Ruby classes `**Resource`:
|
340
342
|
|
341
343
|
```ruby
|
342
|
-
class UserResource
|
344
|
+
class UserResource < ActiveMcp::Resource::Base
|
343
345
|
def initialize(id:)
|
344
346
|
@user = User.find(id)
|
345
347
|
@auth_info = auth_info
|
346
348
|
end
|
347
349
|
|
348
|
-
def
|
350
|
+
def resource_name
|
349
351
|
@user.name
|
350
352
|
end
|
351
353
|
|
@@ -401,8 +403,14 @@ end
|
|
401
403
|
2. **Binary Content** - Use the `blob` method to return binary files:
|
402
404
|
|
403
405
|
```ruby
|
404
|
-
class ImageResource
|
405
|
-
|
406
|
+
class ImageResource < ActiveMcp::Resource::Base
|
407
|
+
class << self
|
408
|
+
def mime_type
|
409
|
+
"image/png"
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
def resource_name
|
406
414
|
"Profile Image"
|
407
415
|
end
|
408
416
|
|
@@ -410,10 +418,6 @@ class ImageResource
|
|
410
418
|
"data://localhost/image"
|
411
419
|
end
|
412
420
|
|
413
|
-
def mime_type
|
414
|
-
"image/png"
|
415
|
-
end
|
416
|
-
|
417
421
|
def description
|
418
422
|
"Profile image"
|
419
423
|
end
|
@@ -446,32 +450,123 @@ MCP Resource Teamplates allow you to define template of resources.
|
|
446
450
|
Resources are Ruby classes `**ResourceTemplates`:
|
447
451
|
|
448
452
|
```ruby
|
449
|
-
class
|
450
|
-
|
451
|
-
|
453
|
+
class UserResource < ActiveMcp::Resource::Base
|
454
|
+
class << self
|
455
|
+
def resource_template_name
|
456
|
+
"Users"
|
457
|
+
end
|
458
|
+
|
459
|
+
def uri_template
|
460
|
+
"data://localhost/users/{id}"
|
461
|
+
end
|
462
|
+
|
463
|
+
def mime_type
|
464
|
+
"application/json"
|
465
|
+
end
|
466
|
+
|
467
|
+
def description
|
468
|
+
"This is a test."
|
469
|
+
end
|
470
|
+
|
471
|
+
def visible?(context:)
|
472
|
+
# Your logic...
|
473
|
+
end
|
452
474
|
end
|
453
475
|
|
454
|
-
|
455
|
-
|
476
|
+
argument :id, ->(value) do
|
477
|
+
User.all.pluck(:id).filter { _1.match(value) }
|
456
478
|
end
|
457
479
|
|
458
|
-
def
|
459
|
-
|
480
|
+
def initialize(id:)
|
481
|
+
@user = User.find(id)
|
482
|
+
end
|
483
|
+
|
484
|
+
def resource_name
|
485
|
+
@user.name
|
460
486
|
end
|
461
487
|
|
462
488
|
def description
|
463
|
-
|
489
|
+
@user.profile
|
464
490
|
end
|
465
491
|
|
466
|
-
def
|
467
|
-
|
492
|
+
def uri
|
493
|
+
"data://localhost/users/#{@user.name}"
|
494
|
+
end
|
495
|
+
|
496
|
+
def text
|
497
|
+
{ name: @user.name }
|
498
|
+
end
|
499
|
+
end
|
500
|
+
```
|
501
|
+
|
502
|
+
```ruby
|
503
|
+
class MySchema < ActiveMcp::Schema::Base
|
504
|
+
User.all.each do |user|
|
505
|
+
resource UserResource.new(id: user.id)
|
506
|
+
end
|
507
|
+
end
|
508
|
+
```
|
509
|
+
|
510
|
+
## 💬 MCP Prompts
|
511
|
+
|
512
|
+
MCP Prompts allow you to define prompt set.
|
513
|
+
|
514
|
+
### Creating Prompt
|
515
|
+
|
516
|
+
Resources are Ruby classes `**Prompt`:
|
517
|
+
|
518
|
+
```ruby
|
519
|
+
class HelloPrompt < ActiveMcp::Prompt::Base
|
520
|
+
class << self
|
521
|
+
def prompt_name
|
522
|
+
"hello"
|
523
|
+
end
|
524
|
+
|
525
|
+
def description
|
526
|
+
"This is a test."
|
527
|
+
end
|
528
|
+
|
529
|
+
def visible?(context:)
|
530
|
+
# Your logic...
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
argument :name, ->(value) do
|
535
|
+
User.all.pluck(:name).filter { _1.match(value) }
|
536
|
+
end
|
537
|
+
|
538
|
+
def initialize(name:)
|
539
|
+
@name = name
|
540
|
+
end
|
541
|
+
|
542
|
+
def messages
|
543
|
+
[
|
544
|
+
ActiveMcp::Message::Text.new(
|
545
|
+
role: "user",
|
546
|
+
text: "Hello! #{@name}"
|
547
|
+
),
|
548
|
+
ActiveMcp::Message::Image.new(
|
549
|
+
role: "assistant",
|
550
|
+
data: File.read(file),
|
551
|
+
mime_type: "image/png"
|
552
|
+
),
|
553
|
+
ActiveMcp::Message::Audio.new(
|
554
|
+
role: "user",
|
555
|
+
data: File.read(file),
|
556
|
+
mime_type: "audio/mpeg"
|
557
|
+
),
|
558
|
+
ActiveMcp::Message::Resource.new(
|
559
|
+
role: "assistant",
|
560
|
+
resource: UserResource.new(name: @name)
|
561
|
+
)
|
562
|
+
]
|
468
563
|
end
|
469
564
|
end
|
470
565
|
```
|
471
566
|
|
472
567
|
```ruby
|
473
568
|
class MySchema < ActiveMcp::Schema::Base
|
474
|
-
|
569
|
+
prompt HelloPrompt
|
475
570
|
end
|
476
571
|
```
|
477
572
|
|
@@ -40,6 +40,19 @@ module ActiveMcp
|
|
40
40
|
@tool_result = execute_tool(params:, context:)
|
41
41
|
@format = :jsonrpc
|
42
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 : schema.prompts)
|
46
|
+
@format = :jsonrpc
|
47
|
+
render "active_mcp/completion_complete", formats: :json
|
48
|
+
when Method::PROMPTS_LIST
|
49
|
+
@prompts = schema.prompts
|
50
|
+
@format = :jsonrpc
|
51
|
+
render 'active_mcp/prompts_list', formats: :json
|
52
|
+
when Method::PROMPTS_GET
|
53
|
+
@prompt = schema.prompts.find { _1.prompt_name == params[:params][:name] }.new(**params[:params][:arguments].permit!.to_h.symbolize_keys)
|
54
|
+
@format = :jsonrpc
|
55
|
+
render 'active_mcp/prompts_get', formats: :json
|
43
56
|
else
|
44
57
|
@format = :jsonrpc
|
45
58
|
render 'active_mcp/no_method', formats: :json
|
@@ -68,6 +81,19 @@ module ActiveMcp
|
|
68
81
|
@tool_result = execute_tool(params:, context:)
|
69
82
|
@format = :json
|
70
83
|
render 'active_mcp/tools_call', formats: :json
|
84
|
+
when Method::COMPLETION_COMPLETE
|
85
|
+
type = params.dig(:params, :ref, :type)
|
86
|
+
@completion = ActiveMcp::Completion.new.complete(params: params[:params], context:, refs: type == "ref/resource" ? schema.resource_templates : schema.prompts)
|
87
|
+
@format = :json
|
88
|
+
render "active_mcp/completion_complete", formats: :json
|
89
|
+
when Method::PROMPTS_LIST
|
90
|
+
@prompts = schema.prompts
|
91
|
+
@format = :json
|
92
|
+
render 'active_mcp/prompts_list', formats: :json
|
93
|
+
when Method::PROMPTS_GET
|
94
|
+
@prompt = schema.prompts&.find { _1.prompt_name == params[:params][:name] }.new(**params[:params][:arguments].permit!.to_h.symbolize_keys)
|
95
|
+
@format = :json
|
96
|
+
render 'active_mcp/prompts_get', formats: :json
|
71
97
|
else
|
72
98
|
@format = :json
|
73
99
|
render 'active_mcp/no_method', formats: :json
|
@@ -37,13 +37,13 @@ module ActiveMcp
|
|
37
37
|
end
|
38
38
|
|
39
39
|
begin
|
40
|
-
if resource.respond_to?(:text) && content = resource.
|
40
|
+
if resource.respond_to?(:text) && content = resource.content
|
41
41
|
return {
|
42
42
|
contents: [
|
43
43
|
{
|
44
44
|
uri:,
|
45
|
-
mimeType: resource.mime_type,
|
46
|
-
text:
|
45
|
+
mimeType: resource.class.mime_type,
|
46
|
+
text: content
|
47
47
|
}
|
48
48
|
]
|
49
49
|
}
|
@@ -52,7 +52,7 @@ module ActiveMcp
|
|
52
52
|
contents: [
|
53
53
|
{
|
54
54
|
uri:,
|
55
|
-
mimeType: resource.mime_type,
|
55
|
+
mimeType: resource.class.mime_type,
|
56
56
|
blob: Base64.strict_encode64(content)
|
57
57
|
}
|
58
58
|
]
|
@@ -65,16 +65,5 @@ module ActiveMcp
|
|
65
65
|
}
|
66
66
|
end
|
67
67
|
end
|
68
|
-
|
69
|
-
def formatted(object)
|
70
|
-
case object
|
71
|
-
when String
|
72
|
-
object
|
73
|
-
when Hash
|
74
|
-
object.to_json
|
75
|
-
else
|
76
|
-
object.to_s
|
77
|
-
end
|
78
|
-
end
|
79
68
|
end
|
80
69
|
end
|
@@ -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
|
@@ -0,0 +1,12 @@
|
|
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.description @prompt.class.description
|
7
|
+
json.messages @prompt.messages.map(&:to_h)
|
8
|
+
end
|
9
|
+
else
|
10
|
+
json.description @prompt.class.description
|
11
|
+
json.messages @prompt.messages.map(&:to_h)
|
12
|
+
end
|
@@ -0,0 +1,22 @@
|
|
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.prompts do
|
7
|
+
json.array!(@prompts) do |prompt|
|
8
|
+
json.name prompt.prompt_name
|
9
|
+
json.description prompt.description
|
10
|
+
json.arguments prompt.arguments.map { _1.except(:complete) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
else
|
15
|
+
json.result do
|
16
|
+
json.array!(@prompts) do |prompt|
|
17
|
+
json.name prompt.prompt_name
|
18
|
+
json.description prompt.description
|
19
|
+
json.arguments prompt.arguments.map { _1.except(:complete) }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -5,7 +5,7 @@ if @format == :jsonrpc
|
|
5
5
|
json.result do
|
6
6
|
json.resourceTemplates do
|
7
7
|
json.array!(@resource_templates) do |resource|
|
8
|
-
json.name resource.
|
8
|
+
json.name resource.resource_template_name
|
9
9
|
json.uriTemplate resource.uri_template
|
10
10
|
json.mimeType resource.mime_type
|
11
11
|
json.description resource.description
|
@@ -15,7 +15,7 @@ if @format == :jsonrpc
|
|
15
15
|
else
|
16
16
|
json.result do
|
17
17
|
json.array!(@resource_templates) do |resource|
|
18
|
-
json.name resource.
|
18
|
+
json.name resource.resource_template_name
|
19
19
|
json.uriTemplate resource.uri_template
|
20
20
|
json.mimeType resource.mime_type
|
21
21
|
json.description resource.description
|
@@ -5,9 +5,9 @@ if @format == :jsonrpc
|
|
5
5
|
json.result do
|
6
6
|
json.resources do
|
7
7
|
json.array!(@resources) do |resource|
|
8
|
-
json.name resource.
|
8
|
+
json.name resource.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
|
@@ -15,9 +15,9 @@ if @format == :jsonrpc
|
|
15
15
|
else
|
16
16
|
json.result do
|
17
17
|
json.array!(@resources) do |resource|
|
18
|
-
json.name resource.
|
18
|
+
json.name resource.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
|
@@ -5,7 +5,7 @@ if @format == :jsonrpc
|
|
5
5
|
json.result do
|
6
6
|
json.tools do
|
7
7
|
json.array!(@tools) do |tool|
|
8
|
-
json.name tool.
|
8
|
+
json.name tool.tool_name
|
9
9
|
json.description tool.description
|
10
10
|
json.inputSchema tool.class.schema
|
11
11
|
end
|
@@ -14,7 +14,7 @@ if @format == :jsonrpc
|
|
14
14
|
else
|
15
15
|
json.result do
|
16
16
|
json.array!(@tools) do |tool|
|
17
|
-
json.name tool.
|
17
|
+
json.name tool.tool_name
|
18
18
|
json.description tool.description
|
19
19
|
json.inputSchema tool.class.schema
|
20
20
|
end
|
@@ -0,0 +1,20 @@
|
|
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
|
+
prompt_class = refs.find { _1.prompt_name == ref_name }
|
15
|
+
values = prompt_class.arguments.find { _1[:name] == arg_name.to_sym }[:complete].call(value)
|
16
|
+
{ values:, total: values.length }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/active_mcp/engine.rb
CHANGED
@@ -7,6 +7,7 @@ module ActiveMcp
|
|
7
7
|
Rails.root.join("app", "mcp", "tools"),
|
8
8
|
Rails.root.join("app", "mcp", "resources"),
|
9
9
|
Rails.root.join("app", "mcp", "resource_templates"),
|
10
|
+
Rails.root.join("app", "mcp", "prompts"),
|
10
11
|
Rails.root.join("app", "mcp", "schemas")
|
11
12
|
].each do |tools_path|
|
12
13
|
if Dir.exist?(tools_path)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ActiveMcp
|
2
|
+
module Message
|
3
|
+
class Audio
|
4
|
+
def initialize(role:, data:, mime_type:)
|
5
|
+
@role = role
|
6
|
+
@data = data
|
7
|
+
@mime_type = mime_type
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_h
|
11
|
+
{
|
12
|
+
role: @role,
|
13
|
+
content: {
|
14
|
+
type: "audio",
|
15
|
+
data: Base64.strict_encode64(@data),
|
16
|
+
mimeType: @mime_type
|
17
|
+
}
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ActiveMcp
|
2
|
+
module Message
|
3
|
+
class Image
|
4
|
+
def initialize(role:, data:, mime_type:)
|
5
|
+
@role = role
|
6
|
+
@data = data
|
7
|
+
@mime_type = mime_type
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_h
|
11
|
+
{
|
12
|
+
role: @role,
|
13
|
+
content: {
|
14
|
+
type: "image",
|
15
|
+
data: Base64.strict_encode64(@data),
|
16
|
+
mimeType: @mime_type
|
17
|
+
}
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ActiveMcp
|
2
|
+
module Message
|
3
|
+
class Resource
|
4
|
+
def initialize(role:, resource:)
|
5
|
+
@role = role
|
6
|
+
@resource = resource
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_h
|
10
|
+
{
|
11
|
+
role: @role,
|
12
|
+
content: {
|
13
|
+
type: "resource",
|
14
|
+
resource: {
|
15
|
+
uri: @resource.uri,
|
16
|
+
mimeType: @resource.class.mime_type,
|
17
|
+
text: @resource.content,
|
18
|
+
}
|
19
|
+
}
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ActiveMcp
|
2
|
+
module Prompt
|
3
|
+
class Base
|
4
|
+
class << self
|
5
|
+
attr_reader :arguments
|
6
|
+
|
7
|
+
def argument(name, required: false, description: nil, complete: ->(){})
|
8
|
+
@arguments ||= []
|
9
|
+
|
10
|
+
@arguments << {
|
11
|
+
name:,
|
12
|
+
description:,
|
13
|
+
required:,
|
14
|
+
complete:
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(*args, context: {})
|
20
|
+
end
|
21
|
+
|
22
|
+
def prompt_name
|
23
|
+
end
|
24
|
+
|
25
|
+
def description
|
26
|
+
end
|
27
|
+
|
28
|
+
def visible?(context: {})
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def messages
|
33
|
+
raise NotImplementedError, "#{self.class.name}#messages must be implemented"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,49 @@
|
|
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 resource_template_name
|
10
|
+
end
|
11
|
+
|
12
|
+
def description
|
13
|
+
end
|
14
|
+
|
15
|
+
def mime_type
|
16
|
+
end
|
17
|
+
|
18
|
+
def argument(name, complete)
|
19
|
+
@arguments = {}
|
20
|
+
@arguments[name] = complete
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
end
|
26
|
+
|
27
|
+
def resource_name
|
28
|
+
end
|
29
|
+
|
30
|
+
def description
|
31
|
+
end
|
32
|
+
|
33
|
+
def visible?(context: {})
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
def content
|
38
|
+
case text
|
39
|
+
when String
|
40
|
+
text
|
41
|
+
when Hash
|
42
|
+
text.to_json
|
43
|
+
else
|
44
|
+
text.to_s
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -2,22 +2,27 @@ module ActiveMcp
|
|
2
2
|
module Schema
|
3
3
|
class Base
|
4
4
|
class << self
|
5
|
-
attr_reader :resources, :resource_templates, :tools
|
5
|
+
attr_reader :resources, :resource_templates, :tools, :prompts
|
6
6
|
|
7
7
|
def resource(klass)
|
8
8
|
@resources ||= []
|
9
9
|
@resources << klass
|
10
|
-
end
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
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
15
|
end
|
16
16
|
|
17
17
|
def tool(klass)
|
18
18
|
@tools ||= []
|
19
19
|
@tools << klass
|
20
20
|
end
|
21
|
+
|
22
|
+
def prompt(klass)
|
23
|
+
@prompts ||= []
|
24
|
+
@prompts << klass
|
25
|
+
end
|
21
26
|
end
|
22
27
|
|
23
28
|
def initialize(context: {})
|
@@ -25,22 +30,28 @@ module ActiveMcp
|
|
25
30
|
end
|
26
31
|
|
27
32
|
def resources
|
28
|
-
self.class.resources
|
33
|
+
self.class.resources&.filter do |resource|
|
29
34
|
!resource.respond_to?(:visible?) || resource.visible?(context: @context)
|
30
35
|
end
|
31
36
|
end
|
32
37
|
|
33
38
|
def resource_templates
|
34
|
-
self.class.resource_templates
|
35
|
-
!
|
39
|
+
self.class.resource_templates&.filter do |template|
|
40
|
+
!template.respond_to?(:visible?) || template.visible?(context: @context)
|
36
41
|
end
|
37
42
|
end
|
38
43
|
|
39
44
|
def tools
|
40
|
-
self.class.tools
|
45
|
+
self.class.tools&.filter do |tool|
|
41
46
|
!tool.respond_to?(:visible?) || tool.visible?(context: @context)
|
42
47
|
end
|
43
48
|
end
|
49
|
+
|
50
|
+
def prompts
|
51
|
+
self.class.prompts&.filter do |resource|
|
52
|
+
!resource.respond_to?(:visible?) || resource.visible?(context: @context)
|
53
|
+
end
|
54
|
+
end
|
44
55
|
end
|
45
56
|
end
|
46
57
|
end
|
@@ -11,5 +11,8 @@ 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"
|
15
|
+
PROMPTS_LIST = "prompts/list"
|
16
|
+
PROMPTS_GET = "prompts/get"
|
14
17
|
end
|
15
18
|
end
|
@@ -31,16 +31,6 @@ module ActiveMcp
|
|
31
31
|
private
|
32
32
|
|
33
33
|
def handle_request(request)
|
34
|
-
allowed_methods = [
|
35
|
-
Method::INITIALIZE,
|
36
|
-
Method::INITIALIZED,
|
37
|
-
Method::PING
|
38
|
-
]
|
39
|
-
|
40
|
-
if !@initialized && !allowed_methods.include?(request[:method])
|
41
|
-
return error_response(request[:id], ErrorCode::NOT_INITIALIZED, "Server not initialized")
|
42
|
-
end
|
43
|
-
|
44
34
|
case request[:method]
|
45
35
|
when Method::INITIALIZE
|
46
36
|
handle_initialize(request)
|
@@ -58,6 +48,12 @@ module ActiveMcp
|
|
58
48
|
handle_call_tool(request)
|
59
49
|
when Method::RESOURCES_READ
|
60
50
|
handle_read_resource(request)
|
51
|
+
when Method::COMPLETION_COMPLETE
|
52
|
+
handle_complete(request)
|
53
|
+
when Method::PROMPTS_LIST
|
54
|
+
handle_list_prompts(request)
|
55
|
+
when Method::PROMPTS_GET
|
56
|
+
handle_get_prompt(request)
|
61
57
|
else
|
62
58
|
error_response(request[:id], ErrorCode::METHOD_NOT_FOUND, "Unknown method: #{request[:method]}")
|
63
59
|
end
|
@@ -86,13 +82,9 @@ module ActiveMcp
|
|
86
82
|
result: {
|
87
83
|
protocolVersion: PROTOCOL_VERSION,
|
88
84
|
capabilities: {
|
89
|
-
resources: {
|
90
|
-
|
91
|
-
|
92
|
-
},
|
93
|
-
tools: {
|
94
|
-
listChanged: false
|
95
|
-
}
|
85
|
+
resources: {},
|
86
|
+
tools: {},
|
87
|
+
prompts: {},
|
96
88
|
},
|
97
89
|
serverInfo: {
|
98
90
|
name: @server.name,
|
@@ -189,7 +181,53 @@ module ActiveMcp
|
|
189
181
|
|
190
182
|
success_response(request[:id], result)
|
191
183
|
rescue => e
|
192
|
-
Server.("Error reading resource #{uri}", e)
|
184
|
+
Server.log_error("Error reading resource #{uri}", e)
|
185
|
+
error_response(request[:id], ErrorCode::INTERNAL_ERROR, "An error occurred while reading the resource")
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def handle_complete(request)
|
190
|
+
begin
|
191
|
+
result = @server.fetch(
|
192
|
+
params: {
|
193
|
+
method: Method::COMPLETION_COMPLETE,
|
194
|
+
params: request[:params],
|
195
|
+
}
|
196
|
+
)
|
197
|
+
success_response(request[:id], { completion: result[:result] })
|
198
|
+
rescue => e
|
199
|
+
Server.log_error("Error reading resource #{uri}", e)
|
200
|
+
error_response(request[:id], ErrorCode::INTERNAL_ERROR, "An error occurred while reading the resource")
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def handle_list_prompts(request)
|
205
|
+
begin
|
206
|
+
result = @server.fetch(params: { method: Method::PROMPTS_LIST })
|
207
|
+
success_response(request[:id], { prompts: result[:result] })
|
208
|
+
rescue => e
|
209
|
+
Server.log_error("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
|
+
|
214
|
+
def handle_get_prompt(request)
|
215
|
+
name = request.dig(:params, :name)
|
216
|
+
arguments = request.dig(:params, :arguments)
|
217
|
+
begin
|
218
|
+
result = @server.fetch(
|
219
|
+
params: {
|
220
|
+
method: Method::PROMPTS_GET,
|
221
|
+
params: {
|
222
|
+
name:,
|
223
|
+
arguments:,
|
224
|
+
}
|
225
|
+
}
|
226
|
+
)
|
227
|
+
|
228
|
+
success_response(request[:id], result)
|
229
|
+
rescue => e
|
230
|
+
Server.log_error("Error reading resource #{uri}", e)
|
193
231
|
error_response(request[:id], ErrorCode::INTERNAL_ERROR, "An error occurred while reading the resource")
|
194
232
|
end
|
195
233
|
end
|
data/lib/active_mcp/tool/base.rb
CHANGED
data/lib/active_mcp/version.rb
CHANGED
data/lib/active_mcp.rb
CHANGED
@@ -5,7 +5,14 @@ require_relative "active_mcp/version"
|
|
5
5
|
require_relative "active_mcp/configuration"
|
6
6
|
require_relative "active_mcp/schema/base"
|
7
7
|
require_relative "active_mcp/tool/base"
|
8
|
+
require_relative "active_mcp/resource/base"
|
9
|
+
require_relative "active_mcp/prompt/base"
|
10
|
+
require_relative "active_mcp/message/text"
|
11
|
+
require_relative "active_mcp/message/image"
|
12
|
+
require_relative "active_mcp/message/audio"
|
13
|
+
require_relative "active_mcp/message/resource"
|
8
14
|
require_relative "active_mcp/server"
|
15
|
+
require_relative "active_mcp/completion"
|
9
16
|
|
10
17
|
if defined? ::Rails
|
11
18
|
require_relative "active_mcp/engine"
|
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.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Moeki Kawakami
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-04-
|
10
|
+
date: 2025-04-08 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rails
|
@@ -73,9 +73,12 @@ files:
|
|
73
73
|
- app/controllers/concerns/active_mcp/resource_readable.rb
|
74
74
|
- app/controllers/concerns/active_mcp/tool_executable.rb
|
75
75
|
- app/views/active_mcp/cancelled.json.jbuilder
|
76
|
+
- app/views/active_mcp/completion_complete.json.jbuilder
|
76
77
|
- app/views/active_mcp/initialize.json.jbuilder
|
77
78
|
- app/views/active_mcp/initialized.json.jbuilder
|
78
79
|
- app/views/active_mcp/no_method.json.jbuilder
|
80
|
+
- app/views/active_mcp/prompts_get.json.jbuilder
|
81
|
+
- app/views/active_mcp/prompts_list.json.jbuilder
|
79
82
|
- app/views/active_mcp/resource_templates_list.json.jbuilder
|
80
83
|
- app/views/active_mcp/resources_list.json.jbuilder
|
81
84
|
- app/views/active_mcp/resources_read.json.jbuilder
|
@@ -83,9 +86,16 @@ files:
|
|
83
86
|
- app/views/active_mcp/tools_list.json.jbuilder
|
84
87
|
- config/routes.rb
|
85
88
|
- lib/active_mcp.rb
|
89
|
+
- lib/active_mcp/completion.rb
|
86
90
|
- lib/active_mcp/configuration.rb
|
87
91
|
- lib/active_mcp/controller/base.rb
|
88
92
|
- lib/active_mcp/engine.rb
|
93
|
+
- lib/active_mcp/message/audio.rb
|
94
|
+
- lib/active_mcp/message/image.rb
|
95
|
+
- lib/active_mcp/message/resource.rb
|
96
|
+
- lib/active_mcp/message/text.rb
|
97
|
+
- lib/active_mcp/prompt/base.rb
|
98
|
+
- lib/active_mcp/resource/base.rb
|
89
99
|
- lib/active_mcp/schema/base.rb
|
90
100
|
- lib/active_mcp/server.rb
|
91
101
|
- lib/active_mcp/server/error_codes.rb
|