actionmcp 0.102.0 → 0.103.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.
- checksums.yaml +4 -4
- data/README.md +11 -3
- data/lib/action_mcp/test_helper/session_store_assertions.rb +0 -70
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +0 -1
- metadata +2 -27
- data/lib/action_mcp/client/active_record_session_store.rb +0 -57
- data/lib/action_mcp/client/base.rb +0 -225
- data/lib/action_mcp/client/blueprint.rb +0 -163
- data/lib/action_mcp/client/catalog.rb +0 -164
- data/lib/action_mcp/client/collection.rb +0 -168
- data/lib/action_mcp/client/elicitation.rb +0 -34
- data/lib/action_mcp/client/json_rpc_handler.rb +0 -202
- data/lib/action_mcp/client/logging.rb +0 -19
- data/lib/action_mcp/client/messaging.rb +0 -28
- data/lib/action_mcp/client/prompt_book.rb +0 -117
- data/lib/action_mcp/client/prompts.rb +0 -47
- data/lib/action_mcp/client/request_timeouts.rb +0 -74
- data/lib/action_mcp/client/resources.rb +0 -100
- data/lib/action_mcp/client/roots.rb +0 -13
- data/lib/action_mcp/client/server.rb +0 -60
- data/lib/action_mcp/client/session_store.rb +0 -39
- data/lib/action_mcp/client/session_store_factory.rb +0 -27
- data/lib/action_mcp/client/streamable_client.rb +0 -264
- data/lib/action_mcp/client/streamable_http_transport.rb +0 -306
- data/lib/action_mcp/client/test_session_store.rb +0 -84
- data/lib/action_mcp/client/toolbox.rb +0 -199
- data/lib/action_mcp/client/tools.rb +0 -47
- data/lib/action_mcp/client/transport.rb +0 -137
- data/lib/action_mcp/client/volatile_session_store.rb +0 -38
- data/lib/action_mcp/client.rb +0 -71
|
@@ -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
|
|
@@ -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
|