Skip to content

MCP Secrets (Floopy Vault)

Outbound MCP servers (web search, GitHub, internal MCPs) need credentials. Inlining them in YAML or in environment variables means anyone with config-read access also has the secrets — and rotating them touches every config that references them.

Floopy Vault holds the values centrally:

  • AES-256-GCM encryption at rest, per-row data-encryption keys.
  • AWS KMS envelope encryption — the DEK is wrapped with your CMK using an EncryptionContext of org_id + dek_version.
  • Plaintext never cached in process memory. The unwrapped DEK is cached briefly via state.dek_cache; the plaintext value is decrypted just-in-time on each tool call and zeroed on drop.
  • Reference, not value — server configs store secret_ref (a name); resolution happens at request time.

Lowercase snake_case, starts with a letter:

^[a-z][a-z0-9_]*$

Examples:

  • mcp_search_api_key
  • github_pat_eval_bot
  • internal_jira_token

Names are unique per organization. Two orgs can share the same name without collision.


From the MCP → Secrets page, click New secret. Provide:

  1. Reference namesnake_case.
  2. Value — the raw secret string (≤ 64 KiB).

The value is encrypted on save. Floopy never displays it again. To rotate, delete the row and create a new one — the reference can be reused without changing server configs that point to it.

The same reference can also be created inline from the Server editor’s Secret Picker, when you realize mid-config that the secret hasn’t been added yet.


mcp_vault_secrets rows are isolated per org by Postgres Row-Level Security:

  • SELECT: any member of the org can list (ref, updated_at) — never the ciphertext.
  • INSERT / UPDATE / DELETE: only members.role IN ('owner','admin').
  • INSERT additionally requires created_by = auth.uid() so audit attribution lines up with the actor.

A token from one org will never see — or even enumerate — secrets owned by another org.


  • vault.read — emitted on every successful resolution. Metadata carries secret_ref and the calling consumer (e.g. mcp_outbound_call_tool).
  • vault.write — audit-first: the row is enqueued BEFORE the upsert. If the audit channel rejects, the upsert is skipped.
  • vault.delete — same audit-first contract.

Read entries are throttled 60s by (org, secret_ref) so a tool that resolves the same secret across many tool calls doesn’t flood the trail.


  1. Create the new secret with a temporary name (*_v2).
  2. Update the affected server config(s) to point to the new ref.
  3. Verify (via the Server editor’s Test button) that the upstream is healthy.
  4. Delete the old secret.
  5. (Optional) Rename — delete the temporary, recreate under the original name.

Automatic rotation is on the roadmap. For now this two-step pattern keeps you safely under no-downtime deploys.