Skip to main content

MCP server

This page connects an MCP client to homebutler so an agent can call its 44 typed tools instead of running shell commands. You need Node.js 16 or later for the npx route, or homebutler installed for the binary route.

homebutler mcp speaks JSON-RPC over stdin and stdout. It opens no network port, so only the client that started it can talk to it. It reads your config file at startup and runs each tool on the machine where it was started, or on a remote server when the call names one.

Choose how the client starts homebutler​

npxInstalled binary
commandnpxhomebutler
args["-y", "homebutler@latest"]["mcp"]
NeedsNode.js 16 or later and access to GitHubhomebutler on the PATH of the client
VersionThe newest GitHub release at install timeThe binary you installed

The npm package is a wrapper. On install it downloads the release archive for your platform from GitHub, and it puts mcp in front of the arguments you give it. Flags such as --demo and --max-risk read therefore follow homebutler@latest directly. The wrapper downloads the newest release, not the release that matches the npm package version, so pinning [email protected] doesn't pin the binary. To control the version, install the binary and use the homebutler route.

Install the Claude Desktop extension​

To add homebutler to Claude Desktop without editing its config, install the extension. Each release has one per platform for macOS, Windows and Linux, with the binary inside, so you need neither Node.js nor an installed homebutler. The bundles aren't signed.

MachineAsset
Mac, Apple siliconhomebutler_<version>_darwin_arm64.mcpb
Mac, Intelhomebutler_<version>_darwin_amd64.mcpb
Windows, x64homebutler_<version>_windows_amd64.mcpb
Windows on Armhomebutler_<version>_windows_arm64.mcpb
Linux, for other clients that install .mcpb bundleshomebutler_<version>_linux_arm64.mcpb, homebutler_<version>_linux_amd64.mcpb

Download the file for your machine from the latest release and open it, or drag it onto Settings → Extensions in Claude Desktop, then select Install.

The extension runs homebutler mcp --max-risk read, as described in Read-only mode. To offer more tools, set Highest tool risk to write or destructive in the extension's settings. Any other value stops the server from starting.

In read mode, report never saves a snapshot, so the extension reports changes only against snapshots saved elsewhere. Run homebutler report by hand or from cron, or set Highest tool risk to write.

The extension reads the same config as the CLI, ~/.config/homebutler/config.yaml. On macOS and Linux it runs with this PATH, so a docker installed anywhere else isn't found:

/usr/local/bin:/usr/local/sbin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin:/snap/bin

On Windows it sets no PATH and uses the one Claude Desktop has, so ssh.exe and docker are found where Windows finds them.

Add homebutler to a client​

All clients read the same server definition, including ChatGPT Desktop, Windsurf, and any other MCP client that starts a stdio server. Replace command and args with the binary route if you installed homebutler.

Run this in your project folder to write a project-scoped entry to .mcp.json:

claude mcp add --scope project homebutler -- npx -y homebutler@latest

Or create .mcp.json yourself:

{
"mcpServers": {
"homebutler": {
"command": "npx",
"args": ["-y", "homebutler@latest"]
}
}
}

For an installed binary, run claude mcp add --scope project homebutler -- homebutler mcp. Without --scope, the entry is local to you and the current project.

Choose the config file​

The server loads the config file the same way the CLI does: --config <path>, then $HOMEBUTLER_CONFIG, then ~/.config/homebutler/config.yaml, then ./homebutler.yaml in the client's working directory. With none of them, built-in defaults apply. To pin a file, add it to args or to env:

{
"mcpServers": {
"homebutler": {
"command": "homebutler",
"args": ["--config", "/home/you/.config/homebutler/config.yaml", "mcp"],
"env": { "HOMEBUTLER_CONFIG": "/home/you/.config/homebutler/config.yaml" }
}
}
}

Either one is enough. The config_validate tool reports which file the running server uses and which rule picked it. A config file that holds a secret and is readable by other users stops the server at startup:

error: config error: config file /home/you/.config/homebutler/config.yaml contains plaintext secrets but permissions are too open (0644); run: chmod 600 /home/you/.config/homebutler/config.yaml

The client shows this as a server that failed to start. Run chmod 600 on the file.

Try it with demo data​

--demo returns realistic sample data and makes no system calls, so it works on a machine with no Docker and no servers:

{
"mcpServers": {
"homebutler": {
"command": "homebutler",
"args": ["mcp", "--demo"]
}
}
}

Demo mode lists all 44 tools. Write and destructive tools answer with sample results and change nothing. Some demo values differ from a real run: docker_restart returns "status": "restarted" where a real call returns "ok".

Check the connection​

Send the three requests a client sends when it starts. This runs against any binary, without a client:

printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"system_status","arguments":{}}}' \
| homebutler mcp

The first response names the server, and the second lists the tools:

{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"homebutler","version":"1.7.0"}}}
The server works when
The third response holds the machine's CPU, memory and disk figures as a JSON string in result.content[0].text.

What a tool returns​

Every tool returns typed JSON as text, not a table or a sentence to parse. A tool classed read changes nothing, a write tool changes something or sends something out, and a destructive tool overwrites or deletes data or stops a service. The tool list carries readOnlyHint and destructiveHint for each tool (see Tool annotations), so a client can ask for confirmation before it runs the riskier ones. See MCP tools for the envelope, the response conventions, and every tool.

To give an agent only the reads, start the server with --max-risk read: see Read-only mode.

Tool annotations​

Each tool in tools/list carries readOnlyHint and destructiveHint, so a client can ask before a destructive call. Both hints are sent on every tool, and they're never less cautious than the tool's class:

ClassreadOnlyHintdestructiveHint
readtruefalse
destructivefalsetrue
writefalsetrue when one call can delete or overwrite something that was already there

A write tool with destructiveHint: false only adds something, or changes something that comes back without anyone acting: docker_restart, proxmox_guest_start, proxmox_guest_reboot, wake, watch_add and notify_test. Those calls still have effects. A restart interrupts whoever is using the container, and notify_test sends a real message to every channel.

These write tools are annotated destructive because of what one call can remove:

ToolWhat a call can delete or overwrite
install_uninstallRuns compose down, which removes the app's containers. The app directory and its data stay, and anything the app wrote inside its container goes with the container.
reportPrunes saved snapshots down to keep, which the caller sets. keep: 1 leaves one.
backup_createPrunes older archives when backup.retention is configured.
watch_checkPrunes incidents past the retention cap.
watch_removeDeletes the target's entry from the watch list.
backup_drillPrunes drill records past the newest 50.
install_appRewrites an existing docker-compose.yml, including your edits to it.

Under --max-risk read, report is annotated as a read, and its description says it never saves a snapshot.

The server doesn't send idempotentHint or openWorldHint, so a client reads the spec's defaults: not idempotent, and may reach things outside the machine. Neither default fits a whole class. Calling docker_stop twice does nothing more, while docker_restart twice restarts twice. network_scan pings the subnet and notify_test posts to a chat service, while docker_list asks only Docker.

Annotations describe what a call does to the machine, not what it returns. A client can ignore them. --max-risk is enforced by the server whatever the client does with its hints. Clients on revision 2024-11-05, which predates annotations, see them as an unknown field.

Commands without a tool​

Some CLI commands have no MCP tool. Run them in a terminal:

  • trust accepts an SSH host key on first use. That is a decision for a person, so there is no tool for it, and there won't be one.
  • deploy installs homebutler on a remote server. It was kept out of the tool set that 1.0 froze, and it can come later as a new tool.
  • upgrade replaces the running binary. serve, init, watch start and watch tui run as daemons or interactively, which doesn't fit a stdio tool call.

Protocol versions​

homebutler implements the MCP revisions 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05. Which one is used depends on how the client opens the session:

Client sendsServer answers
initialize with 2025-11-25, 2025-06-18, 2025-03-26 or 2024-11-05The same version
initialize with 2026-07-282025-11-25
initialize with an unknown protocolVersion2025-11-25, the newest legacy revision
server/discover, or any request with the version in _metaThe modern revision, with resultType on each result
initialize with modern _metaError -32601, method not found: initialize
pingAn empty result. With modern _meta, error -32601.
_meta with a version that isn't 2026-07-28Error -32022, Unsupported protocol version, with data.supported listing 2026-07-28
Any other method, such as resources/listError -32601, method not found: <method>

server/discover lists all five revisions in supportedVersions. The server offers tools only. Resources, prompts, sampling, roots, elicitation, tasks, and Streamable HTTP are out of scope, so for this stdio server every revision describes the same tools. tools/list returns the tools sorted by name, with ttlMs set to 300000 and cacheScope set to public, or private when --max-risk reduced the list.