Quickstart: scan MCP servers locally and in CI
Hecate is a command-line scanner (hecate-mcp) and a runtime guard SDK (@hecate-mcp/sdk). Both need Node.js 20 or later, run entirely on your machine, and are free and open source.
1. Scan a config
npx hecate-mcp@beta scan .mcp.json
Without --connect, Hecate checks the config itself: plaintext secrets, known-bad packages, unpinned versions, plain-HTTP servers. To check the tools too, let it start each server and list its tools:
npx hecate-mcp@beta scan .mcp.json --connect
--connect runs the commands in the config, the same way your MCP client would. Only use it on configs you would run anyway.
Where MCP clients keep their configs
| Client | Config file |
|---|---|
| Claude Code | .mcp.json in the project |
| Claude Desktop (macOS) | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Claude Desktop (Windows) | %APPDATA%\Claude\claude_desktop_config.json |
| Cursor | .cursor/mcp.json in the project, or ~/.cursor/mcp.json |
| VS Code | .vscode/mcp.json, or the mcp section of settings.json |
| Windsurf | ~/.codeium/windsurf/mcp_config.json |
Pass every config one agent uses in the same run, so cross-server risks (the lethal trifecta, shadowing) are visible:
npx hecate-mcp@beta scan .mcp.json ~/.cursor/mcp.json --connect
Exit codes: 0 clean (or only medium and low findings), 1 critical or high findings, 2 an input error. Output: --format pretty (default), json or sarif.
2. Pin the tools you approved
npx hecate-mcp@beta pin .mcp.json --connect # writes hecate.lock.json
npx hecate-mcp@beta check .mcp.json --connect # exit 1 if anything changed
Commit hecate.lock.json. pin refuses definitions with critical or high findings unless you pass --force after reviewing them. More in MCP rug pull attacks.
3. Run it in GitHub Actions
name: mcp-security
on: [pull_request, push]
permissions:
contents: read
security-events: write # upload SARIF to code scanning
jobs:
hecate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- name: Tool definitions match the lockfile
run: npx hecate-mcp@beta check .mcp.json --connect
- name: Scan
run: npx hecate-mcp@beta scan .mcp.json --connect --format sarif > hecate.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always() # upload even when the scan fails the job
with:
sarif_file: hecate.sarif
Findings appear in the repository’s code scanning alerts and on pull requests, pointing at the tool’s line in a tools file or the offending key in a config. Your CI runner needs whatever the servers need to start (for example, the package managers they use).
4. Record reviewed exceptions
Accepted findings go in hecate.policy.json with a reason and an optional expiry. They stay visible but no longer fail the run.
{
"version": 1,
"capabilities": [
{ "server": "fetch", "tool": "fetch", "external-comms": false,
"reason": "Egress proxy only allows api.weather.gov" }
],
"ignore": [
{ "rule": "MCP005", "reason": "Sandboxed agent, no credentials; reviewed by @alice",
"expires": "2026-12-31" }
]
}
See the rules reference for what each rule reports.
5. Guard the agent at runtime
For agents built on the official TypeScript MCP SDK, @hecate-mcp/sdk wraps each client in your agent’s own process:
npm install @hecate-mcp/sdk@beta @modelcontextprotocol/sdk
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { createGuard } from "@hecate-mcp/sdk";
const hecate = createGuard({
policy: "hecate.policy.json", // its "guard" section
lockfile: "hecate.lock.json", // from `hecate pin`
approve: async ({ server, tool, reasons }) => askUser(`${server}/${tool}?`, reasons),
audit: "hecate-audit.jsonl",
});
// Wrap every server the agent uses with the same guard, before connect().
const github = hecate.wrap(new Client({ name: "agent", version: "1.0.0" }), { server: "github" });
const web = hecate.wrap(new Client({ name: "agent", version: "1.0.0" }), { server: "fetch" });
await github.connect(githubTransport);
await web.connect(webTransport);
The guard hides tools whose definitions changed or look poisoned, blocks calls to tools it has not vetted, asks your approve callback before command execution and before outbound tools once the session has read untrusted content, limits URLs in arguments to guard.egress.allow, and writes every decision to a JSONL audit log. Start with mode: "monitor" to see what it would block without blocking anything.