CI integration
MCP configs are code: they change in PRs, and the moment to catch a typosquat, an inline secret, or an unapproved server is review time — before the config ever launches an agent session.
Why scan in CI
Section titled “Why scan in CI”- A committed
.mcp.json/.vscode/mcp.json/opencode.jsonchange is the only signal you get before every dev’s next session loads the new server. - The scan is offline and fast (no network, no account needed) — it costs seconds per build.
- Exit codes are the contract: 0 clean, 1 gating findings, 2 usage error.
GitHub Actions
Section titled “GitHub Actions”name: MCP security scan
on: pull_request: paths: - '**/.mcp.json' - '**/.vscode/mcp.json' - '**/.cursor/mcp.json' - '**/opencode.json' - '**/opencode.jsonc' - '.ringzero/mcp-policy.json'
jobs: mcp-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - name: Validate the policy file run: npx @ringzero/mcp-scan@latest validate - name: Scan MCP server configs run: npx @ringzero/mcp-scan@latest scan --fail-on high --json > mcp-scan.json - uses: actions/upload-artifact@v4 if: always() with: name: mcp-scan path: mcp-scan.jsonScanning on every push instead of config-path filters also works — it’s cheap enough.
Exit-code gating recipes
Section titled “Exit-code gating recipes”| Goal | Command |
|---|---|
| Block criticals and highs (default) | ringzero-mcp scan --fail-on high |
| Strictest — any finding fails | ringzero-mcp scan --fail-on low |
| Allowlist violations only | ringzero-mcp scan --fail-on never with an enforce-mode policy |
| Report-only (never fails) | ringzero-mcp scan --fail-on never with a warn-mode policy |
Two gates compose: --fail-on covers severities, and an enforce-mode
policy makes Unapproved server findings (and requirePinned violations)
gate regardless of --fail-on.
A third gate covers the MCP Registry:
ringzero-mcp registry sync --check exits 1 when committed harness configs
drift from the org catalog — see the
registry deployment guide
for the workflow recipe.
Enforcing the allowlist on PRs
Section titled “Enforcing the allowlist on PRs”With .ringzero/mcp-policy.json committed and "mode": "enforce", any PR
that adds a server outside the allowlist fails CI with:
[unverified] crash-reports — https://mcp.sentry-tools.io/v1 (opencode.jsonc) medium Unapproved server — Server is not on the org allowlist [gating]The dev’s path forward is the policy PR itself: add the server to allow,
get it reviewed, merge — the propose → review → allow loop with your normal
code-review tooling as the review step.
Uploading results
Section titled “Uploading results”Store an API key with the mcp:upload scope (Settings → API keys) as a
repo secret and add --upload:
- name: Scan and upload env: RINGZERO_API_KEY: ${{ secrets.RINGZERO_API_KEY }} run: npx @ringzero/mcp-scan@latest scan --fail-on high --uploadWithout the secret, --upload logs the skip reason and the scan still gates
normally — forks and key-less environments never break.
Uploaded results land in your org (the /v1/mcp/scans backend is live);
the CLI follows the same report-only-without-a-key canon as the Gradle
plugin, so a missing or under-scoped key logs the skip and never breaks the
build.
RingZero stores uploaded configs redacted at rest: env values from discovered servers are replaced with a placeholder (key names survive), URL credentials are stripped, and known token shapes in command arguments are scrubbed before anything is written — a scan that flags a live inline credential uploads fine, and the credential itself never reaches storage.
Open MCP servers in the app to inspect the latest uploaded scan’s inventory and findings. It reflects that one scan, not a merge of independent uploads. AI-generated fix diffs and automatic pull requests are coming soon.