Skip to content

Secrets

mcphub.yaml is a file you edit by hand, commit next to dotfiles, and paste into issues. It should never hold a bearer token. mcphub has two independent ways to keep secrets out of it, both backed by TinyVault (tvault):

  • vault: — for stdio servers. mcphub spawns the server through tvault run, which injects the vault project's secrets as environment variables. The config only ever names the project.
  • tvault:// header refs — for remote (http/sse) servers. A headers value that starts with tvault:// is resolved to the real secret when the gateway connects. The config only ever holds the reference.

Both beat the alternative — an env: { API_KEY: sk-... } sitting in plain text in mcphub.yaml — without changing how you write a server entry.

vault: — spawn-time secrets for stdio servers

Point a server at a tvault project instead of listing its secrets in env:

yaml
servers:
  github:
    command: gh-mcp
    args: [--stdio]
    vault: github            # spawn via `tvault run --project github`
    vault_only: [GH_TOKEN]   # least-privilege allowlist (optional)
    enabled: true
    description: GitHub MCP

When mcphub launches this server — from the gateway (mcphub mcp serve) or written verbatim into a direct-mode agent — it doesn't run gh-mcp directly. It wraps the command:

tvault run --project github --only GH_TOKEN -- gh-mcp --stdio

tvault run unlocks the vault, injects the project's secrets as environment variables into the child process, and execs gh-mcp. gh-mcp sees GH_TOKEN in its environment; mcphub.yaml never does.

Narrowing what gets injected

  • vault_only — a list of secret keys; only these are injected. Least privilege: a server that only needs GH_TOKEN shouldn't also get every other secret in the github project. The keys become a single comma-joined --only flag on the wrapped command.
  • vault_prefix — inject only keys with this prefix instead of an explicit list; becomes --prefix <value> on the wrapped command.

Gateway vs. direct mode

  • gateway — the hub spawns the server itself via the wrapped command, so the secrets reach the downstream process and are never exposed to the agent at all.
  • directsync writes the wrapped command (tvault run --project github --only GH_TOKEN -- gh-mcp --stdio) straight into the agent's config, so the agent launches it the same way. That means tvault must be on the agent'sPATH too, and the vault must be unlockable in the environment the agent runs in — not just yours.

From the CLI

sh
mcphub add github gh-mcp --vault github --vault-only GH_TOKEN

--vault takes the project name; --vault-only is repeatable for multiple keys. There is no --vault-prefix flag yet, so set vault_prefix by editing mcphub.yaml directly if you need it.

vault requires a command

vault wraps a spawned process, so it only applies to stdio servers (command set). A server with both vault and url fails Validate() — mcphub rejects the config with vault injects env into a spawned command and can't be used with a remote url.

Checking it's wired up

mcphub doctor looks for tvault on PATH whenever any enabled server uses vault, and fails the check with a server uses vault but tvault is not on PATH if it isn't (and, when it is, annotates each vaulted server's check with (secrets via tvault:<project>)). mcphub list marks a vaulted server with a [vault:<project>] tag next to its command so you can see at a glance which servers keep their secrets out of the file.

Unlocking the vault

tvault run/tvault get need the vault unlocked in whatever process spawns them — via TVAULT_PASSPHRASE, TVAULT_PASSPHRASE_FILE, TVAULT_IDENTITY_KEY, or a running tvault agent. mcphub preserves the master passphrase / identity key / agent token only for a tvault wrapper or a directly configured tvault process and strips them from ordinary stdio downstreams. TVAULT_PASSPHRASE_FILE is a path, not the secret: it is forwarded to every stdio child, and if unset mcphub injects ~/.config/secrets/env when that file exists so GUI-launched gateways (no login-shell env) can still unlock. For vault_only and vault_prefix, it also removes matching ambient/configured values before the wrapper starts so the selected value comes from TinyVault.

A vaulted server is unavailable

First confirm the server boundary without exposing a secret:

sh
tvault status
mcphub doctor --server github --probe

If TinyVault reports that the vault is locked or its agent is unavailable, use TinyVault's normal interactive unlock or agent workflow in the environment that launches the gateway, then repeat the probe. A GUI-launched harness can have a different environment from an interactive shell. Do not add a passphrase or a decrypted API key to mcphub.yaml, and do not work around a locked vault by exporting either globally. The vaulted downstream fails closed until TinyVault can supply its selected secrets.

tvault:// refs — secrets in remote headers

Remote (http/sse) servers often need an Authorization header rather than a spawned-process env var. headers on a server carries custom HTTP headers sent with every request; any value that starts with tvault:// is resolved by the gateway at connect time instead of being read literally:

yaml
servers:
  obsidian:
    url: "https://127.0.0.1:27124/mcp"
    transport: http
    headers:
      Authorization: "tvault://obsidian/authorization"
    enabled: true
    description: Obsidian Local REST API

Reference syntax:

  • tvault://<project>/<key> — a specific project and key, e.g. tvault://obsidian/authorization.
  • tvault://<key> — just a key, resolved against tvault's currently active project.

Header resolution shares the server's connect_timeout. Decrypted stdout is accepted only up to 64 KiB; a larger value fails without being returned. Failure stderr is retained only as an 8 KiB tail and passes through the same closed, credential-redacting startup diagnostic policy as stdio servers.

  • tvault://current/<key>current is an explicit alias for the active project (equivalent to omitting the project).

A header value with no tvault:// prefix is passed through unchanged, so plain literal headers still work if you genuinely want one inline.

How resolution works

When the gateway is about to dial a remote server that has headers set, it resolves every tvault:// value by shelling out to tvault get <key> -p <project> and substitutes the returned secret before opening the connection. A malformed reference (tvault:// with nothing after it, or an empty key after the /) or a failed tvault get fails that server's connection with a clear error instead of sending a broken or literal header.

Gateway-only — direct-mode agents don't get resolved headers

headers is resolved by whichever process actually dials the URL. In gateway mode that's mcphub's hub, so resolution always happens. In direct mode the agent connects to the remote server itself, and sync does not carry headers into any agent's config at all (it isn't part of the portable server shape sync writes) — a direct-mode agent never sees the header, resolved or not. If an agent must run in direct mode against a header-authenticated remote server, configure that header in the agent's own config instead of relying on mcphub's headers/tvault:// mechanism.

Validation

headers only makes sense on a remote server. A server with headers set but no url fails validation with headers only apply to remote (url) servers.

Two mechanisms, one vault, different shapes

vault:headers: tvault://...
Applies tostdio servers (command)remote servers (url)
Delivers secrets asenvironment variablesHTTP header values
Resolved bytvault run wrapping the spawn commandthe gateway, at connect time
Works in direct modeyes — the wrapped command is written verbatim (needs tvault on the agent's PATH)no — headers aren't synced to agent configs
Narrowingvault_only, vault_prefixper-header, via the reference itself

They compose fine on the same mcphub.yaml — a stdio server can use vault while a separate remote server uses tvault:// headers.

Next

  • Configuration reference — the full vault/vault_only/vault_prefix field table.
  • Concepts — what gateway vs. direct mode changes about who spawns or dials a server.
  • Sync to your agents — why headers isn't part of what sync writes into an agent's config.

Released under the MIT License.