mcpctl v0.1.0

Declare MCP servers once in YAML and sync them into Claude Code and Zed — secrets stay in the OS keychain / Secret Service, never in client configs.

mcpctl

CI

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.yml nor in the secret store, sync refuses and names the entry and the key — never the value. Only a command path, and in args a path or a bare flag (--stdio, not --token=…), may disappear unchecked.
  • Zed's settings.json is edited in place: only the context_servers block 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 while sync runs, it is left alone and sync asks 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.yml is removed. Project-scope servers (.mcp.json) and claude.ai connectors are left alone.
  • Zed: the context_servers block, entirely.

Limits

  • Two clients: Claude Code and Zed.
  • mcp-remote is a third-party npm package, fetched by npx on 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.

Repository

mcpctl

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 2
  • 1 day ago
  • October 3, 2026
License

MIT License

Links
Synced at

Sat, 03 Oct 2026 04:27:44 GMT

Languages