actionmcp 0.102.0 → 0.104.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +46 -3
  3. data/app/models/action_mcp/session.rb +6 -5
  4. data/lib/action_mcp/configuration.rb +44 -8
  5. data/lib/action_mcp/server/base_session.rb +5 -1
  6. data/lib/action_mcp/test_helper/session_store_assertions.rb +0 -70
  7. data/lib/action_mcp/version.rb +1 -1
  8. data/lib/action_mcp.rb +0 -1
  9. data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +4 -4
  10. data/lib/generators/action_mcp/install/templates/mcp.yml +11 -1
  11. data/lib/generators/action_mcp/tool/templates/tool.rb.erb +2 -2
  12. metadata +1 -26
  13. data/lib/action_mcp/client/active_record_session_store.rb +0 -57
  14. data/lib/action_mcp/client/base.rb +0 -225
  15. data/lib/action_mcp/client/blueprint.rb +0 -163
  16. data/lib/action_mcp/client/catalog.rb +0 -164
  17. data/lib/action_mcp/client/collection.rb +0 -168
  18. data/lib/action_mcp/client/elicitation.rb +0 -34
  19. data/lib/action_mcp/client/json_rpc_handler.rb +0 -202
  20. data/lib/action_mcp/client/logging.rb +0 -19
  21. data/lib/action_mcp/client/messaging.rb +0 -28
  22. data/lib/action_mcp/client/prompt_book.rb +0 -117
  23. data/lib/action_mcp/client/prompts.rb +0 -47
  24. data/lib/action_mcp/client/request_timeouts.rb +0 -74
  25. data/lib/action_mcp/client/resources.rb +0 -100
  26. data/lib/action_mcp/client/roots.rb +0 -13
  27. data/lib/action_mcp/client/server.rb +0 -60
  28. data/lib/action_mcp/client/session_store.rb +0 -39
  29. data/lib/action_mcp/client/session_store_factory.rb +0 -27
  30. data/lib/action_mcp/client/streamable_client.rb +0 -264
  31. data/lib/action_mcp/client/streamable_http_transport.rb +0 -306
  32. data/lib/action_mcp/client/test_session_store.rb +0 -84
  33. data/lib/action_mcp/client/toolbox.rb +0 -199
  34. data/lib/action_mcp/client/tools.rb +0 -47
  35. data/lib/action_mcp/client/transport.rb +0 -137
  36. data/lib/action_mcp/client/volatile_session_store.rb +0 -38
  37. data/lib/action_mcp/client.rb +0 -71
@@ -1,225 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "transport"
4
-
5
- module ActionMCP
6
- module Client
7
- # Base client class containing common MCP functionality
8
- class Base
9
- # Include all transport protocol modules
10
- include Messaging
11
- include Tools
12
- include Prompts
13
- include Resources
14
- include Roots
15
- include Elicitation
16
-
17
- attr_reader :logger, :transport,
18
- :connection_error, :server,
19
- :server_capabilities, :session,
20
- :catalog, :blueprint,
21
- :prompt_book, :toolbox
22
-
23
- delegate :connected?, :ready?, to: :transport
24
-
25
- def initialize(transport:, logger: ActionMCP.logger, protocol_version: nil, **options)
26
- @logger = logger
27
- @transport = transport
28
- @session = nil # Session will be created/loaded based on server response
29
- @session_id = options[:session_id] # Optional session ID for resumption
30
- @protocol_version = protocol_version || ActionMCP::DEFAULT_PROTOCOL_VERSION
31
- @server_capabilities = nil
32
- @connection_error = nil
33
- @initialized = false
34
-
35
- # Resource objects
36
- @catalog = Catalog.new([], self)
37
- # Resource template objects
38
- @blueprint = Blueprint.new([], self)
39
- # Prompt objects
40
- @prompt_book = PromptBook.new([], self)
41
- # Tool objects
42
- @toolbox = Toolbox.new([], self)
43
-
44
- setup_transport_callbacks
45
- end
46
-
47
- # Connect to the MCP server
48
- def connect
49
- return true if connected?
50
-
51
- begin
52
- log_debug("Connecting to MCP server via #{transport.class.name}...")
53
- @connection_error = nil
54
-
55
- success = @transport.connect
56
- unless success
57
- log_error("Failed to establish transport connection")
58
- return false
59
- end
60
-
61
- log_debug("Connected to MCP server")
62
- true
63
- rescue StandardError => e
64
- @connection_error = e.message
65
- log_error("Failed to connect to MCP server: #{e.message}")
66
- false
67
- end
68
- end
69
-
70
- # Disconnect from the MCP server
71
- def disconnect
72
- return true unless connected?
73
-
74
- begin
75
- @transport.disconnect
76
- log_debug("Disconnected from MCP server")
77
- true
78
- rescue StandardError => e
79
- log_error("Error disconnecting from MCP server: #{e.message}")
80
- false
81
- end
82
- end
83
-
84
- # Send a request to the MCP server
85
- def write_message(payload)
86
- unless ready?
87
- log_error("Cannot send request - transport not ready")
88
- return false
89
- end
90
-
91
- begin
92
- # Only write to session if it exists (after initialization)
93
- session&.write(payload)
94
- data = payload.to_json unless payload.is_a?(String)
95
- @transport.send_message(data)
96
- true
97
- rescue StandardError => e
98
- log_error("Failed to send request: #{e.message}")
99
- false
100
- end
101
- end
102
-
103
- def server=(server)
104
- @server = if server.is_a?(Client::Server)
105
- server
106
- else
107
- Client::Server.new(server)
108
- end
109
-
110
- # Only update session if it exists
111
- return unless @session
112
-
113
- @session.server_capabilities = server.capabilities
114
- @session.server_info = server.server_info
115
- @session.save
116
- end
117
-
118
- def initialized?
119
- @initialized && @session&.initialized?
120
- end
121
-
122
- def inspect
123
- session_info = @session ? "session: #{@session.id}" : "session: none"
124
- "#<#{self.class.name} transport: #{transport.class.name}, server: #{server}, client_name: #{client_info[:name]}, client_version: #{client_info[:version]}, capabilities: #{client_capabilities}, connected: #{connected?}, initialized: #{initialized?}, #{session_info}>"
125
- end
126
-
127
- protected
128
-
129
- def setup_transport_callbacks
130
- # Create JSON-RPC handler
131
- @json_rpc_handler = JsonRpcHandler.new(session, self)
132
-
133
- # Set up transport callbacks
134
- @transport.on_message do |message|
135
- handle_raw_message(message)
136
- end
137
-
138
- @transport.on_error do |error|
139
- handle_transport_error(error)
140
- end
141
-
142
- @transport.on_connect do
143
- handle_transport_connect
144
- end
145
-
146
- @transport.on_disconnect do
147
- handle_transport_disconnect
148
- end
149
- end
150
-
151
- def handle_raw_message(raw)
152
- @json_rpc_handler.call(raw)
153
- rescue MultiJson::ParseError => e
154
- log_error("JSON parse error: #{e} (raw: #{raw})")
155
- rescue StandardError => e
156
- log_error("Error handling message: #{e} (raw: #{raw})")
157
- end
158
-
159
- def handle_transport_error(error)
160
- @connection_error = error.message
161
- log_error("Transport error: #{error.message}")
162
- end
163
-
164
- def handle_transport_connect
165
- log_debug("Transport connected")
166
- # Send initial capabilities after connection
167
- send_initial_capabilities
168
- end
169
-
170
- def handle_transport_disconnect
171
- log_debug("Transport disconnected")
172
- end
173
-
174
- def send_initial_capabilities
175
- log_debug("Sending client capabilities")
176
-
177
- # If we have a session_id, we're trying to resume
178
- log_debug("Attempting to resume session: #{@session_id}") if @session_id
179
-
180
- params = {
181
- protocolVersion: @protocol_version,
182
- capabilities: client_capabilities,
183
- clientInfo: client_info
184
- }
185
-
186
- # Include session_id if we're trying to resume
187
- params[:sessionId] = @session_id if @session_id
188
-
189
- # Use a unique request ID (not session ID since we don't have one yet)
190
- request_id = SecureRandom.uuid_v7
191
- send_jsonrpc_request("initialize", params: params, id: request_id)
192
- end
193
-
194
- def client_capabilities
195
- {
196
- # Base client capabilities can be defined here
197
- # TODO
198
- }
199
- end
200
-
201
- def user_agent
202
- "ActionMCP-Client"
203
- end
204
-
205
- def client_info
206
- {
207
- name: user_agent,
208
- version: ActionMCP.gem_version.to_s
209
- }
210
- end
211
-
212
- def log_debug(message)
213
- logger.debug("[ActionMCP::#{self.class.name.split('::').last}] #{message}")
214
- end
215
-
216
- def log_info(message)
217
- logger.info("[ActionMCP::#{self.class.name.split('::').last}] #{message}")
218
- end
219
-
220
- def log_error(message)
221
- logger.error("[ActionMCP::#{self.class.name.split('::').last}] #{message}")
222
- end
223
- end
224
- end
225
- end
@@ -1,163 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Client
5
- # Blueprints
6
- #
7
- # A collection that manages and provides access to URI templates (blueprints) for Model Context Protocol (MCP)
8
- # resource discovery. These blueprints allow dynamic construction of resource URIs by filling in
9
- # variable placeholders with specific values. The class supports lazy loading of templates when
10
- # initialized with a client.
11
- #
12
- # Example usage:
13
- # # Eager loading
14
- # template_data = client.list_resource_templates # Returns array of URI template definitions
15
- # blueprints = Blueprint.new(template_data)
16
- #
17
- # # Lazy loading
18
- # blueprints = Blueprint.new([], client)
19
- # templates = blueprints.all # Templates are loaded here
20
- #
21
- # # Access a specific blueprint by pattern
22
- # file_blueprint = Blueprint.find_by_pattern("file://{path}")
23
- #
24
- # # Generate a concrete URI from a blueprint with parameters
25
- # uri = Blueprint.construct("file://{path}", { path: "/logs/app.log" })
26
- #
27
- class Blueprint < Collection
28
- # Initialize a new Blueprints collection with URI template definitions
29
- #
30
- # @param templates [Array<Hash>] Array of URI template definition hashes, each containing
31
- # uriTemplate, name, description, and optionally mimeType keys
32
- # @param client [Object, nil] Optional client for lazy loading of templates
33
- def initialize(templates, client)
34
- super(templates, client)
35
- self.templates = @collection_data
36
- @load_method = :list_resource_templates
37
- end
38
-
39
- # Find a blueprint by its URI pattern
40
- #
41
- # @param pattern [String] URI template pattern to find
42
- # @return [Blueprint, nil] The blueprint with the given pattern, or nil if not found
43
- def find_by_pattern(pattern)
44
- all.find { |blueprint| blueprint.pattern == pattern }
45
- end
46
-
47
- # Find blueprints by name
48
- #
49
- # @param name [String] Name of the blueprints to find
50
- # @return [Array<Blueprint>] Blueprints with the given name
51
- def find_by_name(name)
52
- all.select { |blueprint| blueprint.name == name }
53
- end
54
-
55
- # Construct a concrete URI by applying parameters to a blueprint
56
- #
57
- # @param pattern [String] URI template pattern to use
58
- # @param params [Hash] Parameters to substitute into the pattern
59
- # @return [String] The constructed URI with parameters applied
60
- # @raise [KeyError] If a required parameter is missing
61
- def construct(pattern, params)
62
- blueprint = find_by_pattern(pattern)
63
- raise ArgumentError, "Unknown blueprint pattern: #{pattern}" unless blueprint
64
-
65
- blueprint.construct(params)
66
- end
67
-
68
- # Check if the collection contains a blueprint with the given pattern
69
- #
70
- # @param pattern [String] The blueprint pattern to check for
71
- # @return [Boolean] true if a blueprint with the pattern exists
72
- def contains?(pattern)
73
- all.any? { |blueprint| blueprint.pattern == pattern }
74
- end
75
-
76
- # Group blueprints by their base protocol
77
- #
78
- # @return [Hash<String, Array<Blueprint>>] Hash mapping protocols to arrays of blueprints
79
- def group_by_protocol
80
- all.group_by(&:protocol)
81
- end
82
-
83
- # Convert raw template data into ResourceTemplate objects
84
- #
85
- # @param templates [Array<Hash>] Array of template definition hashes
86
- def templates=(templates)
87
- @collection_data = templates.map { |template_data| ResourceTemplate.new(template_data) }
88
- end
89
-
90
- # Internal Blueprint class to represent individual URI templates
91
- class ResourceTemplate
92
- attr_reader :pattern, :name, :description, :mime_type, :annotations
93
-
94
- # Initialize a new ResourceTemplate instance
95
- #
96
- # @param data [Hash] ResourceTemplate definition hash containing uriTemplate, name, description,
97
- # and optionally mimeType, and annotations
98
- def initialize(data)
99
- @pattern = data["uriTemplate"]
100
- @name = data["name"]
101
- @description = data["description"]
102
- @mime_type = data["mimeType"]
103
- @variable_pattern = /{([^}]+)}/
104
- @annotations = data["annotations"] || {}
105
- end
106
-
107
- # Extract variable names from the template pattern
108
- #
109
- # @return [Array<String>] List of variable names in the pattern
110
- def variables
111
- @pattern.scan(@variable_pattern).flatten
112
- end
113
-
114
- # Get the protocol part of the URI template
115
- #
116
- # @return [String] The protocol (scheme) of the URI template
117
- def protocol
118
- @pattern.split("://").first
119
- end
120
-
121
- # Construct a concrete URI by substituting parameters into the template pattern
122
- #
123
- # @param params [Hash] Parameters to substitute into the pattern
124
- # @return [String] The constructed URI with parameters applied
125
- # @raise [KeyError] If a required parameter is missing
126
- def construct(params)
127
- result = @pattern.dup
128
-
129
- variables.each do |var|
130
- raise KeyError, "Missing required parameter: #{var}" unless params.key?(var.to_sym) || params.key?(var)
131
-
132
- value = params[var.to_sym] || params[var]
133
- result.gsub!("{#{var}}", value.to_s)
134
- end
135
-
136
- result
137
- end
138
-
139
- # Check if this template is compatible with a set of parameters
140
- #
141
- # @param params [Hash] Parameters to check
142
- # @return [Boolean] true if all required variables have corresponding parameters
143
- def compatible_with?(params)
144
- symbolized_params = params.transform_keys(&:to_sym)
145
- variables.all? { |var| symbolized_params.key?(var.to_sym) }
146
- end
147
-
148
- # Generate a hash representation of the blueprint
149
- #
150
- # @return [Hash] Hash containing blueprint details
151
- def to_h
152
- {
153
- "uriTemplate" => @pattern,
154
- "name" => @name,
155
- "description" => @description,
156
- "mimeType" => @mime_type,
157
- "annotations" => @annotations
158
- }
159
- end
160
- end
161
- end
162
- end
163
- end
@@ -1,164 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Client
5
- # Catalog
6
- #
7
- # A collection that manages and provides access to resources.
8
- # This class stores resource definitions and provides methods for
9
- # retrieving, filtering, and accessing resources by URI or other attributes.
10
- # It supports lazy loading of resources when initialized with a client.
11
- #
12
- # Example usage:
13
- # # Eager loading
14
- # resources_data = client.list_resources # Returns array of resource definitions
15
- # catalog = Catalog.new(resources_data)
16
- #
17
- # # Lazy loading
18
- # catalog = Catalog.new([], client)
19
- # resources = catalog.all # Resources are loaded here
20
- #
21
- # # Access a specific resource by URI
22
- # main_file = catalog.find_by_uri("file:///project/src/main.rs")
23
- #
24
- # # Get all resources matching a criteria
25
- # rust_files = catalog.filter { |r| r.mime_type == "text/x-rust" }
26
- #
27
- class Catalog < Collection
28
- # Initialize a new Catalog with resource definitions
29
- #
30
- # @param resources [Array<Hash>] Array of resource definition hashes, each containing
31
- # uri, name, description, and mimeType keys
32
- # @param client [Object, nil] Optional client for lazy loading of resources
33
- def initialize(resources, client)
34
- super(resources, client)
35
- self.resources = @collection_data
36
- @load_method = :list_resources
37
- end
38
-
39
- # Find a resource by URI
40
- #
41
- # @param uri [String] URI of the resource to find
42
- # @return [Resource, nil] The resource with the given URI, or nil if not found
43
- def find_by_uri(uri)
44
- all.find { |resource| resource.uri == uri }
45
- end
46
-
47
- # Find resources by name
48
- #
49
- # @param name [String] Name of the resources to find
50
- # @return [Array<Resource>] Resources with the given name
51
- def find_by_name(name)
52
- all.select { |resource| resource.name == name }
53
- end
54
-
55
- # Find resources by MIME type
56
- #
57
- # @param mime_type [String] MIME type to search for
58
- # @return [Array<Resource>] Resources with the given MIME type
59
- def find_by_mime_type(mime_type)
60
- all.select { |resource| resource.mime_type == mime_type }
61
- end
62
-
63
- # Get a list of all resource URIs
64
- #
65
- # @return [Array<String>] URIs of all resources in the collection
66
- def uris
67
- all.map(&:uri)
68
- end
69
-
70
- # Check if the collection contains a resource with the given URI
71
- #
72
- # @param uri [String] The resource URI to check for
73
- # @return [Boolean] true if a resource with the URI exists
74
- def contains_uri?(uri)
75
- all.any? { |resource| resource.uri == uri }
76
- end
77
-
78
- # Group resources by MIME type
79
- #
80
- # @return [Hash<String, Array<Resource>>] Hash mapping MIME types to arrays of resources
81
- def group_by_mime_type
82
- all.group_by(&:mime_type)
83
- end
84
-
85
- # Search resources by keyword in name or description
86
- #
87
- # @param keyword [String] Keyword to search for
88
- # @return [Array<Resource>] Resources matching the search term
89
- def search(keyword)
90
- keyword = keyword.downcase
91
- all.select do |resource|
92
- resource.name.downcase.include?(keyword) ||
93
- resource.description&.downcase&.include?(keyword)
94
- end
95
- end
96
-
97
- # Convert raw resource data into Resource objects
98
- #
99
- # @param raw_resources [Array<Hash>] Array of resource definition hashes
100
- def resources=(raw_resources)
101
- @collection_data = raw_resources.map { |resource_data| Resource.new(resource_data) }
102
- end
103
-
104
- # Internal Resource class to represent individual resources
105
- class Resource
106
- attr_reader :uri, :name, :description, :mime_type, :annotations
107
-
108
- # Initialize a new Resource instance
109
- #
110
- # @param data [Hash] Resource definition hash containing uri, name, description, and mimeType
111
- def initialize(data = [])
112
- @uri = data["uri"]
113
- @name = data["name"]
114
- @description = data["description"]
115
- @mime_type = data["mimeType"]
116
- @annotations = data["annotations"] || {}
117
- end
118
-
119
- # Get the file extension from the resource name
120
- #
121
- # @return [String, nil] The file extension or nil if no extension
122
- def extension
123
- File.extname(@name)[1..] if @name.include?(".")
124
- end
125
-
126
- # Check if this resource is a text file based on MIME type
127
- #
128
- # @return [Boolean] true if the resource is a text file
129
- def text?
130
- @mime_type&.start_with?("text/")
131
- end
132
-
133
- # Check if this resource is an image based on MIME type
134
- #
135
- # @return [Boolean] true if the resource is an image
136
- def image?
137
- @mime_type&.start_with?("image/")
138
- end
139
-
140
- # Get the path portion of the URI
141
- #
142
- # @return [String, nil] The path component of the URI
143
- def path
144
- URI(@uri).path
145
- rescue StandardError
146
- nil
147
- end
148
-
149
- # Generate a hash representation of the resource
150
- #
151
- # @return [Hash] Hash containing resource details
152
- def to_h
153
- {
154
- "uri" => @uri,
155
- "name" => @name,
156
- "description" => @description,
157
- "mimeType" => @mime_type,
158
- "annotations" => @annotations
159
- }
160
- end
161
- end
162
- end
163
- end
164
- end