Leandc
Claude Chat's remote dev kit: a lean, token-efficient MCP server (Five tools: run, job, read, write, edit)
ドキュメント
LeanDC
A Lightweight MCP Server for Local & Remote Development
LeanDC is a lightweight MCP tool server that lets Claude Chat run commands, read, write and edit files, and manage long-running jobs, on a Mac or on a remote Ubuntu machine, without switching to Claude Code.
Inspired by Desktop Commander MCP, LeanDC is built to keep token use low, with compact tool descriptions and trimmed output.
Tested on macOS (Apple Silicon) and Ubuntu. LeanDC is used from Claude Chat (not Claude Code): the Claude desktop app for the local setup, and a claude.ai custom connector for the remote one.
Five tools: run | job | read | write | edit
Two scenarios covered:
- Local (stdio): run from the Claude desktop app (Chat) on macOS; the tools act on that Mac.
- Remote (HTTPS): run the MCP as a web service on a remote Ubuntu machine; ask Claude in the Chat app to use the tools on the remote machine using a connector. (Needs a TLS reverse proxy on Ubuntu)
Why LeanDC?
Desktop Commander MCP is a capable tool server that already provides such capabilities. The broader scope of Desktop Commander 0.3.1 comes with 25 tools and about 13,500 tokens of tool descriptions.
LeanDC takes a narrower approach: 5 tools and about 2,100 tokens of tool descriptions. LeanDC covers the common cases (run commands, read, write, edit, background jobs) while keeping the tool overhead low.
To see what that does to token use, a benchmark ran 11 development tasks in Claude Code. LeanDC used fewer tokens than Desktop Commander 0.3.1 on all 11 tasks, from 37% to 90% fewer, and every run passed the task check.
Benchmark methodology and results: bench/
The benchmark was run with Claude Code, not Claude Chat. The results reflect the tested conditions and should not be read as a projection of real-world Claude Chat usage. The benchmark measures tokens only and makes no claim about cost or about any plan's usage allowance.
Requirements
- Python 3.12 or newer with
venvandpip - Bash
- For remote: Ubuntu server with public DNS name, ports 80 and 443 open, and a TLS reverse proxy
Setup
-
Run
curl -fsSL https://raw.githubusercontent.com/asif-r-porosh/leandc/refs/heads/main/install.sh | bashOr, clone/download repo,
cdintoleandcand./manual-install.shgit clone https://github.com/asif-r-porosh/leandc.git cd leandc ./manual-install.shOverride default installation (options)
LEANDC_HOME(install dir)LEANDC_CONFIG(config path)LEANDC_PYTHON(which interpreter)
The installation:
- creates a private virtualenv
~/.local/share/leandc/venv - installs the pinned requirements and a copy of leandc
- creates
~/.config/leandc/config.toml - runs the self-check and prints the command to use.
-
On macOS (local stdio): edit
~/.config/leandc/config.tomlto change default values; settarget = "mac"; leave the[http]section unchanged, because stdio does not use the section -
In the Claude desktop app (Chat), click on the Claude menu (macOS top menu bar) →
Settings→Developer→Edit Config. Locate the fileclaude_desktop_config.jsonand add/merge the following in the file"mcpServers": { "leandc": { "command": "/Users/USER/.local/share/leandc/venv/bin/leandc", "args": ["serve", "--config", "/Users/USER/.config/leandc/config.toml"] } }Replace USER with the OS username.
Quit and fully reopen the app (Cmd-Q, not just closing the window). Ask Claude to run
uname -a; if the tools appear, the setup works. -
On the remote Ubuntu server:
a. Use instruction 1 to install.
b. Create a hostname e.g.
leandc.example.compointing to the server's public IP. The hostname must be DNS only. NB:runwaits at most 120 seconds and then returns a job id; any proxy or service in front of leandc must allow requests at least that long.c. Generate a secret with
~/.local/share/leandc/venv/bin/leandc secret, then edit~/.config/leandc/config.toml:target = "vm" [http] host = "127.0.0.1" port = 8765 path_secret = "<the_generated_secret>" allowed_hosts = ["leandc.example.com"] # exactly the hostname Claude will callKeep the file at mode
600.leandc check --config ~/.config/leandc/config.tomlmust pass.d. TLS proxy: reverse proxy host:port from nginx/caddy or any other alternative. For caddy, use
flush_interval -1; reload; do not add alogdirective for the site.e. Run with
~/.local/share/leandc/venv/bin/leandc serve.To make the service persistent, use the supplied
systemdunit, deploy/leandc.service. Run the commands below from a folder that containsdeploy/leandc.service:-
Installed from a clone (
./manual-install.sh): run the commands from the clone's root. The unit is already in thedeploy/folder. -
Installed with the
curl ... | bashone-liner: no clone is kept, so download the unit first:mkdir -p deploy && curl -fsSL -o deploy/leandc.service https://raw.githubusercontent.com/asif-r-porosh/leandc/refs/heads/main/deploy/leandc.service
Then:
sed -e "s|/home/youruser|$HOME|g" -e "s|youruser|$(id -un)|g" deploy/leandc.service > /tmp/leandc.service \ && sudo install -m 644 /tmp/leandc.service /etc/systemd/system/leandc.service \ && rm /tmp/leandc.service \ && sudo systemctl daemon-reload && sudo systemctl enable --now leandc systemctl status leandc --no-pager | head -5The
sedfills in the current username and home directory.The service restarts on failure and starts at boot. Logs:
journalctl -u leandc.Leave
KillModeat the default (control-group): the jobs are killed byleandcon stop, and stray children must not outlive the service. -
-
In claude.ai, Settings → Connectors → Add custom connector, URL
https://leandc.example.com/<the_generated_secret>/mcp; leave OAuth empty; ask Claude to run hostname.<the_generated_secret>from 4.c
Checking the endpoint from the server (use the real secret and hostname):
- a valid MCP request to
/<the_generated_secret>/mcpanswers200 - a bare GET to that path answers
400(a real MCP request is required) - a wrong path answers
404 - a Host header not in
allowed_hostsanswers421
Security
The only protection is one long secret in the URL path plus the allowed-hosts check. Anyone who learns the URL gets a shell as that user on that machine, so use a disposable server that holds no production secrets, and treat the connector URL like a password. leandc's own HTTP access log is off, so the secret is not written to the service log. To rotate the secret: leandc secret, edit the config, sudo systemctl restart leandc, update the connector URL.
Remote reach. leandc does not inspect or block commands. run can use ssh, scp, rsync, sshpass or any other tool to reach other hosts, with whatever keys, agent or credentials that user has. On a machine holding SSH keys or an ssh agent, a leaked connector URL or a prompt injection therefore extends to every host the keys can reach. On a server, use a dedicated user with no private keys and no agent forwarding, and keep the server off networks that should stay out of reach. On a Mac, review any ssh, scp or rsync call before approving the call in the app. leandc has no built-in approval prompt for such commands.
Layout
- Program location:
~/.local/share/leandc - Config:
~/.config/leandc - Logs and job buffers:
~/.local/state/leandc - MCP clients run
~/.local/share/leandc/venv/bin/leandc
Commands
~/.local/share/leandc/venv/bin/leandc check: self-check; the response ends withself-check passedif the check passes.~/.local/share/leandc/venv/bin/leandc serve: start the server; stdio whentarget = "mac", HTTP behind the TLS proxy whentarget = "vm"(--transport stdioor--transport httpoverrides the choice)
Tools
| tool | does |
|---|---|
run(cmd, cwd?, env?, timeout?, max_chars?) | bash, non-login, no rc files, stdin closed. Waits up to timeout (30 s default, max 120), then returns a job id and keeps running. Output is cleaned (ANSI, progress bars, repeated lines) and capped (8000 chars, max 30000, keeping the head and the tail). |
job(id?, action?, offset?, limit?, tail?, grep?, wait?) | list, read, wait for, or kill a job. 1 MB rolling buffer per job, 24 h expiry. Jobs are lost on restart, and the server says so. |
read(path, offset?, limit?) | numbered lines, 300 default and 1000 max, next offset in the header, a negative offset reads from the end. |
write(path, content, mode?) | create, overwrite or append. mode is required if the file exists and is not empty. 800 lines max per call. Atomic. |
edit(path, old, new, replace_all?) | exact match that must be unique; returns the changed region with line numbers; on a miss shows the closest match. |
Unknown parameters are rejected, and every error is one line: ERROR <CODE>: what. next step. cwd and env are passed on every call and are not remembered between calls.
The usage log (~/.local/state/leandc/usage.jsonl, rotated by size) holds metadata only: time, tool, duration, exit code, sizes, an 8-character hash of the path, and for run the command names used with counts, such as {"ls":3,"grep":1}. The log never records arguments, paths or content. Use the log to tune the caps.
Configuration
config.example.toml lists every key with the default. Unknown keys are rejected at startup, and no tool can change the config file. LEANDC_CONFIG or --config selects the file; LEANDC_PATH_SECRET can supply the secret instead of the file. The [caps] section holds the output, read, timeout and buffer limits.
Uninstall
Delete ~/.local/share/leandc, ~/.config/leandc and ~/.local/state/leandc. On a server first run sudo systemctl disable --now leandc && sudo rm /etc/systemd/system/leandc.service (if systemd unit is used).
Troubleshooting
leandc checkshows FAIL: the output says what is missing, usually Python older than 3.12 (reruninstall.shwithLEANDC_PYTHON=/path/to/python3.12) or nobash.- Tools do not appear in the desktop app: the command in the app config must be the full absolute path of
venv/bin/leandc, and the app must be fully restarted. - The connector cannot connect: run the endpoint checks above. 421 means the hostname is not in
allowed_hosts; 404 means the secret in the URL does not match the config; nothing at all means DNS, the proxy, or the service (systemctl status leandc). - A command seems to hang: after 30 s (or the
timeoutset)runreturns a job id; follow the job withjob. - Command flags behave unexpectedly: the userland differs (macOS is BSD, Linux is GNU). The
rundescription names the flavor.
Develop
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt -e '.[dev]'
.venv/bin/python -m pytest # about 35 s
Tests need bash and pgrep. The MCP SDK is pinned (mcp==2.3.0) and the middleware API relied on by leandc is provisional, so re-run the suite after any SDK bump, then regenerate requirements.txt.
RnD: Used Claude Sonnet 5.5