utcp_cli 0.1.1
utcp_cli: ^0.1.1 copied to clipboard
CLI transport for the Dart UTCP client: let LLM agents discover and call tools that run as local command-line programs.
utcp_cli #
The cli transport for the UTCP Dart client
(utcp). A manual is discovered by running commands that print a
UTCP manual, and each tool call runs the tool's commands with its arguments.
Usage #
import 'package:utcp/utcp.dart';
import 'package:utcp_cli/utcp_cli.dart';
Future<void> main() async {
final client = await UtcpClient.create(
config: UtcpClientConfig(
manualCallTemplates: [
// Any command that prints a UTCP manual as JSON.
CliCallTemplate(
name: 'tools',
commands: [const CommandStep('my-tool --utcp-manual')],
),
],
),
protocols: [CliCommunicationProtocol()],
);
print(await client.callTool('tools.greet', {'name': 'World'}));
await client.close();
}
With UtcpClient.fromJson, pass protocols: [CliCommunicationProtocol()] and use
"call_template_type": "cli" in the configuration. A runnable example is in
example/utcp_cli_example.dart.
Call templates #
{
"call_template_type": "cli",
"commands": [
{"command": "cd UTCP_ARG_target_dir_UTCP_END", "append_to_final_output": false},
{"command": "git status --porcelain"}
],
"working_dir": "/tmp",
"env_vars": {"GIT_AUTHOR_NAME": "UTCP Bot"},
"inherit_env_vars": ["PATH", "HOME"]
}
- All
commandsrun in order in one shell process:/bin/bash -con macOS and Linux,powershell.exe -NoProfile -Commandon Windows.cdand variables set by one command carry over to the next. A command must not be blank. UTCP_ARG_<name>_UTCP_ENDtakes the tool argument<name>. Strings are passed as is, other values as JSON. A missing argument, or one containing a NUL character, fails the call.$CMD_<i>_OUTPUTholds the output (stdout and stderr) of commandi.- Only the last command's output is returned, unless commands set
append_to_final_output. Output that starts with{or[and parses as JSON is returned decoded; anything else is returned as trimmed text. A failing command returns its error text rather than throwing. working_dirdefaults to the current directory.- Calls time out after 120 seconds and discovery after 60, and a script may write at most 10 MiB
of output (
callTimeout,discoveryTimeoutandmaxOutputBytesonCliCommunicationProtocol). A script over the limit is killed and the call fails.
Windows (experimental) #
On Windows the commands run in PowerShell. That path is untested: CI runs the bash tests on Linux only, so treat Windows support as experimental.
Security #
- Tool arguments never become script text. Each placeholder is replaced by a reference to an
environment variable that carries the value (
"$V",${V}inside double quotes,'"$V"'inside single quotes;${env:V}in PowerShell), with a random per-call name. The shell expands it after parsing the script, so the value is always one word, never glob-expanded, and never parsed as shell syntax, including inside"$(...)"and backticks. In PowerShell a placeholder inside single quotes is rejected. This matches the Python and TypeScript SDKs. - Bash still evaluates a value used as a number as an arithmetic expression, and an array
subscript in that expression runs command substitutions: with
n='a[$(rm -rf ~)]',[[ "$n" -gt 3 ]]runsrm. A placeholder is therefore rejected (the call fails) inside$(( )),(( ))(includingfor ((...))),$[ ], any${...}, an array subscript (name[...], or[...]=inname=(...)), a[[ ]]that uses-eq,-ne,-lt,-le,-gt,-geor-v, atest -v/[ -v ], aletcommand, adeclare/typeset/localwith-i, and an assignment to a variable declared with-ianywhere in the command. The command checks also fire inside a{ }group or a function body, through the wrapper wordsbuiltin,command,time,exec,nohupandenvand their options, and with a quoted or escaped command name ('let',\let). Compare numbers with[ ]ortest, which accept only integer literals, or pass the value to a program such asexpr. The reference SDKs do not reject these contexts. - The scanner models heredocs (
<<and<<-, quoted and unquoted delimiters;<<in arithmetic is a shift) andcasestatements, so their bodies no longer desync the quoting of later placeholders. A placeholder in a quoted-delimiter heredoc body (<<'EOF'), where bash would not expand it, is rejected, as is a placeholder in acasestatement inside a$(...)or backtick command substitution, which bash 3.2 cannot parse reliably. Bash 3.2 and 5.x also disagree on where a heredoc inside a command substitution ends (EOF), a)or quote in the body), so a placeholder after such a heredoc is rejected. - As defense in depth, independent of the scanner, an argument value with a substitution (
$(,${,$nameor a backtick) inside[...]is rejected, naming the argument: an array subscript is the one shape bash runs code from when it evaluates a value as a number. The values of one command are also checked joined, since two placeholders in one word can build the subscript together. The scanner cannot tell whether a value reaches arithmetic later (through a variable, a function or another program), so this check applies in every context. Text with a substitution outside brackets, such asSee [docs](https://x) and run `make`, and JSON arrays such as[1,2]pass. - These checks only see placeholders. A template must not carry a value into those contexts
some other way: through a shell variable or command output (
n=UTCP_ARG_n_UTCP_END; (( n )),$(( $(cat file) ))), or a builtin that takes the value as a variable name (read,unset,printf -v).eval,source,bash -cand the like run the value as code. - The commands themselves are trusted: never build a template from untrusted input.
- The host environment is not inherited. By default only
PATH,HOME,LANG,LC_ALL,LC_CTYPE,USER,LOGNAME,SHELL,TZandTERM(a Windows equivalent there) pass through;inherit_env_varsreplaces that list, and[]passes nothing.env_varsare always set. - Keep secrets out of
command.UtcpClientsubstitutes${VAR}into the command text, which becomes the shell's command line, and any local user can read that withps. Pass a secret inenv_varsinstead ("env_vars": {"GITHUB_TOKEN": "${GITHUB_TOKEN}"}) and let the program read it from its environment. - A timeout, the output limit, cancelling a
callToolStreamingsubscription, andclose()kill the shell and every process it started.
Differences from the reference SDKs #
- The Python and TypeScript SDKs run each bash command in its own subshell, so
exit,execorset -eends only that command and the script goes on. Here the commands share one shell (socdand variables carry over), and a command that ends it would leave the call without output. Such a call fails instead, with the shell's exit code and error; a script syntax error fails the same way. Return a status withfalseor(exit 3)rather thanexit. A command may install atrap '…' EXIT; the call still completes, and the trap's output, printed as the shell exits after the end marker, is discarded. - Bash placeholders in arithmetic contexts are rejected (see Security), and output is capped by
maxOutputBytes.
Known limitation #
UtcpClient resolves $NAME and ${NAME} placeholders as client variables in every call template
field before the protocol runs (as the Python and TypeScript clients do). Through a client, a
command referencing $CMD_0_OUTPUT or another shell variable therefore needs that variable to be
defined, or the call fails with UtcpVariableNotFoundException. Read shell variables in a way the
substitutor does not match, for example "$(printenv NAME)".
Packages #
| Package | Transports |
|---|---|
utcp |
Core client, http (with OpenAPI conversion), sse, streamable_http, text |
utcp_mcp |
mcp (MCP servers over stdio and streamable HTTP) |
utcp_cli |
cli |
utcp_file |
file, .env variable loader |
utcp_socket |
tcp, udp |
utcp_websocket |
websocket |