Claude (noether) d576886737 contrib/lmcp-tool: --json for machine consumers
A client that drives lmcp through sic — rather than holding a bearer token
and reaching a port itself — needs two things this script discarded:

  1. inputSchema. `list` printed the name and the first line of the
     description only. Without the schema a model guesses parameter names.
  2. The full result of a call: content[] together with isError. Only the
     text was printed, and a non-zero exit stood in for isError. MCP has it
     the other way round — the content is authoritative even when isError
     is set, and belongs in front of the model.

Both live behind --json. The existing output is byte-identical, because
humans and shell scripts read it that way.

    lmcp-tool list --json         -> {"tools":[{name,description,inputSchema},…]}
    lmcp-tool --json <tool> k=v   -> {"content":[…],"isError":false}

Exit codes under --json: 0 as soon as a valid JSON-RPC result exists —
including isError, which is a tool result and not a transport failure. 1
stays reserved for transport and protocol errors, message on stderr.

Verified against a live lmcp server with 31 tools: old form unchanged
(31 lines, exit 0), both JSON forms parse, unknown tool exits 1 in both
forms, inputSchema present on every entry.
2026-08-07 17:12:00 +02:00

sic

Run a command on a remote host — or inside a container several hops deep — without a shell re-parsing its arguments.

ssh host touch 'a b' creates two files: ssh joins its arguments with spaces and hands the result to the remote login shell, which splits it again. sic host touch 'a b' creates one file named a b. sic sends the argument vector to a daemon on the host as length-prefixed fields, and the daemon calls execvp on it. No shell parses the arguments.

Components

  • sic — client. Takes argv on the command line, frames it as netstrings, pipes the frame over ssh to sicd.
  • sicd — daemon on the target host. Reads netstrings from stdin and runs the command.

Both are single static Go binaries (cmd/sic, cmd/sicd). A Python reference implementation of the daemon is in gateway/sicd.

Wire format

Each field is a netstring: <length>:<bytes>,. A frame is a mode field, zero or more argument fields, and an empty terminator:

<mode> <arg>* 0:,

argv = ["touch", "a b"] is 4:exec,5:touch,3:a b,0:,. The length prefix means no byte needs escaping and no delimiter can collide with the payload.

mode selector behaviour
exec (default) 4:exec, execvp(argv), no shell. Arguments are passed through unchanged.
sh 2:sh, one field passed to sh -c. Pipes, redirects, and globs work.

Usage

sic <host> <command> [arg ...]
sic --sh <host> '<shell string>'
sic <host> -- <command> [arg ...]      # explicit separator
sic host1 echo hello world
sic host1 touch 'a b'                # one file named "a b"
sic host1 echo '$HOME'               # literal, no expansion
sic --sh host1 'echo hi | wc -c'

Nesting

The frame survives being wrapped again, so sic reaches into containers with no shell anywhere in the path, at any depth. A target is a host followed by container hops separated by /:

sic host1/app cat /etc/os-release     # into the 'app' guest on host1
sic host2/ct/svc touch 'a b'          # host2 -> ct -> svc, ONE file, three layers deep

Each host's hop runtimes come from /etc/sic/hosts.toml, one stanza per host:

[host1]
nest = ["incus"]            # host1/app     ->  incus exec app -- <argv>

[host2]
nest = ["pct", "docker"]    # host2/ct/svc  ->  pct exec ct -- docker exec svc -- <argv>

This is what separates sic from a shell helper script. The ssh equivalent —

ssh host2 "pct exec ct -- docker exec svc -- touch \"a b\""

nests quotes inside quotes inside quotes, and every layer re-splits the arguments; a body containing a space, $(), a pipe, or a newline breaks a different layer each time. sic frames the argv once and re-frames it at each hop, so touch 'a b' is one file whether it runs on the host or three containers deep. No escaping, at any level.

Build

go build -o bin/sic  ./cmd/sic
go build -o bin/sicd ./cmd/sicd

Install

Client:

cp bin/sic ~/bin/sic

Daemon on each target host. sicd runs what it is sent, so it must only be reachable over an authenticated transport. Pin it to a dedicated ssh key so the target can run nothing else with that key:

scp bin/sicd host:/tmp/sicd
ssh host 'sudo install -m755 /tmp/sicd /usr/local/bin/sicd'
# ~/.ssh/authorized_keys on the target:
# command="sicd",no-port-forwarding,no-pty ssh-ed25519 AAAA...

Do not listen on a TCP or message-bus port.

Test

printf '4:exec,2:id,0:,' | ssh host sicd
sic host echo hello world

See also

  • docs/design.md — wire format, exec modes, transport and security, prior art.
  • docs/design-nested.md — how nested container hops resolve.
  • demos/quoting-hell.py — six cases run both ways, plain ssh versus sic.
  • SKILL.md, SKILL-bg.md — agent skill definitions (foreground and background).
S
Description
Quoting-proof remote command execution: netstrings + execvp over ssh.
Readme MIT 3.4 MiB
Languages
Go 70.2%
Python 23.2%
Shell 6.6%