DataGrout Conduit — Elixir SDK

Production-ready MCP client with mTLS, OAuth 2.1, and semantic discovery for Elixir/OTP.

This is a client library. It connects to remote MCP and JSON-RPC servers over HTTP/HTTPS, sends requests, and parses responses. It does not run any server, accept connections, or handle incoming requests.

Installation

Add datagrout_conduit to your mix.exs dependencies:

def deps do
  [
    {:datagrout_conduit, "~> 0.1.0"}
  ]
end

Quick Start

# Connect to a DataGrout MCP server
{:ok, client} = DatagroutConduit.Client.start_link(
  url: "https://gateway.datagrout.ai/servers/{uuid}/mcp",
  auth: {:bearer, "your-api-key"}
)

# List available tools
{:ok, tools} = DatagroutConduit.Client.list_tools(client)

# Call a tool
{:ok, result} = DatagroutConduit.Client.call_tool(client, "get_invoices", %{status: "unpaid"})

# Extract cost metadata
meta = DatagroutConduit.extract_meta(result)
IO.inspect(meta.receipt)

Authentication

Bearer Token

{:ok, client} = DatagroutConduit.Client.start_link(
  url: "https://gateway.datagrout.ai/servers/{uuid}/mcp",
  auth: {:bearer, "your-token"}
)

OAuth 2.1 (Client Credentials)

{:ok, oauth} = DatagroutConduit.OAuth.start_link(
  client_id: "your-client-id",
  client_secret: "your-secret",
  token_endpoint: "https://gateway.datagrout.ai/servers/{uuid}/oauth/token"
)

{:ok, client} = DatagroutConduit.Client.start_link(
  url: "https://gateway.datagrout.ai/servers/{uuid}/mcp",
  auth: {:oauth, oauth}
)

Token endpoint can be auto-derived from the MCP URL:

endpoint = DatagroutConduit.OAuth.derive_token_endpoint("https://gateway.datagrout.ai/servers/{uuid}/mcp")
# => "https://gateway.datagrout.ai/servers/{uuid}/oauth/token"

Tokens are cached and refreshed automatically 60 seconds before expiry.

mTLS Client Certificates

# Auto-discover identity (searches standard locations)
identity = DatagroutConduit.Identity.try_discover()

# Or from explicit paths
{:ok, identity} = DatagroutConduit.Identity.from_paths("cert.pem", "key.pem", "ca.pem")

# Or from PEM data
{:ok, identity} = DatagroutConduit.Identity.from_pem(cert_pem, key_pem, ca_pem)

# Use with client
{:ok, client} = DatagroutConduit.Client.start_link(
  url: "https://gateway.datagrout.ai/servers/{uuid}/mcp",
  auth: {:bearer, "token"},
  identity: identity
)

Discovery order:

  1. override_dir option
  2. CONDUIT_MTLS_CERT + CONDUIT_MTLS_KEY environment variables
  3. CONDUIT_IDENTITY_DIR environment variable
  4. ~/.conduit/identity.pem + identity_key.pem
  5. .conduit/ relative to current working directory

Check certificate rotation:

if DatagroutConduit.Identity.needs_rotation?(identity, threshold_days: 30) do
  Logger.warning("mTLS certificate expires within 30 days")
end

For DataGrout URLs (datagrout.ai, datagrout.dev), mTLS identity is auto-discovered.

Identity Registration & Bootstrap

Bootstrap a new mTLS identity with a one-time access token:

{:ok, client} = DatagroutConduit.Client.bootstrap_identity(
  url: "https://gateway.datagrout.ai/servers/{uuid}/mcp",
  auth_token: "your-one-time-token",
  name: "my-agent"
)

Or with OAuth client credentials:

{:ok, client} = DatagroutConduit.Client.bootstrap_identity_oauth(
  url: "https://gateway.datagrout.ai/servers/{uuid}/mcp",
  client_id: "id",
  client_secret: "secret",
  name: "my-agent"
)

After bootstrap, subsequent runs auto-discover the saved identity — no tokens needed.

You can also use the registration module directly:

{:ok, {private_pem, public_pem}} = DatagroutConduit.Registration.generate_keypair()
{:ok, response} = DatagroutConduit.Registration.register_identity(public_pem, auth_token: "token")
{:ok, paths} = DatagroutConduit.Registration.save_identity(response.cert_pem, private_pem, response.ca_cert_pem, "~/.conduit")
{:ok, ca_pem} = DatagroutConduit.Registration.fetch_ca_cert()

Transports

MCP (Streamable HTTP) — Default

{:ok, client} = DatagroutConduit.Client.start_link(
  url: "https://example.com/mcp",
  transport: :mcp
)

Sends HTTP POST with JSON-RPC 2.0 bodies and MCP-specific headers. Handles both direct JSON and SSE response formats.

JSON-RPC 2.0

{:ok, client} = DatagroutConduit.Client.start_link(
  url: "https://example.com/jsonrpc",
  transport: :jsonrpc
)

Standard JSON-RPC 2.0 over HTTP POST.

MCP Protocol Methods

# Tools
{:ok, tools} = DatagroutConduit.Client.list_tools(client)
{:ok, result} = DatagroutConduit.Client.call_tool(client, "tool-name", %{arg: "val"})

# Resources
{:ok, resources} = DatagroutConduit.Client.list_resources(client)
{:ok, content} = DatagroutConduit.Client.read_resource(client, "resource://uri")

# Prompts
{:ok, prompts} = DatagroutConduit.Client.list_prompts(client)
{:ok, messages} = DatagroutConduit.Client.get_prompt(client, "prompt-name", %{})

DataGrout Extensions

When connected to a DataGrout server, additional capabilities are available:

Semantic Discovery

Find tools that match a natural-language goal:

{:ok, results} = DatagroutConduit.Client.discover(client, goal: "find unpaid invoices", limit: 10)
# => %DiscoverResult{tools: [%DiscoveredTool{tool: %Tool{...}, score: 0.95, ...}], query: "...", total: 10}

Perform (Enhanced Tool Execution)

Execute tools with demuxing, refraction, and charting:

{:ok, result} = DatagroutConduit.Client.perform(client, "get_data", %{query: "..."}, demux: true)

Guided Execution

Create and execute multi-step plans:

{:ok, plan} = DatagroutConduit.Client.guide(client, goal: "create invoice from lead")
{:ok, result} = DatagroutConduit.Client.flow_into(client, plan)

Or use the interactive GuidedSession:

{:ok, session} = DatagroutConduit.GuidedSession.start(client, goal: "create invoice from lead")
IO.inspect(DatagroutConduit.GuidedSession.options(session))

{:ok, session} = DatagroutConduit.GuidedSession.choose(session, 0)
{:ok, result} = DatagroutConduit.GuidedSession.complete(session)

Prism Focus

Transform data through a lens:

{:ok, result} = DatagroutConduit.Client.prism_focus(client, data: my_data, lens: "summary")

Cost Estimation

{:ok, estimate} = DatagroutConduit.Client.estimate_cost(client, "expensive-tool", %{})
# => %CreditEstimate{estimated_total: 2.5, net_total: 2.0, breakdown: %{...}}

Cost Tracking

Every tool call returns metadata with credit receipts:

{:ok, result} = DatagroutConduit.Client.call_tool(client, "analyze", %{data: "..."})
meta = DatagroutConduit.extract_meta(result)

case meta.receipt do
  %{net_credits: credits, receipt_id: id} ->
    Logger.info("Charged #{credits} credits (receipt: #{id})")
  nil ->
    :ok
end

Intelligent Interface

For DataGrout URLs, the client automatically enables the intelligent interface:

Override this behavior:

{:ok, client} = DatagroutConduit.Client.start_link(
  url: "https://gateway.datagrout.ai/servers/{uuid}/mcp",
  auth: {:bearer, "token"},
  use_intelligent_interface: false  # show all tools including @-prefixed
)

DG URL Detection

DatagroutConduit.is_dg_url?("https://gateway.datagrout.ai/servers/123/mcp")
# => true

DatagroutConduit.is_dg_url?("https://example.com/mcp")
# => false

Links

Labs Papers

License

MIT — DataGrout hello@datagrout.ai