actionmcp 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/app/models/action_mcp/session/message.rb +31 -1
  4. data/app/models/action_mcp/session/resource.rb +33 -0
  5. data/app/models/action_mcp/session/subscription.rb +30 -0
  6. data/app/models/action_mcp/session.rb +35 -1
  7. data/db/migrate/20250316005021_create_action_mcp_session_subscriptions.rb +14 -0
  8. data/db/migrate/20250316005649_create_action_mcp_session_resources.rb +23 -0
  9. data/lib/action_mcp/prompt.rb +50 -31
  10. data/lib/action_mcp/prompt_response.rb +43 -0
  11. data/lib/action_mcp/renderable.rb +6 -15
  12. data/lib/action_mcp/tool.rb +56 -13
  13. data/lib/action_mcp/tool_response.rb +76 -0
  14. data/lib/action_mcp/tools_registry.rb +2 -21
  15. data/lib/action_mcp/version.rb +1 -1
  16. data/lib/generators/action_mcp/install/install_generator.rb +3 -3
  17. data/lib/generators/action_mcp/install/templates/{application_prompt.rb → application_mcp_prompt.rb} +1 -1
  18. data/lib/generators/action_mcp/install/templates/application_mcp_res_template.rb +3 -0
  19. data/lib/generators/action_mcp/install/templates/{application_tool.rb → application_mcp_tool.rb} +1 -1
  20. data/lib/generators/action_mcp/prompt/templates/prompt.rb.erb +1 -1
  21. data/lib/generators/action_mcp/resource_template/resource_template_generator.rb +1 -1
  22. data/lib/generators/action_mcp/resource_template/templates/resource_template.rb.erb +1 -1
  23. data/lib/generators/action_mcp/tool/templates/tool.rb.erb +1 -1
  24. data/lib/generators/action_mcp/tool/tool_generator.rb +1 -1
  25. metadata +11 -5
  26. data/lib/generators/action_mcp/install/templates/mcp_resource_template.rb +0 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a031f1969028d88d34f7f19bc18777fee99067dee692ce73754bbb001477f14
4
- data.tar.gz: 35c84b344020e97ce87d9c7e033220138732c29bd7d7cee0634e4f9c7f38cc13
3
+ metadata.gz: a310c4bc57df9c1cd4d0fceb7b80fd9628414609bb6131e5d420584f5605f59c
4
+ data.tar.gz: 881fb9d39845277adcb24d42e73475dfc9aca5e25afb2b049c5ee55446aca474
5
5
  SHA512:
6
- metadata.gz: 953dd645cb4006c027ca2643aa8274a5011db73910159f4a0a3d5c60e80df82deae14f37ec41de869e863e7b4a8e1440579863bc33daa21185dc169abc4ddcbe
7
- data.tar.gz: 22f1e9622b4330afb13bc1fd943142904f8fcbd07df734e571707c428b2d11c343775900221a33a74df354cae4a47fa36bd13e97d3809aa930b4ad2af40b66b5
6
+ metadata.gz: 7e4caed2e47ffb767e8c6980a07542d093cc5bb7dd1d0438c342e0ec7a9c6af56e0409b108955b229f0fa4147d263a3060ce72a2864f7dc5405aab9f28e66bf6
7
+ data.tar.gz: ddb5fa89098629fecd125c317d935a0d0f3e4d50d828f4cd5d3b1c4b435be648b973235961af62e566431d1f991d4f3f0e11aeb7812b6423e0e4186cb4785b73
data/README.md CHANGED
@@ -105,7 +105,8 @@ bin/rails generate action_mcp:prompt AnalyzeCode
105
105
  This command will create a file at `app/mcp/prompts/analyze_code_prompt.rb` with content similar to:
106
106
 
107
107
  ```ruby
108
- class AnalyzeCodePrompt < ApplicationPrompt
108
+
109
+ class AnalyzeCodePrompt < ApplicationMCPPrompt
109
110
  # Override the prompt_name (otherwise we'd get "analyze_code")
110
111
  prompt_name "analyze-code"
111
112
 
@@ -118,7 +119,7 @@ class AnalyzeCodePrompt < ApplicationPrompt
118
119
 
119
120
  # Add validations
120
121
  validates :language, inclusion: { in: %w[Ruby C Cobol FORTRAN] }
121
-
122
+
122
123
  def call
123
124
  # Implement your prompt logic here
124
125
  render(text: "Analyzing #{language} code: #{code}")
@@ -137,7 +138,7 @@ bin/rails generate action_mcp:tool CalculateSum
137
138
  This command will create a file at `app/mcp/tools/calculate_sum_tool.rb` with content similar to:
138
139
 
139
140
  ```ruby
140
- class CalculateSumTool < ApplicationTool
141
+ class CalculateSumTool < ApplicationMCPTool
141
142
  tool_name "calculate_sum"
142
143
  description "Calculate the sum of two numbers"
143
144
 
@@ -1,4 +1,34 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: action_mcp_session_messages
4
+ #
5
+ # id :bigint not null, primary key
6
+ # direction(The message recipient) :string default("client"), not null
7
+ # is_ping(Whether the message is a ping) :boolean default(FALSE), not null
8
+ # message_json :jsonb
9
+ # message_text :string
10
+ # message_type(The type of the message) :string not null
11
+ # request_acknowledged :boolean default(FALSE), not null
12
+ # request_cancelled :boolean default(FALSE), not null
13
+ # created_at :datetime not null
14
+ # updated_at :datetime not null
15
+ # jsonrpc_id :string
16
+ # session_id :string not null
17
+ #
18
+ # Indexes
19
+ #
20
+ # index_action_mcp_session_messages_on_session_id (session_id)
21
+ #
22
+ # Foreign Keys
23
+ #
24
+ # fk_action_mcp_session_messages_session_id (session_id => action_mcp_sessions.id) ON DELETE => cascade ON UPDATE => cascade
25
+ #
1
26
  module ActionMCP
27
+ ##
28
+ # Represents a message exchanged during an MCP session.
29
+ # Its role is to store the content and metadata of each message,
30
+ # including the direction (client or server), message type (request, response, notification),
31
+ # and any associated JSON-RPC ID.
2
32
  class Session::Message < ApplicationRecord
3
33
  belongs_to :session,
4
34
  class_name: "ActionMCP::Session",
@@ -99,7 +129,7 @@ module ActionMCP
99
129
  )
100
130
  if request_message&.is_ping
101
131
  self.is_ping = true
102
- request_message.update(ping_acknowledged: true)
132
+ request_message.update(request_acknowledged: true)
103
133
  save! if changed?
104
134
  end
105
135
  end
@@ -0,0 +1,33 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: action_mcp_session_resources
4
+ #
5
+ # id :bigint not null, primary key
6
+ # created_by_tool :boolean default(FALSE)
7
+ # description :text
8
+ # last_accessed_at :datetime
9
+ # metadata :json
10
+ # mime_type :string not null
11
+ # name :string
12
+ # uri :string not null
13
+ # created_at :datetime not null
14
+ # updated_at :datetime not null
15
+ # session_id :string not null
16
+ #
17
+ # Indexes
18
+ #
19
+ # index_action_mcp_session_resources_on_session_id (session_id)
20
+ #
21
+ # Foreign Keys
22
+ #
23
+ # fk_rails_... (session_id => action_mcp_sessions.id) ON DELETE => cascade
24
+ #
25
+ module ActionMCP
26
+ ##
27
+ # Represents a resource associated with an MCP session.
28
+ # Its role is to store information about a resource, such as its URI, MIME type, description,
29
+ # and any associated metadata. It also tracks whether the resource was created by a tool and the last time it was accessed.
30
+ class Session::Resource < ApplicationRecord
31
+ belongs_to :session
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: action_mcp_session_subscriptions
4
+ #
5
+ # id :bigint not null, primary key
6
+ # last_notification_at :datetime
7
+ # uri :string not null
8
+ # created_at :datetime not null
9
+ # updated_at :datetime not null
10
+ # session_id :string not null
11
+ #
12
+ # Indexes
13
+ #
14
+ # index_action_mcp_session_subscriptions_on_session_id (session_id)
15
+ #
16
+ # Foreign Keys
17
+ #
18
+ # fk_rails_... (session_id => action_mcp_sessions.id) ON DELETE => cascade
19
+ #
20
+ module ActionMCP
21
+ #
22
+ # Represents a client's subscription to a resource for real-time updates.
23
+ # Its role is to store the URI of the resource being subscribed to and track the last time a notification was sent for the subscription.
24
+ # All Subscriptions are deleted when the session is closed.
25
+ class Session::Subscription < ApplicationRecord
26
+ belongs_to :session,
27
+ class_name: "ActionMCP::Session",
28
+ inverse_of: :subscriptions
29
+ end
30
+ end
@@ -1,11 +1,44 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: action_mcp_sessions
4
+ #
5
+ # id :string not null, primary key
6
+ # client_capabilities(The capabilities of the client) :jsonb
7
+ # client_info(The information about the client) :jsonb
8
+ # ended_at(The time the session ended) :datetime
9
+ # initialized :boolean default(FALSE), not null
10
+ # messages_count :integer default(0), not null
11
+ # protocol_version :string
12
+ # role(The role of the session) :string default("server"), not null
13
+ # server_capabilities(The capabilities of the server) :jsonb
14
+ # server_info(The information about the server) :jsonb
15
+ # status :string default("pre_initialize"), not null
16
+ # created_at :datetime not null
17
+ # updated_at :datetime not null
18
+ #
1
19
  module ActionMCP
20
+ ##
21
+ # Represents an MCP session, which is a connection between a client and a server.
22
+ # Its role is to manage the communication channel and store information about the session,
23
+ # such as client and server capabilities, protocol version, and session status.
24
+ # It also manages the association with messages and subscriptions related to the session.
2
25
  class Session < ApplicationRecord
3
26
  attribute :id, :string, default: -> { SecureRandom.hex(6) }
4
27
  has_many :messages,
5
28
  class_name: "ActionMCP::Session::Message",
6
29
  foreign_key: "session_id",
7
- dependent: :destroy,
30
+ dependent: :delete_all,
8
31
  inverse_of: :session
32
+ has_many :subscriptions,
33
+ class_name: "ActionMCP::Session::Subscription",
34
+ foreign_key: "session_id",
35
+ dependent: :delete_all,
36
+ inverse_of: :session
37
+ has_many :resources,
38
+ class_name: "ActionMCP::Session::Resource",
39
+ foreign_key: "session_id",
40
+ dependent: :delete_all,
41
+ inverse_of: :session
9
42
 
10
43
  scope :pre_initialize, -> { where(status: "pre_initialize") }
11
44
  scope :closed, -> { where(status: "closed") }
@@ -20,6 +53,7 @@ module ActionMCP
20
53
  dummy_callback = ->(*) { } # this callback seem broken
21
54
  adapter.unsubscribe(session_key, dummy_callback)
22
55
  update!(status: "closed", ended_at: Time.zone.now)
56
+ subscriptions.delete_all # delete all subscriptions
23
57
  end
24
58
 
25
59
  def write(data)
@@ -0,0 +1,14 @@
1
+ class CreateActionMCPSessionSubscriptions < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :action_mcp_session_subscriptions do |t|
4
+ t.references :session,
5
+ null: false,
6
+ foreign_key: { to_table: :action_mcp_sessions, on_delete: :cascade },
7
+ type: :string
8
+ t.string :uri, null: false
9
+ t.datetime :last_notification_at
10
+
11
+ t.timestamps
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ class CreateActionMCPSessionResources < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :action_mcp_session_resources do |t|
4
+ t.references :session,
5
+ null: false,
6
+ foreign_key: { to_table: :action_mcp_sessions, on_delete: :cascade },
7
+ type: :string
8
+ t.string :uri, null: false
9
+ t.string :name
10
+ t.text :description
11
+ t.string :mime_type, null: false
12
+ t.boolean :created_by_tool, default: false
13
+ t.datetime :last_accessed_at
14
+ t.json :metadata
15
+
16
+ t.timestamps
17
+ end
18
+ change_column_comment :action_mcp_session_messages, :direction, "The message recipient"
19
+ change_column_comment :action_mcp_session_messages, :is_ping, "Whether the message is a ping"
20
+ rename_column :action_mcp_session_messages, :ping_acknowledged, :request_acknowledged
21
+ add_column :action_mcp_session_messages, :request_cancelled, :boolean, null: false, default: false
22
+ end
23
+ end
@@ -42,7 +42,6 @@ module ActionMCP
42
42
  # @param default [Object] The default value of the argument.
43
43
  # @param enum [Array<String>] The list of allowed values for the argument.
44
44
  # @return [void]
45
- # Argument DSL
46
45
  def self.argument(arg_name, description: "", required: false, default: nil, enum: nil)
47
46
  arg_def = {
48
47
  name: arg_name.to_s,
@@ -88,47 +87,67 @@ module ActionMCP
88
87
  # validates it, and if valid, calls the instance call method.
89
88
  # If invalid, raises a JsonRpcError with code :invalid_params.
90
89
  #
91
- # Usage:
92
- # result = MyPromptClass.call(params)
93
- #
94
- # Raises:
95
- # ActionMCP::JsonRpc::JsonRpcError(:invalid_params) if validation fails.
96
- #
97
90
  # @param params [Hash] The parameters for the prompt.
98
- # @return [Object] The result of the prompt's call method.
91
+ # @return [PromptResponse] The result of the prompt's call method.
99
92
  def self.call(params)
100
93
  prompt = new(params) # Initialize an instance with provided params
101
- unless prompt.valid?
102
- # Collect all validation errors into a single string or array
103
- errors_str = prompt.errors.full_messages.join(", ")
104
-
105
- raise ActionMCP::JsonRpc::JsonRpcError.new(
106
- :invalid_params,
107
- message: "Prompt validation failed: #{errors_str}",
108
- data: { errors: prompt.errors }
109
- )
110
- end
111
94
 
112
95
  # If we reach here, the prompt is valid
113
96
  prompt.call
114
97
  end
115
98
 
116
99
  # ---------------------------------------------------
117
- # Instance call method
100
+ # Instance Methods
118
101
  # ---------------------------------------------------
119
- # By default, does nothing. Override in your subclasses to
120
- # perform custom prompt processing. (Return a payload if needed)
121
- #
122
- # Usage: Called internally after validation in self.call
123
- #
124
- # @raise [NotImplementedError] Subclasses must implement the call method.
125
- # @return [Array<Content>] Array of Content objects is expected as return value
102
+
103
+ # Public entry point for executing the prompt
104
+ # Returns a PromptResponse object containing messages
126
105
  def call
127
- raise NotImplementedError, "Subclasses must implement the call method"
128
- # Default implementation (no-op)
129
- # In a real subclass, you might do:
130
- # # Perform logic, e.g. analyze code, etc.
131
- # # Return something meaningful.
106
+ @response = PromptResponse.new
107
+
108
+ # Check validations before proceeding
109
+ if valid?
110
+ begin
111
+ perform # Invoke the subclass-specific logic if valid
112
+ rescue => e
113
+ # Handle exceptions during execution
114
+ render text: "Error executing prompt: #{e.message}"
115
+ end
116
+ else
117
+ # Handle validation failure
118
+ render text: "Invalid input: #{errors.full_messages.join(', ')}"
119
+ end
120
+
121
+ @response # Return the response with collected messages
122
+ end
123
+
124
+ def inspect
125
+ attributes_hash = attributes.transform_values(&:inspect)
126
+
127
+ response_info = if defined?(@response) && @response
128
+ "response: #{@response.messages.size} message(s)"
129
+ else
130
+ "response: nil"
131
+ end
132
+
133
+ errors_info = errors.any? ? ", errors: #{errors.full_messages}" : ""
134
+
135
+ "#<#{self.class.name} #{attributes_hash.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}, #{response_info}#{errors_info}>"
136
+ end
137
+
138
+ # Override render to collect messages
139
+ def render(**args)
140
+ content = super(**args.slice(:text, :audio, :image, :resource, :mime_type, :blob))
141
+ @response.add_content(content, role: args.fetch(:role, "user")) # Add to the response
142
+ content # Return the content for potential use in perform
143
+ end
144
+
145
+ protected
146
+
147
+ # Abstract method for subclasses to implement their logic
148
+ # Expected to use render to produce Content objects or add_message for messages
149
+ def perform
150
+ raise NotImplementedError, "Subclasses must implement the perform method"
132
151
  end
133
152
  end
134
153
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ class PromptResponse
5
+ attr_reader :messages, :description
6
+
7
+ def initialize
8
+ @messages = []
9
+ end
10
+
11
+ # Add a message to the response
12
+ def add_message(role:, content:)
13
+ @messages << { role: role, content: content }
14
+ self
15
+ end
16
+
17
+ # Add content directly (will be added as a user message)
18
+ def add_content(content, role:)
19
+ add_message(role: role, content: content.to_h)
20
+ self
21
+ end
22
+
23
+ # Convert to hash format expected by MCP protocol
24
+ def to_h
25
+ {
26
+ messages: @messages
27
+ }
28
+ end
29
+
30
+ # Alias to_h to as_json for consistency
31
+ alias_method :to_h, :as_json
32
+
33
+ # Handle to_json directly
34
+ def to_json(options = nil)
35
+ to_h.to_json(options)
36
+ end
37
+
38
+ # Pretty print for better debugging
39
+ def inspect
40
+ "#<#{self.class.name} messages: #{messages.size}"
41
+ end
42
+ end
43
+ end
@@ -8,36 +8,27 @@ module ActionMCP
8
8
  # @param text [String, nil] Text content to render
9
9
  # @param audio [String, nil] Audio content to render
10
10
  # @param image [String, nil] Image content to render
11
- # @param resource [String, nil] Resource content to render
12
- # @param error [Array, nil] Array of error messages to render
11
+ # @param resource [String, nil] URI for resource content
13
12
  # @param mime_type [String, nil] MIME type for audio, image, or resource content
14
- # @param uri [String, nil] URI for resource content
15
13
  # @param blob [String, nil] Binary data for resource content
16
14
  #
17
- # @return [Content::Text, Content::Audio, Content::Image, Content::Resource, Hash]
18
- # The rendered content object or error hash
15
+ # @return [Content::Text, Content::Audio, Content::Image, Content::Resource]
16
+ # The rendered content object
19
17
  #
20
18
  # @raise [ArgumentError] If no valid content parameters are provided
21
19
  #
22
20
  # @example Render text content
23
21
  # render(text: "Hello, world!")
24
22
  #
25
- # @example Render an error
26
- # render(error: ["Invalid input", "Please try again"])
27
- def render(text: nil, audio: nil, image: nil, resource: nil, error: nil, mime_type: nil, uri: nil, blob: nil)
23
+ def render(text: nil, audio: nil, image: nil, resource: nil, mime_type: nil, blob: nil)
28
24
  if text
29
25
  Content::Text.new(text)
30
26
  elsif audio && mime_type
31
27
  Content::Audio.new(audio, mime_type)
32
28
  elsif image && mime_type
33
29
  Content::Image.new(image, mime_type)
34
- elsif resource && uri && mime_type
35
- Content::Resource.new(uri, mime_type, text: text, blob: blob)
36
- elsif error
37
- {
38
- isError: true,
39
- content: error.map { |e| render(text: e) }
40
- }
30
+ elsif resource && mime_type
31
+ Content::Resource.new(resource, mime_type, text: text, blob: blob)
41
32
  else
42
33
  raise ArgumentError, "No content to render"
43
34
  end
@@ -122,24 +122,67 @@ module ActionMCP
122
122
  # --------------------------------------------------------------------------
123
123
  # Instance Methods
124
124
  # --------------------------------------------------------------------------
125
- # Abstract method to perform the tool's action.
126
- #
127
- # Subclasses must implement this method.
128
- #
129
- # @raise [NotImplementedError] Always raised if not implemented in a subclass.
130
- # @return [Array<Content>] Array of Content objects is expected as return value
125
+
126
+ # Public entry point for executing the tool
127
+ # Returns an array of Content objects collected from render calls
131
128
  def call
132
- raise NotImplementedError, "Subclasses must implement the call method"
133
- # Default implementation (no-op)
134
- # In a real subclass, you might do:
135
- # def call
136
- # # Perform logic, e.g. analyze code, etc.
137
- # # Array of Content objects is expected as return value
138
- # end
129
+ @response = ToolResponse.new # Create a new response for each invocation
130
+
131
+ # Check validations before proceeding
132
+ if valid?
133
+ begin
134
+ perform # Invoke the subclass-specific logic if valid
135
+ rescue => e
136
+ # Handle exceptions during execution
137
+ @response.mark_as_error!
138
+ render text: "Error executing tool: #{e.message}"
139
+ end
140
+ else
141
+ # Handle validation failure
142
+ @response.mark_as_error!
143
+ render text: "Invalid input: #{errors.full_messages.join(', ')}"
144
+ end
145
+
146
+ @response # Return the response with collected content
147
+ end
148
+
149
+ def inspect
150
+ attributes_hash = attributes.transform_values(&:inspect)
151
+
152
+ response_info = if defined?(@response) && @response
153
+ "response: #{@response.contents.size} content(s), isError: #{@response.is_error}"
154
+ else
155
+ "response: nil"
156
+ end
157
+
158
+ errors_info = errors.any? ? ", errors: #{errors.full_messages}" : ""
159
+
160
+ "#<#{self.class.name} #{attributes_hash.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}, #{response_info}#{errors_info}>"
161
+ end
162
+
163
+ # Override render to collect Content objects
164
+ def render(**args)
165
+ content = super(**args) # Call Renderable's render method
166
+ @response.add(content) # Add to the response
167
+ content # Return the content for potential use in perform
168
+ end
169
+
170
+ protected
171
+
172
+ # Abstract method for subclasses to implement their logic
173
+ # Expected to use render to produce Content objects
174
+ def perform
175
+ raise NotImplementedError, "Subclasses must implement the perform method"
139
176
  end
140
177
 
141
178
  private
142
179
 
180
+ # Helper method for tools to manually report errors
181
+ def report_error(message)
182
+ @response.mark_as_error!
183
+ render text: message
184
+ end
185
+
143
186
  # Maps a JSON Schema type to an ActiveModel attribute type.
144
187
  #
145
188
  # @param type [String] The JSON Schema type.
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ # Manages the collection of content objects for tool results
5
+ class ToolResponse
6
+ include Enumerable
7
+
8
+ attr_reader :contents, :is_error
9
+
10
+ delegate :empty?, :size, :each, :find, :map, to: :contents
11
+
12
+ def initialize(is_error: false)
13
+ @contents = []
14
+ @is_error = is_error
15
+ end
16
+
17
+ # Add content to the response
18
+ def add(content)
19
+ @contents << content
20
+ content # Return the content for chaining
21
+ end
22
+
23
+ # Mark response as error
24
+ def mark_as_error!
25
+ @is_error = true
26
+ self
27
+ end
28
+
29
+ # Convert to hash format expected by MCP protocol
30
+ def as_json(options = nil)
31
+ {
32
+ content: @contents.map { |c| c.as_json(options) },
33
+ isError: @is_error
34
+ }.compact
35
+ end
36
+
37
+ # Alias to_h to as_json for consistency
38
+ alias_method :to_h, :as_json
39
+
40
+ # Handle to_json directly
41
+ def to_json(options = nil)
42
+ as_json(options).to_json
43
+ end
44
+
45
+ # Compare with hash for easier testing
46
+ # This allows assertions like: assert_equal({content: [...], isError: false}, tool_response)
47
+ def ==(other)
48
+ case other
49
+ when Hash
50
+ # Compare our hash representation with the other hash
51
+ # Use deep symbolization to handle both string and symbol keys
52
+ to_h.deep_symbolize_keys == other.deep_symbolize_keys
53
+ when ToolResponse
54
+ # Direct comparison with another ToolResponse
55
+ contents == other.contents && is_error == other.is_error
56
+ else
57
+ super
58
+ end
59
+ end
60
+
61
+ # Implement eql? for hash key comparison
62
+ def eql?(other)
63
+ self == other
64
+ end
65
+
66
+ # Implement hash method for hash key usage
67
+ def hash
68
+ [ contents, is_error ].hash
69
+ end
70
+
71
+ # Pretty print for better debugging
72
+ def inspect
73
+ "#<#{self.class.name} content: #{contents.inspect}, isError: #{is_error}>"
74
+ end
75
+ end
76
+ end
@@ -19,10 +19,9 @@ module ActionMCP
19
19
  tool_class = find(tool_name)
20
20
  tool = tool_class.new(arguments)
21
21
 
22
- return error_response(tool.errors.full_messages) unless tool.valid?
23
-
24
- process_result(tool.call)
22
+ tool.call.to_h
25
23
  rescue StandardError => e
24
+ # FIXME, we should maybe not return the error message to the user
26
25
  error_response([ "Tool execution failed: #{e.message}" ])
27
26
  end
28
27
 
@@ -32,24 +31,6 @@ module ActionMCP
32
31
 
33
32
  private
34
33
 
35
- def process_result(result)
36
- case result
37
- when Hash
38
- return result if result[:isError]
39
- success_response([ result ])
40
- when String
41
- success_response([ Content::Text.new(result) ])
42
- when Array
43
- success_response(result)
44
- else
45
- success_response([ result ])
46
- end
47
- end
48
-
49
- def success_response(content)
50
- { content: content }
51
- end
52
-
53
34
  def error_response(messages)
54
35
  {
55
36
  content: messages.map { |msg| Content::Text.new(msg) },
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.8.0"
5
+ VERSION = "0.9.0"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
@@ -6,15 +6,15 @@ module ActionMcp
6
6
  source_root File.expand_path("templates", __dir__)
7
7
 
8
8
  def create_application_prompt_file
9
- template "application_prompt.rb", File.join("app/mcp/prompts", "application_prompt.rb")
9
+ template "application_mcp_prompt.rb", File.join("app/mcp/prompts", "application_mcp_prompt.rb")
10
10
  end
11
11
 
12
12
  def create_application_tool_file
13
- template "application_tool.rb", File.join("app/mcp/tools", "application_tool.rb")
13
+ template "application_mcp_tool.rb", File.join("app/mcp/tools", "application_mcp_tool.rb")
14
14
  end
15
15
 
16
16
  def create_mcp_resource_template_file
17
- template "mcp_resource_template.rb", File.join("app/mcp/resource_templates", "mcp_resource_template.rb")
17
+ template "application_mcp_res_template.rb", File.join("app/mcp/resource_templates", "application_mcp_res_template.rb")
18
18
  end
19
19
  end
20
20
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ApplicationPrompt < ActionMCP::Prompt
3
+ class ApplicationMCPPrompt < ActionMCP::Prompt
4
4
  abstract!
5
5
  end
@@ -0,0 +1,3 @@
1
+ class ApplicationMCPResTemplate < ActionMCP::ResourceTemplate
2
+ abstract!
3
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ApplicationTool < ActionMCP::Tool
3
+ class ApplicationMCPTool < ActionMCP::Tool
4
4
  abstract!
5
5
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Template for generating new prompts.
4
- class <%= class_name %> < ApplicationPrompt
4
+ class <%= class_name %> < ApplicationMCPPrompt
5
5
  # Set the prompt name.
6
6
  prompt_name "<%= prompt_name %>"
7
7
 
@@ -5,7 +5,7 @@ module ActionMcp
5
5
  class ResourceTemplateGenerator < Rails::Generators::NamedBase
6
6
  namespace "action_mcp:resource_template"
7
7
  source_root File.expand_path("templates", __dir__)
8
- desc "Creates a ResourceTemplate (in app/mcp/resource_templates) that inherits from MCPResourceTemplate"
8
+ desc "Creates a ResourceTemplate (in app/mcp/resource_templates) that inherits from ApplicationMCPResTemplate"
9
9
 
10
10
  argument :name, type: :string, required: true, banner: "ResourceTemplateName"
11
11
 
@@ -1,4 +1,4 @@
1
- class <%= class_name %> < MCPResourceTemplate
1
+ class <%= class_name %> < ApplicationMCPResTemplate
2
2
  template_name "product"
3
3
  description "Access product information"
4
4
  uri_template "app://products/{product_id}"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Template for generating new tools.
4
- class <%= class_name %> < ApplicationTool
4
+ class <%= class_name %> < ApplicationMCPTool
5
5
  # Set the tool name.
6
6
  tool_name "<%= tool_name %>"
7
7
  description "Calculate the sum of two numbers"
@@ -5,7 +5,7 @@ module ActionMCP
5
5
  class ToolGenerator < Rails::Generators::Base
6
6
  namespace "action_mcp:tool"
7
7
  source_root File.expand_path("templates", __dir__)
8
- desc "Creates a Tool (in app/mcp/tools) that inherits from ApplicationTool"
8
+ desc "Creates a Tool (in app/mcp/tools) that inherits from ApplicationMCPTool"
9
9
 
10
10
  # The generator takes one argument, e.g. "CalculateSum"
11
11
  argument :name, type: :string, required: true, banner: "ToolName"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionmcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-03-15 00:00:00.000000000 Z
10
+ date: 2025-03-16 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: railties
@@ -112,9 +112,13 @@ files:
112
112
  - app/models/action_mcp/application_record.rb
113
113
  - app/models/action_mcp/session.rb
114
114
  - app/models/action_mcp/session/message.rb
115
+ - app/models/action_mcp/session/resource.rb
116
+ - app/models/action_mcp/session/subscription.rb
115
117
  - config/routes.rb
116
118
  - db/migrate/20250308122801_create_action_mcp_sessions.rb
117
119
  - db/migrate/20250314230152_add_is_ping_to_session_message.rb
120
+ - db/migrate/20250316005021_create_action_mcp_session_subscriptions.rb
121
+ - db/migrate/20250316005649_create_action_mcp_session_resources.rb
118
122
  - exe/actionmcp_cli
119
123
  - lib/action_mcp.rb
120
124
  - lib/action_mcp/capability.rb
@@ -136,6 +140,7 @@ files:
136
140
  - lib/action_mcp/json_rpc_handler.rb
137
141
  - lib/action_mcp/logging.rb
138
142
  - lib/action_mcp/prompt.rb
143
+ - lib/action_mcp/prompt_response.rb
139
144
  - lib/action_mcp/prompts_registry.rb
140
145
  - lib/action_mcp/registry_base.rb
141
146
  - lib/action_mcp/renderable.rb
@@ -146,6 +151,7 @@ files:
146
151
  - lib/action_mcp/string_array.rb
147
152
  - lib/action_mcp/test_helper.rb
148
153
  - lib/action_mcp/tool.rb
154
+ - lib/action_mcp/tool_response.rb
149
155
  - lib/action_mcp/tools_registry.rb
150
156
  - lib/action_mcp/transport.rb
151
157
  - lib/action_mcp/transport/capabilities.rb
@@ -160,9 +166,9 @@ files:
160
166
  - lib/action_mcp/version.rb
161
167
  - lib/actionmcp.rb
162
168
  - lib/generators/action_mcp/install/install_generator.rb
163
- - lib/generators/action_mcp/install/templates/application_prompt.rb
164
- - lib/generators/action_mcp/install/templates/application_tool.rb
165
- - lib/generators/action_mcp/install/templates/mcp_resource_template.rb
169
+ - lib/generators/action_mcp/install/templates/application_mcp_prompt.rb
170
+ - lib/generators/action_mcp/install/templates/application_mcp_res_template.rb
171
+ - lib/generators/action_mcp/install/templates/application_mcp_tool.rb
166
172
  - lib/generators/action_mcp/prompt/prompt_generator.rb
167
173
  - lib/generators/action_mcp/prompt/templates/prompt.rb.erb
168
174
  - lib/generators/action_mcp/resource_template/resource_template_generator.rb
@@ -1,3 +0,0 @@
1
- class MCPResourceTemplate < ActionMCP::ResourceTemplate
2
- abstract!
3
- end