actionmcp 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +57 -0
- data/Rakefile +9 -0
- data/exe/action_mcp_stdio +0 -0
- data/lib/action_mcp/content/audio.rb +20 -0
- data/lib/action_mcp/content/image.rb +20 -0
- data/lib/action_mcp/content/resource.rb +27 -0
- data/lib/action_mcp/content/text.rb +19 -0
- data/lib/action_mcp/content.rb +21 -0
- data/lib/action_mcp/gem_version.rb +6 -0
- data/lib/action_mcp/json_rpc/base.rb +12 -0
- data/lib/action_mcp/json_rpc/json_rpc_error.rb +70 -0
- data/lib/action_mcp/json_rpc/notification.rb +20 -0
- data/lib/action_mcp/json_rpc/request.rb +22 -0
- data/lib/action_mcp/json_rpc/response.rb +53 -0
- data/lib/action_mcp/json_rpc.rb +13 -0
- data/lib/action_mcp/prompt.rb +139 -0
- data/lib/action_mcp/prompts_registry.rb +10 -0
- data/lib/action_mcp/railtie.rb +14 -0
- data/lib/action_mcp/registry_base.rb +71 -0
- data/lib/action_mcp/resource.rb +20 -0
- data/lib/action_mcp/resources_bank.rb +95 -0
- data/lib/action_mcp/tool.rb +147 -0
- data/lib/action_mcp/tools_registry.rb +12 -0
- data/lib/action_mcp/transport.rb +244 -0
- data/lib/action_mcp/version.rb +11 -0
- data/lib/action_mcp.rb +39 -0
- data/lib/actionmcp.rb +3 -0
- data/lib/tasks/action_mcp_tasks.rake +6 -0
- metadata +116 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module ActionMCP
|
6
|
+
module ResourcesBank
|
7
|
+
@resources = {} # { uri => content_object }
|
8
|
+
@templates = {} # { uri => template_object }
|
9
|
+
@watchers = {} # { source_uri => watcher }
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Basic resource registration.
|
13
|
+
def register_resource(uri, content)
|
14
|
+
@resources[uri] = content
|
15
|
+
end
|
16
|
+
|
17
|
+
def all_resources
|
18
|
+
@resources.values
|
19
|
+
end
|
20
|
+
|
21
|
+
def read(uri)
|
22
|
+
@resources[uri]
|
23
|
+
end
|
24
|
+
|
25
|
+
def register_template(uri, template)
|
26
|
+
@templates[uri] = template
|
27
|
+
end
|
28
|
+
|
29
|
+
def all_templates
|
30
|
+
@templates.values
|
31
|
+
end
|
32
|
+
|
33
|
+
# Registers a source (file or directory) for resources.
|
34
|
+
#
|
35
|
+
# @param source_uri [String] An identifier for this source.
|
36
|
+
# @param path [String] Filesystem path to the source.
|
37
|
+
# @param watch [Boolean] Whether to watch the source for changes.
|
38
|
+
def register_source(source_uri, path, watch: false)
|
39
|
+
reload_source(source_uri, path) # Initial load
|
40
|
+
|
41
|
+
if watch
|
42
|
+
require "active_support/evented_file_update_checker"
|
43
|
+
# Watch all files under the given path (recursive)
|
44
|
+
file_paths = Dir.glob("#{path}/**/*")
|
45
|
+
watcher = ActiveSupport::EventedFileUpdateChecker.new(file_paths) do |modified, added, removed|
|
46
|
+
Rails.logger.info("Files changed in #{path} - Modified: #{modified.inspect}, Added: #{added.inspect}, Removed: #{removed.inspect}")
|
47
|
+
# Reload resources for this source when changes occur.
|
48
|
+
reload_source(source_uri, path)
|
49
|
+
end
|
50
|
+
@watchers[source_uri] = { path: path, watcher: watcher }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Unregisters a source and stops watching it.
|
55
|
+
#
|
56
|
+
# @param source_uri [String] The identifier for the source.
|
57
|
+
def unregister_source(source_uri)
|
58
|
+
@watchers.delete(source_uri)
|
59
|
+
# Optionally, remove any resources associated with this source.
|
60
|
+
@resources.reject! { |uri, _| uri.start_with?("#{source_uri}://") }
|
61
|
+
end
|
62
|
+
|
63
|
+
# Reloads (or loads) all resources from the given directory.
|
64
|
+
#
|
65
|
+
# @param source_uri [String] The identifier for the source.
|
66
|
+
# @param path [String] Filesystem path to the source.
|
67
|
+
def reload_source(source_uri, path)
|
68
|
+
Rails.logger.info("Reloading resources from #{path} for #{source_uri}")
|
69
|
+
Dir.glob("#{path}/**/*").each do |file|
|
70
|
+
next unless File.file?(file)
|
71
|
+
# Create a resource URI from the source and file path.
|
72
|
+
relative_path = file.sub(%r{\A#{Regexp.escape(path)}/?}, "")
|
73
|
+
resource_uri = "#{source_uri}://#{relative_path}"
|
74
|
+
# For this example, we assume text files.
|
75
|
+
begin
|
76
|
+
text = File.read(file)
|
77
|
+
content = ActionMCP::Content::Text.new(text)
|
78
|
+
register_resource(resource_uri, content)
|
79
|
+
Rails.logger.info("Registered resource: #{resource_uri}")
|
80
|
+
rescue StandardError => e
|
81
|
+
Rails.logger.error("Error reading file #{file}: #{e.message}")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# This method should be called periodically (e.g. via a background thread)
|
87
|
+
# to check if any watched files have changed.
|
88
|
+
def run_watchers
|
89
|
+
@watchers.each_value do |data|
|
90
|
+
data[:watcher].execute_if_updated
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# lib/action_mcp/tool.rb
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module ActionMCP
|
5
|
+
class Tool
|
6
|
+
include ActiveModel::Model
|
7
|
+
include ActiveModel::Attributes
|
8
|
+
|
9
|
+
class_attribute :_tool_name, instance_accessor: false
|
10
|
+
class_attribute :_description, instance_accessor: false, default: ""
|
11
|
+
class_attribute :_schema_properties, instance_accessor: false, default: {}
|
12
|
+
class_attribute :_required_properties, instance_accessor: false, default: []
|
13
|
+
class_attribute :abstract_tool, instance_accessor: false, default: false
|
14
|
+
|
15
|
+
# Register each non-abstract subclass in ToolsRegistry
|
16
|
+
def self.inherited(subclass)
|
17
|
+
super
|
18
|
+
return if subclass == Tool
|
19
|
+
subclass.abstract_tool = false
|
20
|
+
return if "ApplicationTool" == subclass.name
|
21
|
+
|
22
|
+
ToolsRegistry.register(subclass.tool_name, subclass)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Mark this tool as abstract so it won’t be available for use.
|
26
|
+
def self.abstract!
|
27
|
+
self.abstract_tool = true
|
28
|
+
ToolsRegistry.unregister(self.tool_name) if ToolsRegistry.items.key?(self.tool_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.abstract?
|
32
|
+
self.abstract_tool
|
33
|
+
end
|
34
|
+
|
35
|
+
# ---------------------------------------------------
|
36
|
+
# Tool Name & Description
|
37
|
+
# ---------------------------------------------------
|
38
|
+
def self.tool_name(name = nil)
|
39
|
+
if name
|
40
|
+
self._tool_name = name
|
41
|
+
else
|
42
|
+
_tool_name || default_tool_name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.default_tool_name
|
47
|
+
name.demodulize.underscore.dasherize.sub(/-tool$/, "")
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.description(text = nil)
|
51
|
+
if text
|
52
|
+
self._description = text
|
53
|
+
else
|
54
|
+
_description
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# ---------------------------------------------------
|
59
|
+
# Property DSL (Direct Declaration)
|
60
|
+
# ---------------------------------------------------
|
61
|
+
def self.property(prop_name, type: "string", description: nil, required: false, default: nil, **opts)
|
62
|
+
# Build JSON Schema definition for the property.
|
63
|
+
prop_definition = { type: type }
|
64
|
+
prop_definition[:description] = description if description && !description.empty?
|
65
|
+
prop_definition.merge!(opts) if opts.any?
|
66
|
+
|
67
|
+
self._schema_properties = _schema_properties.merge(prop_name.to_s => prop_definition)
|
68
|
+
self._required_properties = _required_properties.dup
|
69
|
+
self._required_properties << prop_name.to_s if required
|
70
|
+
|
71
|
+
# Map our DSL type to an ActiveModel attribute type.
|
72
|
+
am_type = case type.to_s
|
73
|
+
when "number" then :float
|
74
|
+
when "integer" then :integer
|
75
|
+
when "array" then :string
|
76
|
+
else
|
77
|
+
:string
|
78
|
+
end
|
79
|
+
attribute prop_name, am_type, default: default
|
80
|
+
end
|
81
|
+
|
82
|
+
# ---------------------------------------------------
|
83
|
+
# Collection DSL
|
84
|
+
# ---------------------------------------------------
|
85
|
+
# Supports two forms:
|
86
|
+
#
|
87
|
+
# 1. Without a block:
|
88
|
+
# collection :args, type: "string", description: "Command arguments"
|
89
|
+
#
|
90
|
+
# 2. With a block (defining a nested object):
|
91
|
+
# collection :files, description: "List of Files" do
|
92
|
+
# property :file, required: true, description: 'file uri'
|
93
|
+
# property :checksum, required: true, description: 'checksum to verify'
|
94
|
+
# end
|
95
|
+
def self.collection(prop_name, type: nil, description: nil, required: false, default: nil, **opts, &block)
|
96
|
+
if block_given?
|
97
|
+
# Build nested schema for an object.
|
98
|
+
nested_schema = { type: "object", properties: {}, required: [] }
|
99
|
+
dsl = CollectionDSL.new(nested_schema)
|
100
|
+
dsl.instance_eval(&block)
|
101
|
+
collection_definition = { type: "array", description: description, items: nested_schema }
|
102
|
+
else
|
103
|
+
raise ArgumentError, "Type is required for a collection without a block" if type.nil?
|
104
|
+
collection_definition = { type: "array", description: description, items: { type: type } }
|
105
|
+
end
|
106
|
+
|
107
|
+
self._schema_properties = _schema_properties.merge(prop_name.to_s => collection_definition)
|
108
|
+
self._required_properties = _required_properties.dup
|
109
|
+
self._required_properties << prop_name.to_s if required
|
110
|
+
|
111
|
+
# Register the property as an attribute.
|
112
|
+
# (Mapping for a collection can be customized; here we use :string to mimic previous behavior.)
|
113
|
+
attribute prop_name, :string, default: default
|
114
|
+
end
|
115
|
+
|
116
|
+
# DSL for building a nested schema within a collection block.
|
117
|
+
class CollectionDSL
|
118
|
+
attr_reader :schema
|
119
|
+
|
120
|
+
def initialize(schema)
|
121
|
+
@schema = schema
|
122
|
+
end
|
123
|
+
|
124
|
+
def property(prop_name, type: "string", description: nil, required: false, default: nil, **opts)
|
125
|
+
prop_definition = { type: type }
|
126
|
+
prop_definition[:description] = description if description && !description.empty?
|
127
|
+
prop_definition.merge!(opts) if opts.any?
|
128
|
+
|
129
|
+
@schema[:properties][prop_name.to_s] = prop_definition
|
130
|
+
@schema[:required] << prop_name.to_s if required
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# ---------------------------------------------------
|
135
|
+
# Convert Tool Definition to Hash
|
136
|
+
# ---------------------------------------------------
|
137
|
+
def self.to_h
|
138
|
+
schema = { type: "object", properties: self._schema_properties }
|
139
|
+
schema[:required] = self._required_properties if self._required_properties.any?
|
140
|
+
{
|
141
|
+
name: self.tool_name,
|
142
|
+
description: self.description.presence,
|
143
|
+
inputSchema: schema
|
144
|
+
}.compact
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
class Transport
|
5
|
+
HEARTBEAT_INTERVAL = 15 # seconds
|
6
|
+
|
7
|
+
def initialize(output_io)
|
8
|
+
# output_io can be any IO-like object where we write events.
|
9
|
+
@output = output_io
|
10
|
+
@output.sync = true
|
11
|
+
end
|
12
|
+
|
13
|
+
# Sends the capabilities JSON-RPC notification.
|
14
|
+
#
|
15
|
+
# @param request_id [String, Integer] The request identifier.
|
16
|
+
def send_capabilities(request_id)
|
17
|
+
payload = {
|
18
|
+
protocolVersion: "2024-11-05",
|
19
|
+
capabilities: {
|
20
|
+
tools: { listChanged: true },
|
21
|
+
prompts: { listChanged: true },
|
22
|
+
resources: { listChanged: true },
|
23
|
+
logging: {}
|
24
|
+
},
|
25
|
+
serverInfo: {
|
26
|
+
name: Rails.application.name,
|
27
|
+
version: Rails.application.version.to_s
|
28
|
+
}
|
29
|
+
}
|
30
|
+
send_jsonrpc_response(request_id, result: payload)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Sends the tools list JSON-RPC notification.
|
34
|
+
#
|
35
|
+
# @param request_id [String, Integer] The request identifier.
|
36
|
+
def send_tools_list(request_id)
|
37
|
+
tools = format_registry_items(ActionMCP::ToolsRegistry.available_tools)
|
38
|
+
send_jsonrpc_response(request_id, result: { tools: tools })
|
39
|
+
end
|
40
|
+
|
41
|
+
# Sends the resources list JSON-RPC response.
|
42
|
+
#
|
43
|
+
# @param request_id [String, Integer] The request identifier.
|
44
|
+
def send_resources_list(request_id)
|
45
|
+
begin
|
46
|
+
resources = ActionMCP::ResourcesRegistry.all_resources # fetch all resources
|
47
|
+
result_data = { "resources" => resources }
|
48
|
+
send_jsonrpc_response(request_id, result: result_data)
|
49
|
+
Rails.logger.info("resources/list: Returned #{resources.size} resources.")
|
50
|
+
rescue StandardError => e
|
51
|
+
Rails.logger.error("resources/list failed: #{e.message}")
|
52
|
+
error_obj = JsonRpcError.new(
|
53
|
+
:internal_error,
|
54
|
+
message: "Failed to list resources: #{e.message}"
|
55
|
+
).as_json
|
56
|
+
send_jsonrpc_response(request_id, error: error_obj)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Sends the resource templates list JSON-RPC response.
|
61
|
+
#
|
62
|
+
# @param request_id [String, Integer] The request identifier.
|
63
|
+
def send_resource_templates_list(request_id)
|
64
|
+
begin
|
65
|
+
templates = ActionMCP::ResourcesRegistry.all_templates # get all resource templates
|
66
|
+
result_data = { "resourceTemplates" => templates }
|
67
|
+
send_jsonrpc_response(request_id, result: result_data)
|
68
|
+
Rails.logger.info("resources/templates/list: Returned #{templates.size} resource templates.")
|
69
|
+
rescue StandardError => e
|
70
|
+
Rails.logger.error("resources/templates/list failed: #{e.message}")
|
71
|
+
error_obj = JsonRpcError.new(
|
72
|
+
:internal_error,
|
73
|
+
message: "Failed to list resource templates: #{e.message}"
|
74
|
+
).as_json
|
75
|
+
send_jsonrpc_response(request_id, error: error_obj)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Sends the resource read JSON-RPC response.
|
80
|
+
#
|
81
|
+
# @param request_id [String, Integer] The request identifier.
|
82
|
+
# @param params [Hash] The parameters including the 'uri' for the resource.
|
83
|
+
def send_resource_read(request_id, params)
|
84
|
+
uri = params&.fetch("uri", nil)
|
85
|
+
if uri.nil? || uri.empty?
|
86
|
+
Rails.logger.error("resources/read: 'uri' parameter is missing")
|
87
|
+
error_obj = JsonRpcError.new(
|
88
|
+
:invalid_params,
|
89
|
+
message: "Missing 'uri' parameter for resources/read"
|
90
|
+
).as_json
|
91
|
+
return send_jsonrpc_response(request_id, error: error_obj)
|
92
|
+
end
|
93
|
+
|
94
|
+
begin
|
95
|
+
content = ActionMCP::ResourcesRegistry.read(uri) # Expecting an instance of an ActionMCP::Content subclass
|
96
|
+
if content.nil?
|
97
|
+
Rails.logger.error("resources/read: Resource not found for URI #{uri}")
|
98
|
+
error_obj = JsonRpcError.new(
|
99
|
+
:invalid_params,
|
100
|
+
message: "Resource not found: #{uri}"
|
101
|
+
).as_json
|
102
|
+
return send_jsonrpc_response(request_id, error: error_obj)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Use the content object's `to_h` to build the JSON-RPC result.
|
106
|
+
result_data = { "contents" => [ content.to_h ] }
|
107
|
+
send_jsonrpc_response(request_id, result: result_data)
|
108
|
+
|
109
|
+
log_msg = "resources/read: Successfully read content of #{uri}"
|
110
|
+
log_msg += " (#{content.text.size} bytes)" if content.respond_to?(:text) && content.text
|
111
|
+
Rails.logger.info(log_msg)
|
112
|
+
rescue StandardError => e
|
113
|
+
Rails.logger.error("resources/read: Error reading #{uri} - #{e.message}")
|
114
|
+
error_obj = JsonRpcError.new(
|
115
|
+
:internal_error,
|
116
|
+
message: "Failed to read resource: #{e.message}"
|
117
|
+
).as_json
|
118
|
+
send_jsonrpc_response(request_id, error: error_obj)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
# Sends a call to a tool. Currently logs the call details.
|
124
|
+
#
|
125
|
+
# @param request_id [String, Integer] The request identifier.
|
126
|
+
# @param tool_name [String] The name of the tool.
|
127
|
+
# @param params [Hash] The parameters for the tool.
|
128
|
+
def send_tools_call(request_id, tool_name, params)
|
129
|
+
begin
|
130
|
+
tool = ActionMCP::ToolsRegistry.fetch_available_tool(tool_name.to_s)
|
131
|
+
Rails.logger.info("Sending tool call: #{tool_name} with params: #{params}")
|
132
|
+
# TODO: Implement tool call handling and response if needed.
|
133
|
+
rescue StandardError => e
|
134
|
+
Rails.logger.error("tools/call: Failed to call tool #{tool_name} - #{e.message}")
|
135
|
+
error_obj = JsonRpcError.new(
|
136
|
+
:internal_error,
|
137
|
+
message: "Failed to call tool #{tool_name}: #{e.message}"
|
138
|
+
).as_json
|
139
|
+
send_jsonrpc_response(request_id, error: error_obj)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Sends the prompts list JSON-RPC notification.
|
144
|
+
#
|
145
|
+
# @param request_id [String, Integer] The request identifier.
|
146
|
+
def send_prompts_list(request_id)
|
147
|
+
begin
|
148
|
+
prompts = format_registry_items(ActionMCP::PromptsRegistry.available_prompts)
|
149
|
+
send_jsonrpc_response(request_id, result: {prompts: prompts} )
|
150
|
+
rescue StandardError => e
|
151
|
+
Rails.logger.error("prompts/list failed: #{e.message}")
|
152
|
+
error_obj = JsonRpcError.new(
|
153
|
+
:internal_error,
|
154
|
+
message: "Failed to list prompts: #{e.message}"
|
155
|
+
).as_json
|
156
|
+
send_jsonrpc_response(request_id, error: error_obj)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def send_prompts_get(request_id, params)
|
161
|
+
prompt_name = params&.fetch("name", nil)
|
162
|
+
if prompt_name.nil? || prompt_name.strip.empty?
|
163
|
+
Rails.logger.error("prompts/get: 'name' parameter is missing")
|
164
|
+
error_obj = JsonRpcError.new(
|
165
|
+
:invalid_params,
|
166
|
+
message: "Missing 'name' parameter for prompts/get"
|
167
|
+
).as_json
|
168
|
+
return send_jsonrpc_response(request_id, error: error_obj)
|
169
|
+
end
|
170
|
+
|
171
|
+
begin
|
172
|
+
# Assume a method similar to fetch_available_tool exists for prompts.
|
173
|
+
prompt = ActionMCP::PromptsRegistry.fetch_available_prompt(prompt_name.to_s)
|
174
|
+
if prompt.nil?
|
175
|
+
Rails.logger.error("prompts/get: Prompt not found for name #{prompt_name}")
|
176
|
+
error_obj = JsonRpcError.new(
|
177
|
+
:invalid_params,
|
178
|
+
message: "Prompt not found: #{prompt_name}"
|
179
|
+
).as_json
|
180
|
+
return send_jsonrpc_response(request_id, error: error_obj)
|
181
|
+
end
|
182
|
+
|
183
|
+
result_data = { "prompt" => prompt.to_h }
|
184
|
+
send_jsonrpc_response(request_id, result: result_data)
|
185
|
+
Rails.logger.info("prompts/get: Returned prompt #{prompt_name}")
|
186
|
+
rescue StandardError => e
|
187
|
+
Rails.logger.error("prompts/get: Error retrieving prompt #{prompt_name} - #{e.message}")
|
188
|
+
error_obj = JsonRpcError.new(
|
189
|
+
:internal_error,
|
190
|
+
message: "Failed to get prompt: #{e.message}"
|
191
|
+
).as_json
|
192
|
+
send_jsonrpc_response(request_id, error: error_obj)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
# Sends a JSON-RPC pong response.
|
198
|
+
# We don't actually to send any data back because the spec are not fun anymore.
|
199
|
+
#
|
200
|
+
# @param request_id [String, Integer] The request identifier.
|
201
|
+
def send_pong(request_id)
|
202
|
+
send_jsonrpc_response(request_id, result: {})
|
203
|
+
end
|
204
|
+
|
205
|
+
# Sends a JSON-RPC response.
|
206
|
+
#
|
207
|
+
# @param request_id [String, Integer] The request identifier.
|
208
|
+
# @param result [Object] The result data.
|
209
|
+
# @param error [Object, nil] The error data, if any.
|
210
|
+
def send_jsonrpc_response(request_id, result: nil, error: nil)
|
211
|
+
response = JsonRpc::Response.new(id: request_id, result: result, error: error)
|
212
|
+
write_message(response.to_json)
|
213
|
+
end
|
214
|
+
|
215
|
+
# Sends a generic JSON-RPC notification (no response expected).
|
216
|
+
#
|
217
|
+
# @param method [String] The JSON-RPC method.
|
218
|
+
# @param params [Hash] The parameters for the method.
|
219
|
+
def send_jsonrpc_notification(method, params = {})
|
220
|
+
notification = JsonRpc::Notification.new(method: method, params: params)
|
221
|
+
write_message(notification.to_json)
|
222
|
+
end
|
223
|
+
|
224
|
+
private
|
225
|
+
|
226
|
+
# Formats registry items to a hash representation.
|
227
|
+
#
|
228
|
+
# @param registry [Hash] The registry containing tool or prompt definitions.
|
229
|
+
# @return [Array<Hash>] The formatted registry items.
|
230
|
+
def format_registry_items(registry)
|
231
|
+
registry.map { |_, item| item[:class].to_h }
|
232
|
+
end
|
233
|
+
|
234
|
+
# Writes a message to the output IO.
|
235
|
+
#
|
236
|
+
# @param data [String] The data to write.
|
237
|
+
def write_message(data)
|
238
|
+
Rails.logger.debug("Response Sent: #{data}")
|
239
|
+
@output.write("#{data}\n")
|
240
|
+
rescue IOError => e
|
241
|
+
Rails.logger.error("Failed to write message: #{e.message}")
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
data/lib/action_mcp.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails"
|
4
|
+
require "active_support"
|
5
|
+
require "active_model"
|
6
|
+
require "action_mcp/version"
|
7
|
+
require "action_mcp/railtie" if defined?(Rails)
|
8
|
+
|
9
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
10
|
+
inflect.acronym "MCP"
|
11
|
+
end
|
12
|
+
module ActionMCP
|
13
|
+
extend ActiveSupport::Autoload
|
14
|
+
|
15
|
+
autoload :RegistryBase
|
16
|
+
autoload :Resource
|
17
|
+
autoload :ToolsRegistry
|
18
|
+
autoload :PromptsRegistry
|
19
|
+
autoload :Tool
|
20
|
+
autoload :Prompt
|
21
|
+
autoload :JsonRpc
|
22
|
+
|
23
|
+
module_function
|
24
|
+
def tools
|
25
|
+
ToolsRegistry.tools
|
26
|
+
end
|
27
|
+
|
28
|
+
def prompts
|
29
|
+
PromptsRegistry.prompts
|
30
|
+
end
|
31
|
+
|
32
|
+
def available_tools
|
33
|
+
ToolsRegistry.available_tools
|
34
|
+
end
|
35
|
+
|
36
|
+
def available_prompts
|
37
|
+
PromptsRegistry.available_prompts
|
38
|
+
end
|
39
|
+
end
|
data/lib/actionmcp.rb
ADDED
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: actionmcp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Abdelkader Boudih
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-02-14 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: activemodel
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 8.0.1
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 8.0.1
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: activesupport
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 8.0.1
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 8.0.1
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: multi_json
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
description: It offers base classes and helpers for creating MCP applications, making
|
55
|
+
it easier to integrate your Ruby/Rails application with the MCP standard
|
56
|
+
email:
|
57
|
+
- terminale@gmail.com
|
58
|
+
executables:
|
59
|
+
- action_mcp_stdio
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- MIT-LICENSE
|
64
|
+
- README.md
|
65
|
+
- Rakefile
|
66
|
+
- exe/action_mcp_stdio
|
67
|
+
- lib/action_mcp.rb
|
68
|
+
- lib/action_mcp/content.rb
|
69
|
+
- lib/action_mcp/content/audio.rb
|
70
|
+
- lib/action_mcp/content/image.rb
|
71
|
+
- lib/action_mcp/content/resource.rb
|
72
|
+
- lib/action_mcp/content/text.rb
|
73
|
+
- lib/action_mcp/gem_version.rb
|
74
|
+
- lib/action_mcp/json_rpc.rb
|
75
|
+
- lib/action_mcp/json_rpc/base.rb
|
76
|
+
- lib/action_mcp/json_rpc/json_rpc_error.rb
|
77
|
+
- lib/action_mcp/json_rpc/notification.rb
|
78
|
+
- lib/action_mcp/json_rpc/request.rb
|
79
|
+
- lib/action_mcp/json_rpc/response.rb
|
80
|
+
- lib/action_mcp/prompt.rb
|
81
|
+
- lib/action_mcp/prompts_registry.rb
|
82
|
+
- lib/action_mcp/railtie.rb
|
83
|
+
- lib/action_mcp/registry_base.rb
|
84
|
+
- lib/action_mcp/resource.rb
|
85
|
+
- lib/action_mcp/resources_bank.rb
|
86
|
+
- lib/action_mcp/tool.rb
|
87
|
+
- lib/action_mcp/tools_registry.rb
|
88
|
+
- lib/action_mcp/transport.rb
|
89
|
+
- lib/action_mcp/version.rb
|
90
|
+
- lib/actionmcp.rb
|
91
|
+
- lib/tasks/action_mcp_tasks.rake
|
92
|
+
homepage: https://github.com/seuros/action_mcp
|
93
|
+
licenses:
|
94
|
+
- MIT
|
95
|
+
metadata:
|
96
|
+
homepage_uri: https://github.com/seuros/action_mcp
|
97
|
+
source_code_uri: https://github.com/seuros/action_mcp
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
requirements: []
|
112
|
+
rubygems_version: 3.6.2
|
113
|
+
specification_version: 4
|
114
|
+
summary: Provides essential tooling for building Model Context Protocol (MCP) capable
|
115
|
+
servers
|
116
|
+
test_files: []
|