actionmcp 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|