actionmcp 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1183f05df43cca3948c84aa105f6e38c5f7cd414c1b38619cfb6c9afa528d442
4
- data.tar.gz: 02bdfed4f0a3b26d04203430c4b62183c615fb0a47a3b28d281b42f07928e5a8
3
+ metadata.gz: 40bfac6cf4d8eff1e92f16eb58c315e3da4578ad69d10bdc83293dcfad100d5b
4
+ data.tar.gz: 564672c4c804b061fd53f3f905c6f092ae06b0cb596d3e4bea50adfc0fce8449
5
5
  SHA512:
6
- metadata.gz: 77b9a85435b5e19a35268db9fbcc3139222c01abc7c67e5af61a44a460d55fe5ec6b9819209fa8bfd493a3d0c648ba7d7fa5c440f889871556880f5f961a1da0
7
- data.tar.gz: e77783308752dbbe95498ca9d3548471b44bafbfe72a130f6e070c906220eeeecd48fb8d719f150d2f69a14a416d7f0cba19c3fd33b6479eab54f564633eed73
6
+ metadata.gz: ed00e79436b6a3afbbeb5cc611d4ddc0f761b71d76ec90e368aa1463c42d98aaa13fbadc73d0871d6715244b4bf3ad5f8ad3b13dc5d0aafc33399c2973885372
7
+ data.tar.gz: f40213363967fea34a30ec731b3048b0c54b2bb112a4f3e3f793605946a1c8f2753d9a124f15efe87f79cf53d412700393ec65d56d606b05f5601b7ad07699c9
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionMCP
2
4
  module Content
3
5
  extend ActiveSupport::Autoload
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionMCP
2
4
  # Returns the currently loaded version of Active MCP as a +Gem::Version+.
3
5
  def self.gem_version
@@ -2,19 +2,19 @@
2
2
 
3
3
  module ActionMCP
4
4
  module JsonRpc
5
- Notification = Data.define(:method, :params) do
6
- def initialize(method:, params: nil)
7
- super(method: method, params: params)
8
- end
5
+ Notification = Data.define(:method, :params) do
6
+ def initialize(method:, params: nil)
7
+ super(method: method, params: params)
8
+ end
9
9
 
10
- def to_h
11
- hash = {
12
- jsonrpc: "2.0",
13
- method: method
14
- }
15
- hash[:params] = params if params
16
- hash
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
- 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
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
- def to_h
12
- hash = {
13
- jsonrpc: "2.0",
14
- id: id,
15
- method: method
16
- }
17
- hash[:params] = params if params
18
- hash
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
- 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
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
- def to_h
14
- hash = {
15
- jsonrpc: "2.0",
16
- id: id
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
- hash[:error][:data] = error[:data] if error[:data]
24
- else
25
- hash[:result] = result
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
- private
30
+ private
31
31
 
32
- def process_error(error)
33
- case error
34
- when Symbol
35
- ErrorCodes[error]
36
- when Hash
37
- validate_error!(error)
38
- error
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
- 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
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
- 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
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
@@ -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" == subclass.name
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
- self.abstract_prompt
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) # Initialize an instance with provided 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(", ")
@@ -3,8 +3,8 @@
3
3
  module ActionMCP
4
4
  class PromptsRegistry < RegistryBase
5
5
  class << self
6
- alias_method :prompts, :items
7
- alias_method :available_prompts, :enabled
6
+ alias prompts items
7
+ alias available_prompts enabled
8
8
  end
9
9
  end
10
10
  end
@@ -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
- # TODO: fix this to be a proper railtie if you going to to opensource it
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
- 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 }
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}"
@@ -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" == subclass.name
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(self.tool_name) if ToolsRegistry.items.key?(self.tool_name)
29
+ ToolsRegistry.unregister(tool_name) if ToolsRegistry.items.key?(tool_name)
29
30
  end
30
31
 
31
32
  def self.abstract?
32
- self.abstract_tool
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
- self._required_properties << prop_name.to_s if required
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, **opts, &block)
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
- self._required_properties << prop_name.to_s if required
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: self._schema_properties }
139
- schema[:required] = self._required_properties if self._required_properties.any?
140
+ schema = { type: "object", properties: _schema_properties }
141
+ schema[:required] = _required_properties if _required_properties.any?
140
142
  {
141
- name: self.tool_name,
142
- description: self.description.presence,
143
+ name: tool_name,
144
+ description: description.presence,
143
145
  inputSchema: schema
144
146
  }.compact
145
147
  end
@@ -5,8 +5,8 @@
5
5
  module ActionMCP
6
6
  class ToolsRegistry < RegistryBase
7
7
  class << self
8
- alias_method :tools, :items
9
- alias_method :available_tools, :enabled
8
+ alias tools items
9
+ alias available_tools enabled
10
10
  end
11
11
  end
12
12
  end
@@ -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: Rails.application.name,
27
- version: Rails.application.version.to_s
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
- 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
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
- 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
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::ResourcesRegistry.read(uri) # Expecting an instance of an ActionMCP::Content subclass
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
- 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
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
- 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
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
  #
@@ -3,7 +3,7 @@
3
3
  require_relative "gem_version"
4
4
 
5
5
  module ActionMCP
6
- VERSION = "0.1.0"
6
+ VERSION = "0.1.2"
7
7
  # Returns the currently loaded version of Active MCP as a +Gem::Version+.
8
8
  def self.version
9
9
  gem_version
data/lib/action_mcp.rb CHANGED
@@ -16,11 +16,25 @@ module ActionMCP
16
16
  autoload :Resource
17
17
  autoload :ToolsRegistry
18
18
  autoload :PromptsRegistry
19
+ autoload :ResourcesBank
19
20
  autoload :Tool
20
21
  autoload :Prompt
21
22
  autoload :JsonRpc
23
+ eager_autoload do
24
+ autoload :Configuration
25
+ end
26
+
27
+ # Accessor for the configuration instance.
28
+ def self.configuration
29
+ @configuration ||= Configuration.new
30
+ end
31
+
32
+ def self.configure
33
+ yield(configuration)
34
+ end
22
35
 
23
36
  module_function
37
+
24
38
  def tools
25
39
  ToolsRegistry.tools
26
40
  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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationPrompt < ActionMCP::Prompt
4
+ abstract!
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationTool < ActionMCP::Tool
4
+ abstract!
5
+ 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.0
4
+ version: 0.1.2
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: