T3
An independent Elixir SDK for T3 Code: MCP, HTTP JSON, WebSocket RPC, raw media/device streams, and the local CLI. Targets Elixir 1.20 and OTP 29 on Linux or macOS. Not yet published to Hex.
The pinned inventory covers 71 MCP tools, 38 HTTP routes, 211 WebSocket RPC methods (27 streams), 45 additional HTTP operations, 4 raw WebSocket routes, and 16 CLI command families. Named helpers are backed by generic transports for future server additions. INTERFACES.md lists every operation, its source revision, authentication and coverage limits.
Documentation
- This README: installation and interface examples.
- Authentication: MCP, native credentials, guest cookies and media tickets.
- Troubleshooting: errors, scopes, lifecycle and stream ACKs.
- Supervised board reader: reconnect, local service auth and normalized events (new in 0.2.0).
- Interface inventory: every pinned operation and compatibility limits.
- Live testing, contributing and release checklist.
Generate the searchable API reference with mix docs and open doc/index.html.
The published API reference is available on HexDocs.
Installation
Add the Hex dependency:
{:t3, "~> 0.2.0"}
For a Git checkout, use {:t3, github: "dl-alexandre/t3-elixir", tag: "v0.2.0"}.
Local development can use {:t3, path: "/path/to/t3-elixir"}.
Dependencies handle the hard parts: ConduitMCP for MCP/OAuth, Finch for HTTP,
Mint.WebSocket for WebSocket framing, Multipart for uploads, Exile for subprocess IO/lifecycle, and ExDoc
for documentation. Exile builds native code and requires a C compiler and make.
No JavaScript runtime or Python is required to use the SDK.
MCP: sign in and list threads
mix deps.get
mix run examples/list_threads.exs
Open the printed URL and approve read-only access in T3, then select a project.
A temporary loopback listener handles the callback. Set T3_MCP_URL for another
environment; the default is http://127.0.0.1:3773/mcp. Set T3_PROJECT_ID to skip
the project prompt. Credentials stay in memory; the example closes its connection.
Inside your own tool:
mcp_url = System.get_env("T3_MCP_URL", "http://127.0.0.1:3773/mcp")
client = T3.new(url: mcp_url)
{:ok, client} = T3.OAuth.login(client, open: &IO.puts/1)
{:ok, client} = T3.connect(client)
try do
{:ok, result} = T3.Projects.list(client)
{:ok, %{"projects" => projects}} = T3.Result.data(result)
case projects do
[%{"id" => project_id} | _] ->
{:ok, result} = T3.Threads.list(client, %{"projectId" => project_id, "limit" => 10})
IO.inspect(T3.Result.data(result), label: "Threads in the first project")
[] ->
IO.puts("Register a project in T3 first.")
end
after
T3.close(client)
end
connect/1 starts a linked supervised connection. Helpers return the full MCP
result envelope; T3.Result.data/1 extracts structuredContent or a JSON text block.
Failures return {:error, %T3.Error{}}; a tool's isError becomes kind: :tool.
Optional argument maps use T3's original string keys. External clients must supply
explicit project/thread targets; they do not inherit the agent context of this IDE.
Every known MCP operation has an exact wire-name helper:
T3.Tools.t3_thread_configure(client, %{"threadId" => id})
T3.Tools.t3_queue(client, %{"threadId" => id, "action" => "list"})
T3.Tools.t3_worktree_list(client, %{"threadId" => id})
T3.Tools.list_thread_pull_requests(client, %{"threadId" => id})
T3.call_tool(client, "any_new_advertised_tool", %{})
Shorter category modules cover threads, projects, tasks, environments, schedules,
queues, pending requests, attachments, worktrees, pull requests, previews, devices
and HTML rendering. T3.Catalog.compare/1 reports differences from a server's
actual discovered tools. Some tools require a T3-launched agent context and are
not exposed to external OAuth clients even with full access.
HTTP JSON
HTTP credentials are configured separately; an MCP OAuth token does not imply
access to the application HTTP APIs. See native credential setup
for a read-scoped token issued by the T3 host. Later examples use caller-selected
thread_id, tab_id, URLs and media/share inputs.
environment_origin = System.get_env("T3_HTTP_URL", "http://127.0.0.1:3773")
http = T3.HTTP.new(base_url: environment_origin,
token: System.fetch_env!("T3_ENVIRONMENT_TOKEN"))
{:ok, response} = T3.HTTP.Orchestration.thread_transcript(http, thread_id)
response.body
T3.HTTP.call(http, :orchestration_thread_history_page,
params: %{"threadId" => thread_id}, query: %{"cursor" => cursor})
Named modules cover metadata, auth, OAuth, orchestration, projects, PR diffs,
Connect and webhooks. Use json: map, form: map, or body: bytes for the
contract's payload encoding. T3.HTTP.upload/4 posts attachment bytes to a
same-origin relative upload URL, preserving signed queries. request/4 permits
new same-origin routes. Responses retain status/headers/body; Inspect hides data.
No automatic redirects, retries, cookie jar or token renewal is enabled. Set
auth_scheme: "DPoP" and a dpop proof header for proof-bound credentials.
WebSocket RPC and subscriptions
Obtain an RPC ticket with an appropriately scoped environment credential:
{:ok, response} = T3.HTTP.Auth.web_socket_ticket_for(http, "rpc")
{:ok, rpc} = T3.RPC.start_link(url: websocket_url, ticket: response.body["ticket"])
try do
{:ok, config} = T3.RPC.Methods.server_get_config(rpc)
{:ok, id} = T3.RPC.subscribe(rpc, "orchestration.subscribeShell", %{})
receive do
{:t3_rpc, ^id, {:chunk, values}} ->
IO.inspect(values)
:ok = T3.RPC.ack(rpc, id)
{:t3_rpc, ^id, {:done, _}} -> :ok
{:t3_rpc, ^id, {:error, error}} -> IO.puts(Exception.message(error))
after
5_000 -> IO.puts("No update within five seconds")
end
:ok = T3.RPC.unsubscribe(rpc, id)
after
T3.RPC.close(rpc)
end
Alternatively supply an environment token: directly, with caller-supplied
dpop: proof when required. T3 rejects MCP OAuth client credentials on this
surface. T3.RPC.Methods exposes every catalog method; call/4 and subscribe/4
accept arbitrary method strings. Streams require explicit ACK after consuming a
chunk. Deadlines, bounded pending requests, subscriber monitoring, cancellation,
connection cleanup and server-request replies are implemented. Calls are never
replayed after disconnect. Constructing/signing DPoP proofs remains caller-owned.
Guest shares, browser and device streams
Additional routes outside the typed HTTP contract are covered by T3.HTTP.Raw,
Guest, Media and DeviceHub. These include guest redemption/messages/history,
SSE updates, asset downloads/ranges, capability uploads, browser file pickers,
OTLP traces, screenshots, accessibility and device MJPEG/AVCC/WebRTC signalling.
Guest cookies, share parameters and media credentials are supplied explicitly.
T3.HTTP.Guest.stream(http, fn
{:event, event} -> IO.puts(event.data); :cont
_ -> :cont
end, query: %{"share" => share_id}, headers: [{"cookie", guest_cookie}], timeout: 60_000)
multipart = T3.HTTP.multipart(%{}, [{"files", "/absolute/path/to/file.png"}])
T3.HTTP.Media.preview_upload(http, multipart,
query: %{"threadId" => thread_id, "tabId" => tab_id, "chooser" => chooser_id})
HTTP.stream/5 provides binary chunks or incremental SSE events with callback
backpressure, byte/event bounds and hard deadlines. start_stream/5 and
cancel_stream/1 manage cancellable Tasks. Callback self() is its isolated
worker; capture the caller PID when forwarding events. Media bytes are not
transcoded, and WebRTC signalling does not itself decode/play a WebRTC session.
For preview JPEG frames or device raw WebSockets, use a media ticket and the separate raw transport (not Effect RPC):
{:ok, response} = T3.HTTP.Auth.web_socket_ticket_for(http, "media")
{:ok, socket} = T3.WebSocket.Routes.preview(websocket_origin, thread_id, tab_id,
ticket: response.body["ticket"])
try do
receive do
{:t3_web_socket, ^socket, ref, {:binary, jpeg}} ->
consume_jpeg(jpeg)
:ok = T3.WebSocket.Routes.preview_ack(socket, ref)
{:t3_web_socket, ^socket, ref, {:text, _status}} ->
:ok = T3.WebSocket.ack(socket, ref)
{:t3_web_socket, ^socket, {:closed, _}} -> :ok
{:t3_web_socket, ^socket, {:error, error}} -> IO.puts(Exception.message(error))
after
5_000 -> IO.puts("No frame within five seconds")
end
after
T3.WebSocket.close(socket)
end
Other raw routes cover device discovery, simulator and emulator channels.
T3.WebSocket preserves text/binary frames and supports send/ACK/ping/pong,
backlog/partial-message limits, owner monitoring and idle/lifetime deadlines.
Local ACK releases delivery memory; preview ACK also sends its server protocol ACK.
Local CLI
cli = T3.CLI.new(executable: "t3", timeout: 30_000)
T3.CLI.version(cli)
T3.CLI.commands(cli) # discover this installed version
T3.CLI.project(cli, ["--help"])
T3.CLI.run(cli, ["any-command", "literal argument with spaces"])
T3.CLI.run(cli, ["thread", "send", thread_id, "-"], input: "message via stdin")
Arguments are passed without a shell. Command-family helpers and a generic runner
support the installed executable, with bounded output/deadlines, streaming,
stdin/EOF and cancellation. Linux/macOS are supported; there is no PTY emulator
or arbitrary inherited-file-descriptor API. CLI commands can differ by release:
our installed t3 v0.1.1 lacks newer t3 thread commands. The SDK does not upgrade
or start T3 automatically. See T3.CLI documentation for interactive process IO.
Authentication and lifecycle
MCP sign-in explicitly requests orchestration:read by default. To request writes,
pass scope: "orchestration:read orchestration:operate"; T3 approval still sets
the allowed permission mode. HTTPS/WSS are required except on loopback.
For apps with their own callback, use T3.OAuth.begin/2 and complete/2. Store the
flow privately and consume it once. Tokens are secret-wrapped. T3.Application
runs a shared in-memory store; oauth_options: [store: {YourStore, options}] can
replace it. Loopback OAuth enables Conduit's explicit development exception;
private HTTPS OAuth hosts may need oauth_options: [allow_private: ["your-CIDR"]].
The simple MCP facade uses the signed-in bearer token; it does not automatically
refresh an existing connection. Reauthorize and reconnect when needed, or use
Conduit's managed OAuth APIs for refresh. Do not log callbacks or secret results.
After an ambiguous mutation timeout, inspect T3 state before retrying: work may have started. The SDK preserves T3's original wire payloads and relies on the server for TypeScript schema validation; it does not invent local typed models or elevate permissions. Static inventories describe the pinned upstream contract, not a guarantee of access to every operation on every installed server.
Development and package inventory
mix t3.interfaces
mix t3.interfaces --json
mix format --check-formatted
mix compile --warnings-as-errors
mix test --warnings-as-errors
mix docs --warnings-as-errors
mix hex.build
Tests cover actual ephemeral HTTP/WebSocket fixtures and subprocesses, complete catalog routing, session handling, OAuth, streaming back-pressure, cancellation, errors and bounds. The running T3 server's metadata, OAuth discovery/registration, CLI version/help and unauthenticated WebSocket protocol responses were checked. Authenticated live HTTP/RPC operations and destructive operations were not run.
Conduit is pinned to ~> 0.11.0; it includes Plug/Bandit, server-related runtime
processes and an exact NimbleOptions 1.1.1 dependency. Dependency experiments are
in docs/dependency-evaluation.md and evaluation/README.md.
The package/application name is t3, maintained in dl-alexandre/t3-elixir.
The archive is local and has not been published. Publication still requires green
CI, live workflow validation with appropriate credentials, and Hex authentication;
see the release checklist.
CI, live testing and release readiness
The local fixture suite excludes live tests by default. See LIVE_TESTING.md for opt-in read-only probes and RELEASE.md for the CI matrix and publication checklist. GitHub Actions is configured for Linux/macOS with Elixir 1.20.5 / OTP 29.1.2; it runs when the repository is pushed. No CI publishing or live-environment credentials are configured.