Keys for your AI tools, kept out of their configs
BetaAn 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.
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.
# 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-githubWhat lands in the client's config:
{
"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.
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.
| Client | Scopes (default first) | Where |
|---|---|---|
claude-code | local, user, project | Through claude mcp add-json; project writes .mcp.json |
claude-desktop | user | claude_desktop_config.json |
cursor | user, project | ~/.cursor/mcp.json or .cursor/mcp.json |
vscode | project | .vscode/mcp.json |
windsurf | user | ~/.codeium/windsurf/mcp_config.json |
| Anything else | — | --config <path>, or --print and paste it |
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 … -- …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.
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.
$ 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>`.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.
# 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