Help us shape the future of Go! Take the 2026 Go Developer Survey

tw

command module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 16 Imported by: 0

README

tw

A CLI client for Tidewave MCP servers. Every Tidewave tool becomes a shell subcommand, so any agent that can run a command can introspect your running Elixir application — no MCP client, no JSON-RPC by hand, and no HTTP port if you would rather use a unix socket.

$ brew install elepedus/tap/tw
$ tw --url http://localhost:4000 eval "1 + 1"
2

That is the whole setup. The rest of this file is detail.

Install

$ brew install elepedus/tap/tw          # macOS
$ go install github.com/elepedus/tw@latest   # anywhere with Go

Or take a binary for your platform from the releases page — tw is a single file with no runtime dependencies, so putting it on your PATH is the whole install.

Connect once, then forget about it

Repeating --url gets old. Drop a .tidewave file at the root of your project:

socket = /tmp/myapp.sock

Now tw finds the server from anywhere in the project:

$ tw eval "MyApp.Repo.aggregate(MyApp.Accounts.User, :count)"
42

tw looks for the connection in this order, and the first answer wins:

  1. --socket PATH or --url URL
  2. .tidewave in the current directory or any ancestor
  3. TW_SOCKET or TW_URL
  4. /tmp/tidewave.sock
Named targets

A [section] names another way to reach a server. Everything above the first section is the default target, and --target picks any other:

# The app's own socket.
socket = /tmp/myapp.sock

# The same app reached through the dev proxy, for when you are working behind it.
[proxy]
url = http://myapp.localhost:4000
$ tw logs --tail 20                 # default: the socket
$ tw --target proxy logs --tail 20  # through the proxy

Give a target either a socket or a URL. Set both and the socket wins with nothing said about it, so the URL looks used when it is not. tw does not fail over between targets either — --target is how you move.

Several projects from one file

Because resolution walks up from the working directory, a file above several projects covers them all:

# ~/code/.tidewave
[api]
socket = /tmp/api.sock

[web]
socket = /tmp/web.sock
$ tw --target web schemas

Two things to know. A .tidewave inside a project shadows this one, since the nearest file wins rather than the two merging. And a file with no default target leaves plain tw with nothing to resolve to — see Known rough edge.

A worktree per branch

Each worktree is its own directory, so each one holds its own .tidewave and nothing needs naming or switching:

# ~/code/myapp/.tidewave
socket = /tmp/myapp.sock
# ~/code/myapp-feature-x/.tidewave
socket = /tmp/myapp-feature-x.sock

tw eval then reaches whichever worktree's server you are standing in, which is usually what you meant.

Commands

$ tw --help
Command Tidewave tool Notes
tw eval <code> project_eval
tw logs get_logs
tw source <ref> get_source_location
tw docs <ref> get_docs
tw schemas get_ecto_schemas needs :ecto_repos
tw sql <query> execute_sql_query needs :ecto_repos
tw ash get_ash_resources needs Ash
tw browser <action> browser_eval needs an open browser

The Notes column is where a command can be unavailable through no fault of tw: Tidewave registers schemas, sql, and ash only under those conditions, and browser needs something on the other end. See Server-side requirements.

eval
$ tw eval ":erlang.system_info(:process_count)"
418

The result comes back as Elixir would inspect it.

--arg passes values the code reads as arguments, in order:

$ tw eval "Enum.sum(arguments)" --arg 1 --arg 41
42

Each value is read as JSON when it parses as one, so --arg 1 is the integer 1 and --arg true is a boolean. Anything else is a string, which is what an unquoted word means on a command line. To force a string that looks like a number, quote it for JSON as well as for the shell: --arg '"1"'.

--timeout MS overrides the 30s the tool allows itself. tw waits longer than whatever you set, so a slow evaluation is reported by the server rather than cut off here.

logs
$ tw logs --tail 100 --grep timeout --level error

--tail defaults to 20. --level takes one of emergency, alert, critical, error, warning, notice, info, debug.

source and docs
$ tw source MyApp.Accounts.create_user/1
lib/my_app/accounts.ex:112

$ tw docs Enum.sum/1
# Enum.sum/1

```elixir
sum(enumerable)
```

Returns the sum of all elements.

Both take a module, Module.function, or Module.function/arity. docs also takes callbacks as c:GenServer.init, and source takes dependencies as dep:tidewave. source covers your project and its dependencies, not Elixir's own core modules.

schemas and sql
$ tw schemas
* MyApp.Accounts.User at lib/my_app/accounts/user.ex:1
* MyApp.Blog.Post at lib/my_app/blog/post.ex:1

$ tw sql "SELECT id, name FROM users"
%Exqlite.Result{
  command: :execute,
  columns: ["id", "name"],
  rows: [[1, "Alice"], [2, "Bob"]],
  num_rows: 2
}

The result is your adapter's own struct, inspected — %Postgrex.Result{} against Postgres, and so on.

--arg binds query parameters positionally, with the same JSON reading as eval, so a numeric parameter matches an integer column:

$ tw sql "SELECT name FROM users WHERE id = $1" --arg 2

Use whichever placeholder syntax your adapter wants — $1 for Postgres, ?1 for SQLite. --repo picks a repository when the application has more than one; without it Tidewave uses the first.

Results are capped server-side, and a query returning more rows than the cap says so in its output.

A query can start with --, which the shell hands over as if it were a flag. Put -- first to end flag parsing:

$ tw sql -- "-- a comment
SELECT 1"
browser

Drives a browser attached to Tidewave's control page at <your-app>/tidewave. The actions belong to the browser rather than to Tidewave, so ask it:

$ tw browser help

--arg KEY=VALUE builds the action's parameters, values read as JSON as elsewhere. --sid targets one session when several are open:

$ tw browser navigate --arg url=/users --sid nice-cactus#1

Tidewave waits on a named session indefinitely, so tw sets no deadline of its own for a --sid call. Interrupt it, or bound it with --wait.

Output and exit codes

The tool's text goes to stdout with nothing added, so it pipes. Errors and diagnostics go to stderr.

Exit Meaning
0 success
1 the server ran the tool and it failed
2 bad usage, or no answer from the server

--json prints the whole JSON-RPC response instead of the text:

$ tw --json eval "1 + 1"
{"id":1,"result":{"content":[{"type":"text","text":"2"}]},"jsonrpc":"2.0"}

Waiting

A call is bounded at 60s by default. Commands raise that when they know the server is entitled to take longer: eval outlasts its own --timeout, and browser --sid is not bounded at all.

--wait SECONDS overrides all of it, and --wait 0 waits indefinitely:

$ tw --wait 300 sql "SELECT * FROM a_very_large_table"

execute_sql_query has no server-side limit of its own, so --wait is the only way to allow a query past the default.

When the wait runs out, the call is abandoned but the tool is not: it keeps running on the server, and tw says so rather than reporting a bare deadline.

Server-side requirements

tw speaks to Tidewave, so anything Tidewave will not do is out of its hands. Tested against Tidewave 0.8.1.

Four tools are conditional, and asking for one the server has not registered fails with server error: Method not found:

  • schemas and sql need the host application to declare :ecto_repos.
  • ash needs Ash to be loaded.
  • browser needs a browser open on Tidewave's control page, otherwise it reports that nothing is connected.

Connecting over a unix socket needs allow_remote_access: true on the Tidewave plug. Tidewave's guard accepts only loopback IPv4/IPv6 peers, and a unix-domain peer has no address to match, so it is refused despite being strictly more local than a TCP connection. Gate access with filesystem permissions on the socket instead. Over HTTP on loopback, no such setting is needed.

Development

$ go test ./...

The tests run against a real Elixir server rather than a mock. It is built and started for you if mix is on PATH; if not, those tests skip and the rest still run. See testserver/README.md.

Known rough edge

A target tw cannot find in the .tidewave file falls through to the environment and then to the default socket, instead of saying the target is unknown. That covers a misspelt --target, and a file whose targets are all named, leaving plain tw with no default to resolve to. Either way the failure is a refusal to reach /tmp/tidewave.sock, and the path in the message is the clue that the file was read but the target was not found in it.

License

MIT.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
Package testserver boots the Elixir/Tidewave fixture in testserver/app and exposes it over a unix socket, so tw's subcommands can be exercised against a real MCP server instead of a mock.
Package testserver boots the Elixir/Tidewave fixture in testserver/app and exposes it over a unix socket, so tw's subcommands can be exercised against a real MCP server instead of a mock.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL