TILLSECRETS · MCP SERVERS

Keys for your AI tools, kept out of their configs

Beta

An MCP server usually wants its API key pasted into the client's config file, where it stays in plain text: in backups, in dotfile repos, and in reach of anything that reads your home folder. tilldev mcp add writes the entry with a reference instead. The client starts the server through tilldev mcp run, which fetches the value and hands it to that server alone, only when it starts.

01Add

Register a server

Everything after -- is the server's own command. Each --env names a variable and the secret it comes from. A value may mix text and references, such as AUTH=Bearer secretref://…. A plain value is refused, because it would be written into the config. Use --set KEY=value for settings that aren't secret. The references are checked when you add them, so a typo fails here, not at the first start.

terminal
# Store the key once (it prompts for the value, hidden).
tilldev secrets set RUNPOD_API_KEY --env dev --project ai

# Register the server. The key is named, never written.
tilldev mcp add runpod --env RUNPOD_API_KEY=secretref://ai/dev/RUNPOD_API_KEY -- npx -y @runpod/mcp-server

# Several keys from one environment: --from, then bare names.
tilldev mcp add github --client cursor --from ai/dev --env GITHUB_TOKEN -- npx -y @modelcontextprotocol/server-github

What lands in the client's config:

~/.cursor/mcp.json
{
  "mcpServers": {
    "runpod": {
      "command": "/usr/local/bin/node",
      "args": [
        "/usr/local/lib/node_modules/@tillstack/cli/dist/index.js",
        "mcp", "run", "runpod",
        "--env", "RUNPOD_API_KEY=secretref://ai/dev/RUNPOD_API_KEY",
        "--", "npx", "-y", "@runpod/mcp-server"
      ],
      "env": { "PATH": "/usr/local/bin:/usr/bin:/bin" }
    }
  }
}

Desktop clients start servers without your shell's PATH, so the entry carries it for commands such as npx and uvx. Other servers in the file, and its other settings, are left as they were. A file that isn't valid JSON is left untouched.

02Clients

Where it writes

Without --client, the CLI uses the one client it finds on the machine and asks when there are several. --client cursor,claude-desktop writes to both.

ClientScopes (default first)Where
claude-codelocal, user, projectThrough claude mcp add-json; project writes .mcp.json
claude-desktopuserclaude_desktop_config.json
cursoruser, project~/.cursor/mcp.json or .cursor/mcp.json
vscodeproject.vscode/mcp.json
windsurfuser~/.codeium/windsurf/mcp_config.json
Anything else—--config <path>, or --print and paste it
manage
tilldev mcp ls [--client cursor] [--json]
tilldev mcp rm runpod [--client cursor] [--scope user]
tilldev mcp add runpod --print --env … -- …      # print the entry instead of writing it
tilldev mcp add runpod --config ~/some-client/mcp.json --env … -- …
03Starts

Approve the first start, then let it repeat

Each start asks to put those secrets into that exact command, which is a secret.resolve approval. The first start opens the approval page in your browser and waits up to 5 minutes (TILLDEV_APPROVAL_WAIT changes it). The page names the server that asked. Allow it for 1, 7 or 30 days and later starts go straight through. If the client gives up before you approve, approve anyway and restart the server from the client; the request it made is still the one waiting.

If a reference no longer resolves, the server does not start, and the reason goes to the client's log. Nothing but the server writes to its output, so the MCP connection stays clean. Your TillDev session token is not passed to the server.

What this protects
The key is no longer in the config file, in its backups or in a repository. While the server runs it holds the value, as it must to use it. If you write the server yourself, the broker can make its HTTP calls without it ever holding the key.
04Audit

Find keys already in plain text

tilldev mcp ls reads every client's config on the machine and marks each server: keys from the vault, keys in plain text (named, never printed), or a launcher whose path has moved. tilldev mcp import stores a server's plain-text environment values in the environment you name and rewrites its entry to use references. It stops before changing anything if a key already exists there (--overwrite stores a new version) and leaves any variable you pass to --keep. A key passed on the server's command line is reported but not moved.

terminal
$ tilldev mcp ls
NAME     CLIENT                 SECRETS          COMMAND                           STATE
runpod   cursor (user)          RUNPOD_API_KEY   npx -y @runpod/mcp-server         from the vault
openai   claude-desktop (user)  -                npx openai-mcp --api-key ***      plain text: OPENAI_API_KEY, --api-key
! 1 server(s) keep keys in plain text. Move them into the vault: tilldev mcp import <name> --to <project>/<env>

$ tilldev mcp import openai --to ai/dev
OK Moved OPENAI_API_KEY into ai/dev. openai now gets them from the vault when it starts.
! Those values sat in a plain-text file, so treat them as seen: rotate them with the provider, then `tilldev secrets set <KEY> --env <env>`.
05Teams

Commit it with the project

With --scope project the entry goes in the project's shared file and calls tilldev by name, with no paths from your machine. It holds only references, so it can be committed. Everyone who opens the project needs the CLI and read access to those secrets, and each start is approved by the person running it.

terminal
# In the repo: .mcp.json (Claude Code), .cursor/mcp.json or .vscode/mcp.json
tilldev mcp add runpod --client claude-code --scope project \
  --env RUNPOD_API_KEY=secretref://ai/dev/RUNPOD_API_KEY -- npx -y @runpod/mcp-server
git add .mcp.json   # references only; each teammate resolves them with their own login