mcpctl v0.1.0
mcpctl
Declare your MCP servers once, in one YAML file, and let mcpctl write them into Claude Code and Zed — with every secret kept in the OS secret store, never in a client config.
$ mcpctl sync --check
claude: + openobserve
claude: - aws-mcp
zed: ~ mcp-server-grafana
$ mcpctl sync
Why
Each MCP client has its own config file, its own format and its own idea of where a token goes. Keeping them aligned by hand drifts, and the token usually ends up in plain text — Zed in particular has no way to read a secret from anywhere: an Authorization header is written as is in settings.json.
Tools that sync MCP configs across clients exist (mcpr, mcp-sync, MCP Dock…), but they copy the values — secrets included — into each client. mcpctl is built around the opposite rule:
- Secrets stay in the OS store — macOS keychain, or the Secret Service on Linux. A client config only ever holds an indirection to
mcpctl. - Token-protected HTTP servers work in Zed, through a stdio bridge fed by a FIFO: the token is never in a process's arguments (readable by
ps) and never on disk. - A sync never destroys the only readable copy of a token: if a value would be dropped from Zed or from Claude Code and is neither declared in
servers.ymlnor in the secret store,syncrefuses and names the entry and the key — never the value. Only a command path, and inargsa path or a bare flag (--stdio, not--token=…), may disappear unchecked. - Zed's
settings.jsonis edited in place: only thecontext_serversblock is replaced (or appended, when the file has none yet), the rest of the file — comments included — stays byte for byte. If the file changes whilesyncruns, it is left alone andsyncasks to be run again. - Claude Code is changed through its own CLI (
claude mcp add-json/remove --scope user), never by rewriting~/.claude.json, which Claude Code itself keeps rewriting.
How a server reaches its secret
| Server | Claude Code | Zed |
|---|---|---|
| No secret | the command or URL, as declared | same |
stdio + secret_env |
mcpctl launch <name>: reads the secrets, exports them, execs the server |
same |
HTTP + secret_headers |
headersHelper: mcpctl headers — Claude Code asks for the headers on each connection |
mcpctl launch <name>: starts mcp-remote as a stdio bridge and hands it the headers through a FIFO |
The FIFO: mcp-remote reads its --header-file once, at startup. mcpctl creates the FIFO in a 0700 directory, starts the bridge, opens the FIFO for writing as soon as the bridge has opened it for reading, removes it, then writes the headers. Nothing is left behind once the handshake is done.
Install
brew install mnemodoc/tap/mcpctl
Or from source, with mise:
git clone https://github.com/mnemodoc/mcpctl && cd mcpctl
mise install && mise dev:deps && mise release:build # bin/mcpctl
Requirements at run time: the claude CLI for the Claude Code side; npx for HTTP servers that need the bridge in Zed; secret-tool (package libsecret-tools) on Linux.
Configure
mcpctl reads $MCPCTL_CONFIG, else $XDG_CONFIG_HOME/mcpctl/servers.yml, else ~/.config/mcpctl/servers.yml. Start from servers.example.yml.
servers:
grafana:
targets: [claude, zed] # one name on both sides: it is what de-duplicates a server
# Zed forwards to an agent that also declares it
command: /opt/homebrew/bin/mcp-grafana
env:
GRAFANA_URL: https://grafana.example.com
secret_env:
GRAFANA_SERVICE_ACCOUNT_TOKEN: mcp.grafana # name of the entry in the secret store
observability:
targets: [claude, zed]
url: https://observability.example.com/mcp
secret_headers:
Authorization: mcp.observability
| Key | Meaning |
|---|---|
targets |
claude, zed, or both |
command, args |
stdio server; a leading ~/ is expanded |
env |
non-secret environment |
url, headers |
HTTP server, non-secret headers |
secret_env |
VARIABLE: store-entry, read by mcpctl launch |
secret_headers |
Header: store-entry, read by mcpctl headers (Claude Code) or passed through the FIFO (Zed) |
enabled |
false declares the server disabled in Zed and leaves it out of Claude Code |
note |
copied as comments above the entry in Zed |
zed_raw (top level) |
entries copied verbatim into Zed — servers provided by a Zed extension |
settings (top level) |
mcpctl, npx, mcp_remote, zed_settings, claude_json — machine-specific paths, all optional |
settings.mcpctl defaults to the mcpctl found in PATH, kept unresolved (/opt/homebrew/bin/mcpctl, not a versioned Cellar path): it is written into the client configs, so it has to survive an upgrade.
Store a secret
Always through standard input — a value passed as an argument is visible to ps and lands in the shell history.
# macOS
read -rs v && printf '%s\n%s\n' "$v" "$v" | security add-generic-password -U -a "$USER" -s mcp.grafana -w; unset v
# Linux
secret-tool store --label="mcp.grafana" service mcp.grafana # prompts for the value
Rotating a secret needs no sync: it is read each time the server starts.
Commands
| Command | Does |
|---|---|
mcpctl sync --check |
lists the changes per entry, writes nothing, prints no value; exits 0 when up to date, 3 when there are changes to apply, 1 when the guard refuses or an error occurs |
mcpctl sync |
applies; idempotent — a second run reports up to date |
mcpctl launch <name> |
started by the clients, not by hand |
mcpctl headers |
Claude Code headersHelper; the server comes from CLAUDE_CODE_MCP_SERVER_NAME |
mcpctl licenses |
third-party notices baked into the binary |
After a sync, Zed reloads its settings live; an open Claude Code session keeps its servers until it restarts.
After uninstalling a Zed extension, run mcpctl sync again: Zed deletes the context_servers entry carrying the extension's id, even when that entry has become a command declared here.
What sync owns
- Claude Code: every user-scope MCP server. One that is not in
servers.ymlis removed. Project-scope servers (.mcp.json) and claude.ai connectors are left alone. - Zed: the
context_serversblock, entirely.
Limits
- Two clients: Claude Code and Zed.
mcp-remoteis a third-party npm package, fetched bynpxon first use.- On Linux the Secret Service needs an unlocked D-Bus session — not available on a headless server.
Development
mise dev:deps
mise dev:spec # specs
mise dev:ameba # static analysis
mise dev:format-check
License
MIT — see LICENSE. Third-party notices: mcpctl licenses.
mcpctl
- 0
- 0
- 0
- 0
- 2
- 1 day ago
- October 3, 2026
MIT License
Sat, 03 Oct 2026 04:27:44 GMT