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.
@@ -1,199 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Client
5
- # Toolbox
6
- #
7
- # A collection that manages and provides access to tools from the server.
8
- # This class stores tool definitions along with their input schemas and
9
- # provides methods for retrieving, filtering, and accessing tools.
10
- #
11
- # Example usage:
12
- # tools_data = client.list_tools # Returns array of tool definitions
13
- # toolbox = Toolbox.new(tools_data)
14
- #
15
- # # Access a specific tool by name
16
- # weather_tool = toolbox.find("weather_forecast")
17
- #
18
- # # Get all tools matching a criteria
19
- # calculation_tools = toolbox.filter { |t| t.name.include?("calculate") }
20
- #
21
- class Toolbox < Collection
22
- # Initialize a new Toolbox with tool definitions
23
- #
24
- # @param tools [Array<Hash>] Array of tool definition hashes, each containing
25
- # name, description, and inputSchema keys
26
- def initialize(tools, client)
27
- super(tools, client)
28
- self.tools = @collection_data
29
- @load_method = :list_tools
30
- end
31
-
32
- # Find a tool by name
33
- #
34
- # @param name [String] Name of the tool to find
35
- # @return [Tool, nil] The tool with the given name, or nil if not found
36
- def find(name)
37
- all.find { |tool| tool.name == name }
38
- end
39
-
40
- # Filter tools based on a given block
41
- #
42
- # @yield [tool] Block that determines whether to include a tool
43
- # @yieldparam tool [Tool] A tool from the collection
44
- # @yieldreturn [Boolean] true to include the tool, false to exclude it
45
- # @return [Array<Tool>] Tools that match the filter criteria
46
- def filter(&block)
47
- all.select(&block)
48
- end
49
-
50
- # Get a list of all tool names
51
- #
52
- # @return [Array<String>] Names of all tools in the collection
53
- def names
54
- all.map(&:name)
55
- end
56
-
57
- # Number of tools in the collection
58
- #
59
- # @return [Integer] The number of tools
60
- def size
61
- all.size
62
- end
63
-
64
- # Check if the collection contains a tool with the given name
65
- #
66
- # @param name [String] The tool name to check for
67
- # @return [Boolean] true if a tool with the name exists
68
- def contains?(name)
69
- all.any? { |tool| tool.name == name }
70
- end
71
-
72
- # Get tools by category or type
73
- #
74
- # @param keyword [String] Keyword to search for in tool names and descriptions
75
- # @return [Array<Tool>] Tools containing the keyword
76
- def search(keyword)
77
- all.select do |tool|
78
- tool.name.include?(keyword) ||
79
- tool.description&.downcase&.include?(keyword.downcase)
80
- end
81
- end
82
-
83
- # Generate a hash representation of all tools in the collection based on provider format
84
- #
85
- # @param provider [Symbol] The provider format to use (:claude, :openai, or :default)
86
- # @return [Hash] Hash containing all tools formatted for the specified provider
87
- def to_h(provider = :default)
88
- case provider
89
- when :claude
90
- # Claude format
91
- { "tools" => all.map(&:to_claude_h) }
92
- when :openai
93
- # OpenAI format
94
- { "tools" => all.map(&:to_openai_h) }
95
- else
96
- # Default format (same as original)
97
- { "tools" => all.map(&:to_h) }
98
- end
99
- end
100
-
101
- def tools=(tools)
102
- @collection_data = tools.map { |tool_data| Tool.new(tool_data) }
103
- end
104
-
105
- # Internal Tool class to represent individual tools
106
- class Tool
107
- attr_reader :name, :description, :input_schema, :annotations
108
-
109
- # Initialize a new Tool instance
110
- #
111
- # @param data [Hash] Tool definition hash containing name, description, and inputSchema
112
- # and optionally annotations
113
- def initialize(data)
114
- @name = data["name"]
115
- @description = data["description"]
116
- @input_schema = data["inputSchema"] || {}
117
- @annotations = data["annotations"] || {}
118
- end
119
-
120
- # Get all required properties for this tool
121
- #
122
- # @return [Array<String>] Array of required property names
123
- def required_properties
124
- @input_schema["required"] || []
125
- end
126
-
127
- # Get all properties for this tool
128
- #
129
- # @return [Hash] Hash of property definitions
130
- def properties
131
- @input_schema["properties"] || {}
132
- end
133
-
134
- # Check if the tool requires a specific property
135
- #
136
- # @param name [String] Name of the property to check
137
- # @return [Boolean] true if the property is required
138
- def requires?(name)
139
- required_properties.include?(name)
140
- end
141
-
142
- # Check if the tool has a specific property
143
- #
144
- # @param name [String] Name of the property to check
145
- # @return [Boolean] true if the property exists
146
- def has_property?(name)
147
- properties.key?(name)
148
- end
149
-
150
- # Get property details by name
151
- #
152
- # @param name [String] Name of the property
153
- # @return [Hash, nil] Property details or nil if not found
154
- def property(name)
155
- properties[name]
156
- end
157
-
158
- # Generate a hash representation of the tool (default format)
159
- #
160
- # @return [Hash] Hash containing tool details
161
- def to_h
162
- {
163
- "name" => @name,
164
- "description" => @description,
165
- "inputSchema" => @input_schema,
166
- "annotations" => @annotations
167
- }
168
- end
169
-
170
- # Generate a hash representation of the tool in Claude format
171
- #
172
- # @return [Hash] Hash containing tool details formatted for Claude
173
- def to_claude_h
174
- {
175
- "name" => @name,
176
- "description" => @description,
177
- "input_schema" => @input_schema.transform_keys { |k| k == "inputSchema" ? "input_schema" : k },
178
- "annotations" => @annotations
179
- }
180
- end
181
-
182
- # Generate a hash representation of the tool in OpenAI format
183
- #
184
- # @return [Hash] Hash containing tool details formatted for OpenAI
185
- def to_openai_h
186
- {
187
- "type" => "function",
188
- "function" => {
189
- "name" => @name,
190
- "description" => @description,
191
- "parameters" => @input_schema,
192
- "annotations" => @annotations
193
- }
194
- }
195
- end
196
- end
197
- end
198
- end
199
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Client
5
- module Tools
6
- # List all available tools 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_tools(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("tools/list",
20
- params: request_params.empty? ? nil : request_params,
21
- id: request_id)
22
-
23
- # Return request ID for timeout tracking
24
- request_id
25
- end
26
-
27
- # Call a specific tool on the server
28
- # @param name [String] Name of the tool to call
29
- # @param arguments [Hash] Arguments to pass to the tool
30
- # @return [String] Request ID for tracking the request
31
- def call_tool(name, arguments)
32
- request_id = SecureRandom.uuid_v7
33
-
34
- # Send request
35
- send_jsonrpc_request("tools/call",
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,137 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Client
5
- # Base transport interface for MCP client connections
6
- module Transport
7
- # Called when transport should establish connection
8
- def connect
9
- raise NotImplementedError, "#{self.class} must implement #connect"
10
- end
11
-
12
- # Called when transport should close connection
13
- def disconnect
14
- raise NotImplementedError, "#{self.class} must implement #disconnect"
15
- end
16
-
17
- # Send a message through the transport
18
- def send_message(message)
19
- raise NotImplementedError, "#{self.class} must implement #send_message"
20
- end
21
-
22
- # Check if transport is ready to send/receive
23
- def ready?
24
- raise NotImplementedError, "#{self.class} must implement #ready?"
25
- end
26
-
27
- # Check if transport is connected
28
- def connected?
29
- raise NotImplementedError, "#{self.class} must implement #connected?"
30
- end
31
-
32
- # Set callback for received messages
33
- def on_message(&block)
34
- @message_callback = block
35
- end
36
-
37
- # Set callback for errors
38
- def on_error(&block)
39
- @error_callback = block
40
- end
41
-
42
- # Set callback for connection events
43
- def on_connect(&block)
44
- @connect_callback = block
45
- end
46
-
47
- # Set callback for disconnection events
48
- def on_disconnect(&block)
49
- @disconnect_callback = block
50
- end
51
-
52
- protected
53
-
54
- def handle_message(message)
55
- @message_callback&.call(message)
56
- end
57
-
58
- def handle_error(error)
59
- @error_callback&.call(error)
60
- end
61
-
62
- def handle_connect
63
- @connect_callback&.call
64
- end
65
-
66
- def handle_disconnect
67
- @disconnect_callback&.call
68
- end
69
- end
70
-
71
- # Base class for transport implementations
72
- class TransportBase
73
- include Transport
74
-
75
- attr_reader :url, :options, :session_store
76
-
77
- def initialize(url, session_store:, logger: ActionMCP.logger, **options)
78
- @url = url
79
- @session_store = session_store
80
- @logger = logger
81
- @options = options
82
- @connected = false
83
- @ready = false
84
- end
85
-
86
- def connected?
87
- @connected
88
- end
89
-
90
- def ready?
91
- @ready
92
- end
93
-
94
- protected
95
-
96
- def set_connected(state)
97
- @connected = state
98
- state ? handle_connect : handle_disconnect
99
- end
100
-
101
- def set_ready(state)
102
- @ready = state
103
- end
104
-
105
- # Logging methods for transport classes
106
- def log_debug(message)
107
- @logger.debug("[ActionMCP::#{self.class.name.split('::').last}] #{message}")
108
- end
109
-
110
- def log_info(message)
111
- @logger.info("[ActionMCP::#{self.class.name.split('::').last}] #{message}")
112
- end
113
-
114
- def log_error(message)
115
- @logger.error("[ActionMCP::#{self.class.name.split('::').last}] #{message}")
116
- end
117
-
118
- private
119
-
120
- def handle_connect
121
- @connect_callback&.call
122
- end
123
-
124
- def handle_disconnect
125
- @disconnect_callback&.call
126
- end
127
-
128
- def handle_error(error)
129
- @error_callback&.call(error)
130
- end
131
-
132
- def handle_message(message)
133
- @message_callback&.call(message)
134
- end
135
- end
136
- end
137
- end
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Client
5
- # Volatile session store for development (data lost on restart)
6
- class VolatileSessionStore
7
- include SessionStore
8
-
9
- def initialize
10
- @sessions = Concurrent::Hash.new
11
- end
12
-
13
- def load_session(session_id)
14
- @sessions[session_id]
15
- end
16
-
17
- def save_session(session_id, session_data)
18
- @sessions[session_id] = session_data.dup
19
- end
20
-
21
- def delete_session(session_id)
22
- @sessions.delete(session_id)
23
- end
24
-
25
- def session_exists?(session_id)
26
- @sessions.key?(session_id)
27
- end
28
-
29
- def clear_all
30
- @sessions.clear
31
- end
32
-
33
- def session_count
34
- @sessions.size
35
- end
36
- end
37
- end
38
- end
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "client/transport"
4
- require_relative "client/session_store"
5
- require_relative "client/streamable_http_transport"
6
-
7
- module ActionMCP
8
- # Creates a client appropriate for the given endpoint.
9
- #
10
- # @param endpoint [String] The endpoint to connect to (URL).
11
- # @param transport [Symbol] The transport type to use (:streamable_http, :sse for legacy)
12
- # @param session_store [Symbol] The session store type (:memory, :active_record)
13
- # @param session_id [String] Optional session ID for resuming connections
14
- # @param protocol_version [String] The MCP protocol version to use (defaults to ActionMCP::DEFAULT_PROTOCOL_VERSION)
15
- # @param logger [Logger] The logger to use. Default is Logger.new($stdout).
16
- # @param options [Hash] Additional options to pass to the client constructor.
17
- #
18
- # @return [Client::Base] An instance of the appropriate client.
19
- #
20
- # @example Basic usage
21
- # client = ActionMCP.create_client("http://127.0.0.1:3001/action_mcp")
22
- # client.connect
23
- #
24
- # @example With specific transport and session store
25
- # client = ActionMCP.create_client(
26
- # "http://127.0.0.1:3001/action_mcp",
27
- # transport: :streamable_http,
28
- # session_store: :active_record,
29
- # session_id: "existing-session-123"
30
- # )
31
- #
32
- # @example Memory-based for development
33
- # client = ActionMCP.create_client(
34
- # "http://127.0.0.1:3001/action_mcp",
35
- # session_store: :memory
36
- # )
37
- #
38
- def self.create_client(endpoint, transport: :streamable_http, session_store: nil, session_id: nil,
39
- protocol_version: nil, logger: Logger.new($stdout), **options)
40
- unless endpoint =~ %r{\Ahttps?://}
41
- raise ArgumentError, "Only HTTP(S) endpoints are supported. STDIO and other transports are not supported."
42
- end
43
-
44
- # Create session store
45
- store = Client::SessionStoreFactory.create(session_store, **options)
46
-
47
- # Create transport
48
- transport_instance = create_transport(transport, endpoint, session_store: store, session_id: session_id,
49
- protocol_version: protocol_version, logger: logger, **options)
50
-
51
- logger.info("Creating #{transport} client for endpoint: #{endpoint}")
52
- # Pass session_id and protocol_version to the client
53
- Client::Base.new(transport: transport_instance, logger: logger, session_id: session_id,
54
- protocol_version: protocol_version, **options)
55
- end
56
-
57
- private_class_method def self.create_transport(type, endpoint, **options)
58
- case type.to_sym
59
- when :streamable_http
60
- Client::StreamableHttpTransport.new(endpoint, **options)
61
- when :sse
62
- # Legacy SSE transport (wrapped for compatibility)
63
- Client::StreamableClient.new(endpoint, **options)
64
- else
65
- raise ArgumentError, "Unknown transport type: #{type}"
66
- end
67
- end
68
-
69
- module Client
70
- end
71
- end