actionmcp 0.20.0 → 0.22.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/action_mcp/messages_controller.rb +2 -2
  3. data/app/models/action_mcp/session/message.rb +12 -1
  4. data/app/models/action_mcp/session.rb +8 -4
  5. data/lib/action_mcp/capability.rb +2 -3
  6. data/lib/action_mcp/client/base.rb +222 -0
  7. data/lib/action_mcp/client/blueprint.rb +227 -0
  8. data/lib/action_mcp/client/catalog.rb +226 -0
  9. data/lib/action_mcp/client/json_rpc_handler.rb +109 -0
  10. data/lib/action_mcp/client/logging.rb +20 -0
  11. data/lib/action_mcp/{transport → client}/messaging.rb +1 -1
  12. data/lib/action_mcp/client/prompt_book.rb +183 -0
  13. data/lib/action_mcp/client/prompts.rb +33 -0
  14. data/lib/action_mcp/client/resources.rb +70 -0
  15. data/lib/action_mcp/client/roots.rb +13 -0
  16. data/lib/action_mcp/client/server.rb +60 -0
  17. data/lib/action_mcp/{transport → client}/sse_client.rb +70 -111
  18. data/lib/action_mcp/{transport → client}/stdio_client.rb +38 -38
  19. data/lib/action_mcp/client/toolbox.rb +236 -0
  20. data/lib/action_mcp/client/tools.rb +33 -0
  21. data/lib/action_mcp/client.rb +20 -231
  22. data/lib/action_mcp/engine.rb +1 -3
  23. data/lib/action_mcp/instrumentation/controller_runtime.rb +1 -1
  24. data/lib/action_mcp/instrumentation/instrumentation.rb +2 -0
  25. data/lib/action_mcp/instrumentation/resource_instrumentation.rb +1 -0
  26. data/lib/action_mcp/json_rpc_handler_base.rb +106 -0
  27. data/lib/action_mcp/log_subscriber.rb +2 -0
  28. data/lib/action_mcp/logging.rb +1 -1
  29. data/lib/action_mcp/{transport → server}/capabilities.rb +2 -2
  30. data/lib/action_mcp/server/json_rpc_handler.rb +121 -0
  31. data/lib/action_mcp/server/messaging.rb +28 -0
  32. data/lib/action_mcp/{transport → server}/notifications.rb +1 -1
  33. data/lib/action_mcp/{transport → server}/prompts.rb +1 -1
  34. data/lib/action_mcp/{transport → server}/resources.rb +1 -18
  35. data/lib/action_mcp/{transport → server}/roots.rb +1 -1
  36. data/lib/action_mcp/{transport → server}/sampling.rb +1 -1
  37. data/lib/action_mcp/server/sampling_request.rb +115 -0
  38. data/lib/action_mcp/{transport → server}/tools.rb +1 -1
  39. data/lib/action_mcp/server/transport_handler.rb +41 -0
  40. data/lib/action_mcp/uri_ambiguity_checker.rb +6 -10
  41. data/lib/action_mcp/version.rb +1 -1
  42. data/lib/action_mcp.rb +2 -1
  43. metadata +29 -33
  44. data/lib/action_mcp/base_json_rpc_handler.rb +0 -97
  45. data/lib/action_mcp/client_json_rpc_handler.rb +0 -69
  46. data/lib/action_mcp/json_rpc_handler.rb +0 -229
  47. data/lib/action_mcp/sampling_request.rb +0 -113
  48. data/lib/action_mcp/server_json_rpc_handler.rb +0 -90
  49. data/lib/action_mcp/transport/transport_base.rb +0 -126
  50. data/lib/action_mcp/transport_handler.rb +0 -39
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Server
5
+ class SamplingRequest
6
+ class << self
7
+ attr_reader :default_messages, :default_system_prompt, :default_context,
8
+ :default_model_hints, :default_intelligence_priority,
9
+ :default_max_tokens, :default_temperature
10
+
11
+ def configure
12
+ yield self
13
+ end
14
+
15
+ def messages(messages = nil)
16
+ if messages
17
+ @default_messages = messages.map do |msg|
18
+ mutate_content(msg)
19
+ end
20
+ end
21
+ @messages ||= []
22
+ end
23
+
24
+ def system_prompt(prompt = nil)
25
+ @default_system_prompt = prompt if prompt
26
+ @default_system_prompt
27
+ end
28
+
29
+ def include_context(context = nil)
30
+ @default_context = context if context
31
+ @default_context
32
+ end
33
+
34
+ def model_hints(hints = nil)
35
+ @default_model_hints = hints if hints
36
+ @model_hints ||= []
37
+ end
38
+
39
+ def intelligence_priority(priority = nil)
40
+ @default_intelligence_priority = priority if priority
41
+ @intelligence_priority ||= 0.9
42
+ end
43
+
44
+ def max_tokens(tokens = nil)
45
+ @default_max_tokens = tokens if tokens
46
+ @max_tokens ||= 500
47
+ end
48
+
49
+ def temperature(temp = nil)
50
+ @default_temperature = temp if temp
51
+ @temperature ||= 0.7
52
+ end
53
+
54
+ private
55
+
56
+ def mutate_content(msg)
57
+ content = msg[:content]
58
+ if content.is_a?(ActionMCP::Content) || (content.respond_to?(:to_h) && !content.is_a?(Hash))
59
+ { role: msg[:role], content: content.to_h }
60
+ else
61
+ msg
62
+ end
63
+ end
64
+ end
65
+
66
+ attr_accessor :system_prompt, :model_hints, :intelligence_priority, :max_tokens, :temperature
67
+ attr_reader :messages, :context
68
+
69
+ def initialize
70
+ @messages = self.class.default_messages.dup
71
+ @system_prompt = self.class.default_system_prompt
72
+ @context = self.class.default_context
73
+ @model_hints = self.class.default_model_hints.dup
74
+ @intelligence_priority = self.class.default_intelligence_priority
75
+ @max_tokens = self.class.default_max_tokens
76
+ @temperature = self.class.default_temperature
77
+
78
+ yield self if block_given?
79
+ end
80
+
81
+ def messages=(value)
82
+ @messages = value.map do |msg|
83
+ self.class.send(:mutate_content, msg)
84
+ end
85
+ end
86
+
87
+ def include_context=(value)
88
+ @context = value
89
+ end
90
+
91
+ def add_message(content, role: "user")
92
+ if content.is_a?(Content::Base) || (content.respond_to?(:to_h) && !content.is_a?(Hash))
93
+ @messages << { role: role, content: content.to_h }
94
+ else
95
+ content = Content::Text.new(content).to_h if content.is_a?(String)
96
+ @messages << { role: role, content: content }
97
+ end
98
+ end
99
+
100
+ def to_h
101
+ {
102
+ messages: messages.map { |msg| { role: msg[:role], content: msg[:content] } },
103
+ systemPrompt: system_prompt,
104
+ includeContext: context,
105
+ modelPreferences: {
106
+ hints: model_hints.map { |name| { name: name } },
107
+ intelligencePriority: intelligence_priority
108
+ },
109
+ maxTokens: max_tokens,
110
+ temperature: temperature
111
+ }.compact
112
+ end
113
+ end
114
+ end
115
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionMCP
4
- module Transport
4
+ module Server
5
5
  module Tools
6
6
  def send_tools_list(request_id)
7
7
  tools = format_registry_items(ToolsRegistry.non_abstract)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Server
5
+ class TransportHandler
6
+ attr_reader :session
7
+
8
+ delegate :initialize!, :initialized?, to: :session
9
+ delegate :read, :write, to: :session
10
+ include Logging
11
+
12
+ include Messaging
13
+ include Capabilities
14
+ include Tools
15
+ include Prompts
16
+ include Resources
17
+ include Notifications
18
+ include Sampling
19
+ include Roots
20
+
21
+ # @param [ActionMCP::Session] session
22
+ def initialize(session)
23
+ @session = session
24
+ end
25
+
26
+ def send_pong(request_id)
27
+ send_jsonrpc_response(request_id, result: {})
28
+ end
29
+
30
+ private
31
+
32
+ def write_message(data)
33
+ session.write(data)
34
+ end
35
+
36
+ def format_registry_items(registry)
37
+ registry.map { |item| item.klass.to_h }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionMCP
2
4
  module UriAmbiguityChecker
3
5
  extend ActiveSupport::Concern
@@ -17,9 +19,7 @@ module ActionMCP
17
19
  segments2 = pattern2.split("/")
18
20
 
19
21
  # If different number of segments, they can't be ambiguous
20
- if segments1.size != segments2.size
21
- return false
22
- end
22
+ return false if segments1.size != segments2.size
23
23
 
24
24
  # Extract literals (non-parameters) from each pattern
25
25
  literals1 = []
@@ -34,14 +34,12 @@ module ActionMCP
34
34
  end
35
35
 
36
36
  # Check each segment for direct literal mismatches
37
- segments1.zip(segments2).each_with_index do |(seg1, seg2), index|
37
+ segments1.zip(segments2).each_with_index do |(seg1, seg2), _index|
38
38
  param1 = parameter?(seg1)
39
39
  param2 = parameter?(seg2)
40
40
 
41
41
  # When both segments are literals, they must match exactly
42
- if !param1 && !param2 && seg1 != seg2
43
- return false
44
- end
42
+ return false if !param1 && !param2 && seg1 != seg2
45
43
  end
46
44
 
47
45
  # Check for structural incompatibility in the literals
@@ -60,9 +58,7 @@ module ActionMCP
60
58
  common_literal_indices2 = common_literals.map { |lit| lit_values2.index(lit) }
61
59
 
62
60
  # If the relative ordering is different, patterns are not ambiguous
63
- if common_literal_indices1 != common_literal_indices2
64
- return false
65
- end
61
+ return false if common_literal_indices1 != common_literal_indices2
66
62
  end
67
63
  end
68
64
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.20.0"
5
+ VERSION = "0.22.0"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
data/lib/action_mcp.rb CHANGED
@@ -30,6 +30,7 @@ end.setup
30
30
 
31
31
  module ActionMCP
32
32
  require_relative "action_mcp/version"
33
+ require_relative "action_mcp/client"
33
34
  include Logging
34
35
  PROTOCOL_VERSION = "2024-11-05"
35
36
 
@@ -86,6 +87,6 @@ module ActionMCP
86
87
  ActiveModel::Type.register(:integer_array, IntegerArray)
87
88
  end
88
89
 
89
- ActiveSupport.on_load(:action_mcp) do
90
+ ActiveSupport.on_load(:action_mcp, run_once: true) do
90
91
  self.logger = ::Rails.logger
91
92
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionmcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.22.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-03-23 00:00:00.000000000 Z
11
+ date: 2025-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actioncable
@@ -38,20 +38,6 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: 8.0.1
41
- - !ruby/object:Gem::Dependency
42
- name: faraday
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '2.0'
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '2.0'
55
41
  - !ruby/object:Gem::Dependency
56
42
  name: multi_json
57
43
  requirement: !ruby/object:Gem::Requirement
@@ -122,12 +108,25 @@ files:
122
108
  - db/migrate/20250316005649_create_action_mcp_session_resources.rb
123
109
  - exe/actionmcp_cli
124
110
  - lib/action_mcp.rb
125
- - lib/action_mcp/base_json_rpc_handler.rb
126
111
  - lib/action_mcp/base_response.rb
127
112
  - lib/action_mcp/callbacks.rb
128
113
  - lib/action_mcp/capability.rb
129
114
  - lib/action_mcp/client.rb
130
- - lib/action_mcp/client_json_rpc_handler.rb
115
+ - lib/action_mcp/client/base.rb
116
+ - lib/action_mcp/client/blueprint.rb
117
+ - lib/action_mcp/client/catalog.rb
118
+ - lib/action_mcp/client/json_rpc_handler.rb
119
+ - lib/action_mcp/client/logging.rb
120
+ - lib/action_mcp/client/messaging.rb
121
+ - lib/action_mcp/client/prompt_book.rb
122
+ - lib/action_mcp/client/prompts.rb
123
+ - lib/action_mcp/client/resources.rb
124
+ - lib/action_mcp/client/roots.rb
125
+ - lib/action_mcp/client/server.rb
126
+ - lib/action_mcp/client/sse_client.rb
127
+ - lib/action_mcp/client/stdio_client.rb
128
+ - lib/action_mcp/client/toolbox.rb
129
+ - lib/action_mcp/client/tools.rb
131
130
  - lib/action_mcp/configuration.rb
132
131
  - lib/action_mcp/content.rb
133
132
  - lib/action_mcp/content/audio.rb
@@ -145,7 +144,7 @@ files:
145
144
  - lib/action_mcp/json_rpc/notification.rb
146
145
  - lib/action_mcp/json_rpc/request.rb
147
146
  - lib/action_mcp/json_rpc/response.rb
148
- - lib/action_mcp/json_rpc_handler.rb
147
+ - lib/action_mcp/json_rpc_handler_base.rb
149
148
  - lib/action_mcp/log_subscriber.rb
150
149
  - lib/action_mcp/logging.rb
151
150
  - lib/action_mcp/prompt.rb
@@ -157,27 +156,24 @@ files:
157
156
  - lib/action_mcp/resource_callbacks.rb
158
157
  - lib/action_mcp/resource_template.rb
159
158
  - lib/action_mcp/resource_templates_registry.rb
160
- - lib/action_mcp/sampling_request.rb
161
159
  - lib/action_mcp/server.rb
162
- - lib/action_mcp/server_json_rpc_handler.rb
160
+ - lib/action_mcp/server/capabilities.rb
161
+ - lib/action_mcp/server/json_rpc_handler.rb
162
+ - lib/action_mcp/server/messaging.rb
163
+ - lib/action_mcp/server/notifications.rb
164
+ - lib/action_mcp/server/prompts.rb
165
+ - lib/action_mcp/server/resources.rb
166
+ - lib/action_mcp/server/roots.rb
167
+ - lib/action_mcp/server/sampling.rb
168
+ - lib/action_mcp/server/sampling_request.rb
169
+ - lib/action_mcp/server/tools.rb
170
+ - lib/action_mcp/server/transport_handler.rb
163
171
  - lib/action_mcp/string_array.rb
164
172
  - lib/action_mcp/test_helper.rb
165
173
  - lib/action_mcp/tool.rb
166
174
  - lib/action_mcp/tool_response.rb
167
175
  - lib/action_mcp/tools_registry.rb
168
176
  - lib/action_mcp/transport.rb
169
- - lib/action_mcp/transport/capabilities.rb
170
- - lib/action_mcp/transport/messaging.rb
171
- - lib/action_mcp/transport/notifications.rb
172
- - lib/action_mcp/transport/prompts.rb
173
- - lib/action_mcp/transport/resources.rb
174
- - lib/action_mcp/transport/roots.rb
175
- - lib/action_mcp/transport/sampling.rb
176
- - lib/action_mcp/transport/sse_client.rb
177
- - lib/action_mcp/transport/stdio_client.rb
178
- - lib/action_mcp/transport/tools.rb
179
- - lib/action_mcp/transport/transport_base.rb
180
- - lib/action_mcp/transport_handler.rb
181
177
  - lib/action_mcp/uri_ambiguity_checker.rb
182
178
  - lib/action_mcp/version.rb
183
179
  - lib/actionmcp.rb
@@ -1,97 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- # Base handler for common functionality
5
- class BaseJsonRpcHandler
6
- delegate :initialize!, :initialized?, to: :transport
7
- delegate :write, :read, to: :transport
8
- attr_reader :transport
9
-
10
- # @param transport [ActionMCP::TransportHandler]
11
- def initialize(transport)
12
- @transport = transport
13
- end
14
-
15
- # Process a single line of input.
16
- # @param line [String, Hash]
17
- def call(line)
18
- request = parse_request(line)
19
- return unless request
20
-
21
- process_request(request)
22
- end
23
-
24
- protected
25
-
26
- def parse_request(line)
27
- if line.is_a?(String)
28
- line.strip!
29
- return if line.empty?
30
-
31
- begin
32
- MultiJson.load(line)
33
- rescue MultiJson::ParseError => e
34
- Rails.logger.error("Failed to parse JSON: #{e.message}")
35
- nil
36
- end
37
- else
38
- line
39
- end
40
- end
41
-
42
- # @param request [Hash]
43
- def process_request(request)
44
- unless request["jsonrpc"] == "2.0"
45
- puts "Invalid request: #{request}"
46
- return
47
- end
48
- read(request)
49
- return if request["error"]
50
- return if request["result"] == {} # Probably a pong
51
-
52
- rpc_method = request["method"]
53
- id = request["id"]
54
- params = request["params"]
55
-
56
- # Common methods (both directions)
57
- case rpc_method
58
- when "ping" # [BOTH] Ping message
59
- transport.send_pong(id)
60
- when "initialize" # [BOTH] Initialization
61
- handle_initialize(id, params)
62
- when %r{^notifications/}
63
- process_common_notifications(rpc_method, params)
64
- else
65
- handle_specific_method(rpc_method, id, params)
66
- end
67
- end
68
-
69
- # Override in subclasses
70
- def handle_initialize(id, params)
71
- raise NotImplementedError, "Subclasses must implement #handle_initialize"
72
- end
73
-
74
- # Override in subclasses
75
- def handle_specific_method(rpc_method, id, params)
76
- raise NotImplementedError, "Subclasses must implement #handle_specific_method"
77
- end
78
-
79
- def process_common_notifications(rpc_method, params)
80
- case rpc_method
81
- when "notifications/initialized" # [BOTH] Initialization complete
82
- puts "Initialized"
83
- transport.initialize!
84
- when "notifications/cancelled" # [BOTH] Request cancellation
85
- puts "Request #{params['requestId']} cancelled: #{params['reason']}"
86
- # Handle cancellation
87
- else
88
- handle_specific_notification(rpc_method, params)
89
- end
90
- end
91
-
92
- # Override in subclasses
93
- def handle_specific_notification(rpc_method, params)
94
- raise NotImplementedError, "Subclasses must implement #handle_specific_notification"
95
- end
96
- end
97
- end
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- # Handler for client-side requests (server -> client)
5
- class ClientJsonRpcHandler < BaseJsonRpcHandler
6
- def handle_initialize(id, params)
7
- # Client-specific initialization
8
- transport.send_client_capabilities(id, params)
9
- end
10
-
11
- def handle_specific_method(rpc_method, id, params)
12
- case rpc_method
13
- when "client/setLoggingLevel" # [CLIENT] Server configuring client logging
14
- transport.set_client_logging_level(id, params["level"])
15
- when %r{^roots/} # [CLIENT] Roots management
16
- process_roots(rpc_method, id, params)
17
- when %r{^sampling/} # [CLIENT] Sampling requests
18
- process_sampling(rpc_method, id, params)
19
- else
20
- Rails.logger.warn("Unknown client method: #{rpc_method}")
21
- end
22
- end
23
-
24
- def handle_specific_notification(rpc_method, params)
25
- case rpc_method
26
- when "notifications/resources/updated" # [CLIENT] Resource update notification
27
- puts "Resource #{params['uri']} was updated"
28
- # Handle resource update notification
29
- when "notifications/tools/list_changed" # [CLIENT] Tool list change notification
30
- puts "Tool list has changed"
31
- # Handle tool list change notification
32
- when "notifications/prompts/list_changed" # [CLIENT] Prompt list change notification
33
- puts "Prompt list has changed"
34
- # Handle prompt list change notification
35
- when "notifications/resources/list_changed" # [CLIENT] Resource list change notification
36
- puts "Resource list has changed"
37
- # Handle resource list change notification
38
- else
39
- Rails.logger.warn("Unknown client notification: #{rpc_method}")
40
- end
41
- end
42
-
43
- private
44
-
45
- # @param rpc_method [String]
46
- # @param id [String]
47
- # @param params [Hash]
48
- def process_roots(rpc_method, id, params)
49
- case rpc_method
50
- when "roots/list" # [CLIENT] List available roots
51
- transport.send_roots_list(id)
52
- else
53
- Rails.logger.warn("Unknown roots method: #{rpc_method}")
54
- end
55
- end
56
-
57
- # @param rpc_method [String]
58
- # @param id [String]
59
- # @param params [Hash]
60
- def process_sampling(rpc_method, id, params)
61
- case rpc_method
62
- when "sampling/createMessage" # [CLIENT] Create a message using AI
63
- transport.send_sampling_create_message(id, params)
64
- else
65
- Rails.logger.warn("Unknown sampling method: #{rpc_method}")
66
- end
67
- end
68
- end
69
- end