mcp-vault 0.16.0 · MCP security for your repository

Offline security check for MCP configs in CI.

A GitHub Action, pre-commit hook and CLI that flag unpinned MCP servers, plaintext secrets, typosquats, tool poisoning and toxic tool flows — offline, zero dependencies, no telemetry.

Add it to CI

One workflow file. It checks the Model Context Protocol configs your repository commits — .mcp.json (Claude Code), .cursor/mcp.json (Cursor), .vscode/mcp.json (VS Code) — on every pull request, before anyone’s editor launches them.

# .github/workflows/mcp-vault.yml
name: mcp-vault
on: pull_request
permissions:
  contents: read
jobs:
  mcp-vault:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1   # v7.0.1
        with:
          persist-credentials: false
      - uses: froggychips/mcp-skills-vault@449bac6f1f66f240f906d6ad25cc1da909169677 # v0.16.0

Pin the action by its full commit SHA, not by a tag. The action runs its own checkout — the code at the commit your uses: line names. Nothing is installed from npm and there is no install script to run, so pinning the action by SHA pins the gate by SHA. It makes no network call and reads no token unless you set offline: false. Add sarif: true to put each finding on the config line in GitHub code scanning.

Install: Action, pre-commit, CLI → Action docs →

What the MCP config scanner catches

Real output of mcp-vault check --as-of 2026-10-01 on the test fixtures in the repository, trimmed where it says …. ! is a warning (fails with --strict), ✗ fails the run.

Unpinned launches

npx -y pkg@latest runs whatever was published last, at every start — not the artifact anyone reviewed. A range or a missing version is the same problem. Where the vault knows the server, the fix names the version it verified.

$ mcp-vault check tests/fixtures/action/bad
tests/fixtures/action/bad/.mcp.json
  …
  :3 playwright-mcp
    ! this config launches @playwright/mcp@latest, a tag rather than a version: whatever is latest at each start runs [config/unpinned-launch: warn; so pin/missing: unknown]
      fix: pin it to the version the vault verified (playwright-mcp): "args": ["-y","@playwright/mcp@0.0.75"]

Secrets in plain text

Secret scanning over env, args, headers and url: GitHub, AWS, Slack, OpenAI, Anthropic and Stripe tokens, PEM keys, database URLs with a password. The value is never printed — not in text, JSON or SARIF. A ${VAR} reference is not a finding.

$ mcp-vault check tests/fixtures/launch-shapes/npx-yes-flags.json
  …
  :8 db
    ✗ postgresql connection string with a password in plain text at mcpServers.db.args[2] (13 chars, …); the file is tracked by git, so the value is in history [secrets/connection-string: deny, fails]
      fix: move the value to the environment and reference it by name, in whatever syntax this host documents; it is in git history, so rotate it

FAIL — 1 failing, 7 to look at · 5 servers in 1 config · fail on deny · offline

Typosquatting and swapped package sources

A typosquat passes every registry check — it is a real package, just not the one you meant. Names are compared offline against every known server: doubled letters, homoglyphs, dropped or foreign npm scopes, added affixes. A launch from another registry or index is reported instead of being checked as if it came from the public one.

$ mcp-vault check
.mcp.json
  :3 memmory
    …
    ! npm package @modelcontextprotocol/server-memmory is not in the vault but looks like mcp-server-memory (npm @modelcontextprotocol/server-memory) — a doubled letter: @modelcontextprotocol/server-memmory vs @modelcontextprotocol/server-memory. Likely an impersonation of mcp-server-memory. [lookalike/doubled-letter: warn]
  :4 tool
    ! this config launches some-pkg from a source other than the public registry: installs from another registry (--registry). [config/launch-source-override: unknown; so verify/unverified: unknown]

Tool poisoning and toxic tool flows

A tool description is an instruction the model follows on every request. tool-scan reads it for hidden Unicode, injected instructions and credential paths, and check applies the stored scans. It also looks at what the servers of one config do together: untrusted content, private data and an outward channel in one session is how data leaves.

$ mcp-vault check tests/fixtures/launch-shapes/npx-yes-flags.json
  (the config as a whole)
    ! untrusted content + private data + outward sink across browser, context, fs [flows/lethal-trifecta: warn]
      fix: keep browser, context and fs in separate profiles (different project configs or hosts), or drop the outward tools of browser

$ mcp-vault tool-scan tools.json --server calc     # a sample tools/list payload
calc  1 tools, 9 strings — 3 high, 1 medium, 1 low
  HIGH   conceal-from-user        add: description — tells the model to keep something from the user
  HIGH   injection-markup         add: description — prompt-control markup (<IMPORTANT>, <SYSTEM>, [INST], <|im_start|>)
  HIGH   credential-path          add: description — names a credential or MCP-config path (~/.ssh, id_rsa, mcp.json, .aws/credentials)
  …

The same pass also compares known packages with their pinned hashes, applies stored advisories, and enforces your organisation’s policy file — allow-lists, denied capabilities, required evidence. One decision, one exit code: 0 clean, 1 something fails, 2 a config or the policy could not be read.

How it behaves

For known servers

A curated database of known MCP servers is a bonus on top of the check: when a config launches one of them, the vault compares the hash, version and advisories against evidence recorded with a date. Browse it in the registry. The same build renders a README badge and an evidence page per entry, and exports the database as a static sub-registry of the official MCP Registry (API v0.1), so a host that speaks that API can point at this site.

Open the registry → CLI reference →

Sister project

mcp-trace experimental

A stdio proxy that logs MCP JSON-RPC metadata — method, tool, latency, errors — to a local SQLite file. Version 0.1, installed from git.

Status and install →