actionmcp 0.20.0 → 0.24.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/app/controllers/action_mcp/messages_controller.rb +2 -2
- data/app/models/action_mcp/session/message.rb +21 -5
- data/app/models/action_mcp/session.rb +8 -4
- data/lib/action_mcp/capability.rb +2 -3
- data/lib/action_mcp/client/base.rb +222 -0
- data/lib/action_mcp/client/blueprint.rb +161 -0
- data/lib/action_mcp/client/catalog.rb +160 -0
- data/lib/action_mcp/client/collection.rb +93 -0
- data/lib/action_mcp/client/json_rpc_handler.rb +114 -0
- data/lib/action_mcp/client/logging.rb +20 -0
- data/lib/action_mcp/{transport → client}/messaging.rb +1 -1
- data/lib/action_mcp/client/prompt_book.rb +117 -0
- data/lib/action_mcp/client/prompts.rb +39 -0
- data/lib/action_mcp/client/request_timeouts.rb +76 -0
- data/lib/action_mcp/client/resources.rb +85 -0
- data/lib/action_mcp/client/roots.rb +13 -0
- data/lib/action_mcp/client/server.rb +60 -0
- data/lib/action_mcp/{transport → client}/sse_client.rb +70 -111
- data/lib/action_mcp/{transport → client}/stdio_client.rb +38 -38
- data/lib/action_mcp/client/toolbox.rb +194 -0
- data/lib/action_mcp/client/tools.rb +39 -0
- data/lib/action_mcp/client.rb +20 -231
- data/lib/action_mcp/engine.rb +1 -3
- data/lib/action_mcp/instrumentation/controller_runtime.rb +1 -1
- data/lib/action_mcp/instrumentation/instrumentation.rb +2 -0
- data/lib/action_mcp/instrumentation/resource_instrumentation.rb +1 -0
- data/lib/action_mcp/json_rpc_handler_base.rb +106 -0
- data/lib/action_mcp/log_subscriber.rb +2 -0
- data/lib/action_mcp/logging.rb +1 -1
- data/lib/action_mcp/{transport → server}/capabilities.rb +2 -2
- data/lib/action_mcp/server/json_rpc_handler.rb +121 -0
- data/lib/action_mcp/server/messaging.rb +28 -0
- data/lib/action_mcp/{transport → server}/notifications.rb +1 -1
- data/lib/action_mcp/{transport → server}/prompts.rb +1 -1
- data/lib/action_mcp/{transport → server}/resources.rb +1 -18
- data/lib/action_mcp/{transport → server}/roots.rb +1 -1
- data/lib/action_mcp/{transport → server}/sampling.rb +1 -1
- data/lib/action_mcp/server/sampling_request.rb +115 -0
- data/lib/action_mcp/{transport → server}/tools.rb +1 -1
- data/lib/action_mcp/server/transport_handler.rb +41 -0
- data/lib/action_mcp/uri_ambiguity_checker.rb +6 -10
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +2 -1
- metadata +31 -33
- data/lib/action_mcp/base_json_rpc_handler.rb +0 -97
- data/lib/action_mcp/client_json_rpc_handler.rb +0 -69
- data/lib/action_mcp/json_rpc_handler.rb +0 -229
- data/lib/action_mcp/sampling_request.rb +0 -113
- data/lib/action_mcp/server_json_rpc_handler.rb +0 -90
- data/lib/action_mcp/transport/transport_base.rb +0 -126
- data/lib/action_mcp/transport_handler.rb +0 -39
@@ -0,0 +1,93 @@
|
|
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
|
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
|
+
end
|
17
|
+
|
18
|
+
def all
|
19
|
+
silence_logs { load_items unless @loaded }
|
20
|
+
@collection_data
|
21
|
+
end
|
22
|
+
|
23
|
+
def all!(timeout: DEFAULT_TIMEOUT)
|
24
|
+
silence_logs { load_items(force: true, timeout: timeout) }
|
25
|
+
@collection_data
|
26
|
+
end
|
27
|
+
|
28
|
+
# Filter items based on a given block
|
29
|
+
#
|
30
|
+
# @yield [item] Block that determines whether to include an item
|
31
|
+
# @yieldparam item [Object] An item from the collection
|
32
|
+
# @yieldreturn [Boolean] true to include the item, false to exclude it
|
33
|
+
# @return [Array<Object>] Items that match the filter criteria
|
34
|
+
def filter(&block)
|
35
|
+
all.select(&block)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Number of items in the collection
|
39
|
+
#
|
40
|
+
# @return [Integer] The number of items
|
41
|
+
def size
|
42
|
+
all.size
|
43
|
+
end
|
44
|
+
|
45
|
+
# Implements enumerable functionality
|
46
|
+
include Enumerable
|
47
|
+
|
48
|
+
def each(&block)
|
49
|
+
all.each(&block)
|
50
|
+
end
|
51
|
+
|
52
|
+
alias loaded? loaded
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def load_items(force: false, timeout: DEFAULT_TIMEOUT)
|
57
|
+
return if @loaded && !force
|
58
|
+
|
59
|
+
# Make sure @load_method is defined in the subclass
|
60
|
+
raise NotImplementedError, "Subclass must define @load_method" unless defined?(@load_method)
|
61
|
+
|
62
|
+
# Use the RequestTimeouts module to handle the request
|
63
|
+
load_with_timeout(@load_method, force: force, timeout: timeout)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def silence_logs
|
69
|
+
return yield unless @silence_sql
|
70
|
+
|
71
|
+
original_log_level = Session.logger&.level
|
72
|
+
begin
|
73
|
+
# Temporarily increase log level to suppress SQL queries
|
74
|
+
Session.logger.level = Logger::WARN if Session.logger
|
75
|
+
yield
|
76
|
+
ensure
|
77
|
+
# Restore original log level
|
78
|
+
Session.logger.level = original_log_level if Session.logger
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def log_error(message)
|
83
|
+
# Safely handle logging - don't assume Rails.logger exists
|
84
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
85
|
+
Rails.logger.error("[#{self.class.name}] #{message}")
|
86
|
+
else
|
87
|
+
# Fall back to puts if Rails.logger is not available
|
88
|
+
puts "[ERROR] [#{self.class.name}] #{message}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Client
|
5
|
+
class JsonRpcHandler < JsonRpcHandlerBase
|
6
|
+
attr_reader :client
|
7
|
+
def initialize(transport, client)
|
8
|
+
super(transport)
|
9
|
+
@client = client
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
# Handle client-specific methods
|
15
|
+
# @param rpc_method [String]
|
16
|
+
# @param id [String, Integer]
|
17
|
+
# @param params [Hash]
|
18
|
+
def handle_method(rpc_method, id, params)
|
19
|
+
puts "\e[31mUnknown server method: #{rpc_method} #{id} #{params}\e[0m"
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param rpc_method [String]
|
23
|
+
# @param id [String]
|
24
|
+
def process_roots(rpc_method, id)
|
25
|
+
case rpc_method
|
26
|
+
when "roots/list" # List available roots
|
27
|
+
transport.send_roots_list(id)
|
28
|
+
else
|
29
|
+
Rails.logger.warn("Unknown roots method: #{rpc_method}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param rpc_method [String]
|
34
|
+
# @param id [String]
|
35
|
+
# @param params [Hash]
|
36
|
+
def process_sampling(rpc_method, id, params)
|
37
|
+
case rpc_method
|
38
|
+
when "sampling/createMessage" # Create a message using AI
|
39
|
+
# @param id [String]
|
40
|
+
# @param params [SamplingRequest]
|
41
|
+
transport.send_sampling_create_message(id, params)
|
42
|
+
else
|
43
|
+
Rails.logger.warn("Unknown sampling method: #{rpc_method}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param rpc_method [String]
|
48
|
+
def process_notifications(rpc_method, params)
|
49
|
+
case rpc_method
|
50
|
+
when "notifications/resources/updated" # Resource update notification
|
51
|
+
puts "\e[31m Resource #{params['uri']} was updated\e[0m"
|
52
|
+
# Handle resource update notification
|
53
|
+
# TODO: fetch updated resource or mark it as stale
|
54
|
+
when "notifications/tools/list_changed" # Tool list change notification
|
55
|
+
puts "\e[31m Tool list has changed\e[0m"
|
56
|
+
# Handle tool list change notification
|
57
|
+
# TODO: fetch new tools or mark them as stale
|
58
|
+
when "notifications/prompts/list_changed" # Prompt list change notification
|
59
|
+
puts "\e[31m Prompt list has changed\e[0m"
|
60
|
+
# Handle prompt list change notification
|
61
|
+
# TODO: fetch new prompts or mark them as stale
|
62
|
+
when "notifications/resources/list_changed" # Resource list change notification
|
63
|
+
puts "\e[31m Resource list has changed\e[0m"
|
64
|
+
# Handle resource list change notification
|
65
|
+
# TODO: fetch new resources or mark them as stale
|
66
|
+
else
|
67
|
+
super
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def process_response(id, result)
|
72
|
+
if transport.id == id
|
73
|
+
## This initializes the transport
|
74
|
+
client.server = Client::Server.new(result)
|
75
|
+
return send_initialized_notification
|
76
|
+
end
|
77
|
+
|
78
|
+
request = transport.messages.requests.find_by(jsonrpc_id: id)
|
79
|
+
return unless request
|
80
|
+
|
81
|
+
# Mark the request as acknowledged
|
82
|
+
request.update(request_acknowledged: true)
|
83
|
+
|
84
|
+
case request.rpc_method
|
85
|
+
when "tools/list"
|
86
|
+
client.toolbox.tools = result["tools"]
|
87
|
+
return true
|
88
|
+
when "prompts/list"
|
89
|
+
client.prompt_book.prompts = result["prompts"]
|
90
|
+
return true
|
91
|
+
when "resources/list"
|
92
|
+
client.catalog.resources = result["resources"]
|
93
|
+
return true
|
94
|
+
when "resources/templates/list"
|
95
|
+
client.blueprint.templates = result["resourceTemplates"]
|
96
|
+
return true
|
97
|
+
end
|
98
|
+
|
99
|
+
puts "\e[31mUnknown response: #{id} #{result}\e[0m"
|
100
|
+
end
|
101
|
+
|
102
|
+
def process_error(id, error)
|
103
|
+
# Do something ?
|
104
|
+
puts "\e[31mUnknown error: #{id} #{error}\e[0m"
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
def send_initialized_notification
|
109
|
+
transport.initialize!
|
110
|
+
client.send_jsonrpc_notification("notifications/initialized")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,20 @@
|
|
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
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,117 @@
|
|
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
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Client
|
5
|
+
module Prompts
|
6
|
+
# List all available prompts from the server
|
7
|
+
# @return [String] Request ID for tracking the request
|
8
|
+
def list_prompts
|
9
|
+
request_id = SecureRandom.uuid_v7
|
10
|
+
|
11
|
+
# Send request
|
12
|
+
send_jsonrpc_request("prompts/list", id: request_id)
|
13
|
+
|
14
|
+
# Return request ID for tracking the request
|
15
|
+
request_id
|
16
|
+
end
|
17
|
+
|
18
|
+
# Get a specific prompt with arguments
|
19
|
+
# @param name [String] Name of the prompt to get
|
20
|
+
# @param arguments [Hash] Arguments to pass to the prompt
|
21
|
+
# @return [String] Request ID for tracking the request
|
22
|
+
def get_prompt(name, arguments = {})
|
23
|
+
request_id = SecureRandom.uuid_v7
|
24
|
+
|
25
|
+
# Send request
|
26
|
+
send_jsonrpc_request("prompts/get",
|
27
|
+
params: {
|
28
|
+
name: name,
|
29
|
+
arguments: arguments
|
30
|
+
},
|
31
|
+
id: request_id
|
32
|
+
)
|
33
|
+
|
34
|
+
# Return request ID for tracking the request
|
35
|
+
request_id
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,76 @@
|
|
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
|
+
while !@loaded && (Time.now - start_time) < timeout
|
26
|
+
sleep(0.1)
|
27
|
+
end
|
28
|
+
|
29
|
+
# If we timed out
|
30
|
+
unless @loaded
|
31
|
+
request = client.session.messages.requests.find_by(jsonrpc_id: request_id)
|
32
|
+
|
33
|
+
if request && !request.request_acknowledged?
|
34
|
+
# Send cancel notification
|
35
|
+
client.send_jsonrpc_notification("notifications/cancelled", {
|
36
|
+
requestId: request_id,
|
37
|
+
reason: "Request timed out after #{timeout} seconds"
|
38
|
+
})
|
39
|
+
|
40
|
+
# Mark as cancelled in the database
|
41
|
+
request.update(request_cancelled: true)
|
42
|
+
|
43
|
+
log_error("Request #{method_name} timed out after #{timeout} seconds")
|
44
|
+
end
|
45
|
+
|
46
|
+
# Mark as loaded even though we timed out
|
47
|
+
@loaded = true
|
48
|
+
return false
|
49
|
+
end
|
50
|
+
|
51
|
+
# Collection was successfully loaded
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def handle_timeout(request_id, method_name, timeout)
|
58
|
+
# Find the request
|
59
|
+
request = client.session.messages.requests.find_by(jsonrpc_id: request_id)
|
60
|
+
|
61
|
+
if request && !request.request_acknowledged?
|
62
|
+
# Send cancel notification
|
63
|
+
client.send_jsonrpc_notification("notifications/cancelled", {
|
64
|
+
requestId: request_id,
|
65
|
+
reason: "Request timed out after #{timeout} seconds"
|
66
|
+
})
|
67
|
+
|
68
|
+
# Mark as cancelled in the database
|
69
|
+
request.update(request_cancelled: true)
|
70
|
+
|
71
|
+
log_error("Request #{method_name} timed out after #{timeout} seconds")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Client
|
5
|
+
module Resources
|
6
|
+
# List all available resources from the server
|
7
|
+
# @return [String] Request ID for tracking the request
|
8
|
+
def list_resources
|
9
|
+
request_id = SecureRandom.uuid_v7
|
10
|
+
|
11
|
+
# Send request
|
12
|
+
send_jsonrpc_request("resources/list", id: request_id)
|
13
|
+
|
14
|
+
# Return request ID for tracking the request
|
15
|
+
request_id
|
16
|
+
end
|
17
|
+
|
18
|
+
# List resource templates from the server
|
19
|
+
# @return [String] Request ID for tracking the request
|
20
|
+
def list_resource_templates
|
21
|
+
request_id = SecureRandom.uuid_v7
|
22
|
+
|
23
|
+
# Send request
|
24
|
+
send_jsonrpc_request("resources/templates/list", id: request_id)
|
25
|
+
|
26
|
+
# Return request ID for tracking the request
|
27
|
+
request_id
|
28
|
+
end
|
29
|
+
|
30
|
+
# Read a specific resource
|
31
|
+
# @param uri [String] URI of the resource to read
|
32
|
+
# @return [String] Request ID for tracking the request
|
33
|
+
def read_resource(uri)
|
34
|
+
request_id = SecureRandom.uuid_v7
|
35
|
+
|
36
|
+
# Send request
|
37
|
+
send_jsonrpc_request("resources/read",
|
38
|
+
params: { uri: uri },
|
39
|
+
id: request_id
|
40
|
+
)
|
41
|
+
|
42
|
+
# Return request ID for tracking the request
|
43
|
+
request_id
|
44
|
+
end
|
45
|
+
|
46
|
+
# Subscribe to updates for a specific resource
|
47
|
+
# @param uri [String] URI of the resource to subscribe to
|
48
|
+
# @param update_callback [Proc] Callback for resource updates
|
49
|
+
# @return [String] Request ID for tracking the request
|
50
|
+
def subscribe_resource(uri, update_callback)
|
51
|
+
@resource_subscriptions ||= {}
|
52
|
+
@resource_subscriptions[uri] = update_callback
|
53
|
+
|
54
|
+
request_id = SecureRandom.uuid_v7
|
55
|
+
|
56
|
+
# Send request
|
57
|
+
send_jsonrpc_request("resources/subscribe",
|
58
|
+
params: { uri: uri },
|
59
|
+
id: request_id
|
60
|
+
)
|
61
|
+
|
62
|
+
# Return request ID for tracking the request
|
63
|
+
request_id
|
64
|
+
end
|
65
|
+
|
66
|
+
# Unsubscribe from updates for a specific resource
|
67
|
+
# @param uri [String] URI of the resource to unsubscribe from
|
68
|
+
# @return [String] Request ID for tracking the request
|
69
|
+
def unsubscribe_resource(uri)
|
70
|
+
@resource_subscriptions&.delete(uri)
|
71
|
+
|
72
|
+
request_id = SecureRandom.uuid_v7
|
73
|
+
|
74
|
+
# Send request
|
75
|
+
send_jsonrpc_request("resources/unsubscribe",
|
76
|
+
params: { uri: uri },
|
77
|
+
id: request_id
|
78
|
+
)
|
79
|
+
|
80
|
+
# Return request ID for tracking the request
|
81
|
+
request_id
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Client
|
5
|
+
module Roots
|
6
|
+
# Notify the server that the roots list has changed
|
7
|
+
def roots_list_changed_notification
|
8
|
+
send_jsonrpc_notification("notifications/roots/list_changed")
|
9
|
+
true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module ActionMCP
|
2
|
+
module Client
|
3
|
+
class Server
|
4
|
+
attr_reader :name, :version
|
5
|
+
|
6
|
+
attr_reader :server_info, :capabilities
|
7
|
+
|
8
|
+
def initialize(data)
|
9
|
+
# Store protocol version if needed for later use
|
10
|
+
@protocol_version = data["protocolVersion"]
|
11
|
+
|
12
|
+
# Extract server information
|
13
|
+
@server_info = data["serverInfo"] || {}
|
14
|
+
@name = server_info["name"]
|
15
|
+
@version = server_info["version"]
|
16
|
+
|
17
|
+
# Store capabilities for dynamic checking
|
18
|
+
@capabilities = data["capabilities"] || {}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Check if 'tools' capability is present
|
22
|
+
def tools?
|
23
|
+
@capabilities.key?("tools")
|
24
|
+
end
|
25
|
+
|
26
|
+
# Check if 'prompts' capability is present
|
27
|
+
def prompts?
|
28
|
+
@capabilities.key?("prompts")
|
29
|
+
end
|
30
|
+
|
31
|
+
# Check if tools have a dynamic state based on listChanged flag
|
32
|
+
def dynamic_tools?
|
33
|
+
tool_cap = @capabilities["tools"] || {}
|
34
|
+
tool_cap["listChanged"] == true
|
35
|
+
end
|
36
|
+
|
37
|
+
# Check if logging capability exists
|
38
|
+
def logging?
|
39
|
+
@capabilities.key?("logging")
|
40
|
+
end
|
41
|
+
|
42
|
+
# Check if resources capability exists
|
43
|
+
def resources?
|
44
|
+
@capabilities.key?("resources")
|
45
|
+
end
|
46
|
+
|
47
|
+
# Check if resources have a dynamic state based on listChanged flag
|
48
|
+
def dynamic_resources?
|
49
|
+
resources_cap = @capabilities["resources"] || {}
|
50
|
+
resources_cap["listChanged"] == true
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect
|
54
|
+
"#<#{self.class.name} name: #{name}, version: #{version} with resources: #{resources?}, tools: #{tools?}, prompts: #{prompts?}, logging: #{logging?}>"
|
55
|
+
end
|
56
|
+
|
57
|
+
alias to_s inspect
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|