Skip to content

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.

  • A committed .mcp.json / .vscode/mcp.json / opencode.json change 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/workflows/mcp-scan.yml
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.json

Scanning on every push instead of config-path filters also works — it’s cheap enough.

GoalCommand
Block criticals and highs (default)ringzero-mcp scan --fail-on high
Strictest — any finding failsringzero-mcp scan --fail-on low
Allowlist violations onlyringzero-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.

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.

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 --upload

Without 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.