actionmcp 0.1.0 → 0.1.1
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 +76 -0
- data/lib/action_mcp/configuration.rb +15 -0
- data/lib/action_mcp/content.rb +2 -0
- data/lib/action_mcp/gem_version.rb +2 -0
- data/lib/action_mcp/json_rpc/notification.rb +12 -12
- data/lib/action_mcp/json_rpc/request.rb +14 -14
- data/lib/action_mcp/json_rpc/response.rb +38 -38
- data/lib/action_mcp/prompt.rb +4 -3
- data/lib/action_mcp/prompts_registry.rb +2 -2
- data/lib/action_mcp/railtie.rb +14 -1
- data/lib/action_mcp/resources_bank.rb +11 -10
- data/lib/action_mcp/tool.rb +12 -10
- data/lib/action_mcp/tools_registry.rb +2 -2
- data/lib/action_mcp/transport.rb +57 -60
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +13 -0
- data/lib/generators/action_mcp/install/install_generator.rb +16 -0
- data/lib/generators/action_mcp/install/templates/application_prompt.rb +5 -0
- data/lib/generators/action_mcp/install/templates/application_tool.rb +5 -0
- data/lib/generators/action_mcp/prompt/prompt_generator.rb +38 -0
- data/lib/generators/action_mcp/prompt/templates/prompt.rb.erb +19 -0
- data/lib/generators/action_mcp/tool/templates/tool.rb.erb +13 -0
- data/lib/generators/action_mcp/tool/tool_generator.rb +38 -0
- metadata +9 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6c2f845436205ecdf42bff99aa7e2d8557f715f943dfdf787d138ef1791e150c
|
4
|
+
data.tar.gz: dd4487fec77766938bc440b238b74fe12719b378d408bc9835ca4b443fe232e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b04ab72e098936f9395e20b7cfa51372855b7020682e41f775f437da58009798ae6848584c88b9313d965fb576cda2a02c207847bad4d8b47c117c3ca9a91bb
|
7
|
+
data.tar.gz: f66a011bbd7b0fc1eeda252a73d27867b9dd44a7c400ec3caa980ba0008a27e5fb68928e74e0cd1f29202326bb8ee3928f20d48b28d26348c94a36429b8b6559
|
data/README.md
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# ActionMCP
|
2
2
|
|
3
3
|
**ActionMCP** is a Ruby gem that provides essential tooling for building Model Context Protocol (MCP) capable servers.
|
4
|
+
|
4
5
|
It offers base classes and helpers for creating MCP applications, making it easier to integrate your Ruby/Rails application with the MCP standard.
|
6
|
+
|
5
7
|
With ActionMCP, you can focus on your app's logic while it handles the boilerplate for MCP compliance.
|
6
8
|
|
7
9
|
## Introduction
|
@@ -44,6 +46,80 @@ ActionMCP provides three core abstractions to streamline MCP server development:
|
|
44
46
|
These correspond to key MCP concepts and let you define what context or capabilities your server exposes to LLMs.
|
45
47
|
Below is an overview of each component and how you might use it:
|
46
48
|
|
49
|
+
### Configuration
|
50
|
+
ActionMCP is configured via config.action_mcp in your Rails application.
|
51
|
+
By default, the name is set to your application's name and the version defaults to "0.0.1".
|
52
|
+
You can override these settings in your configuration (e.g., in config/application.rb):
|
53
|
+
```ruby
|
54
|
+
module Tron
|
55
|
+
class Application < Rails::Application
|
56
|
+
config.action_mcp.name = "Friendly MCP (Master Control Program)" # defaults to Rails.application.name
|
57
|
+
config.action_mcp.version = "1.2.3" # defaults to "0.0.1"
|
58
|
+
config.action_mcp.logging_enabled = true # defaults to true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
For dynamic versioning, consider adding the rails_app_version gem.
|
63
|
+
|
64
|
+
## Generators
|
65
|
+
|
66
|
+
ActionMCP includes Rails generators to help you quickly set up your MCP server components. You can generate the base classes for your MCP Prompt and Tool using the following commands.
|
67
|
+
|
68
|
+
To generate both the ApplicationPrompt and ApplicationTool files in your application, run:
|
69
|
+
|
70
|
+
```bash
|
71
|
+
bin/rails generate action_mcp:install
|
72
|
+
```
|
73
|
+
|
74
|
+
This command will create:
|
75
|
+
• app/prompts/application_prompt.rb
|
76
|
+
• app/tools/application_tool.rb
|
77
|
+
|
78
|
+
### Generate a New Prompt
|
79
|
+
|
80
|
+
Run the following command to generate a new prompt class:
|
81
|
+
|
82
|
+
```bash
|
83
|
+
bin/rails generate action_mcp:prompt AnalyzeCode
|
84
|
+
```
|
85
|
+
This command will create a file at app/prompts/analyze_code_prompt.rb with content similar to:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class AnalyzeCodePrompt < ApplicationPrompt
|
89
|
+
# Override the prompt_name (otherwise we'd get "analyze-code")
|
90
|
+
prompt_name "analyze-code"
|
91
|
+
|
92
|
+
# Provide a user-facing description for your prompt.
|
93
|
+
description "Analyze code for potential improvements"
|
94
|
+
|
95
|
+
# Configure arguments via the new DSL
|
96
|
+
argument :language, description: "Programming language", default: "Ruby"
|
97
|
+
argument :code, description: "Code to explain", required: true
|
98
|
+
|
99
|
+
# Add validations (note: "Ruby" is not allowed per the validation)
|
100
|
+
validates :language, inclusion: { in: %w[C Cobol FORTRAN] }
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
## Generate a New Tool
|
105
|
+
Similarly, run the following command to generate a new tool class:
|
106
|
+
|
107
|
+
```bash
|
108
|
+
bin/rails generate action_mcp:tool CalculateSum
|
109
|
+
```
|
110
|
+
|
111
|
+
This command will create a file at app/tools/calculate_sum_tool.rb with content similar to:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
class CalculateSumTool < ApplicationTool
|
115
|
+
tool_name "calculate-sum"
|
116
|
+
description "Calculate the sum of two numbers"
|
117
|
+
|
118
|
+
property :a, type: "number", description: "First number", required: true
|
119
|
+
property :b, type: "number", description: "Second number", required: true
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
47
123
|
### ActionMCP::Prompt
|
48
124
|
|
49
125
|
Make Rails Say Sexy stuff
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
# Configuration class to hold settings for the ActionMCP server.
|
5
|
+
class Configuration
|
6
|
+
attr_accessor :name, :version, :logging_enabled
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
# Use Rails.application values if available, or fallback to defaults.
|
10
|
+
@name = defined?(Rails) && Rails.respond_to?(:application) && Rails.application.respond_to?(:name) ? Rails.application.name : "ActionMCP"
|
11
|
+
@version = defined?(Rails) && Rails.respond_to?(:application) && Rails.application.respond_to?(:version) ? Rails.application.version.to_s.presence : "0.0.1"
|
12
|
+
@logging_enabled = true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/action_mcp/content.rb
CHANGED
@@ -2,19 +2,19 @@
|
|
2
2
|
|
3
3
|
module ActionMCP
|
4
4
|
module JsonRpc
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
Notification = Data.define(:method, :params) do
|
6
|
+
def initialize(method:, params: nil)
|
7
|
+
super(method: method, params: params)
|
8
|
+
end
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
10
|
+
def to_h
|
11
|
+
hash = {
|
12
|
+
jsonrpc: "2.0",
|
13
|
+
method: method
|
14
|
+
}
|
15
|
+
hash[:params] = params if params
|
16
|
+
hash
|
17
|
+
end
|
17
18
|
end
|
18
19
|
end
|
19
|
-
end
|
20
20
|
end
|
@@ -2,21 +2,21 @@
|
|
2
2
|
|
3
3
|
module ActionMCP
|
4
4
|
module JsonRpc
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
Request = Data.define(:jsonrpc, :id, :method, :params) do
|
6
|
+
def initialize(id:, method:, params: nil)
|
7
|
+
validate_id(id)
|
8
|
+
super(id: id, method: method, params: params)
|
9
|
+
end
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
end
|
11
|
+
def to_h
|
12
|
+
hash = {
|
13
|
+
jsonrpc: "2.0",
|
14
|
+
id: id,
|
15
|
+
method: method
|
16
|
+
}
|
17
|
+
hash[:params] = params if params
|
18
|
+
hash
|
20
19
|
end
|
20
|
+
end
|
21
21
|
end
|
22
22
|
end
|
@@ -2,52 +2,52 @@
|
|
2
2
|
|
3
3
|
module ActionMCP
|
4
4
|
module JsonRpc
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
5
|
+
Response = Data.define(:id, :result, :error) do
|
6
|
+
def initialize(id:, result: nil, error: nil)
|
7
|
+
processed_error = process_error(error)
|
8
|
+
processed_result = error ? nil : result
|
9
|
+
validate_result_error!(processed_result, processed_error)
|
10
|
+
super(id: id, result: processed_result, error: processed_error)
|
11
|
+
end
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
}
|
18
|
-
if error
|
19
|
-
hash[:error] = {
|
20
|
-
code: error[:code],
|
21
|
-
message: error[:message]
|
13
|
+
def to_h
|
14
|
+
hash = {
|
15
|
+
jsonrpc: "2.0",
|
16
|
+
id: id
|
22
17
|
}
|
23
|
-
|
24
|
-
|
25
|
-
|
18
|
+
if error
|
19
|
+
hash[:error] = {
|
20
|
+
code: error[:code],
|
21
|
+
message: error[:message]
|
22
|
+
}
|
23
|
+
hash[:error][:data] = error[:data] if error[:data]
|
24
|
+
else
|
25
|
+
hash[:result] = result
|
26
|
+
end
|
27
|
+
hash
|
26
28
|
end
|
27
|
-
hash
|
28
|
-
end
|
29
29
|
|
30
|
-
|
30
|
+
private
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
32
|
+
def process_error(error)
|
33
|
+
case error
|
34
|
+
when Symbol
|
35
|
+
ErrorCodes[error]
|
36
|
+
when Hash
|
37
|
+
validate_error!(error)
|
38
|
+
error
|
39
|
+
end
|
39
40
|
end
|
40
|
-
end
|
41
41
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
42
|
+
def validate_error!(error)
|
43
|
+
raise Error, "Error code must be an integer" unless error[:code].is_a?(Integer)
|
44
|
+
raise Error, "Error message is required" unless error[:message].is_a?(String)
|
45
|
+
end
|
46
46
|
|
47
|
-
|
48
|
-
|
49
|
-
|
47
|
+
def validate_result_error!(result, error)
|
48
|
+
raise Error, "Either result or error must be set" unless result || error
|
49
|
+
raise Error, "Cannot set both result and error" if result && error
|
50
|
+
end
|
50
51
|
end
|
51
52
|
end
|
52
|
-
end
|
53
53
|
end
|
data/lib/action_mcp/prompt.rb
CHANGED
@@ -15,7 +15,8 @@ module ActionMCP
|
|
15
15
|
def self.inherited(subclass)
|
16
16
|
super
|
17
17
|
return if subclass == Prompt
|
18
|
-
return if "ApplicationPrompt"
|
18
|
+
return if subclass.name == "ApplicationPrompt"
|
19
|
+
|
19
20
|
subclass.abstract_prompt = false
|
20
21
|
|
21
22
|
# Automatically register the subclass with the PromptsRegistry
|
@@ -28,7 +29,7 @@ module ActionMCP
|
|
28
29
|
end
|
29
30
|
|
30
31
|
def self.abstract?
|
31
|
-
|
32
|
+
abstract_prompt
|
32
33
|
end
|
33
34
|
|
34
35
|
# ---------------------------------------------------
|
@@ -102,7 +103,7 @@ module ActionMCP
|
|
102
103
|
# ActionMCP::JsonRpc::JsonRpcError(:invalid_params) if validation fails.
|
103
104
|
#
|
104
105
|
def self.call(params)
|
105
|
-
prompt = new(params)
|
106
|
+
prompt = new(params) # Initialize an instance with provided params
|
106
107
|
unless prompt.valid?
|
107
108
|
# Collect all validation errors into a single string or array
|
108
109
|
errors_str = prompt.errors.full_messages.join(", ")
|
data/lib/action_mcp/railtie.rb
CHANGED
@@ -1,9 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "rails"
|
2
4
|
require "active_model/railtie"
|
3
5
|
|
4
6
|
module ActionMCP
|
5
7
|
class Railtie < Rails::Railtie # :nodoc:
|
6
|
-
#
|
8
|
+
# Provide a configuration namespace for ActionMCP
|
9
|
+
config.action_mcp = ActiveSupport::OrderedOptions.new
|
10
|
+
|
11
|
+
config.after_initialize do |app|
|
12
|
+
options = app.config.action_mcp.to_h.symbolize_keys
|
13
|
+
|
14
|
+
# Override the default configuration if specified in the Rails app.
|
15
|
+
ActionMCP.configuration.name = options[:name] if options.key?(:name)
|
16
|
+
ActionMCP.configuration.version = options[:version] if options.key?(:version)
|
17
|
+
ActionMCP.configuration.logging_enabled = options.fetch(:logging_enabled, true)
|
18
|
+
end
|
19
|
+
|
7
20
|
initializer "action_mcp.clear_registry" do |app|
|
8
21
|
app.config.to_prepare do
|
9
22
|
ActionMCP::ToolsRegistry.clear!
|
@@ -38,17 +38,17 @@ module ActionMCP
|
|
38
38
|
def register_source(source_uri, path, watch: false)
|
39
39
|
reload_source(source_uri, path) # Initial load
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
@watchers[source_uri] = { path: path, watcher: watcher }
|
41
|
+
return unless watch
|
42
|
+
|
43
|
+
require "active_support/evented_file_update_checker"
|
44
|
+
# Watch all files under the given path (recursive)
|
45
|
+
file_paths = Dir.glob("#{path}/**/*")
|
46
|
+
watcher = ActiveSupport::EventedFileUpdateChecker.new(file_paths) do |modified, added, removed|
|
47
|
+
Rails.logger.info("Files changed in #{path} - Modified: #{modified.inspect}, Added: #{added.inspect}, Removed: #{removed.inspect}")
|
48
|
+
# Reload resources for this source when changes occur.
|
49
|
+
reload_source(source_uri, path)
|
51
50
|
end
|
51
|
+
@watchers[source_uri] = { path: path, watcher: watcher }
|
52
52
|
end
|
53
53
|
|
54
54
|
# Unregisters a source and stops watching it.
|
@@ -68,6 +68,7 @@ module ActionMCP
|
|
68
68
|
Rails.logger.info("Reloading resources from #{path} for #{source_uri}")
|
69
69
|
Dir.glob("#{path}/**/*").each do |file|
|
70
70
|
next unless File.file?(file)
|
71
|
+
|
71
72
|
# Create a resource URI from the source and file path.
|
72
73
|
relative_path = file.sub(%r{\A#{Regexp.escape(path)}/?}, "")
|
73
74
|
resource_uri = "#{source_uri}://#{relative_path}"
|
data/lib/action_mcp/tool.rb
CHANGED
@@ -16,8 +16,9 @@ module ActionMCP
|
|
16
16
|
def self.inherited(subclass)
|
17
17
|
super
|
18
18
|
return if subclass == Tool
|
19
|
+
|
19
20
|
subclass.abstract_tool = false
|
20
|
-
return if "ApplicationTool"
|
21
|
+
return if subclass.name == "ApplicationTool"
|
21
22
|
|
22
23
|
ToolsRegistry.register(subclass.tool_name, subclass)
|
23
24
|
end
|
@@ -25,11 +26,11 @@ module ActionMCP
|
|
25
26
|
# Mark this tool as abstract so it won’t be available for use.
|
26
27
|
def self.abstract!
|
27
28
|
self.abstract_tool = true
|
28
|
-
ToolsRegistry.unregister(
|
29
|
+
ToolsRegistry.unregister(tool_name) if ToolsRegistry.items.key?(tool_name)
|
29
30
|
end
|
30
31
|
|
31
32
|
def self.abstract?
|
32
|
-
|
33
|
+
abstract_tool
|
33
34
|
end
|
34
35
|
|
35
36
|
# ---------------------------------------------------
|
@@ -66,7 +67,7 @@ module ActionMCP
|
|
66
67
|
|
67
68
|
self._schema_properties = _schema_properties.merge(prop_name.to_s => prop_definition)
|
68
69
|
self._required_properties = _required_properties.dup
|
69
|
-
|
70
|
+
_required_properties << prop_name.to_s if required
|
70
71
|
|
71
72
|
# Map our DSL type to an ActiveModel attribute type.
|
72
73
|
am_type = case type.to_s
|
@@ -92,7 +93,7 @@ module ActionMCP
|
|
92
93
|
# property :file, required: true, description: 'file uri'
|
93
94
|
# property :checksum, required: true, description: 'checksum to verify'
|
94
95
|
# end
|
95
|
-
def self.collection(prop_name, type: nil, description: nil, required: false, default: nil, **
|
96
|
+
def self.collection(prop_name, type: nil, description: nil, required: false, default: nil, **_opts, &block)
|
96
97
|
if block_given?
|
97
98
|
# Build nested schema for an object.
|
98
99
|
nested_schema = { type: "object", properties: {}, required: [] }
|
@@ -101,12 +102,13 @@ module ActionMCP
|
|
101
102
|
collection_definition = { type: "array", description: description, items: nested_schema }
|
102
103
|
else
|
103
104
|
raise ArgumentError, "Type is required for a collection without a block" if type.nil?
|
105
|
+
|
104
106
|
collection_definition = { type: "array", description: description, items: { type: type } }
|
105
107
|
end
|
106
108
|
|
107
109
|
self._schema_properties = _schema_properties.merge(prop_name.to_s => collection_definition)
|
108
110
|
self._required_properties = _required_properties.dup
|
109
|
-
|
111
|
+
_required_properties << prop_name.to_s if required
|
110
112
|
|
111
113
|
# Register the property as an attribute.
|
112
114
|
# (Mapping for a collection can be customized; here we use :string to mimic previous behavior.)
|
@@ -135,11 +137,11 @@ module ActionMCP
|
|
135
137
|
# Convert Tool Definition to Hash
|
136
138
|
# ---------------------------------------------------
|
137
139
|
def self.to_h
|
138
|
-
schema = { type: "object", properties:
|
139
|
-
schema[:required] =
|
140
|
+
schema = { type: "object", properties: _schema_properties }
|
141
|
+
schema[:required] = _required_properties if _required_properties.any?
|
140
142
|
{
|
141
|
-
name:
|
142
|
-
description:
|
143
|
+
name: tool_name,
|
144
|
+
description: description.presence,
|
143
145
|
inputSchema: schema
|
144
146
|
}.compact
|
145
147
|
end
|
data/lib/action_mcp/transport.rb
CHANGED
@@ -14,17 +14,24 @@ module ActionMCP
|
|
14
14
|
#
|
15
15
|
# @param request_id [String, Integer] The request identifier.
|
16
16
|
def send_capabilities(request_id)
|
17
|
+
capabilities = {}
|
18
|
+
|
19
|
+
# Only include each capability if the corresponding registry is non-empty.
|
20
|
+
capabilities[:tools] = { listChanged: true } if ActionMCP::ToolsRegistry.available_tools.any?
|
21
|
+
|
22
|
+
capabilities[:prompts] = { listChanged: true } if ActionMCP::PromptsRegistry.available_prompts.any?
|
23
|
+
|
24
|
+
capabilities[:resources] = { listChanged: true } if ActionMCP::ResourcesBank.all_resources.any?
|
25
|
+
|
26
|
+
# Add logging capability only if enabled by configuration.
|
27
|
+
capabilities[:logging] = {} if ActionMCP.configuration.logging_enabled
|
28
|
+
|
17
29
|
payload = {
|
18
30
|
protocolVersion: "2024-11-05",
|
19
|
-
capabilities:
|
20
|
-
tools: { listChanged: true },
|
21
|
-
prompts: { listChanged: true },
|
22
|
-
resources: { listChanged: true },
|
23
|
-
logging: {}
|
24
|
-
},
|
31
|
+
capabilities: capabilities,
|
25
32
|
serverInfo: {
|
26
|
-
name:
|
27
|
-
version:
|
33
|
+
name: ActionMCP.configuration.name,
|
34
|
+
version: ActionMCP.configuration.version
|
28
35
|
}
|
29
36
|
}
|
30
37
|
send_jsonrpc_response(request_id, result: payload)
|
@@ -42,38 +49,34 @@ module ActionMCP
|
|
42
49
|
#
|
43
50
|
# @param request_id [String, Integer] The request identifier.
|
44
51
|
def send_resources_list(request_id)
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
send_jsonrpc_response(request_id, error: error_obj)
|
57
|
-
end
|
52
|
+
resources = ActionMCP::ResourcesBank.all_resources # fetch all resources
|
53
|
+
result_data = { "resources" => resources }
|
54
|
+
send_jsonrpc_response(request_id, result: result_data)
|
55
|
+
Rails.logger.info("resources/list: Returned #{resources.size} resources.")
|
56
|
+
rescue StandardError => e
|
57
|
+
Rails.logger.error("resources/list failed: #{e.message}")
|
58
|
+
error_obj = JsonRpcError.new(
|
59
|
+
:internal_error,
|
60
|
+
message: "Failed to list resources: #{e.message}"
|
61
|
+
).as_json
|
62
|
+
send_jsonrpc_response(request_id, error: error_obj)
|
58
63
|
end
|
59
64
|
|
60
65
|
# Sends the resource templates list JSON-RPC response.
|
61
66
|
#
|
62
67
|
# @param request_id [String, Integer] The request identifier.
|
63
68
|
def send_resource_templates_list(request_id)
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
send_jsonrpc_response(request_id, error: error_obj)
|
76
|
-
end
|
69
|
+
templates = ActionMCP::ResourcesBank.all_templates # get all resource templates
|
70
|
+
result_data = { "resourceTemplates" => templates }
|
71
|
+
send_jsonrpc_response(request_id, result: result_data)
|
72
|
+
Rails.logger.info("resources/templates/list: Returned #{templates.size} resource templates.")
|
73
|
+
rescue StandardError => e
|
74
|
+
Rails.logger.error("resources/templates/list failed: #{e.message}")
|
75
|
+
error_obj = JsonRpcError.new(
|
76
|
+
:internal_error,
|
77
|
+
message: "Failed to list resource templates: #{e.message}"
|
78
|
+
).as_json
|
79
|
+
send_jsonrpc_response(request_id, error: error_obj)
|
77
80
|
end
|
78
81
|
|
79
82
|
# Sends the resource read JSON-RPC response.
|
@@ -92,7 +95,7 @@ module ActionMCP
|
|
92
95
|
end
|
93
96
|
|
94
97
|
begin
|
95
|
-
content = ActionMCP::
|
98
|
+
content = ActionMCP::ResourcesBank.read(uri) # Expecting an instance of an ActionMCP::Content subclass
|
96
99
|
if content.nil?
|
97
100
|
Rails.logger.error("resources/read: Resource not found for URI #{uri}")
|
98
101
|
error_obj = JsonRpcError.new(
|
@@ -119,42 +122,37 @@ module ActionMCP
|
|
119
122
|
end
|
120
123
|
end
|
121
124
|
|
122
|
-
|
123
125
|
# Sends a call to a tool. Currently logs the call details.
|
124
126
|
#
|
125
127
|
# @param request_id [String, Integer] The request identifier.
|
126
128
|
# @param tool_name [String] The name of the tool.
|
127
129
|
# @param params [Hash] The parameters for the tool.
|
128
130
|
def send_tools_call(request_id, tool_name, params)
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
send_jsonrpc_response(request_id, error: error_obj)
|
140
|
-
end
|
131
|
+
ActionMCP::ToolsRegistry.fetch_available_tool(tool_name.to_s)
|
132
|
+
Rails.logger.info("Sending tool call: #{tool_name} with params: #{params}")
|
133
|
+
# TODO: Implement tool call handling and response if needed.
|
134
|
+
rescue StandardError => e
|
135
|
+
Rails.logger.error("tools/call: Failed to call tool #{tool_name} - #{e.message}")
|
136
|
+
error_obj = JsonRpcError.new(
|
137
|
+
:internal_error,
|
138
|
+
message: "Failed to call tool #{tool_name}: #{e.message}"
|
139
|
+
).as_json
|
140
|
+
send_jsonrpc_response(request_id, error: error_obj)
|
141
141
|
end
|
142
142
|
|
143
143
|
# Sends the prompts list JSON-RPC notification.
|
144
144
|
#
|
145
145
|
# @param request_id [String, Integer] The request identifier.
|
146
146
|
def send_prompts_list(request_id)
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
send_jsonrpc_response(request_id, error: error_obj)
|
157
|
-
end
|
147
|
+
prompts = format_registry_items(ActionMCP::PromptsRegistry.available_prompts)
|
148
|
+
send_jsonrpc_response(request_id, result: { prompts: prompts })
|
149
|
+
rescue StandardError => e
|
150
|
+
Rails.logger.error("prompts/list failed: #{e.message}")
|
151
|
+
error_obj = JsonRpcError.new(
|
152
|
+
:internal_error,
|
153
|
+
message: "Failed to list prompts: #{e.message}"
|
154
|
+
).as_json
|
155
|
+
send_jsonrpc_response(request_id, error: error_obj)
|
158
156
|
end
|
159
157
|
|
160
158
|
def send_prompts_get(request_id, params)
|
@@ -193,7 +191,6 @@ module ActionMCP
|
|
193
191
|
end
|
194
192
|
end
|
195
193
|
|
196
|
-
|
197
194
|
# Sends a JSON-RPC pong response.
|
198
195
|
# We don't actually to send any data back because the spec are not fun anymore.
|
199
196
|
#
|
data/lib/action_mcp/version.rb
CHANGED
data/lib/action_mcp.rb
CHANGED
@@ -19,8 +19,21 @@ module ActionMCP
|
|
19
19
|
autoload :Tool
|
20
20
|
autoload :Prompt
|
21
21
|
autoload :JsonRpc
|
22
|
+
eager_autoload do
|
23
|
+
autoload :Configuration
|
24
|
+
end
|
25
|
+
|
26
|
+
# Accessor for the configuration instance.
|
27
|
+
def self.configuration
|
28
|
+
@configuration ||= Configuration.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.configure
|
32
|
+
yield(configuration)
|
33
|
+
end
|
22
34
|
|
23
35
|
module_function
|
36
|
+
|
24
37
|
def tools
|
25
38
|
ToolsRegistry.tools
|
26
39
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ActionMCP
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
namespace "action_mcp:install"
|
5
|
+
source_root File.expand_path("templates", __dir__)
|
6
|
+
desc "Installs both ApplicationPrompt and ApplicationTool"
|
7
|
+
def create_application_prompt
|
8
|
+
template "application_prompt.rb", "app/prompts/application_prompt.rb"
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_application_tool
|
12
|
+
template "application_tool.rb", "app/tools/application_tool.rb"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Generators
|
5
|
+
class PromptGenerator < Rails::Generators::Base
|
6
|
+
namespace "action_mcp:prompt"
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
8
|
+
desc "Creates a Prompt (in app/prompts) that inherits from ApplicationPrompt"
|
9
|
+
|
10
|
+
# The generator takes one argument, e.g. "AnalyzeCode"
|
11
|
+
argument :name, type: :string, required: true, banner: "PromptName"
|
12
|
+
|
13
|
+
def create_prompt_file
|
14
|
+
template "prompt.rb.erb", "app/prompts/#{file_name}.rb"
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# Build the class name, ensuring it ends with "Prompt"
|
20
|
+
def class_name
|
21
|
+
"#{name.camelize}#{name.camelize.end_with?('Prompt') ? '' : 'Prompt'}"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Build the file name (underscore and ensure it ends with _prompt)
|
25
|
+
def file_name
|
26
|
+
base = name.underscore
|
27
|
+
base.end_with?("_prompt") ? base : "#{base}_prompt"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Build the DSL prompt name (a dashed version, without the "Prompt" suffix)
|
31
|
+
def prompt_name
|
32
|
+
base = name.to_s
|
33
|
+
base = base[0...-6] if base.end_with?("Prompt")
|
34
|
+
base.underscore.dasherize
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class <%= class_name %> < ApplicationPrompt
|
4
|
+
prompt_name "<%= prompt_name %>"
|
5
|
+
|
6
|
+
# Provide a user-facing description for your prompt.
|
7
|
+
description "Analyze code for potential improvements"
|
8
|
+
|
9
|
+
# Configure arguments via the new DSL
|
10
|
+
argument :language, description: "Programming language", default: "Ruby"
|
11
|
+
argument :code, description: "Code to explain", required: true
|
12
|
+
|
13
|
+
# Add validations (note: "Ruby" is not allowed per the validation)
|
14
|
+
validates :language, inclusion: { in: %w[Ruby C Cobol FORTRAN] }
|
15
|
+
|
16
|
+
def call
|
17
|
+
# Implement your prompt's behavior here
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class <%= class_name %> < ApplicationTool
|
4
|
+
tool_name "<%= tool_name %>"
|
5
|
+
description "Calculate the sum of two numbers"
|
6
|
+
|
7
|
+
property :a, type: "number", description: "First number", required: true
|
8
|
+
property :b, type: "number", description: "Second number", required: true
|
9
|
+
|
10
|
+
def call
|
11
|
+
a + b
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Generators
|
5
|
+
class ToolGenerator < Rails::Generators::Base
|
6
|
+
namespace "action_mcp:tool"
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
8
|
+
desc "Creates a Tool (in app/tools) that inherits from ApplicationTool"
|
9
|
+
|
10
|
+
# The generator takes one argument, e.g. "CalculateSum"
|
11
|
+
argument :name, type: :string, required: true, banner: "ToolName"
|
12
|
+
|
13
|
+
def create_tool_file
|
14
|
+
template "tool.rb.erb", "app/tools/#{file_name}.rb"
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# Compute the class name ensuring it ends with "Tool"
|
20
|
+
def class_name
|
21
|
+
"#{name.camelize}#{name.camelize.end_with?('Tool') ? '' : 'Tool'}"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Compute the file name ensuring it ends with _tool.rb
|
25
|
+
def file_name
|
26
|
+
base = name.underscore
|
27
|
+
base.end_with?("_tool") ? base : "#{base}_tool"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Compute the DSL tool name (a dashed version, without the "Tool" suffix)
|
31
|
+
def tool_name
|
32
|
+
base = name.to_s
|
33
|
+
base = base[0...-4] if base.end_with?("Tool")
|
34
|
+
base.underscore.dasherize
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: actionmcp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Abdelkader Boudih
|
@@ -65,6 +65,7 @@ files:
|
|
65
65
|
- Rakefile
|
66
66
|
- exe/action_mcp_stdio
|
67
67
|
- lib/action_mcp.rb
|
68
|
+
- lib/action_mcp/configuration.rb
|
68
69
|
- lib/action_mcp/content.rb
|
69
70
|
- lib/action_mcp/content/audio.rb
|
70
71
|
- lib/action_mcp/content/image.rb
|
@@ -88,6 +89,13 @@ files:
|
|
88
89
|
- lib/action_mcp/transport.rb
|
89
90
|
- lib/action_mcp/version.rb
|
90
91
|
- lib/actionmcp.rb
|
92
|
+
- lib/generators/action_mcp/install/install_generator.rb
|
93
|
+
- lib/generators/action_mcp/install/templates/application_prompt.rb
|
94
|
+
- lib/generators/action_mcp/install/templates/application_tool.rb
|
95
|
+
- lib/generators/action_mcp/prompt/prompt_generator.rb
|
96
|
+
- lib/generators/action_mcp/prompt/templates/prompt.rb.erb
|
97
|
+
- lib/generators/action_mcp/tool/templates/tool.rb.erb
|
98
|
+
- lib/generators/action_mcp/tool/tool_generator.rb
|
91
99
|
- lib/tasks/action_mcp_tasks.rake
|
92
100
|
homepage: https://github.com/seuros/action_mcp
|
93
101
|
licenses:
|