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,168 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Client
5
- # Base collection class for MCP client collections
6
- class Collection
7
- include RequestTimeouts
8
-
9
- attr_reader :client, :loaded, :next_cursor, :total
10
-
11
- def initialize(items, client, silence_sql: true)
12
- @collection_data = items || []
13
- @client = client
14
- @loaded = !@collection_data.empty?
15
- @silence_sql = silence_sql
16
- @next_cursor = nil
17
- @total = items&.size || 0
18
- end
19
-
20
- def all(limit: nil)
21
- if limit
22
- # If a limit is provided, use pagination
23
- result = []
24
- each_page(limit: limit) { |page| result.concat(page) }
25
- result
26
- else
27
- # Otherwise, maintain the old behavior
28
- silence_logs { load_items unless @loaded }
29
- @collection_data
30
- end
31
- end
32
-
33
- def all!(timeout: DEFAULT_TIMEOUT)
34
- silence_logs { load_items(force: true, timeout: timeout) }
35
- @collection_data
36
- end
37
-
38
- # Fetch a single page of results
39
- #
40
- # @param cursor [String, nil] Optional cursor for pagination
41
- # @param limit [Integer, nil] Optional limit for page size
42
- # @return [Array<Object>] The page of items
43
- def page(cursor: nil, limit: nil)
44
- silence_logs { load_page(cursor: cursor, limit: limit) }
45
- @collection_data
46
- end
47
-
48
- # Check if there are more pages available
49
- #
50
- # @return [Boolean] true if there are more pages to fetch
51
- def has_more_pages?
52
- !@next_cursor.nil?
53
- end
54
-
55
- # Fetch the next page of results
56
- #
57
- # @param limit [Integer, nil] Optional limit for page size
58
- # @return [Array<Object>] The next page of items, or empty array if no more pages
59
- def next_page(limit: nil)
60
- return [] unless has_more_pages?
61
-
62
- page(cursor: @next_cursor, limit: limit)
63
- end
64
-
65
- # Iterate through all pages of results
66
- #
67
- # @param limit [Integer, nil] Optional limit for page size
68
- # @yield [page] Block to process each page
69
- # @yieldparam page [Array<Object>] A page of items
70
- def each_page(limit: nil)
71
- return unless block_given?
72
-
73
- current_page = page(limit: limit)
74
- yield current_page
75
-
76
- while has_more_pages?
77
- current_page = next_page(limit: limit)
78
- yield current_page unless current_page.empty?
79
- end
80
- end
81
-
82
- # Filter items based on a given block
83
- #
84
- # @yield [item] Block that determines whether to include an item
85
- # @yieldparam item [Object] An item from the collection
86
- # @yieldreturn [Boolean] true to include the item, false to exclude it
87
- # @return [Array<Object>] Items that match the filter criteria
88
- def filter(&block)
89
- all.select(&block)
90
- end
91
-
92
- # Number of items in the collection
93
- #
94
- # @return [Integer] The number of items
95
- def size
96
- all.size
97
- end
98
-
99
- # Implements enumerable functionality
100
- include Enumerable
101
-
102
- def each(&block)
103
- all.each(&block)
104
- end
105
-
106
- alias loaded? loaded
107
-
108
- protected
109
-
110
- def load_items(force: false, timeout: DEFAULT_TIMEOUT)
111
- return if @loaded && !force
112
-
113
- # Make sure @load_method is defined in the subclass
114
- raise NotImplementedError, "Subclass must define @load_method" unless defined?(@load_method)
115
-
116
- # Use the RequestTimeouts module to handle the request
117
- load_with_timeout(@load_method, force: force, timeout: timeout)
118
- end
119
-
120
- def load_page(cursor: nil, limit: nil, timeout: DEFAULT_TIMEOUT)
121
- # Make sure @load_method is defined in the subclass
122
- raise NotImplementedError, "Subclass must define @load_method" unless defined?(@load_method)
123
-
124
- # Use the RequestTimeouts module to handle the request with pagination params
125
- params = {}
126
- params[:cursor] = cursor if cursor
127
- params[:limit] = limit if limit
128
-
129
- client.send(@load_method, params)
130
-
131
- start_time = Time.now
132
- sleep(0.1) while !@loaded && (Time.now - start_time) < timeout
133
-
134
- # Update @loaded status even if we timed out
135
- @loaded = true
136
-
137
- # Return the loaded data
138
- @collection_data
139
- end
140
-
141
- private
142
-
143
- def silence_logs
144
- return yield unless @silence_sql
145
-
146
- original_log_level = ActionMCP::Session.logger&.level
147
- begin
148
- # Temporarily increase log level to suppress SQL queries
149
- ActionMCP::Session.logger.level = Logger::WARN if ActionMCP::Session.logger
150
- yield
151
- ensure
152
- # Restore original log level
153
- ActionMCP::Session.logger.level = original_log_level if ActionMCP::Session.logger && original_log_level
154
- end
155
- end
156
-
157
- def log_error(message)
158
- # Safely handle logging - don't assume Rails.logger exists
159
- if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
160
- Rails.logger.error("[#{self.class.name}] #{message}")
161
- else
162
- # Fall back to puts if Rails.logger is not available
163
- puts "[ERROR] [#{self.class.name}] #{message}"
164
- end
165
- end
166
- end
167
- end
168
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Client
5
- # Handles elicitation requests from servers
6
- module Elicitation
7
- # Process elicitation request from server
8
- # @param id [String, Integer] The request ID
9
- # @param params [Hash] The elicitation parameters
10
- def process_elicitation_request(id, params)
11
- params["message"]
12
- params["requestedSchema"]
13
-
14
- # In a real implementation, this would prompt the user
15
- # For now, we'll just return a decline response
16
- # Actual implementations should override this method
17
- send_jsonrpc_response(id, result: {
18
- action: "decline"
19
- })
20
- end
21
-
22
- # Send elicitation response
23
- # @param id [String, Integer] The request ID
24
- # @param action [String] The action taken ("accept", "decline", "cancel")
25
- # @param content [Hash, nil] The form data if action is "accept"
26
- def send_elicitation_response(id, action:, content: nil)
27
- result = { action: action }
28
- result[:content] = content if action == "accept" && content
29
-
30
- send_jsonrpc_response(id, result: result)
31
- end
32
- end
33
- end
34
- end
@@ -1,202 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Client
5
- class JsonRpcHandler < JsonRpcHandlerBase
6
- attr_reader :client
7
-
8
- def initialize(transport, client)
9
- super(transport)
10
- @client = client
11
- end
12
-
13
- # Handle client-side JSON-RPC requests/responses
14
- # @param request [JSON_RPC::Request, JSON_RPC::Notification, JSON_RPC::Response]
15
- def call(request)
16
- case request
17
- when JSON_RPC::Request
18
- handle_request(request)
19
- when JSON_RPC::Notification
20
- handle_notification(request)
21
- when JSON_RPC::Response
22
- handle_response(request)
23
- end
24
- end
25
-
26
- private
27
-
28
- def handle_request(request)
29
- id = request.id
30
- rpc_method = request.method
31
- params = request.params
32
-
33
- handle_method(rpc_method, id, params)
34
- end
35
-
36
- def handle_notification(notification)
37
- # Handle server notifications to client
38
- client.log_debug("Received notification: #{notification.method}")
39
- end
40
-
41
- def handle_response(response)
42
- # Handle server responses to client requests
43
- client.log_debug("Received response: #{response.id} - #{response.result ? 'success' : 'error'}")
44
- end
45
-
46
- protected
47
-
48
- # Handle client-specific methods
49
- # @param rpc_method [String]
50
- # @param id [String, Integer]
51
- # @param params [Hash]
52
- def handle_method(rpc_method, id, params)
53
- case rpc_method
54
- when Methods::ELICITATION_CREATE
55
- client.process_elicitation_request(id, params)
56
- when %r{^roots/}
57
- process_roots(rpc_method, id)
58
- when %r{^sampling/}
59
- process_sampling(rpc_method, id, params)
60
- else
61
- common_result = handle_common_methods(rpc_method, id, params)
62
- client.log_warn("Unknown server method: #{rpc_method} #{id} #{params}") if common_result.nil?
63
- end
64
- end
65
-
66
- # @param rpc_method [String]
67
- # @param id [String]
68
- def process_roots(rpc_method, id)
69
- case rpc_method
70
- when "roots/list" # List available roots
71
- transport.send_roots_list(id)
72
- else
73
- Rails.logger.warn("Unknown roots method: #{rpc_method}")
74
- end
75
- end
76
-
77
- # @param rpc_method [String]
78
- # @param id [String]
79
- # @param params [Hash]
80
- def process_sampling(rpc_method, id, params)
81
- case rpc_method
82
- when "sampling/createMessage" # Create a message using AI
83
- # @param id [String]
84
- # @param params [SamplingRequest]
85
- transport.send_sampling_create_message(id, params)
86
- else
87
- Rails.logger.warn("Unknown sampling method: #{rpc_method}")
88
- end
89
- end
90
-
91
- # @param rpc_method [String]
92
- def process_notifications(rpc_method, params)
93
- case rpc_method
94
- when "notifications/resources/updated" # Resource update notification
95
- client.log_debug("Resource #{params['uri']} was updated")
96
- # Handle resource update notification
97
- # TODO: fetch updated resource or mark it as stale
98
- when "notifications/tools/list_changed" # Tool list change notification
99
- client.log_debug("Tool list has changed")
100
- # Handle tool list change notification
101
- # TODO: fetch new tools or mark them as stale
102
- when "notifications/prompts/list_changed" # Prompt list change notification
103
- client.log_debug("Prompt list has changed")
104
- # Handle prompt list change notification
105
- # TODO: fetch new prompts or mark them as stale
106
- when "notifications/resources/list_changed" # Resource list change notification
107
- client.log_debug("Resource list has changed")
108
- # Handle resource list change notification
109
- # TODO: fetch new resources or mark them as stale
110
- else
111
- super
112
- end
113
- end
114
-
115
- def process_response(id, result)
116
- # Check if this is a response to an initialize request
117
- # We need to check the actual request method, not just compare IDs
118
- request = client.session ? transport.messages.requests.find_by(jsonrpc_id: id) : nil
119
-
120
- # If no session yet, this might be the initialize response
121
- if !client.session && result["serverInfo"]
122
- handle_initialize_response(id, result)
123
- return send_initialized_notification
124
- end
125
-
126
- return unless request
127
-
128
- # Mark the request as acknowledged
129
- request.update(request_acknowledged: true)
130
-
131
- case request.rpc_method
132
- when "tools/list"
133
- client.toolbox.tools = result["tools"]
134
- client.toolbox.instance_variable_set(:@next_cursor, result["nextCursor"])
135
- client.toolbox.instance_variable_set(:@total, result["tools"]&.size || 0)
136
- return true
137
- when "prompts/list"
138
- client.prompt_book.prompts = result["prompts"]
139
- client.prompt_book.instance_variable_set(:@next_cursor, result["nextCursor"])
140
- client.prompt_book.instance_variable_set(:@total, result["prompts"]&.size || 0)
141
- return true
142
- when "resources/list"
143
- client.catalog.resources = result["resources"]
144
- client.catalog.instance_variable_set(:@next_cursor, result["nextCursor"])
145
- client.catalog.instance_variable_set(:@total, result["resources"]&.size || 0)
146
- return true
147
- when "resources/templates/list"
148
- client.blueprint.templates = result["resourceTemplates"]
149
- client.blueprint.instance_variable_set(:@next_cursor, result["nextCursor"])
150
- client.blueprint.instance_variable_set(:@total, result["resourceTemplates"]&.size || 0)
151
- return true
152
- end
153
-
154
- client.log_warn("Unknown response: #{id} #{result}")
155
- end
156
-
157
- def process_error(id, error)
158
- # Do something ?
159
- client.log_error("Unknown error: #{id} #{error}")
160
- end
161
-
162
- def handle_initialize_response(_request_id, result)
163
- # Session ID comes from HTTP headers, not the response body
164
- # The transport should have already extracted it
165
- session_id = transport.instance_variable_get(:@session_id)
166
-
167
- if session_id.nil?
168
- client.log_error("No session ID received from server")
169
- return
170
- end
171
-
172
- # Check if we're resuming an existing session
173
- if client.instance_variable_get(:@session_id) && session_id == client.instance_variable_get(:@session_id)
174
- # We're resuming an existing session
175
- client.instance_variable_set(:@session, ActionMCP::Session.find(session_id))
176
- client.log_info("Resumed existing session: #{session_id}")
177
- else
178
- # Create a new session with the server-provided ID
179
- client.instance_variable_set(:@session, ActionMCP::Session.from_client.new(
180
- id: session_id,
181
- protocol_version: result["protocolVersion"] || ActionMCP::DEFAULT_PROTOCOL_VERSION,
182
- client_info: client.client_info,
183
- client_capabilities: client.client_capabilities,
184
- server_info: result["serverInfo"],
185
- server_capabilities: result["capabilities"]
186
- ))
187
- client.session.save
188
- client.log_info("Created new session: #{session_id}")
189
- end
190
-
191
- # Set the server info
192
- client.server = Client::Server.new(result)
193
- client.instance_variable_set(:@initialized, true)
194
- end
195
-
196
- def send_initialized_notification
197
- transport.initialize! if transport.respond_to?(:initialize!)
198
- client.send_jsonrpc_notification("notifications/initialized")
199
- end
200
- end
201
- end
202
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Client
5
- module Logging
6
- # Set the client's logging level
7
- # @param level [String] Logging level (debug, info, warning, error, etc.)
8
- # @return [Boolean] Success status
9
- def set_logging_level(level)
10
- request_id = SecureRandom.uuid_v7
11
-
12
- # Send request
13
- send_jsonrpc_request("client/setLoggingLevel",
14
- params: { level: level },
15
- id: request_id)
16
- end
17
- end
18
- end
19
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Client
5
- module Messaging
6
- def send_jsonrpc_request(method, params: nil, id: SecureRandom.uuid_v7)
7
- request = JSON_RPC::Request.new(id: id, method: method, params: params)
8
- write_message(request)
9
- end
10
-
11
- def send_jsonrpc_response(request_id, result: nil, error: nil)
12
- response = JSON_RPC::Response.new(id: request_id, result: result, error: error)
13
- write_message(response)
14
- end
15
-
16
- def send_jsonrpc_notification(method, params = nil)
17
- notification = JSON_RPC::Notification.new(method: method, params: params)
18
- write_message(notification)
19
- end
20
-
21
- def send_jsonrpc_error(request_id, symbol, message, data = nil)
22
- error = JSON_RPC::JsonRpcError.new(symbol, message:, data:)
23
- response = JSON_RPC::Response.new(id: request_id, error:)
24
- write_message(response)
25
- end
26
- end
27
- end
28
- end
@@ -1,117 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Client
5
- # PromptBook
6
- #
7
- # A collection that manages and provides access to prompt templates from the MCP server.
8
- # The class stores prompt definitions along with their arguments and provides methods
9
- # for retrieving, filtering, and accessing prompts. It supports lazy loading of prompts
10
- # when initialized with a client.
11
- #
12
- # Example usage:
13
- # # Eager loading
14
- # prompts_data = client.list_prompts # Returns array of prompt definitions
15
- # book = PromptBook.new(prompts_data)
16
- #
17
- # # Lazy loading
18
- # book = PromptBook.new([], client)
19
- # prompts = book.all # Prompts are loaded here
20
- #
21
- # # Access a specific prompt by name
22
- # summary_prompt = book.find("summarize_text")
23
- #
24
- # # Get all prompts matching a criteria
25
- # text_prompts = book.filter { |p| p.name.include?("text") }
26
- #
27
- class PromptBook < Collection
28
- # Initialize a new PromptBook with prompt definitions
29
- #
30
- # @param prompts [Array<Hash>] Array of prompt definition hashes, each containing
31
- # name, description, and arguments keys
32
- # @param client [Object, nil] Optional client for lazy loading of prompts
33
- def initialize(prompts, client)
34
- super(prompts, client)
35
- self.prompts = @collection_data
36
- @load_method = :list_prompts
37
- end
38
-
39
- # Find a prompt by name
40
- #
41
- # @param name [String] Name of the prompt to find
42
- # @return [Prompt, nil] The prompt with the given name, or nil if not found
43
- def find(name)
44
- all.find { |prompt| prompt.name == name }
45
- end
46
-
47
- # Get a list of all prompt names
48
- #
49
- # @return [Array<String>] Names of all prompts in the collection
50
- def names
51
- all.map(&:name)
52
- end
53
-
54
- # Check if the collection contains a prompt with the given name
55
- #
56
- # @param name [String] The prompt name to check for
57
- # @return [Boolean] true if a prompt with the name exists
58
- def contains?(name)
59
- all.any? { |prompt| prompt.name == name }
60
- end
61
-
62
- # Convert raw prompt data into Prompt objects
63
- #
64
- # @param prompts [Array<Hash>] Array of prompt definition hashes
65
- def prompts=(prompts)
66
- @collection_data = prompts.map { |data| Prompt.new(data) }
67
- end
68
-
69
- # Internal Prompt class to represent individual prompts
70
- class Prompt
71
- attr_reader :name, :description, :arguments
72
-
73
- # Initialize a new Prompt instance
74
- #
75
- # @param data [Hash] Prompt definition hash containing name, description, and arguments
76
- def initialize(data)
77
- @name = data["name"]
78
- @description = data["description"]
79
- @arguments = data["arguments"] || []
80
- end
81
-
82
- # Get all required arguments for this prompt
83
- #
84
- # @return [Array<Hash>] Array of argument hashes that are required
85
- def required_arguments
86
- @arguments.select { |arg| arg["required"] }
87
- end
88
-
89
- # Get all optional arguments for this prompt
90
- #
91
- # @return [Array<Hash>] Array of argument hashes that are optional
92
- def optional_arguments
93
- @arguments.reject { |arg| arg["required"] }
94
- end
95
-
96
- # Check if the prompt has a specific argument
97
- #
98
- # @param name [String] Name of the argument to check for
99
- # @return [Boolean] true if the argument exists
100
- def has_argument?(name)
101
- @arguments.any? { |arg| arg["name"] == name }
102
- end
103
-
104
- # Generate a hash representation of the prompt
105
- #
106
- # @return [Hash] Hash containing prompt details
107
- def to_h
108
- {
109
- "name" => @name,
110
- "description" => @description,
111
- "arguments" => @arguments
112
- }
113
- end
114
- end
115
- end
116
- end
117
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Client
5
- module Prompts
6
- # List all available prompts from the server
7
- # @param params [Hash] Optional parameters for pagination
8
- # @option params [String] :cursor Pagination cursor for fetching next page
9
- # @option params [Integer] :limit Maximum number of items to return
10
- # @return [String] Request ID for tracking the request
11
- def list_prompts(params = {})
12
- request_id = SecureRandom.uuid_v7
13
-
14
- # Send request with pagination parameters if provided
15
- request_params = {}
16
- request_params[:cursor] = params[:cursor] if params[:cursor]
17
- request_params[:limit] = params[:limit] if params[:limit]
18
-
19
- send_jsonrpc_request("prompts/list",
20
- params: request_params.empty? ? nil : request_params,
21
- id: request_id)
22
-
23
- # Return request ID for tracking the request
24
- request_id
25
- end
26
-
27
- # Get a specific prompt with arguments
28
- # @param name [String] Name of the prompt to get
29
- # @param arguments [Hash] Arguments to pass to the prompt
30
- # @return [String] Request ID for tracking the request
31
- def get_prompt(name, arguments = {})
32
- request_id = SecureRandom.uuid_v7
33
-
34
- # Send request
35
- send_jsonrpc_request("prompts/get",
36
- params: {
37
- name: name,
38
- arguments: arguments
39
- },
40
- id: request_id)
41
-
42
- # Return request ID for tracking the request
43
- request_id
44
- end
45
- end
46
- end
47
- end
@@ -1,74 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Client
5
- module RequestTimeouts
6
- # Default timeout in seconds
7
- DEFAULT_TIMEOUT = 1.0
8
-
9
- # Load resources with timeout support - blocking until response or timeout
10
- # @param method_name [Symbol] The method to call for loading (e.g., :list_resources)
11
- # @param force [Boolean] Whether to force reload even if already loaded
12
- # @param timeout [Float] Timeout in seconds
13
- # @return [Boolean] Success status
14
- def load_with_timeout(method_name, force: false, timeout: DEFAULT_TIMEOUT)
15
- return true if @loaded && !force
16
-
17
- # Make the request and store its ID
18
- request_id = client.send(method_name)
19
-
20
- start_time = Time.now
21
-
22
- # Wait until either:
23
- # 1. The collection is loaded (@loaded becomes true from JsonRpcHandler)
24
- # 2. The timeout is reached
25
- sleep(0.1) while !@loaded && (Time.now - start_time) < timeout
26
-
27
- # If we timed out
28
- unless @loaded
29
- request = client.session.messages.requests.find_by(jsonrpc_id: request_id)
30
-
31
- if request && !request.request_acknowledged?
32
- # Send cancel notification
33
- client.send_jsonrpc_notification("notifications/cancelled", {
34
- requestId: request_id,
35
- reason: "Request timed out after #{timeout} seconds"
36
- })
37
-
38
- # Mark as cancelled in the database
39
- request.update(request_cancelled: true)
40
-
41
- log_error("Request #{method_name} timed out after #{timeout} seconds")
42
- end
43
-
44
- # Mark as loaded even though we timed out
45
- @loaded = true
46
- return false
47
- end
48
-
49
- # Collection was successfully loaded
50
- true
51
- end
52
-
53
- private
54
-
55
- def handle_timeout(request_id, method_name, timeout)
56
- # Find the request
57
- request = client.session.messages.requests.find_by(jsonrpc_id: request_id)
58
-
59
- return unless request && !request.request_acknowledged?
60
-
61
- # Send cancel notification
62
- client.send_jsonrpc_notification("notifications/cancelled", {
63
- requestId: request_id,
64
- reason: "Request timed out after #{timeout} seconds"
65
- })
66
-
67
- # Mark as cancelled in the database
68
- request.update(request_cancelled: true)
69
-
70
- log_error("Request #{method_name} timed out after #{timeout} seconds")
71
- end
72
- end
73
- end
74
- end