Skip to main content
Glama
LianghaiLi-root

Vault MCP Server

Vault MCP Server

Other languages: Chinese

A deployable, multi-user credential vault exposing both:

  • a web console (login + TOTP 2FA + per-user credential management), and

  • a remote MCP endpoint (/mcp, Streamable HTTP) so an AI client can use secrets without them ever being typed into a chat.

Secrets are encrypted at rest with AES-256-GCM. The master key is provided by the platform: Windows DPAPI (zero-config, bound to your account) or PBKDF2-HMAC-SHA256 of VAULT_MASTER_PASSWORD (Linux / servers). Each user has an isolated namespace; access control is enforced server-side.

⚠️ On a remote/trusted server the host holds VAULT_MASTER_PASSWORD and can decrypt every user's vault. That is the intended trade-off of a hosted vault versus the purely local DPAPI model. Protect that password like a root key.


Requirements

Runtime

Item

Requirement

Python

3.10 or newer (requires-python = ">=3.10")

Operating system

Linux, macOS, or Windows

Disk

A few MB for the code plus the encrypted store (grows with your data)

Network

Outbound HTTPS only if you enable Cloudflare Turnstile or use vault_http

Python packages — installed automatically from requirements.txt:

Package

Purpose

cryptography

AES-256-GCM, PBKDF2

fastapi, uvicorn[standard]

HTTP server

python-multipart, jinja2

Form parsing and HTML templates

pyyaml

config.yaml

mcp==1.30.0, sse-starlette

Remote MCP transport

Pillow (optional)

Renders the image CAPTCHA

Optional extras

  • Pillow — needed for the numeric and image CAPTCHA modes. Without it those modes degrade to an arithmetic question rather than failing open.

  • nginx (or another TLS terminator) — required for a production deployment.

  • Cloudflare — only if you want the Access edge gate or Turnstile.

On Windows the vault uses DPAPI and needs no master password. On Linux / macOS / Docker you must set VAULT_FORCE_PBKDF2=1 and supply VAULT_MASTER_PASSWORD.


Related MCP server: mcpvault

Installation

1. Get the code

git clone https://github.com/LianghaiLi-root/vault-mcp-server.git
cd vault-mcp-server

2. Create a virtual environment and install

python3 -m venv .venv
. .venv/bin/activate            # Windows: .venv\Scripts\activate
pip install -r requirements.txt

Or install it as a package, which also gives you the vault-mcp-server and vault-mcp-cli commands:

pip install .

3. Create your configuration

cp config.example.yaml config.yaml

Open config.yaml and set the public hostname(s) you will serve under mcp.allowed_hosts — the remote MCP endpoint rejects any other Host header with HTTP 421 (this is deliberate anti-DNS-rebinding protection).

4. Set the environment

export VAULT_MASTER_PASSWORD="$(openssl rand -hex 32)"   # Linux / macOS only
export VAULT_FORCE_PBKDF2=1
export VAULT_DIR=/var/lib/vault-mcp/vault

5. Provision your first user

python -m vault_mcp.cli add-user admin --role admin

This writes a scrypt password hash and a fresh MCP token into config.yaml. Later accounts default to the ordinary role:

python -m vault_mcp.cli add-user alice

6. Run

VAULT_COOKIE_SECURE=1 uvicorn vault_mcp.server:app --host 0.0.0.0 --port 8080
# or simply:
python -m vault_mcp

Open http://<host>:8080/, log in, and optionally enable Google Authenticator under Two-factor authentication. Set VAULT_COOKIE_SECURE=1 only when the site is served over HTTPS.

Docker

echo "VAULT_MASTER_PASSWORD=$(openssl rand -hex 32)" > .env
python -m vault_mcp.cli add-user admin --role admin   # generates config.yaml + token
docker compose up --build

Mount your own config.yaml and a volume for /data (the vault store).


Features

Role model

Role

Permissions

admin

Super-user. May rename itself, add / rename / delete any user, reset anyone's password and 2FA, re-issue tokens, and sees the full user list plus the global hardening switches.

user

Ordinary account. Manages only itself. It cannot see the user list, has no Ops / Users tab, and every /api/admin/* route returns 404 (not 403) so the endpoints' existence is never confirmed.

An account without a role key is treated as user. Always keep at least one admin; the last remaining admin cannot be deleted.

Web console

Glassmorphism UI; tabs are shown according to your role.

Tab

Visible to

What you can do

Credentials

everyone

Add / edit / delete typed credentials (SSH / web / API / DB / generic). The form renders the fields for the selected type automatically.

Security

everyone

Enroll / reset TOTP 2FA, view and regenerate your MCP token, change your login password.

Ops / Users

admin only

Server status; user management (add user, rename user, reset password, reset 2FA, re-issue MCP token, delete user) written straight back to config.yaml; web hardening; human verification; credential-store integrity check.

Web hardening

Admin-only, persisted under the security key of config.yaml.

  • Cloudflare Access pre-check — when on, every browser request must carry a Cloudflare Access JWT (Cf-Access-Jwt-Assertion header or CF_Authorization cookie). Requests without it get 403 before the login page even renders. Pair it with Cloudflare Zero Trust for a "before login" access layer.

  • Force secure cookie — adds the Secure flag to the session cookie.

  • Custom edge header — a shared secret injected by your own nginx / CDN (e.g. add_header X-Edge-Secret "xxx";); a mismatch is 403.

  • Login throttling — N failures from one source locks it out for M seconds (429).

Edge-check exemptions: /mcp and any request carrying a valid Bearer token skip the edge check. The former is machine-facing and authenticates on its own; the latter is a machine credential a casual visitor cannot forge.

This exemption is what keeps the feature safe to use: the hardening switches are themselves configured through POST /api/admin/security (which accepts a Bearer token). If the gate also blocked that call, enabling Cloudflare Access would lock the admin out of every authenticated path — including the one needed to switch it back off — leaving hand-editing config.yaml on the host as the only recovery. The UI asks for confirmation before enabling the gate so you configure the Cloudflare Access application first.

Human verification

Login-page CAPTCHA, admin-only, persisted under the captcha key of config.yaml. It blocks automated credential-stuffing before the password comparison. Pick one mode:

Mode

What it is

off

Disabled

numeric

Digits only, rendered as an image

image

Letters + digits, rendered as an image with noise

turnstile

Cloudflare Turnstile — automatic, usually just a checkbox

  • Images are rendered to PNG with Pillow, deliberately not SVG — text inside an SVG is plain markup a bot can read, which would defeat the purpose. Without Pillow the mode degrades to an arithmetic question; it never degrades to "no challenge". The admin card warns you when that happens.

  • Turnstile cannot be enabled without credentials. Both the site key and the secret key are required, and a save missing either is rejected with a message naming what is absent. The secret is never echoed back to the browser — the UI only reports whether one is stored. Send a new value to replace it, or use "clear secret" to wipe it.

  • Answers stay server-side: the browser only receives an opaque id. Answers live in memory, expire after 5 minutes and are single-use, so a captured id cannot be replayed. Failed challenges count toward the login throttle.

  • A wrong key produces a readable error on the login page (including the Cloudflare error code) instead of a silent blank box.

MCP tools

vault_save, vault_get, vault_describe, vault_list, vault_delete, and vault_http (an AI-blind HTTP call — the secret is injected server-side and never returned to the model).

Reads are field-aware, so a client can fetch the host and port along with the password instead of only the password:

  • vault_describe(name) — the shape of a record: every stored field key, the value of non-secret fields (host, port, username, url, ...), a length for secrets, plus missing and complete so an incomplete entry is visible before you rely on it.

  • vault_get(name, field="host") — one named field, verbatim. vault_get(name) on its own still returns just the primary secret, so existing clients are unaffected.

  • vault_list() — adds each record's field keys and its complete flag.

vault_save is strict about shape: an unknown type, an unknown field name, or a missing required field (host for ssh, url for web, token for api) returns an error and stores nothing, so a record can never land in a state the read path cannot serve.


Credential types

Every credential carries a type that drives its input fields in the web console, and tells an AI client which field is the primary secret returned by vault_get / vault_http:

Type

Fields

generic

Secret / password

ssh

Host / IP · port · username · password · private key

web

URL · username / e-mail · password

api

Token / API key · endpoint URL

db

Host · port · database · username · password

The list view shows a type badge, and each entry has Edit and Delete actions. Editing prefills every field; renaming an entry replaces the old record with the new name. All fields are encrypted at rest in a single AES-256-GCM blob — only name, type and note live in plaintext metadata, so the list (and vault_list) never expose values.


Remote MCP client

Point your MCP client at http://<host>:8080/mcp and authenticate with the user's mcp_token as a Bearer token. The five tools — vault_save, vault_get, vault_describe, vault_list, vault_delete — operate only on that user's namespace. There is also an AI-blind tool:

  • vault_http(name, url, method, headers, body, secret_header) — the server fetches url with the stored secret injected server-side (placeholder {{secret}} in headers/body, or a named secret_header), and returns only the HTTP response. The secret is never returned to the model and is redacted from the response. Only https is allowed; the optional http.allow_hosts setting restricts destinations. Use this instead of vault_get whenever the AI only needs to call an API, not see the secret.

DNS-rebinding protection

The /mcp endpoint validates the Host header and answers HTTP 421 for anything not whitelisted. List your public domain(s) in config.yaml:

mcp:
  allowed_hosts:
    - vault.example.com
    - 127.0.0.1:8080
    - localhost:8080

allowed_origins defaults to https://<each allowed_host>. This protection is never disabled — it is a deliberate anti-DNS-rebinding guard.

Example mcp.json snippet for WorkBuddy:

{
  "mcpServers": {
    "vault-remote": {
      "url": "https://vault.example.com/mcp",
      "headers": { "Authorization": "Bearer <admin mcp_token>" }
    }
  }
}

Note: your client must support remote (URL-based) MCP. If it only supports local stdio, run python -m vault_mcp.stdio on the server and connect over stdio, or put a local stdio-to-HTTP shim in front.


Layout

Path

Purpose

vault_mcp/vault_core.py

Crypto + per-user encrypted storage (stdlib + cryptography)

vault_mcp/auth.py

scrypt password hashing, TOTP (RFC 6238), HMAC session tokens

vault_mcp/config.py

Loads/writes config.yaml, user CRUD, role checks, web-hardening and CAPTCHA settings, 2FA state persistence

vault_mcp/captcha.py

Login human verification: image rendering, single-use answer store, Cloudflare Turnstile verification

vault_mcp/credtypes.py

Credential type catalog + shape rules (required fields, completeness, secret masking, write validation), shared by both front-ends

vault_mcp/server.py

FastAPI app: web console + remote MCP (/mcp)

vault_mcp/stdio.py

Local stdio MCP server (agent use on this machine)

vault_mcp/cli.py

Operator CLI to provision users / tokens

vault_mcp/__main__.py

python -m vault_mcp entry point (runs the server)

pyproject.toml

Package metadata, dependencies, console entry points

deploy/

systemd unit, nginx TLS proxy, .env template

config.example.yaml

Operator config template (users + server + vault dir)

tests/

Pytest suite (see Tests)

⚠️ Read this before editing the web templates

DASH_TPL_SRC / LOGIN_TPL_SRC in vault_mcp/server.py are non-raw triple-quoted Python strings with JavaScript inside. A \n written in them is compiled by Python into a real newline, which splits the JS string literal and makes the whole <script> block fail with SyntaxError: Invalid or unexpected token — every event handler on the page dies silently (dead tabs, an apparently-empty type dropdown). It bites in comments too.

Write \\n instead, or build the string as ['a','b'].join(NL) with an explicit const NL='\\n';.

To stop this regressing, the server runs a self-check at import time: it reads the .py source and scans inside <script>...</script> for the raw sequence, raising with an exact line number on a hit. Note this bug cannot be detected by inspecting a runtime string — after compilation the backslash is already gone — so the guard must look at the raw source.


Tests

pip install -e ".[dev]"
pytest                       # unit + in-process tool tests + stdio transport
pytest -m integration        # only the subprocess/transport layer
pytest -m live               # opt-in: hits a real endpoint (see below)

The suite never reads a real vault. tests/conftest.py writes its own config.yaml, points VAULT_DIR at a temporary directory before any vault module is imported, and re-points it per test case — so running pytest on a box that hosts a live credential store cannot touch it.

File

Covers

tests/test_credtypes.py

Type catalog, required/complete field rules, secret masking, write validation

tests/test_tools.py

Both tool layers (server.py + stdio.py) in-process: field reads, masking, strict saves, namespace checks

tests/test_stdio_transport.py

Real python -m vault_mcp.stdio subprocess over JSON-RPC

tests/test_remote_mcp.py

Live /mcp endpoint — skipped unless MCP_URL and MCP_TOKEN are set

The live tests carry no endpoint, token or credential name: they are read from the environment and the probe record gets a random name that is deleted again. pyproject.toml (addopts = -m "not live") keeps them out of a default run, so no deployment detail ever lands in the repository or in a CI log:

MCP_URL=https://vault.example.com/mcp MCP_TOKEN=... pytest -m live

Production deployment (systemd + nginx)

Files in deploy/ wire the server behind TLS on a Linux box:

# 1. install as a dedicated, unprivileged user
useradd --system --home /opt/vault-mcp-server --create-home vault
git clone https://github.com/LianghaiLi-root/vault-mcp-server.git /opt/vault-mcp-server
cd /opt/vault-mcp-server
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt

# 2. provision config + secrets
cp config.example.yaml config.yaml
.venv/bin/python -m vault_mcp.cli add-user admin --role admin
cp deploy/env.example .env
#   edit .env: set VAULT_MASTER_PASSWORD (openssl rand -hex 32), then chmod 600 .env

# 3. systemd
cp deploy/vault-mcp.service /etc/systemd/system/
mkdir -p /var/lib/vault-mcp && chown -R vault:vault /var/lib/vault-mcp
systemctl daemon-reload && systemctl enable --now vault-mcp

# 4. nginx (TLS)
cp deploy/nginx-vault-mcp.conf /etc/nginx/sites-available/vault-mcp.conf
ln -s /etc/nginx/sites-available/vault-mcp.conf /etc/nginx/sites-enabled/
#   edit server_name + certificate paths, then: nginx -t && systemctl reload nginx

The unit binds uvicorn to 127.0.0.1:8080 only; nginx terminates TLS and proxies both / (web console) and /mcp (remote MCP). Set VAULT_COOKIE_SECURE=1 once TLS is in place.

systemd sandbox note. The shipped unit uses ProtectSystem=full with ReadWritePaths=/var/lib/vault-mcp /opt/vault-mcp-server and pins the code directory read-only via ReadOnlyPaths. Do not switch to ProtectSystem=strict: it remounts the whole filesystem read-only and systemd silently drops a ReadWritePaths entry nested inside an already read-only tree, which breaks every admin write with [Errno 30] Read-only file system: '.../config.yaml.tmp'.


Security checklist

  • VAULT_MASTER_PASSWORD from a secret manager, never committed.

  • TLS in front (reverse proxy / Cloudflare); set VAULT_COOKIE_SECURE=1.

  • config.yaml (hashes + tokens) not committed; .gitignored.

  • Per-user 2FA enabled after first login.

  • Rate-limit / WAF the /login and /mcp endpoints.

  • Consider a CAPTCHA mode if the login page is reachable from the internet.


License

MIT — see LICENSE.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Encrypted credential vault with a 21-tool MCP server. Store and manage LLM API keys, service API keys, and OAuth credentials — then let your AI agent list, reveal, rotate, rename, pause, and proxy calls through them.
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Local, encrypted credential vault for AI agents that enables secure multi-account management across services like Supabase, GitHub, Vercel, and Stripe, allowing seamless account switching without restarting the chat client.
    9 npm
    2
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Provides a zero-knowledge credential vault for AI agents, allowing secure storage, retrieval, and management of secrets with cross-agent delegation and tamper-evident audit.
    MIT