Class: DatagroutConduit::Client
- Inherits:
-
Object
- Object
- DatagroutConduit::Client
- Defined in:
- lib/datagrout_conduit/client.rb
Overview
Main Conduit client. Connects to remote MCP / JSONRPC servers over HTTP, sends requests, and parses responses. This is purely a client library —it does NOT run a server or accept connections.
Constant Summary collapse
- PROTOCOL_VERSION =
"2025-03-26"- CLIENT_NAME =
"datagrout-conduit-ruby"
Instance Attribute Summary collapse
-
#server_info ⇒ Object
readonly
Returns the value of attribute server_info.
-
#transport ⇒ Object
readonly
Returns the value of attribute transport.
-
#use_intelligent_interface ⇒ Object
readonly
Returns the value of attribute use_intelligent_interface.
Class Method Summary collapse
-
.bootstrap_identity(url:, auth_token:, name: "conduit-client", identity_dir: nil) ⇒ Object
Bootstrap an mTLS identity: discover existing or register a new one.
-
.bootstrap_identity_oauth(url:, client_id:, client_secret:, name: "conduit-client", identity_dir: nil) ⇒ Object
Bootstrap an mTLS identity using OAuth 2.1
client_credentials.
Instance Method Summary collapse
- #call_tool(name, arguments = {}) ⇒ Object
- #connect ⇒ Object
- #deliverables ⇒ Object
-
#dg(tool_short_name, params = {}) ⇒ Object
Call any DataGrout first-party tool by short name.
- #disconnect ⇒ Object
-
#discover(goal: nil, query: nil, limit: 10, min_score: 0.0, integrations: [], servers: []) ⇒ Object
Semantic discovery — find tools by natural language goal or query.
- #ephemerals ⇒ Object
-
#estimate_cost(tool_name, arguments = {}) ⇒ Object
Estimate cost before execution.
- #flow ⇒ Object
- #get_prompt(name, arguments = {}) ⇒ Object
-
#guide(goal: nil, session_id: nil, choice: nil) ⇒ Object
Start or continue a guided workflow.
-
#initialize(url:, auth: {}, transport: :mcp, identity: nil, identity_dir: nil, use_intelligent_interface: nil, max_retries: 3, logger: nil, disable_mtls: false) ⇒ Client
constructor
A new instance of Client.
- #initialized? ⇒ Boolean
- #list_prompts ⇒ Object
- #list_resources ⇒ Object
-
#list_tools ⇒ Object
Standard MCP Methods ================================================================.
- #logic ⇒ Object
-
#perform(tool_name, arguments = {}, demux: false, demux_mode: nil) ⇒ Object
Execute a tool call through the DataGrout intelligent interface.
-
#perform_batch(calls) ⇒ Object
Execute multiple tool calls in a single gateway request.
-
#plan(goal: nil, query: nil, **opts) ⇒ Object
Semantic discovery plan — return a ranked list of tools for a goal.
-
#prism ⇒ Object
Namespace Accessors ================================================================.
- #read_resource(uri) ⇒ Object
-
#subscribe(topic) ⇒ Object
Subscribe to a server-push topic over a WebSocket transport.
-
#unsubscribe(subscription) ⇒ Object
Cancel a push subscription.
- #warden ⇒ Object
Constructor Details
#initialize(url:, auth: {}, transport: :mcp, identity: nil, identity_dir: nil, use_intelligent_interface: nil, max_retries: 3, logger: nil, disable_mtls: false) ⇒ Client
Returns a new instance of Client.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/datagrout_conduit/client.rb', line 33 def initialize(url:, auth: {}, transport: :mcp, identity: nil, identity_dir: nil, use_intelligent_interface: nil, max_retries: 3, logger: nil, disable_mtls: false) @url = url @auth = auth @transport_mode = transport @identity = identity @identity_dir = identity_dir @disable_mtls = disable_mtls @max_retries = max_retries @initialized = false @server_info = nil @logger = logger || default_logger @is_dg = DatagroutConduit.dg_url?(url) @dg_warned = false @use_intelligent_interface = if use_intelligent_interface.nil? @is_dg else use_intelligent_interface end resolve_identity! @transport = build_transport end |
Instance Attribute Details
#server_info ⇒ Object (readonly)
Returns the value of attribute server_info.
14 15 16 |
# File 'lib/datagrout_conduit/client.rb', line 14 def server_info @server_info end |
#transport ⇒ Object (readonly)
Returns the value of attribute transport.
14 15 16 |
# File 'lib/datagrout_conduit/client.rb', line 14 def transport @transport end |
#use_intelligent_interface ⇒ Object (readonly)
Returns the value of attribute use_intelligent_interface.
14 15 16 |
# File 'lib/datagrout_conduit/client.rb', line 14 def use_intelligent_interface @use_intelligent_interface end |
Class Method Details
.bootstrap_identity(url:, auth_token:, name: "conduit-client", identity_dir: nil) ⇒ Object
Bootstrap an mTLS identity: discover existing or register a new one.
Checks the auto-discovery chain first. If an existing identity is found and not near expiry it is used as-is. Otherwise, a new keypair is generated, registered with DataGrout using the provided bearer token, saved to the identity directory, and loaded as the active identity.
After the first successful bootstrap the identity is persisted locally and auto-discovered on subsequent runs — no token or API key is needed.
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/datagrout_conduit/client.rb', line 67 def self.bootstrap_identity(url:, auth_token:, name: "conduit-client", identity_dir: nil) dir = identity_dir || Registration.default_identity_dir || File.join(Dir.home, ".conduit") identity = Identity.try_discover(override_dir: dir) if identity && !identity.needs_rotation? return new(url: url, identity: identity) end private_pem, public_pem = Registration.generate_keypair reg = Registration.register_identity( public_pem, auth_token: auth_token, name: name ) Registration.save_identity(reg.cert_pem, private_pem, dir, ca_pem: reg.ca_cert_pem) ca_path = reg.ca_cert_pem ? File.join(dir, "ca.pem") : nil identity = Identity.from_paths( File.join(dir, "identity.pem"), File.join(dir, "identity_key.pem"), ca_path: ca_path ) new(url: url, identity: identity) end |
.bootstrap_identity_oauth(url:, client_id:, client_secret:, name: "conduit-client", identity_dir: nil) ⇒ Object
Bootstrap an mTLS identity using OAuth 2.1 client_credentials.
Same flow as bootstrap_identity but obtains the bearer token via OAuth client_credentials exchange first.
97 98 99 100 101 102 103 104 105 |
# File 'lib/datagrout_conduit/client.rb', line 97 def self.bootstrap_identity_oauth(url:, client_id:, client_secret:, name: "conduit-client", identity_dir: nil) provider = OAuth::TokenProvider.new( client_id: client_id, client_secret: client_secret, token_endpoint: OAuth::TokenProvider.derive_token_endpoint(url) ) token = provider.get_token bootstrap_identity(url: url, auth_token: token, name: name, identity_dir: identity_dir) end |
Instance Method Details
#call_tool(name, arguments = {}) ⇒ Object
169 170 171 172 173 174 175 176 |
# File 'lib/datagrout_conduit/client.rb', line 169 def call_tool(name, arguments = {}) ensure_initialized! params = { "name" => name.to_s, "arguments" => normalize_hash(arguments) } response = send_with_retry("tools/call", params) result = response.is_a?(Hash) ? (response["result"] || response) : response unwrap_content(result) end |
#connect ⇒ Object
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/datagrout_conduit/client.rb', line 107 def connect @transport.connect params = { "protocolVersion" => PROTOCOL_VERSION, "clientInfo" => { "name" => CLIENT_NAME, "version" => DatagroutConduit::VERSION }, "capabilities" => { "tools" => {} } } response = @transport.send_request("initialize", params) if response.is_a?(Hash) && response["result"] result = response["result"] @server_info = result["serverInfo"] end @transport.send_request("notifications/initialized", nil, id: nil) @initialized = true self end |
#deliverables ⇒ Object
229 230 231 |
# File 'lib/datagrout_conduit/client.rb', line 229 def deliverables @deliverables ||= DeliverablesNamespace.new(self) end |
#dg(tool_short_name, params = {}) ⇒ Object
Call any DataGrout first-party tool by short name. e.g. client.dg(“prism.render”, { payload: data, goal: “summary” })
326 327 328 329 |
# File 'lib/datagrout_conduit/client.rb', line 326 def dg(tool_short_name, params = {}) ensure_initialized! call_dg_tool("data-grout/#{tool_short_name}", params) end |
#disconnect ⇒ Object
128 129 130 131 132 |
# File 'lib/datagrout_conduit/client.rb', line 128 def disconnect @transport.disconnect @initialized = false self end |
#discover(goal: nil, query: nil, limit: 10, min_score: 0.0, integrations: [], servers: []) ⇒ Object
Semantic discovery — find tools by natural language goal or query.
246 247 248 249 250 251 252 253 254 255 256 257 258 259 |
# File 'lib/datagrout_conduit/client.rb', line 246 def discover(goal: nil, query: nil, limit: 10, min_score: 0.0, integrations: [], servers: []) warn_if_not_dg("discover") ensure_initialized! params = { "limit" => limit, "min_score" => min_score } params["goal"] = goal if goal params["query"] = query if query params["integrations"] = integrations unless integrations.empty? params["servers"] = servers unless servers.empty? result = call_dg_tool("data-grout/discovery.discover", params) DiscoverResult.from_hash(result) end |
#ephemerals ⇒ Object
233 234 235 |
# File 'lib/datagrout_conduit/client.rb', line 233 def ephemerals @ephemerals ||= EphemeralsNamespace.new(self) end |
#estimate_cost(tool_name, arguments = {}) ⇒ Object
Estimate cost before execution.
332 333 334 335 336 337 |
# File 'lib/datagrout_conduit/client.rb', line 332 def estimate_cost(tool_name, arguments = {}) ensure_initialized! args = normalize_hash(arguments).merge("estimate_only" => true) call_dg_tool(tool_name.to_s, args) end |
#flow ⇒ Object
237 238 239 |
# File 'lib/datagrout_conduit/client.rb', line 237 def flow @flow ||= FlowNamespace.new(self) end |
#get_prompt(name, arguments = {}) ⇒ Object
202 203 204 205 206 207 208 209 210 211 |
# File 'lib/datagrout_conduit/client.rb', line 202 def get_prompt(name, arguments = {}) ensure_initialized! params = { "name" => name.to_s } params["arguments"] = normalize_hash(arguments) unless arguments.nil? || arguments.empty? response = send_with_retry("prompts/get", params) result = response.is_a?(Hash) ? (response["result"] || response) : response result["messages"] || [] end |
#guide(goal: nil, session_id: nil, choice: nil) ⇒ Object
Start or continue a guided workflow.
291 292 293 294 295 296 297 298 299 300 301 302 |
# File 'lib/datagrout_conduit/client.rb', line 291 def guide(goal: nil, session_id: nil, choice: nil) warn_if_not_dg("guide") ensure_initialized! params = {} params["goal"] = goal if goal params["session_id"] = session_id if session_id params["choice"] = choice if choice result = call_dg_tool("data-grout/discovery.guide", params) GuidedSession.new(self, GuideState.from_hash(result)) end |
#initialized? ⇒ Boolean
134 135 136 |
# File 'lib/datagrout_conduit/client.rb', line 134 def initialized? @initialized end |
#list_prompts ⇒ Object
194 195 196 197 198 199 200 |
# File 'lib/datagrout_conduit/client.rb', line 194 def list_prompts ensure_initialized! response = send_with_retry("prompts/list", {}) result = response.is_a?(Hash) ? (response["result"] || response) : response result["prompts"] || [] end |
#list_resources ⇒ Object
178 179 180 181 182 183 184 |
# File 'lib/datagrout_conduit/client.rb', line 178 def list_resources ensure_initialized! response = send_with_retry("resources/list", {}) result = response.is_a?(Hash) ? (response["result"] || response) : response result["resources"] || [] end |
#list_tools ⇒ Object
Standard MCP Methods
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/datagrout_conduit/client.rb', line 142 def list_tools ensure_initialized! all_tools = [] cursor = nil loop do params = {} params["cursor"] = cursor if cursor response = send_with_retry("tools/list", params) result = response.is_a?(Hash) ? (response["result"] || response) : response tools_data = result["tools"] || [] tools_data.each { |t| all_tools << Tool.from_hash(t) } cursor = result["nextCursor"] || result["next_cursor"] break unless cursor end if @use_intelligent_interface all_tools.reject! { |t| t.name.include?("@") } end all_tools end |
#logic ⇒ Object
221 222 223 |
# File 'lib/datagrout_conduit/client.rb', line 221 def logic @logic ||= LogicNamespace.new(self) end |
#perform(tool_name, arguments = {}, demux: false, demux_mode: nil) ⇒ Object
Execute a tool call through the DataGrout intelligent interface.
262 263 264 265 266 267 268 269 270 271 |
# File 'lib/datagrout_conduit/client.rb', line 262 def perform(tool_name, arguments = {}, demux: false, demux_mode: nil) warn_if_not_dg("perform") ensure_initialized! params = { "tool" => tool_name.to_s, "args" => normalize_hash(arguments) } params["demux"] = demux if demux params["demux_mode"] = demux_mode if demux_mode call_dg_tool("data-grout/discovery.perform", params) end |
#perform_batch(calls) ⇒ Object
Execute multiple tool calls in a single gateway request.
Each element should be a hash with “tool” and “args” keys. Returns an array of results in the same order as the input calls.
results = client.perform_batch([
{ "tool" => "data-grout/data.count", "args" => { "data" => [1, 2, 3] } },
{ "tool" => "data-grout/data.keys", "args" => { "data" => { "a" => 1 } } }
])
282 283 284 285 286 287 288 |
# File 'lib/datagrout_conduit/client.rb', line 282 def perform_batch(calls) warn_if_not_dg("perform_batch") ensure_initialized! result = call_dg_tool("data-grout/discovery.perform", calls) result.is_a?(Array) ? result : [result] end |
#plan(goal: nil, query: nil, **opts) ⇒ Object
Semantic discovery plan — return a ranked list of tools for a goal. At least one of ‘goal:` or `query:` must be provided.
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 |
# File 'lib/datagrout_conduit/client.rb', line 306 def plan(goal: nil, query: nil, **opts) raise ArgumentError, "plan() requires at least one of goal: or query:" unless goal || query params = {} params["goal"] = goal if goal params["query"] = query if query params["server"] = opts[:server] if opts[:server] params["k"] = opts[:k] if opts[:k] params["policy"] = opts[:policy] if opts[:policy] params["have"] = opts[:have] if opts[:have] params["return_call_handles"] = opts[:return_call_handles] if opts.key?(:return_call_handles) params["expose_virtual_skills"] = opts[:expose_virtual_skills] if opts.key?(:expose_virtual_skills) params["model_overrides"] = opts[:model_overrides] if opts[:model_overrides] warn_if_not_dg("plan") ensure_initialized! call_dg_tool("data-grout/discovery.plan", params) end |
#prism ⇒ Object
Namespace Accessors
217 218 219 |
# File 'lib/datagrout_conduit/client.rb', line 217 def prism @prism ||= PrismNamespace.new(self) end |
#read_resource(uri) ⇒ Object
186 187 188 189 190 191 192 |
# File 'lib/datagrout_conduit/client.rb', line 186 def read_resource(uri) ensure_initialized! response = send_with_retry("resources/read", { "uri" => uri.to_s }) result = response.is_a?(Hash) ? (response["result"] || response) : response result["contents"] || [] end |
#subscribe(topic) ⇒ Object
Subscribe to a server-push topic over a WebSocket transport. Returns a Transport::Ws::Subscription. Raises RuntimeError when transport is not :websocket.
19 20 21 22 23 |
# File 'lib/datagrout_conduit/client.rb', line 19 def subscribe(topic) raise "subscribe() requires transport: :websocket" unless @transport.is_a?(Transport::Ws) @transport.subscribe(topic) end |
#unsubscribe(subscription) ⇒ Object
Cancel a push subscription. Accepts a Subscription object or a subscription ID string.
27 28 29 30 31 |
# File 'lib/datagrout_conduit/client.rb', line 27 def unsubscribe(subscription) raise "unsubscribe() requires transport: :websocket" unless @transport.is_a?(Transport::Ws) @transport.unsubscribe(subscription) end |
#warden ⇒ Object
225 226 227 |
# File 'lib/datagrout_conduit/client.rb', line 225 def warden @warden ||= WardenNamespace.new(self) end |