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:
--socket PATHor--url URL.tidewavein the current directory or any ancestorTW_SOCKETorTW_URL/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:
schemasandsqlneed the host application to declare:ecto_repos.ashneeds Ash to be loaded.browserneeds 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
¶
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. |