actionmcp 0.50.8 → 0.50.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94384a0f8fcb5f0f77bac5d814fea53addbc8732007a3771b843f2bcca24ab47
4
- data.tar.gz: 812125b6acf82b418257610ba5b58b3af4cb01c5b3152ac995a1c614df02a263
3
+ metadata.gz: a500e24390bf32c0e74cba14755f67257c91b69ba6ffe786e3b692efd82c87cc
4
+ data.tar.gz: d122b12b45799b719f077d742a1a5edd85eac4cbe96e2a85259eb7673c858f36
5
5
  SHA512:
6
- metadata.gz: 7e693c3b47925b0f1babb7d1912605766851b054f764ba6c0b6270f5f6d34739231300654a3dca83cb3d4a12a00cc29f20400bfdd7e93b4358bd296f9b9881ae
7
- data.tar.gz: fba759c19cab4145614ac93b7c14866261f663a41f7539d5741712708136f34c29d43fcedbedbe06834f5989ac6c6d9d2aa2aa6b6a3f7e44b46e891a160bc224
6
+ metadata.gz: f7878f2188781097208e5a703cdd6b3c6fcffa6cea0fef3db5e13538499bae935eebfc3ec6ffbbb77267f016023abe2c9b1e476b51b1d87910c81d4f44c5f4b7
7
+ data.tar.gz: 72048e1f3c8d3062bbc17840c14d5abfbc04fe620710d5dde9d3f98ce44a866347b95884fa03fb402d2d2cab078d7f9d6e9eb49690d338f29484b64fcad3caa5
@@ -160,8 +160,10 @@ module ActionMCP
160
160
 
161
161
  transport_handler = Server::TransportHandler.new(session)
162
162
  json_rpc_handler = Server::JsonRpcHandler.new(transport_handler)
163
- handler_results = json_rpc_handler.call(jsonrpc_params)
164
- process_handler_results(handler_results, session, session_initially_missing, is_initialize_request)
163
+
164
+ result = json_rpc_handler.call(jsonrpc_params)
165
+
166
+ process_handler_results(result, session, session_initially_missing, is_initialize_request)
165
167
  rescue ActionController::Live::ClientDisconnected, IOError => e
166
168
  Rails.logger.debug "Unified SSE (POST): Client disconnected during response: #{e.message}"
167
169
  begin
@@ -257,45 +259,24 @@ module ActionMCP
257
259
  end
258
260
 
259
261
  # Processes the results from the JsonRpcHandler.
260
- def process_handler_results(results, session, session_initially_missing, is_initialize_request)
261
- results ||= {}
262
- is_notification = jsonrpc_params.is_a?(JSON_RPC::Notification)
263
- request_id = nil
264
- if results.is_a?(Hash)
265
- request_id = results[:request_id] || results[:id]
266
- request_id ||= results[:payload][:id] if results[:payload].is_a?(Hash) && results[:payload][:id]
262
+ def process_handler_results(result, session, session_initially_missing, is_initialize_request)
263
+ # Handle empty result (notifications)
264
+ if result.nil?
265
+ return head :accepted
267
266
  end
268
- result_type = results[:type]
269
- result_payload = results[:payload] || {}
270
- result_payload[:id] = request_id if result_payload.is_a?(Hash) && request_id && !result_payload.key?(:id)
271
-
272
- case result_type
273
- when :error
274
- error_payload = result_payload
275
- error_payload[:id] = request_id if error_payload.is_a?(Hash) && !error_payload.key?(:id) && request_id
276
- render json: error_payload, status: results.fetch(:status, :bad_request)
277
- when :notifications_only
278
- head :accepted
279
- when :responses
280
- server_preference = ActionMCP.configuration.post_response_preference
281
- use_sse = (server_preference == :sse)
282
- add_session_header = is_initialize_request && session_initially_missing && session.persisted?
283
- if use_sse
284
- render_sse_response(result_payload, session, add_session_header)
285
- else
286
- render_json_response(result_payload, session, add_session_header)
287
- end
267
+
268
+ # Convert to hash for rendering
269
+ payload = result.message_json
270
+
271
+ # Determine response format
272
+ server_preference = ActionMCP.configuration.post_response_preference
273
+ use_sse = (server_preference == :sse)
274
+ add_session_header = is_initialize_request && session_initially_missing && session.persisted?
275
+
276
+ if use_sse
277
+ render_sse_response(payload, session, add_session_header)
288
278
  else
289
- Rails.logger.error "Unknown handler result type: #{result_type.inspect}"
290
- if is_notification
291
- head :accepted
292
- else
293
- render json: {
294
- jsonrpc: "2.0",
295
- id: request_id,
296
- result: result_payload
297
- }, status: :ok
298
- end
279
+ render_json_response(payload, session, add_session_header)
299
280
  end
300
281
  end
301
282
 
@@ -56,12 +56,8 @@ module ActionMCP
56
56
  case rpc_method
57
57
  when Methods::PING
58
58
  transport.send_pong(id)
59
- true
60
59
  when %r{^notifications/}
61
60
  process_notifications(rpc_method, params)
62
- true
63
- else
64
- false
65
61
  end
66
62
  end
67
63
 
@@ -15,48 +15,32 @@ module ActionMCP
15
15
  client_capabilities = params["capabilities"]
16
16
 
17
17
  unless client_protocol_version.is_a?(String) && client_protocol_version.present?
18
- send_jsonrpc_error(request_id, :invalid_params, "Missing or invalid 'protocolVersion'")
19
- return { type: :error, id: request_id,
20
- payload: { jsonrpc: "2.0", id: request_id, error: { code: -32_602, message: "Missing or invalid 'protocolVersion'" } } }
18
+ return send_jsonrpc_error(request_id, :invalid_params, "Missing or invalid 'protocolVersion'")
21
19
  end
22
- # Check if the protocol version is supported
23
20
  unless ActionMCP.configuration.vibed_ignore_version || ActionMCP::SUPPORTED_VERSIONS.include?(client_protocol_version)
24
21
  error_data = {
25
22
  supported: ActionMCP::SUPPORTED_VERSIONS,
26
23
  requested: client_protocol_version
27
24
  }
28
- send_jsonrpc_error(request_id, :invalid_params, "Unsupported protocol version", error_data)
29
- return { type: :error, id: request_id,
30
- payload: { jsonrpc: "2.0", id: request_id, error: { code: -32_602, message: "Unsupported protocol version", data: error_data } } }
25
+ return send_jsonrpc_error(request_id, :invalid_params, "Unsupported protocol version", error_data)
31
26
  end
32
27
 
33
28
  unless client_info.is_a?(Hash)
34
- send_jsonrpc_error(request_id, :invalid_params, "Missing or invalid 'clientInfo'")
35
- return { type: :error, id: request_id,
36
- payload: { jsonrpc: "2.0", id: request_id, error: { code: -32_602, message: "Missing or invalid 'clientInfo'" } } }
29
+ return send_jsonrpc_error(request_id, :invalid_params, "Missing or invalid 'clientInfo'")
37
30
  end
38
31
  unless client_capabilities.is_a?(Hash)
39
- send_jsonrpc_error(request_id, :invalid_params, "Missing or invalid 'capabilities'")
40
- return { type: :error, id: request_id,
41
- payload: { jsonrpc: "2.0", id: request_id, error: { code: -32_602, message: "Missing or invalid 'capabilities'" } } }
32
+ return send_jsonrpc_error(request_id, :invalid_params, "Missing or invalid 'capabilities'")
42
33
  end
43
34
 
44
- # Store client information
45
35
  session.store_client_info(client_info)
46
36
  session.store_client_capabilities(client_capabilities)
47
37
  session.set_protocol_version(client_protocol_version)
48
38
 
49
- # Initialize the session
50
39
  unless session.initialize!
51
- send_jsonrpc_error(request_id, :internal_error, "Failed to initialize session")
52
- return { type: :error, id: request_id,
53
- payload: { jsonrpc: "2.0", id: request_id, error: { code: -32_603, message: "Failed to initialize session" } } }
40
+ return send_jsonrpc_error(request_id, :internal_error, "Failed to initialize session")
54
41
  end
55
42
 
56
- # Send the successful response with the correct protocol version
57
43
  capabilities_payload = session.server_capabilities_payload
58
- # If vibed_ignore_version is true, always use the latest supported version in the response
59
- # Otherwise, use the client's requested version
60
44
  capabilities_payload[:protocolVersion] = if ActionMCP.configuration.vibed_ignore_version
61
45
  PROTOCOL_VERSION
62
46
  else
@@ -64,7 +48,6 @@ module ActionMCP
64
48
  end
65
49
 
66
50
  send_jsonrpc_response(request_id, result: capabilities_payload)
67
- { type: :responses, id: request_id, payload: { jsonrpc: "2.0", id: request_id, result: capabilities_payload } }
68
51
  end
69
52
  end
70
53
  end
@@ -12,19 +12,10 @@ module ActionMCP
12
12
  when Symbol
13
13
  JSON_RPC::JsonRpcError.new(error_or_symbol, message: message, data: data)
14
14
  else
15
- # If it's already an error hash
16
15
  error_or_symbol
17
16
  end
18
17
 
19
- {
20
- type: :error,
21
- request_id: id,
22
- payload: {
23
- jsonrpc: "2.0",
24
- id: id,
25
- error: json_rpc_error.to_h
26
- }
27
- }
18
+ JSON_RPC::Response.new(id: id, error: json_rpc_error)
28
19
  end
29
20
 
30
21
  # Helper method to create error response from any exception
@@ -32,14 +32,11 @@ module ActionMCP
32
32
  def handle_prompts_get(id, params)
33
33
  name = extract_name(params)
34
34
  arguments = extract_arguments(params)
35
-
36
- message = transport.send_prompts_get(id, name, arguments)
37
- extract_message_payload(message, id)
35
+ transport.send_prompts_get(id, name, arguments)
38
36
  end
39
37
 
40
38
  def handle_prompts_list(id, _params)
41
- message = transport.send_prompts_list(id)
42
- extract_message_payload(message, id)
39
+ transport.send_prompts_list(id)
43
40
  end
44
41
 
45
42
  def extract_name(params)
@@ -33,20 +33,16 @@ module ActionMCP
33
33
  end
34
34
 
35
35
  def handle_resources_list(id, _params)
36
- message = transport.send_resources_list(id)
37
- extract_message_payload(message, id)
36
+ transport.send_resources_list(id)
38
37
  end
39
38
 
40
39
  def handle_resources_templates_list(id, _params)
41
- message = transport.send_resource_templates_list(id)
42
- extract_message_payload(message, id)
40
+ transport.send_resource_templates_list(id)
43
41
  end
44
42
 
45
43
  def handle_resources_read(id, params)
46
44
  validate_params_present(params, "Resource URI is required")
47
-
48
- message = transport.send_resource_read(id, params)
49
- extract_message_payload(message, id)
45
+ transport.send_resource_read(id, params)
50
46
  end
51
47
 
52
48
  def handle_resources_subscribe(id, params)
@@ -30,16 +30,13 @@ module ActionMCP
30
30
  end
31
31
 
32
32
  def handle_tools_list(id, params)
33
- message = transport.send_tools_list(id, params)
34
- extract_message_payload(message, id)
33
+ transport.send_tools_list(id, params)
35
34
  end
36
35
 
37
36
  def handle_tools_call(id, params)
38
37
  name = validate_required_param(params, "name", "Tool name is required")
39
38
  arguments = extract_arguments(params)
40
-
41
- message = transport.send_tools_call(id, name, arguments)
42
- extract_message_payload(message, id)
39
+ transport.send_tools_call(id, name, arguments)
43
40
  end
44
41
 
45
42
  def extract_arguments(params)
@@ -32,10 +32,8 @@ module ActionMCP
32
32
  params = request.params
33
33
 
34
34
  with_error_handling(id) do
35
- # Try to handle common methods first (like ping)
36
- return if handle_common_methods(rpc_method, id, params)
37
-
38
- # Route to appropriate handler
35
+ common_method = handle_common_methods(rpc_method, id, params)
36
+ return common_method if common_method
39
37
  route_to_handler(rpc_method, id, params)
40
38
  end
41
39
  end
@@ -58,8 +56,7 @@ module ActionMCP
58
56
  end
59
57
 
60
58
  def handle_initialize(id, params)
61
- message = transport.send_capabilities(id, params)
62
- extract_message_payload(message, id)
59
+ transport.send_capabilities(id, params)
63
60
  end
64
61
 
65
62
  def handle_notification(notification)
@@ -67,29 +64,17 @@ module ActionMCP
67
64
  params = notification.params || {}
68
65
 
69
66
  process_notifications(method_name, params)
70
- { type: :notifications_only }
67
+ nil
71
68
  end
72
69
 
73
70
  def handle_response(response)
74
71
  Rails.logger.debug("Received response: #{response.inspect}")
75
-
76
- {
77
- type: :responses,
78
- request_id: response.id,
79
- payload: build_response_payload(response)
80
- }
72
+ response
81
73
  end
82
74
 
83
- def process_completion_complete(id, params)
84
- params ||= {}
85
75
 
86
- result = transport.send_jsonrpc_response(id, result: build_completion_result)
87
-
88
- if result.is_a?(ActionMCP::Session::Message)
89
- extract_message_payload(result, id)
90
- else
91
- wrap_transport_result(result, id)
92
- end
76
+ def process_completion_complete(id, params)
77
+ transport.send_jsonrpc_response(id, result: build_completion_result)
93
78
  end
94
79
 
95
80
  def process_notifications(rpc_method, params)
@@ -101,30 +86,6 @@ module ActionMCP
101
86
  end
102
87
  end
103
88
 
104
- def extract_message_payload(message, id)
105
- if message.is_a?(ActionMCP::Session::Message)
106
- {
107
- type: :responses,
108
- request_id: id,
109
- payload: message.message_json
110
- }
111
- else
112
- message
113
- end
114
- end
115
-
116
- def wrap_transport_result(transport_result, id)
117
- if transport_result.is_a?(Hash) && transport_result[:type]
118
- transport_result
119
- else
120
- {
121
- type: :responses,
122
- request_id: id,
123
- payload: transport_result
124
- }
125
- end
126
- end
127
-
128
89
  def build_response_payload(response)
129
90
  {
130
91
  jsonrpc: "2.0",
@@ -56,22 +56,6 @@ module ActionMCP
56
56
  if raw_message.is_a?(String) && valid_json_format?(raw_message)
57
57
  message = MultiJson.load(raw_message)
58
58
  callback&.call(message)
59
- elsif raw_message.respond_to?(:message) && raw_message.message.is_a?(String) && valid_json_format?(raw_message.message)
60
- message = MultiJson.load(raw_message.message)
61
- callback&.call(message)
62
- elsif raw_message.respond_to?(:to_json)
63
- # Try to serialize the message object to JSON if it responds to to_json
64
- message_json = raw_message.to_json
65
- if valid_json_format?(message_json)
66
- message = MultiJson.load(message_json)
67
- callback&.call(message)
68
- else
69
- Rails.logger.warn "SSEListener: Message cannot be converted to valid JSON"
70
- end
71
- else
72
- # Log that we received an invalid message format
73
- display_message = raw_message.to_s[0..100]
74
- Rails.logger.warn "SSEListener: Received invalid JSON format: #{display_message}..."
75
59
  end
76
60
  rescue StandardError => e
77
61
  Rails.logger.error "SSEListener: Error processing message: #{e.message}"
@@ -52,12 +52,28 @@ module ActionMCP
52
52
  end
53
53
 
54
54
  # Convenience methods for common annotations
55
+ def title(value = nil)
56
+ if value
57
+ annotate(:title, value)
58
+ else
59
+ _annotations["title"]
60
+ end
61
+ end
62
+
55
63
  def destructive(enabled = true)
56
- annotate(:destructive, enabled)
64
+ annotate(:destructiveHint, enabled)
57
65
  end
58
66
 
59
67
  def read_only(enabled = true)
60
- annotate(:readOnly, enabled)
68
+ annotate(:readOnlyHint, enabled)
69
+ end
70
+
71
+ def idempotent(enabled = true)
72
+ annotate(:idempotentHint, enabled)
73
+ end
74
+
75
+ def open_world(enabled = true)
76
+ annotate(:openWorldHint, enabled)
61
77
  end
62
78
 
63
79
  # Return annotations for the tool
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.50.8"
5
+ VERSION = "0.50.11"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
@@ -6,16 +6,20 @@ class <%= class_name %> < ApplicationMCPPrompt
6
6
  prompt_name "<%= prompt_name %>"
7
7
 
8
8
  # Provide a user-facing description for your prompt.
9
- description "Analyze code for potential improvements"
9
+ description "<%= description || 'Describe what this prompt does' %>"
10
10
 
11
- # Configure arguments via the new DSL
12
- argument :language, description: "Programming language", default: "Ruby"
13
- argument :code, description: "Code to explain", required: true
11
+ # Configure arguments (example structure override as needed)
12
+ argument :input, description: "Main input", required: true
14
13
 
15
- # Add validations (note: "Ruby" is not allowed per the validation)
16
- validates :language, inclusion: { in: %w[Ruby C Cobol FORTRAN] }
14
+ # Optional: add more arguments if needed
15
+ # argument :context, description: "Context for the input", default: ""
17
16
 
18
- # Implement your prompt's behavior here
19
- def call
17
+ # Optional: validations can be added as needed
18
+ # validates :input, presence: true
19
+ # validates :context, length: { maximum: 500 }
20
+
21
+ # Main logic for prompt
22
+ def perform
23
+ # Implement behavior here
20
24
  end
21
25
  end
@@ -1,17 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Template for generating new tools.
4
3
  class <%= class_name %> < ApplicationMCPTool
5
- # Set the tool name.
6
4
  tool_name "<%= tool_name %>"
7
- description "Calculate the sum of two numbers"
5
+ description "<%= description %>"
6
+ <% if options[:title] %>
7
+ title "<%= options[:title] %>"
8
+ <% end %>
9
+ <% if options[:read_only] %>
10
+ read_only
11
+ <% end %>
12
+ <% if options[:destructive] %>
13
+ destructive
14
+ <% end %>
15
+ <% if options[:idempotent] %>
16
+ idempotent
17
+ <% end %>
18
+ <% if options[:open_world] %>
19
+ open_world
20
+ <% end %>
21
+ <% annotations.each do |k, v| %>
22
+ <% unless [:read_only, :destructive, :idempotent, :open_world, :title].include?(k) %>
23
+ annotate(:<%= k %>, <%= v.inspect %>)
24
+ <% end %>
25
+ <% end %>
8
26
 
9
- # Define input properties.
10
- property :a, type: "number", description: "First number", required: true
11
- property :b, type: "number", description: "Second number", required: true
27
+ <% if properties.empty? %>
28
+ property :input, type: "string", description: "Input", required: true
29
+ <% else %>
30
+ <% properties.each do |prop| %>
31
+ property :<%= prop[:name] %>, type: "<%= prop[:type] %>", description: "<%= prop[:description] %>"<%= ", required: true" if prop[:required] %>
32
+ <% end %>
33
+ <% end %>
12
34
 
13
- # Implement the tool's logic here.
14
- def call
15
- render(text: a + b)
35
+ def perform
36
+ render(text: "Processing <%= properties.map { |p| p[:name] }.join(', ') %>")
37
+
38
+ # Optional outputs:
39
+ # render(audio: "<base64_data>", mime_type: "audio/mpeg")
40
+ # render(image: "<base64_data>", mime_type: "image/png")
41
+ # render(resource: "file://path", mime_type: "application/json", text: "{}")
42
+ # render(resource: "file://path", mime_type: "application/octet-stream", blob: "<base64_data>")
43
+ rescue => e
44
+ render(error: ["Error: #{e.message}"])
16
45
  end
17
46
  end
@@ -7,32 +7,58 @@ module ActionMCP
7
7
  source_root File.expand_path("templates", __dir__)
8
8
  desc "Creates a Tool (in app/mcp/tools) that inherits from ApplicationMCPTool"
9
9
 
10
- # The generator takes one argument, e.g. "CalculateSum"
11
10
  argument :name, type: :string, required: true, banner: "ToolName"
12
11
 
12
+ class_option :description, type: :string, default: "Describe what this tool does"
13
+ class_option :read_only, type: :boolean, default: false
14
+ class_option :destructive, type: :boolean, default: false
15
+ class_option :category, type: :string, default: nil
16
+ class_option :properties, type: :array, default: [], banner: "name:type:description:required"
17
+
13
18
  def create_tool_file
14
19
  template "tool.rb.erb", "app/mcp/tools/#{file_name}.rb"
15
20
  end
16
21
 
17
22
  private
18
23
 
19
- # Compute the class name ensuring it ends with "Tool"
20
24
  def class_name
21
25
  "#{name.camelize}#{name.camelize.end_with?('Tool') ? '' : 'Tool'}"
22
26
  end
23
27
 
24
- # Compute the file name ensuring it ends with _tool.rb
25
28
  def file_name
26
29
  base = name.underscore
27
30
  base.end_with?("_tool") ? base : "#{base}_tool"
28
31
  end
29
32
 
30
- # Compute the DSL tool name (a dashed version, without the "Tool" suffix)
31
33
  def tool_name
32
34
  base = name.to_s
33
- base = base[0...-4] if base.end_with?("Tool")
35
+ base = base.end_with?("Tool") ? base[0..-5] : base
34
36
  base.underscore.dasherize
35
37
  end
38
+
39
+ def description
40
+ options[:description]
41
+ end
42
+
43
+ def annotations
44
+ ann = {}
45
+ ann[:read_only] = true if options[:read_only]
46
+ ann[:destructive] = true if options[:destructive]
47
+ ann[:category] = options[:category] if options[:category]
48
+ ann
49
+ end
50
+
51
+ def properties
52
+ options[:properties].map do |prop|
53
+ parts = prop.split(":")
54
+ {
55
+ name: parts[0],
56
+ type: parts[1] || "string",
57
+ description: parts[2] || "No description provided",
58
+ required: parts[3] == "true"
59
+ }
60
+ end
61
+ end
36
62
  end
37
63
  end
38
64
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionmcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.50.8
4
+ version: 0.50.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-05-15 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activerecord
@@ -220,7 +219,6 @@ metadata:
220
219
  source_code_uri: https://github.com/seuros/action_mcp
221
220
  changelog_uri: https://github.com/seuros/action_mcp/blob/master/CHANGELOG.md
222
221
  rubygems_mfa_required: 'true'
223
- post_install_message:
224
222
  rdoc_options: []
225
223
  require_paths:
226
224
  - lib
@@ -235,8 +233,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
235
233
  - !ruby/object:Gem::Version
236
234
  version: '0'
237
235
  requirements: []
238
- rubygems_version: 3.5.22
239
- signing_key:
236
+ rubygems_version: 3.6.7
240
237
  specification_version: 4
241
238
  summary: Provides essential tooling for building Model Context Protocol (MCP) capable
242
239
  servers