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
@@ -1,229 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- class JsonRpcHandler
5
- delegate :initialize!, :initialized?, to: :transport
6
- delegate :write, :read, to: :transport
7
- attr_reader :transport
8
-
9
- # @param transport [ActionMCP::TransportHandler]
10
- def initialize(transport)
11
- @transport = transport
12
- end
13
-
14
- # Process a single line of input.
15
- # @param line [String, Hash]
16
- def call(line)
17
- request = if line.is_a?(String)
18
- line.strip!
19
- return if line.empty?
20
-
21
- begin
22
- MultiJson.load(line)
23
- rescue MultiJson::ParseError => e
24
- Rails.logger.error("Failed to parse JSON: #{e.message}")
25
- return
26
- end
27
- else
28
- line
29
- end
30
- process_request(request)
31
- end
32
-
33
- private
34
-
35
- # @param request [Hash]
36
- def process_request(request)
37
- unless request["jsonrpc"] == "2.0"
38
- puts "Invalid request: #{request}"
39
- return
40
- end
41
- read(request)
42
- return if request["error"]
43
- return if request["result"] == {} # Probably a pong
44
-
45
- rpc_method = request["method"]
46
- id = request["id"]
47
- params = request["params"]
48
-
49
- # Common methods (both directions)
50
- case rpc_method
51
- when "initialize" # [SERVER] Client initializing the connection
52
- transport.send_capabilities(id, params)
53
- when "ping" # [BOTH] Client ping
54
- transport.send_pong(id)
55
-
56
- # Methods that servers must implement (client → server)
57
- when %r{^prompts/} # [SERVER] Prompt-related requests
58
- process_prompts(rpc_method, id, params)
59
- when %r{^resources/} # [SERVER] Resource-related requests
60
- process_resources(rpc_method, id, params)
61
- when %r{^tools/} # [SERVER] Tool-related requests
62
- process_tools(rpc_method, id, params)
63
- when "completion/complete" # [SERVER] Completion requests
64
- process_completion_complete(id, params)
65
-
66
- # Methods that clients must implement (server → client)
67
- when "client/setLoggingLevel" # [CLIENT] Server configuring client logging
68
- process_client_logging(id, params)
69
- when %r{^roots/} # [CLIENT] Roots management
70
- process_roots(rpc_method, id, params)
71
- when %r{^sampling/} # [CLIENT] Sampling requests
72
- process_sampling(rpc_method, id, params)
73
-
74
- # Notifications (can go both ways)
75
- when %r{^notifications/}
76
- puts "\e[31mProcessing notifications\e[0m"
77
- process_notifications(rpc_method, params)
78
- else
79
- puts "\e[31mUnknown method: #{rpc_method} #{request}\e[0m"
80
- end
81
- end
82
-
83
- # @param rpc_method [String]
84
- def process_notifications(rpc_method, params)
85
- case rpc_method
86
- when "notifications/initialized" # [SERVER] Client initialization complete
87
- puts "\e[31mInitialized\e[0m"
88
- transport.initialize!
89
- when "notifications/cancelled" # [BOTH] Request cancellation
90
- puts "\e[31m Request #{params['requestId']} cancelled: #{params['reason']}\e[0m"
91
- # we don't need to do anything here
92
- when "notifications/resources/updated" # [CLIENT] Resource update notification
93
- puts "\e[31m Resource #{params['uri']} was updated\e[0m"
94
- # Handle resource update notification
95
- when "notifications/tools/list_changed" # [CLIENT] Tool list change notification
96
- puts "\e[31m Tool list has changed\e[0m"
97
- # Handle tool list change notification
98
- when "notifications/prompts/list_changed" # [CLIENT] Prompt list change notification
99
- puts "\e[31m Prompt list has changed\e[0m"
100
- # Handle prompt list change notification
101
- when "notifications/resources/list_changed" # [CLIENT] Resource list change notification
102
- puts "\e[31m Resource list has changed\e[0m"
103
- # Handle resource list change notification
104
- else
105
- Rails.logger.warn("Unknown notifications method: #{rpc_method}")
106
- end
107
- end
108
-
109
- # Server methods (client → server)
110
-
111
- # @param id [String]
112
- # @param params [Hash]
113
- # @example {
114
- # "ref": {
115
- # "type": "ref/prompt",
116
- # "name": "code_review"
117
- # },
118
- # "argument": {
119
- # "name": "language",
120
- # "value": "py"
121
- # }
122
- # }
123
- # @return [Hash]
124
- # @example {
125
- # "completion": {
126
- # "values": ["python", "pytorch", "pyside"],
127
- # "total": 10,
128
- # "hasMore": true
129
- # }
130
- # }
131
- def process_completion_complete(id, params)
132
- # TODO: Not Implemented, but to remove the error message in the inspector
133
- transport.send_jsonrpc_response(id, result: { completion: { values: [], total: 0, hasMore: false } })
134
- case params["ref"]["type"]
135
- when "ref/prompt"
136
- # TODO: Implement completion
137
- when "ref/resource"
138
- # TODO: Implement completion
139
- end
140
- end
141
-
142
- # @param rpc_method [String]
143
- # @param id [String]
144
- # @param params [Hash]
145
- def process_prompts(rpc_method, id, params)
146
- case rpc_method
147
- when "prompts/get" # [SERVER] Get specific prompt
148
- transport.send_prompts_get(id, params["name"], params["arguments"])
149
- when "prompts/list" # [SERVER] List available prompts
150
- transport.send_prompts_list(id)
151
- else
152
- Rails.logger.warn("Unknown prompts method: #{rpc_method}")
153
- end
154
- end
155
-
156
- # @param rpc_method [String]
157
- # @param id [String]
158
- # @param params [Hash]
159
- def process_resources(rpc_method, id, params)
160
- case rpc_method
161
- when "resources/list" # [SERVER] List available resources
162
- transport.send_resources_list(id)
163
- when "resources/templates/list" # [SERVER] List resource templates
164
- transport.send_resource_templates_list(id)
165
- when "resources/read" # [SERVER] Read resource content
166
- transport.send_resource_read(id, params)
167
- when "resources/subscribe" # [SERVER] Subscribe to resource updates
168
- transport.send_resource_subscribe(id, params["uri"])
169
- when "resources/unsubscribe" # [SERVER] Unsubscribe from resource updates
170
- transport.send_resource_unsubscribe(id, params["uri"])
171
- else
172
- Rails.logger.warn("Unknown resources method: #{rpc_method}")
173
- end
174
- end
175
-
176
- # @param rpc_method [String]
177
- # @param id [String]
178
- # @param params [Hash]
179
- def process_tools(rpc_method, id, params)
180
- case rpc_method
181
- when "tools/list" # [SERVER] List available tools
182
- transport.send_tools_list(id)
183
- when "tools/call" # [SERVER] Call a tool
184
- transport.send_tools_call(id, params&.dig("name"), params&.dig("arguments"))
185
- else
186
- Rails.logger.warn("Unknown tools method: #{rpc_method}")
187
- end
188
- end
189
-
190
- # Client methods (server → client)
191
-
192
- # @param id [String]
193
- # @param params [Hash]
194
- def process_client_logging(id, params)
195
- level = params["level"]
196
- transport.set_client_logging_level(id, level)
197
- end
198
-
199
- # @param rpc_method [String]
200
- # @param id [String]
201
- # @param params [Hash]
202
- def process_roots(rpc_method, id, params)
203
- case rpc_method
204
- when "roots/list" # [CLIENT] List available roots
205
- transport.send_roots_list(id)
206
- when "roots/add" # [CLIENT] Add a root
207
- transport.send_roots_add(id, params["uri"], params["name"])
208
- when "roots/remove" # [CLIENT] Remove a root
209
- transport.send_roots_remove(id, params["uri"])
210
- else
211
- Rails.logger.warn("Unknown roots method: #{rpc_method}")
212
- end
213
- end
214
-
215
- # @param rpc_method [String]
216
- # @param id [String]
217
- # @param params [Hash]
218
- def process_sampling(rpc_method, id, params)
219
- case rpc_method
220
- when "sampling/createMessage" # [CLIENT] Create a message using AI
221
- # @param id [String]
222
- # @param params [SamplingRequest]
223
- transport.send_sampling_create_message(id, params)
224
- else
225
- Rails.logger.warn("Unknown sampling method: #{rpc_method}")
226
- end
227
- end
228
- end
229
- end
@@ -1,113 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- class SamplingRequest
5
- class << self
6
- attr_reader :default_messages, :default_system_prompt, :default_context,
7
- :default_model_hints, :default_intelligence_priority,
8
- :default_max_tokens, :default_temperature
9
-
10
- def configure
11
- yield self
12
- end
13
-
14
- def messages(messages = nil)
15
- if messages
16
- @default_messages = messages.map do |msg|
17
- mutate_content(msg)
18
- end
19
- end
20
- @default_messages ||= []
21
- end
22
-
23
- def system_prompt(prompt = nil)
24
- @default_system_prompt = prompt if prompt
25
- @default_system_prompt
26
- end
27
-
28
- def include_context(context = nil)
29
- @default_context = context if context
30
- @default_context
31
- end
32
-
33
- def model_hints(hints = nil)
34
- @default_model_hints = hints if hints
35
- @model_hints ||= []
36
- end
37
-
38
- def intelligence_priority(priority = nil)
39
- @default_intelligence_priority = priority if priority
40
- @intelligence_priority ||= 0.9
41
- end
42
-
43
- def max_tokens(tokens = nil)
44
- @default_max_tokens = tokens if tokens
45
- @max_tokens ||= 500
46
- end
47
-
48
- def temperature(temp = nil)
49
- @default_temperature = temp if temp
50
- @temperature ||= 0.7
51
- end
52
-
53
- private
54
-
55
- def mutate_content(msg)
56
- content = msg[:content]
57
- if content.is_a?(ActionMCP::Content) || (content.respond_to?(:to_h) && !content.is_a?(Hash))
58
- { role: msg[:role], content: content.to_h }
59
- else
60
- msg
61
- end
62
- end
63
- end
64
-
65
- attr_accessor :system_prompt, :model_hints, :intelligence_priority, :max_tokens, :temperature
66
- attr_reader :messages, :context
67
-
68
- def initialize
69
- @messages = self.class.default_messages.dup
70
- @system_prompt = self.class.default_system_prompt
71
- @context = self.class.default_context
72
- @model_hints = self.class.default_model_hints.dup
73
- @intelligence_priority = self.class.default_intelligence_priority
74
- @max_tokens = self.class.default_max_tokens
75
- @temperature = self.class.default_temperature
76
-
77
- yield self if block_given?
78
- end
79
-
80
- def messages=(value)
81
- @messages = value.map do |msg|
82
- self.class.send(:mutate_content, msg)
83
- end
84
- end
85
-
86
- def include_context=(value)
87
- @context = value
88
- end
89
-
90
- def add_message(content, role: "user")
91
- if content.is_a?(Content::Base) || (content.respond_to?(:to_h) && !content.is_a?(Hash))
92
- @messages << { role: role, content: content.to_h }
93
- else
94
- content = Content::Text.new(content).to_h if content.is_a?(String)
95
- @messages << { role: role, content: content }
96
- end
97
- end
98
-
99
- def to_h
100
- {
101
- messages: messages.map { |msg| { role: msg[:role], content: msg[:content] } },
102
- systemPrompt: system_prompt,
103
- includeContext: context,
104
- modelPreferences: {
105
- hints: model_hints.map { |name| { name: name } },
106
- intelligencePriority: intelligence_priority
107
- },
108
- maxTokens: max_tokens,
109
- temperature: temperature
110
- }.compact
111
- end
112
- end
113
- end
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- # Handler for server-side requests (client -> server)
5
- class ServerJsonRpcHandler < BaseJsonRpcHandler
6
- def handle_initialize(id, params)
7
- # Server-specific initialization
8
- transport.send_capabilities(id, params)
9
- end
10
-
11
- def handle_specific_method(rpc_method, id, params)
12
- case rpc_method
13
- when %r{^prompts/} # [SERVER] Prompt-related requests
14
- process_prompts(rpc_method, id, params)
15
- when %r{^resources/} # [SERVER] Resource-related requests
16
- process_resources(rpc_method, id, params)
17
- when %r{^tools/} # [SERVER] Tool-related requests
18
- process_tools(rpc_method, id, params)
19
- when "completion/complete" # [SERVER] Completion requests
20
- process_completion_complete(id, params)
21
- else
22
- Rails.logger.warn("Unknown server method: #{rpc_method}")
23
- end
24
- end
25
- def handle_specific_notification(rpc_method, _params)
26
- # Server-specific notifications would go here
27
- case rpc_method
28
- when "notifications/initialized" # [SERVER] Initialization complete
29
- puts "Initialized"
30
- transport.initialize!
31
- else
32
- Rails.logger.warn("Unknown server notification: #{rpc_method}")
33
- end
34
- end
35
-
36
- private
37
-
38
- # All the server-specific methods below...
39
-
40
- def process_completion_complete(id, params)
41
- # Implementation as in original code
42
- transport.send_jsonrpc_response(id, result: { completion: { values: [], total: 0, hasMore: false } })
43
- case params["ref"]["type"]
44
- when "ref/prompt"
45
- # TODO: Implement completion
46
- when "ref/resource"
47
- # TODO: Implement completion
48
- end
49
- end
50
-
51
- def process_prompts(rpc_method, id, params)
52
- case rpc_method
53
- when "prompts/get" # [SERVER] Get specific prompt
54
- transport.send_prompts_get(id, params["name"], params["arguments"])
55
- when "prompts/list" # [SERVER] List available prompts
56
- transport.send_prompts_list(id)
57
- else
58
- Rails.logger.warn("Unknown prompts method: #{rpc_method}")
59
- end
60
- end
61
-
62
- def process_resources(rpc_method, id, params)
63
- case rpc_method
64
- when "resources/list" # [SERVER] List available resources
65
- transport.send_resources_list(id)
66
- when "resources/templates/list" # [SERVER] List resource templates
67
- transport.send_resource_templates_list(id)
68
- when "resources/read" # [SERVER] Read resource content
69
- transport.send_resource_read(id, params)
70
- when "resources/subscribe" # [SERVER] Subscribe to resource updates
71
- transport.send_resource_subscribe(id, params["uri"])
72
- when "resources/unsubscribe" # [SERVER] Unsubscribe from resource updates
73
- transport.send_resource_unsubscribe(id, params["uri"])
74
- else
75
- Rails.logger.warn("Unknown resources method: #{rpc_method}")
76
- end
77
- end
78
-
79
- def process_tools(rpc_method, id, params)
80
- case rpc_method
81
- when "tools/list" # [SERVER] List available tools
82
- transport.send_tools_list(id)
83
- when "tools/call" # [SERVER] Call a tool
84
- transport.send_tools_call(id, params&.dig("name"), params&.dig("arguments"))
85
- else
86
- Rails.logger.warn("Unknown tools method: #{rpc_method}")
87
- end
88
- end
89
- end
90
- end
@@ -1,126 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Transport
5
- class TransportBase
6
- attr_reader :logger, :client_capabilities, :server_capabilities
7
-
8
- def initialize(logger: Logger.new($stdout))
9
- @logger = logger
10
- @on_message = nil
11
- @on_error = nil
12
- @client_capabilities = default_capabilities
13
- @server_capabilities = nil
14
- @initialize_request_id = SecureRandom.hex(6)
15
- @initialization_sent = false
16
- end
17
-
18
- def on_message(&block)
19
- @on_message = block
20
- end
21
-
22
- def on_error(&block)
23
- @on_error = block
24
- end
25
-
26
- def send_initial_capabilities
27
- return if @initialization_sent
28
-
29
- log_info("Sending client capabilities: #{@client_capabilities}")
30
-
31
- request = JsonRpc::Request.new(
32
- id: @initialize_request_id,
33
- method: "initialize",
34
- params: {
35
- protocolVersion: PROTOCOL_VERSION,
36
- capabilities: @client_capabilities,
37
- clientInfo: {
38
- name: user_agent,
39
- version: ActionMCP.gem_version.to_s
40
- }
41
- }
42
- )
43
- @initialization_sent = true
44
- send_message(request.to_json)
45
- end
46
-
47
- def handle_initialize_response(response)
48
- return if @server_capabilities
49
-
50
- if response.result
51
- @server_capabilities = response.result["capabilities"]
52
- send_initialized_notification
53
- else
54
- log_error("Server initialization failed: #{response.error}")
55
- end
56
- end
57
-
58
- protected
59
-
60
- def handle_raw_message(raw)
61
- # Debug - log all raw messages
62
- log_debug("\e[31m<-- #{raw}\e[0m")
63
-
64
- begin
65
- msg_hash = MultiJson.load(raw)
66
- response = nil
67
-
68
- if msg_hash.key?("jsonrpc")
69
- response = if msg_hash.key?("id")
70
- JsonRpc::Response.new(**msg_hash.slice("id", "result", "error").symbolize_keys)
71
- else
72
- JsonRpc::Notification.new(**msg_hash.slice("method", "params").symbolize_keys)
73
- end
74
- end
75
- # Check if this is a response to our initialize request
76
- if response && @initialize_request_id && response.id == @initialize_request_id
77
- handle_initialize_response(response)
78
- elsif response
79
- @on_message&.call(response)
80
- end
81
- rescue MultiJson::ParseError => e
82
- log_error("JSON parse error: #{e} (raw: #{raw})")
83
- @on_error&.call(e)
84
- rescue StandardError => e
85
- log_error("Error handling message: #{e} (raw: #{raw})")
86
- @on_error&.call(e)
87
- end
88
- end
89
-
90
- # Send the initialized notification to the server
91
- def send_initialized_notification
92
- notification = JsonRpc::Notification.new(
93
- method: "initialized"
94
- )
95
-
96
- logger.info("Sent initialized notification to server")
97
- send_message(notification)
98
- end
99
-
100
- def default_capabilities
101
- {
102
- # Base client capabilities
103
- # roots: {}, # Remove from now.
104
- }
105
- end
106
-
107
- def log_debug(message)
108
- @logger.debug("[#{log_prefix}] #{message}")
109
- end
110
-
111
- def log_info(message)
112
- @logger.info("[#{log_prefix}] #{message}")
113
- end
114
-
115
- def log_error(message)
116
- @logger.error("[#{log_prefix}] #{message}")
117
- end
118
-
119
- private
120
-
121
- def log_prefix
122
- self.class.name.split("::").last
123
- end
124
- end
125
- end
126
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- class TransportHandler
5
- attr_reader :session
6
-
7
- delegate :initialize!, :initialized?, to: :session
8
- delegate :read, :write, to: :session
9
- include Logging
10
-
11
- include Transport::Messaging
12
- include Transport::Capabilities
13
- include Transport::Tools
14
- include Transport::Prompts
15
- include Transport::Resources
16
- include Transport::Notifications
17
- include Transport::Sampling
18
- include Transport::Roots
19
-
20
- # @param [ActionMCP::Session] session
21
- def initialize(session)
22
- @session = session
23
- end
24
-
25
- def send_pong(request_id)
26
- send_jsonrpc_response(request_id, result: {})
27
- end
28
-
29
- private
30
-
31
- def write_message(data)
32
- session.write(data)
33
- end
34
-
35
- def format_registry_items(registry)
36
- registry.map { |item| item.klass.to_h }
37
- end
38
- end
39
- end