Skip to main content

Vexar-MCP — security, schema and token-cost checks for Model Context Protocol servers

Vexar-MCP

Static security, schema and token-cost analysis for Model Context Protocol servers — with a policy gate for CI.
Find the risky pattern, the untyped argument, and the context-window tax before you ship the tool.

CI License: MIT Latest release Stars Open issues

Python 3.10+ Runtime dependencies: none on 3.11+ MCP SARIF 2.1.0 mypy strict ruff pytest GitHub Actions

Quickstart · Quick start · Example output · Rules · Usage · Policies · Baselines · Live audit · Architecture · Contributing


What this is

Every tool an MCP server exposes becomes prompt context the model pays for on every turn — and every tool argument becomes an input an attacker can try to control. That produces two failure modes that are trivially easy to ship and very hard to notice in review: a handler that pipes a model-chosen string into shell=True or raw SQL, and a tool catalogue so thin the model can't route to it or so fat it taxes every single request.

Vexar-MCP finds both, statically, in one command — then keeps them from coming back by gating the result in CI under a named, reviewable policy. It reads your code with ast.parse and never executes it, so auditing a third-party server is safe.

Why it isn't just another linter

It reads, never runs. Python is parsed with ast.parse; manifests with the standard library. Your server is never imported, so auditing someone else's code is safe. Scanning is fully offline.
It refuses to guess. A scan that could not parse your files is never graded. You get Health Score: n/a, Status: PARTIAL, and exit 3 — not a comforting 100/100.
It gates, not just reports. One policy name (strict-prod, enterprise-governance) applies the same fail-on threshold, score floor and token budget on a laptop and in CI.
It is honest about limits. What it cannot catch is written down in docs/limitations.md rather than implied.

Not a replacement for a security review. It catches a specific, documented set of MCP failure modes early, and makes the standard you enforce explicit and reviewable.

Demo

Vexar-MCP scanning an insecure example server, then refusing to grade a file it could not parse

Generated from real output by scripts/make_demo_svg.py — if the tool changes, so does this animation. CI fails when it drifts.


✨ Key Features

  • 🔒 15 built-in rules across 4 categories — security (MCP001–MCP015), schema quality, contract/ergonomics, and token budgets. All findings carry a stable rule ID, exact file:line, severity, category, and a concrete remediation.
  • 🧠 Taint-aware sink detection — traces tool parameters into shell=True, os.system, eval, exec, outbound HTTP, filesystem paths, SQL .execute(...), and unsafe deserialization. Taint propagates one hop into module-local helpers, so a tool that delegates to a private _run_cmd() is no longer invisible.
  • 🚫 Ungraded, not falsely clean — unreadable inputs become a ParseIssue; the score is withheld (null), status is partial, exit code is 3. "Nothing found" and "nothing read" can never be confused.
  • 🛡️ Protected rules that cannot be silenced — under strict-prod, --ignore MCP005 is a hard configuration error, not a quiet downgrade. The most common way a security gate dies is being defeated one exemption at a time.
  • 🏛️ Policy inheritance (extends) — org → team → repo chains. Nearest layer wins for scalars; ignore, exclude and rule-packs accumulate. Protected categories accumulate monotonically: no layer in the chain can re-open them.
  • 🧾 Six report formats from one scan — terminal, JSON (vexar-mcp-report/v1), SARIF 2.1.0, Markdown, GitHub annotations (::error on PR diff lines), and a self-contained offline HTML dashboard.
  • 💸 Token-cost auditing — per-tool context leaderboard (token-report) and monthly bill projection (simulate) priced against a dated, explicit model table.
  • 🩺 Live server auditing — audit-server speaks stdio JSON-RPC, negotiates the protocol revision the server actually supports, follows tools/list pagination, and runs the full rule set over runtime-generated tools.
  • 🧱 Adopt-on-dirty-code baselines — fingerprint findings as rule + tool + file, commit the baseline, and fail only on new violations.
  • 🧩 Rule packs without forking — declarative TOML/JSON organization rules. The MCP namespace is reserved so a pack can never impersonate a built-in check.
  • 🐍 Zero runtime dependencies on Python 3.11+ — tomli is installed only on 3.10; PyYAML is an optional extra only for YAML manifests.
  • 🤖 Composite GitHub Action — inline PR annotations, SARIF upload, job summary, and step outputs (status, score, grade, risk-index, new-findings, analysis-complete, files-failed, exit-code).

🧰 Tech Stack

Layer Technology Why it is here
Language Python Stdlib ast is the parser; no build toolchain to audit your own code.
CLI argparse Subcommands with no runtime framework dependency.
Packaging setuptools PyPI src/ layout, py.typed marker, console script vexar-mcp.
Config TOML vexar-mcp.toml, .mcp-doctor.toml, or [tool.vexar-mcp] in pyproject.toml.
Manifests JSON YAML MCP tools/list, OpenAI & Anthropic tool schemas; TS/JS parsed statically.
Protocol MCP Live audit across five protocol revisions, handshake and stateless discovery.
Types mypy strict = true in pyproject.toml; all public API typed.
Lint / test ruff pytest CI enforces --cov-fail-under=85.
CI/CD GitHub Actions Release chain: tag → GitHub Release → PyPI → clean-venv install smoke test.
Outputs SARIF HTML Code Scanning, PR annotations, offline dashboards.

🏗️ Architecture & System Flow

Scan lifecycle

flowchart TD
    subgraph CLI["vexar-mcp CLI"]
        A["scan · token-report · simulate<br/>baseline · audit-server · init"]
    end

    subgraph Inputs["Untrusted input — never executed"]
        B["Python source<br/><code>ast.parse</code>"]
        C["TS / JS MCP SDK<br/>static parse, no Node runtime"]
        D["JSON / YAML manifest<br/>stdlib json + optional PyYAML"]
        E["Running MCP server<br/>stdio JSON-RPC"]
    end

    subgraph Pipeline["Analysis core"]
        F["Loaders<br/>normalise to <code>ToolSpec</code>"]
        G["15 rules<br/>security · schema · contract · tokens"]
        H["Risk index + health score"]
        I["Policy resolution<br/>extends chain → fail-on · score floor · budgets"]
        J["Baseline delta<br/>fingerprint = rule + tool + file"]
    end

    subgraph Output["Reports — one scan, many renderings"]
        K["Terminal"]
        L["JSON · vexar-mcp-report/v1"]
        M["SARIF 2.1.0"]
        N["Markdown · job summary"]
        O["GitHub annotations"]
        P["HTML dashboard"]
    end

    A --> B & C & D & E
    B & C & D & E --> F --> G --> H --> I --> J
    J --> K & L & M & N & O & P
    I --> Q{"Gate"}
    Q -->|"pass"| R["exit 0"]
    Q -->|"violation"| S["exit 1"]
    Q -->|"config error"| T["exit 2"]
    F -. "unreadable file" .-> U["ParseIssue<br/>score withheld · exit 3"]
    U --> J

    classDef untrusted fill:#2b1a1a,stroke:#f87171,color:#fecaca
    classDef core fill:#12212b,stroke:#2dd4bf,color:#ccfbf1
    classDef out fill:#1b2b1b,stroke:#4ade80,color:#dcfce7
    class B,C,D,E untrusted
    class F,G,H,I,J core
    class K,L,M,N,O,P out

The dotted path is the whole thesis: a file that cannot be read becomes a ParseIssue, not an empty result. The report and the exit code both record that analysis was incomplete.

Policy resolution (extends chains)

flowchart LR
    A["org/vexar-mcp.toml<br/><code>policy = strict-prod</code>"] --> B["team/vexar-mcp.toml<br/><code>extends = org</code><br/>adds ignore = MCP004"]
    B --> C["repo/vexar-mcp.toml<br/><code>extends = ../team</code><br/>adds rule-packs"]
    C --> D["Effective config<br/>nearest layer wins for scalars<br/>ignore · exclude · rule-packs accumulate"]
    D --> E{"Protected category<br/>re-opened anywhere?"}
    E -->|yes| F["exit 2 — configuration error<br/>message names the file or flag"]
    E -->|no| G["Scan runs under resolved policy<br/><code>--print-config</code> shows provenance of every key"]

    classDef bad fill:#2b1a1a,stroke:#f87171,color:#fecaca
    class F bad

Repository layout

src/vexar_mcp/
├── cli.py                 # argparse surface: scan, audit-server, baseline, policy, rules,
│                          # token-report, simulate, init
├── engine.py              # discovery → load → rule execution → report assembly
├── models.py              # ToolSpec, ParameterSpec, Diagnostic, ScanReport, ScanConfig
├── policies.py            # presets + the protected-rule invariant
├── config.py              # TOML / pyproject loading, extends resolution, provenance
├── baseline.py            # accepted-risk snapshots and delta gating
├── risk.py                # deterministic risk index
├── tokens.py              # the single offline token estimator
├── live.py                # stdio JSON-RPC client (argv-only, deadline, caps)
├── loaders/               # python_ast · typescript_loader · json_manifest · yaml_manifest
├── rules/                 # security · schema · contract · token rules + plugin packs
├── reporters/             # terminal · json · sarif · markdown · github_actions · html
└── simulate/              # cost_model.py + pricing.json (dated, review-checked rates)

Full internals: ARCHITECTURE.md.


📋 Prerequisites

Requirement Version Notes
Python >= 3.10 Declared in pyproject.toml. Tested on 3.10–3.13.
pip any recent pip install "git+https://github.com/RANJITHROSAN17/Vexar-MCP.git@main"
Git any Only for installing from source.
PyYAML (optional) >= 6.0.1 Needed only for .yaml manifests: pip install user/vexar-mcp[yaml]
Node.js (optional) — Not required. TypeScript/JS servers are parsed statically.
Docker (optional) — Not required by the tool; handy if you want to audit a containerised server.

Runtime dependencies: none on Python 3.11+. On 3.10, tomli>=2.0.1 is installed automatically.


Quick start

Requirements: Python 3.10+. No runtime dependencies on Python 3.11+. Time: about two minutes. New to the project? QUICKSTART.md walks the same path and shows the output to expect at each step.

# 1. Install (from source -- there is no PyPI release of this package yet)
pip install "git+https://github.com/RANJITHROSAN17/Vexar-MCP.git@main"

# 2. See it catch real problems in an intentionally broken example
vexar-mcp scan examples/

# 3. Point it at your own server and enforce a policy
vexar-mcp scan path/to/your_mcp_server --policy strict-prod

That is the whole on-ramp. If step 2 prints findings and step 3 exits 1, it is working.

Developing on Vexar-MCP itself
git clone https://github.com/RANJITHROSAN17/Vexar-MCP.git
cd Vexar-MCP
python3 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
python -m pip install -e ".[dev]"

Add .[yaml] alongside .[dev] if you want the YAML manifest loader:

python -m pip install -e ".[dev,yaml]"

Then run the same gate CI runs:

ruff check . && mypy src && pytest --cov=vexar_mcp --cov-fail-under=85
Scaffold a project in one command
vexar-mcp init . --policy strict-prod --github-action

Writes vexar-mcp.toml and .github/workflows/vexar-mcp.yml, ready to commit. Use --dry-run to preview, --force to overwrite.

PyPI status: not published yet -- the import name is already owned on PyPI, tracked as D1 in docs/roadmap-90-days.md. The source install above is the supported path.


⚙️ Configuration & Environment Variables

Configuration keys

Settings resolve in this precedence order (highest wins for scalars): CLI flags → vexar-mcp.toml (nearest) → extends chain → selected policy preset → built-in defaults.

Vexar-MCP reads vexar-mcp.toml, .mcp-doctor.toml, or a [tool.vexar-mcp] / [tool.mcp-doctor] table in pyproject.toml.

Key Type Default Required Description
extends string — no Preset name ("strict-prod") or path to another config file to inherit from. Chains nest; cycles and chains > 16 levels are exit 2.
policy string default no Enforcement preset: default, ci, strict-prod, enterprise-governance.
fail-on string preset-dependent no Minimum severity that fails the run: error, warning, info, never.
min-score int preset-dependent no Fail when the health score is below this value (0–100).
max-tool-tokens int 250 no Per-tool token budget before MCP002 fires.
max-server-tokens int preset-dependent no Whole-catalogue budget before MCP014 fires.
ignore list[string] [] no Rule IDs to silence project-wide. Ignoring a protected rule is a configuration error.
exclude list[string] built-in list no Directories never walked. Replaces the built-in list, so keep it complete.
rule-packs list[string] [] no Opt-in pack specs: .toml / .json paths, or a trusted Python module path.
baseline string — no Path to a committed baseline; gate then applies to new findings only.

Example vexar-mcp.toml:

# Org standard is inherited; this file narrows only what this repo must.
extends     = "../org/vexar-mcp.toml"
policy      = "strict-prod"
fail-on     = "warning"
min-score   = 90

ignore      = ["MCP004"]                  # info-level; protected rules would be rejected
exclude     = [".git", ".venv", "node_modules", "dist", "build", "__pycache__"]
rule-packs  = ["./policies/acme-rules.toml"]
baseline    = ".vexar-baseline.json"

Inspect exactly what will be enforced, and which file contributed each setting:

vexar-mcp scan . --print-config

Environment variables

Vexar-MCP is deliberately configuration-file driven — it reads no application secrets or API keys, and makes no network calls during a scan. The variables below are the ones that affect behaviour, all owned by the CI platform rather than the tool:

Variable Owner Default Required Purpose
GITHUB_STEP_SUMMARY GitHub Actions — no When set (and the action's step-summary: true), the Markdown report is appended to the job summary.
GITHUB_OUTPUT GitHub Actions — no Where the action writes score, grade, risk-index, new-findings, analysis-complete, files-failed, exit-code.
GITHUB_ACTION_PATH GitHub Actions — no Used by action.yml to locate .github/scripts/action_outputs.py.
INPUT_* (INPUT_PATH, INPUT_POLICY, INPUT_FAIL_ON, INPUT_BASELINE, …) GitHub Actions mapped from with: no Action input → CLI flag bridge. You set these via with: keys, not manually.
VIRTUAL_ENV / PATH shell — no Standard venv activation so vexar-mcp resolves on PATH.

No .env file is required or read. If you were expecting API_KEY-style variables: there are none by design. Scanning is offline, and MCP009 records only the shape of a credential match — never the literal value — so a report can never become a second copy of a leaked key.


📖 Usage Guide

CLI command surface

vexar-mcp scan [PATHS...]        # static analysis: files, dirs, manifests
vexar-mcp token-report [PATHS...] # per-tool context-window leaderboard
vexar-mcp simulate [PATHS...]     # monthly $ projection for a model + volume
vexar-mcp audit-server -- CMD...  # live stdio JSON-RPC audit
vexar-mcp baseline write|show     # accepted-risk snapshots
vexar-mcp policy                  # list enforcement presets
vexar-mcp rules                   # list the 15 rules
vexar-mcp init [DIRECTORY]        # scaffold config (+ CI workflow)

Scan

# Whole repository
vexar-mcp scan .

# A single server, a JSON manifest, and a YAML manifest in one run
vexar-mcp scan src/server.py tools.json tools.yaml

# Release gate
vexar-mcp scan . --policy strict-prod

# Explicit gates
vexar-mcp scan . --fail-on warning --min-score 90 --max-tokens 200

# Rule packs (repeatable)
vexar-mcp scan . --rule-pack ./policies/acme-rules.toml --rule-pack ./policies/rgpd.json

# Fail when the target contains nothing recognizable (exit 4)
vexar-mcp scan . --fail-on-no-matches

# Machine-readable outputs, one scan
vexar-mcp scan . --format json --output report.json
vexar-mcp scan . --format sarif --output vexar.sarif
vexar-mcp scan . --format markdown --output summary.md
vexar-mcp scan . --format github              # inline ::error / ::warning on PR lines
vexar-mcp scan . --format html --output report.html

# One scan, many artefacts — the format is rendered from a single analysis
vexar-mcp scan . --emit sarif=out.sarif --emit html=report.html --emit json=-

Auditing a running server

# Standard: launch the server yourself; argv-only, no shell
vexar-mcp audit-server -- python my_server.py

# A published server, with SARIF output
vexar-mcp audit-server --format sarif --output live.sarif -- npx -y @acme/mcp-server

It negotiates the revision the server actually speaks, follows tools/list pagination, validates conformance (MCP012), then runs the full rule set over the tools that are really exposed — through the same normalisation pipeline as file scans, so results are directly comparable with a static baseline.

Revision Negotiation Notes
2026-07-28 stateless Current. No handshake; version per request in _meta, agreed via server/discover.
2025-11-25 handshake Last initialization-based revision.
2025-06-18 handshake
2025-03-26 handshake
2024-11-05 handshake

Bounded by a hard deadline, an 8 MiB response cap and a 50-page cap; the child is killed on expiry. This is the only command that starts a process.

Token cost

vexar-mcp token-report examples/

vexar-mcp simulate . --model claude-sonnet-4.5 --calls-per-day 2000

# Your negotiated contract rate, for a model not in the reviewed table
vexar-mcp simulate . --model my-finetune --input-price 0.80 --calls-per-day 5000
$ vexar-mcp token-report examples/clean_server.py examples/tools_manifest.json
TOOL NAME                         EST. TOKENS   PARAMS  SOURCE
------------------------------------------------------------------------------
list_kubeflow_runs                        186        2  examples/tools_manifest.json:1
read_workspace_note                       142        2  examples/clean_server.py:24
check_package_metadata                     97        1  examples/clean_server.py:41
------------------------------------------------------------------------------
Total context-window tax across 3 tool(s): ~425 tokens/turn

Prices are a dated, explicit input — each rate in simulate/pricing.json carries the date a human last checked it (PRICING_AS_OF = "2026-10" is the oldest). An entry that goes unreviewed for more than 180 days fails the test suite and makes simulate print REVIEW OVERDUE; an unknown model raises rather than silently falling back. Counts are deterministic offline estimates, not a tokenizer round-trip — see docs/limitations.md.

Adopting a gate on dirty code

vexar-mcp scan . --write-baseline .vexar-baseline.json     # accept what exists today
vexar-mcp scan . --baseline .vexar-baseline.json           # fail only on what's new
vexar-mcp baseline show .vexar-baseline.json               # inspect accepted risk

Programmatic API (Python)

from pathlib import Path

from vexar_mcp import ScanConfig, scan_paths
from vexar_mcp.reporters.json_reporter import render_json

report = scan_paths([Path("src/")], ScanConfig())

print(report.status.value)  # clean | findings | partial | no_matches | error
print(report.summary.score)  # int | None -> None when ungraded. Never default it to 100.
print(report.summary.grade)  # "A+" ... "F" | "n/a"
print(report.coverage.files_analyzed, report.coverage.files_failed)

for diag in report.diagnostics:
    print(f"{diag.rule_id} {diag.source_file}:{diag.line} — {diag.message}")
    print(f"  fix: {diag.remediation}")

Path("report.json").write_text(render_json(report))

JSON report shape (vexar-mcp-report/v1)

Additive-only within a minor release: existing keys never disappear or change type.

{
  "schema": "vexar-mcp-report/v1",
  "version": "0.2.6",
  "policy": { "name": "strict-prod" },
  "status": "partial",
  "analysis_complete": false,
  "live": null,
  "coverage": {
    "candidate_files": 2,
    "files_analyzed": 1,
    "files_failed": 1,
    "files_with_tools": 1,
    "tools_analyzed": 2
  },
  "summary": {
    "files_scanned": 2,
    "tools_scanned": 2,
    "total_estimated_tokens": 105,
    "errors": 4,
    "warnings": 5,
    "infos": 3,
    "total_diagnostics": 12,
    "score": null,
    "grade": "n/a",
    "risk_index": 41,
    "risk_band": "elevated"
  },
  "parse_issues": [
    {
      "kind": "syntax_error",
      "source_file": "src/servers/broken.py",
      "line": 6,
      "message": "SyntaxError: invalid syntax"
    }
  ],
  "tools": [
    { "name": "run", "source_file": "examples/vulnerable_server.py", "line": 23,
      "estimated_tokens": 52, "parameter_count": 1 }
  ],
  "diagnostics": [
    {
      "rule_id": "MCP005",
      "rule_slug": "dangerous-shell-execution",
      "severity": "error",
      "category": "security",
      "tool_name": "run",
      "source_file": "examples/vulnerable_server.py",
      "line": 25,
      "message": "Tool 'run' invokes dangerous execution sink `subprocess.run(shell=True)` with parameter 'cmd'.",
      "remediation": "Use `subprocess.run([cmd, arg], shell=False, check=True)` with validated arguments.",
      "risk": 9,
      "fingerprint": "MCP005:run:examples/vulnerable_server.py"
    }
  ]
}

CI integration

name: MCP audit
on: [pull_request]

permissions:
  contents: read
  security-events: write      # only if you upload SARIF

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: RANJITHROSAN17/Vexar-MCP@main
        id: vexar
        with:
          path: .
          policy: strict-prod
          sarif-output: vexar-mcp.sarif
          step-summary: true
          allow-incomplete: false   # an unreadable file fails the job

      - run: echo "score=${{ steps.vexar.outputs.score }} grade=${{ steps.vexar.outputs.grade }}"

      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: vexar-mcp.sarif
Action input Default Purpose
path . File or directory to scan.
policy — Preset; overrides individual gates.
fail-on preset error | warning | info | never.
min-score preset Score floor (0–100).
max-tokens preset Per-tool token budget.
ignore — Comma-separated rule IDs. Protected rules are rejected.
baseline — Committed baseline; only new findings fail.
rule-packs — Comma-separated pack specs.
config — Explicit config path.
sarif-output — SARIF v2.1.0 path for Code Scanning.
html-output — Standalone HTML dashboard path.
step-summary true Append Markdown to the job summary.
allow-incomplete false Do not fail on unanalyzable files. Off by default.

What it checks

Fifteen rules across four categories — fourteen static, plus MCP012 for live protocol conformance. vexar-mcp rules --format json emits the machine-readable catalog.

Rule Severity Category Flags
MCP001 error contract Tool has no meaningful description (< 12 chars), degrading tool selection
MCP002 warning tokens A single tool definition exceeds the per-tool token budget (default 250)
MCP003 warning schema Parameter has no type annotation, or uses unconstrained Any / bare dict
MCP004 info schema Pagination / limit / batch parameter has no upper bound
MCP005 error security shell=True, os.system, eval, exec reached from a tool handler
MCP006 warning security Tool input flows into an outbound HTTP call without visible host/scheme validation
MCP007 warning security Tool input flows into a filesystem path without base-directory confinement
MCP008 error contract Duplicate, generic, or non-conforming tool name
MCP009 error security Hardcoded credential literal inside a tool handler (provider prefixes, bearer headers) — never echoes the value
MCP010 warning security Outbound HTTP call with no explicit timeout=
MCP011 error security SQL built by interpolating tool input into .execute(...) / .executemany(...)
MCP012 warning contract Live only: unsupported revision, missing tools capability, malformed tools/list entries, runaway pagination
MCP013 error security pickle.loads, yaml.load without SafeLoader, and similar unsafe deserialization
MCP014 warning tokens Whole catalogue exceeds the server-wide token budget, naming its largest contributors
MCP015 info schema A tool's parameters have no description, so the model must guess their meaning

Rules are static pattern and data-flow heuristics, not proof of exploitability. Read docs/limitations.md before treating a finding as a confirmed vulnerability. Per-rule detail and remediation: docs/rules.md.

See it work

Example output

$ vexar-mcp scan examples/vulnerable_server.py
==============================================================================
Vexar-MCP  |  Health Score: 4/100 (F)  |  Tools: 2 across 1 file(s)
Context Footprint: ~105 tokens  |  Errors: 4  Warnings: 5  Info: 3
==============================================================================

[ERROR] MCP001 (missing-tool-description) at examples/vulnerable_server.py:23 [run]
        Tool 'run' has an empty or overly brief description (0 chars; minimum 12 recommended).
        Fix: Document purpose, inputs, and side effects in the tool docstring or `description`.
[ERROR] MCP008 (ambiguous-or-duplicate-tool-name) at examples/vulnerable_server.py:23 [run]
        Tool name 'run' is overly generic or violates `[a-z][a-z0-9_-]{2,63}`.
        Fix: Use specific `verb_noun` `snake_case` naming such as `fetch_invoice_summary`.
[WARN ] MCP003 (untyped-or-any-parameter) at examples/vulnerable_server.py:23 [run]
        Parameter 'cmd' in tool 'run' has no type annotation.
        Fix: Provide an explicit scalar, `Literal[...]`, `TypedDict`, or Pydantic `BaseModel` type.
...
[ERROR] MCP005 (dangerous-shell-execution) at examples/vulnerable_server.py:25 [run]
        Tool 'run' invokes dangerous execution sink `subprocess.run(shell=True)` with parameter 'cmd'.
        Fix: Use `subprocess.run([cmd, arg], shell=False, check=True)` with validated arguments.

A reference implementation that passes every rule scores 100:

$ vexar-mcp scan examples/clean_server.py examples/tools_manifest.json
==============================================================================
Vexar-MCP  |  Health Score: 100/100 (A+)  |  Tools: 3 across 2 file(s)
Context Footprint: ~425 tokens  |  Errors: 0  Warnings: 0  Info: 0
==============================================================================
All checks passed! Zero security, schema, contract, or token-budget findings.

Scan status & exit codes

A passing gate must mean "we read your code and it is fine" — not "we found nothing to read."

Status Meaning Graded Exit
clean Everything analyzed, nothing fired ✅ 0
findings Everything analyzed, issues found ✅ 0 / 1
partial Some files could not be read — not a clean result ❌ 3
no_matches Nothing recognizable was found ❌ 0 (4 with --fail-on-no-matches)
error Nothing could be analyzed ❌ 3

Exit codes: 0 pass · 1 a gate failed · 2 bad invocation or configuration · 3 incomplete analysis · 4 nothing matched. Details: docs/scan-status.md.

Policy presets

Preset Fails on Score floor Token budget / tool Unignorable
default error — 250 —
ci warning 80 (B) 230 security
strict-prod warning 90 (A) 200 security
enterprise-governance info 97 (A+) 180 security, contract

A protected rule cannot be ignored — not from the CLI, not from a config file:

$ vexar-mcp scan . --policy strict-prod --ignore MCP005
vexar-mcp: error: policy 'strict-prod' protects security rules; these cannot be ignored: MCP005.

Report formats

Format Flag Intended use
Terminal (default) Local development, scorecard and diagnostics
JSON --format json Your own tooling, dashboards, artifact storage
SARIF --format sarif GitHub Code Scanning, any SARIF-consuming IDE
Markdown --format markdown $GITHUB_STEP_SUMMARY, PR comments
Annotations --format github Inline ::error / ::warning / ::notice on PR diff lines — no Advanced Security needed
HTML --format html --output report.html One self-contained file: scorecards, filters, search, tool costs, trend sparkline. No CDN, works offline

Rule packs

[[rule]]
id = "ACME001"
slug = "acme-tools-must-be-namespaced"
severity = "warning"
category = "contract"
summary = "All tools must live in the acme_ namespace."
remediation = "Rename the tool with an acme_ prefix."
message = "Tool '{tool}' is outside the ACME namespace."
  [rule.match]
  name_regex = "^acme_"
  invert = true
vexar-mcp scan . --rule-pack ./policies/acme-rules.toml

Declarative packs are data, never executed — the recommended format. The MCP rule-ID namespace is reserved, so a pack can never shadow a built-in check. See docs/rule-packs.md.


🧪 Testing & Verification

Run the full quality gate locally before pushing — this is exactly what CI enforces:

# Lint + format
ruff check .
ruff format --check .

# Strict type checking (33 source files)
mypy src

# Test suite with coverage gate (CI fails under 85%)
pytest --cov=vexar_mcp --cov-report=term-missing --cov-fail-under=85

# Release wiring: version / tag / changelog agreement
python scripts/verify_release.py

# Validate the 15 CI workflow files
python .github/scripts/validate_workflows.py

# Validate the roadmap bot manifest
python .github/scripts/sync_roadmap_issues.py --dry-run

Targeted suites worth knowing:

pytest tests/test_rules.py                    # rule correctness, positive + negative cases
pytest tests/test_analysis_completeness.py    # the "never let an incomplete scan look clean" contract
pytest tests/test_policies.py tests/test_policy_inheritance.py
pytest tests/test_baseline.py tests/test_live.py tests/test_loaders.py

Dogfood the gate against this repository:

vexar-mcp scan . --policy strict-prod          # the project scans itself with vexar-mcp.toml
vexar-mcp scan examples/clean_server.py        # must score 100/A+ under every preset

CI also assert-fails deliberately: examples/ contains an intentionally insecure server, and a ci gate must return non-zero on it. A gate that cannot fail proves nothing.


🗺️ Roadmap

Shipped:

  • Core contract — what a finding, a pass, and a failure mean
  • Stable, documented exit codes (0 pass · 1 gate · 2 config · 3 incomplete · 4 no matches)
  • Partial/incomplete analysis behaviour with withheld scores
  • Policy presets (default, ci, strict-prod, enterprise-governance)
  • Baselines — adopt current findings, fail on new ones only
  • SARIF 2.1.0, JSON, Markdown, GitHub annotation and offline HTML reporters
  • Composite GitHub Action with step outputs and job summary
  • Live server auditing across five protocol revisions (handshake + stateless)
  • TypeScript/JS MCP SDK static parsing (no Node runtime required)
  • Organization/team policy inheritance with monotonic protection
  • One-hop taint propagation into module-local helpers
  • Rule packs without forking the tool
  • Honest limitations.md

Planned:

  • JSON Schema for machine contracts — report and baseline artifacts published as versioned schemas
  • SARIF output validated against the official schema — currently tested for content, not conformance
  • Expanded benchmark corpus — 5–10 vulnerable + 5 clean servers with expected findings
  • PyPI availability — unblocked once the import-name collision (D1) is resolved
  • Release discipline — align pyproject.toml version with the latest published release
  • Autofix companion — vexar-autofix for mechanical remediations

Full plans: docs/roadmap-90-days.md · docs/roadmap-6-month.md · .github/roadmap.json


🤝 Contributing

Contributions are welcome — every rule, loader and reporter is intentionally modular so a meaningful PR fits in one sitting.

Development setup

git clone https://github.com/<your-username>/Vexar-MCP.git
cd Vexar-MCP
python3 -m venv .venv && source .venv/bin/activate
python -m pip install -e ".[dev]"

PR workflow

  1. Find work. Browse good first issue or help wanted. Comment on the issue to claim it.
  2. Branch. git checkout -b feat/mcp016-new-rule
  3. Implement, with tests. Conventional Commits titles are enforced by the governance bot: feat(rules): …, fix(ast): …, docs: ….
  4. Quantify impact. Every PR must include a Quantifiable Impact section — before/after/improvement percentages for speed, memory or coverage. Qualitative claims alone are rejected.
  5. Run the gate locally. ruff check . && mypy src && pytest --cov=vexar_mcp --cov-fail-under=85
  6. Open the PR referencing Closes #<issue>.

Adding a rule in ten minutes

@register_rule(
    rule_id="MCP016",
    slug="example-rule",
    severity=Severity.WARNING,
    category=Category.SECURITY,
    summary="One sentence on what is risky.",
    remediation="The concrete fix a developer should apply.",
)
def check_something(tools: Sequence[ToolSpec], config: ScanConfig) -> list[Diagnostic]: ...

Then add positive and negative tests in tests/test_rules.py, document it in README.md + docs/rules.md, and confirm examples/clean_server.py still scores 100/100 (A+).

The one rule that outranks the others

Never let an incomplete scan look clean. If you touch a loader, the engine, or a reporter, ask: if this input could not be read, would the result still look like a pass?

Loaders append a ParseIssue instead of swallowing errors; the engine withholds the score on partial/error; reporters never print "all checks passed" for an ungraded scan; exit 3 stays distinguishable from 1. tests/test_analysis_completeness.py is the regression suite for this contract.

Full guidelines: CONTRIBUTING.md · CODE_OF_CONDUCT.md · SECURITY.md


🔐 Security Model

  • No code execution during scans. Python targets are inspected with ast.parse only — no imports, no subprocesses, no plugin loading of scanned files.
  • Two opt-in exceptions, both documented. audit-server launches a server you name on the command line (argv only, hard deadline, response and pagination caps); Python rule packs are imported only when passed with --rule-pack.
  • No network calls. Scanning is fully offline; nothing about your source leaves the machine.
  • No secrets in output. MCP009 records the shape of a credential match (pattern name, variable name, line) and never the literal value.
  • Your files are read, not modified. Output goes to stdout or a file you name.
  • Reports cannot become a second leak. HTML findings are embedded as escaped JSON, so server-controlled strings cannot break out of the document.

Reporting a vulnerability in Vexar-MCP itself: use GitHub Private Vulnerability Reporting via SECURITY.md, not a public issue.


📄 License

Released under the MIT License. See LICENSE.

MIT License · Copyright (c) 2026 Ranjith Rosan

Built for people shipping MCP servers into production — and for anyone who has ever been told "the scan passed" by a scanner that never read the file.

Back to top ↑

Python import migration (0.3)

Use from vexar_mcp import ScanConfig, scan_paths and python -m vexar_mcp. The vexar-mcp console command and distribution name are unchanged. The old mcp_doctor import and module command are compatibility shims for 0.3.x only, planned for removal in 0.4. Submodule aliases share the canonical implementation and class objects, rather than loading a second scanner. Do not install the unrelated mcp-doctor distribution alongside this temporary compatibility shim: that distribution owns the same legacy namespace. vexar-autofix uses its own vexar_autofix namespace and the canonical scanner imports.

Metadata

Release files for Vexar-MCP 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for Vexar-MCP 0.3.0
File Size Uploaded Provenance Actions
vexar_mcp-0.3.0.tar.gz 238.6 kB With provenance Details for vexar_mcp-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for Vexar-MCP 0.3.0
File Interpreter ABI Platform Actions
vexar_mcp-0.3.0-py3-none-any.whl Python 3 none any Details for vexar_mcp-0.3.0-py3-none-any.whl

Total release size: 398.4 kB

Release files / vexar_mcp-0.3.0.tar.gz

Download URL vexar_mcp-0.3.0.tar.gz
Size 238.6 kB
Tags Source
SHA-256 checksum
How to use checksums
131365b883d9612435bf4fc1dc5e6918e8e55702ffda3b8d5b0839a26744ad81
BLAKE2b-256 checksum
How to use checksums
2dbed302bdbdaac79f580148c676f221649e07a105b83ce183c9f2364ec357dd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

Release files / vexar_mcp-0.3.0-py3-none-any.whl

Download URL vexar_mcp-0.3.0-py3-none-any.whl
Size 159.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ad77d7a6ac1ef4d4562ba075808d736728513fbe81d2ba3b5b50b72d8b4ffbf7
BLAKE2b-256 checksum
How to use checksums
6d3a887ace4c77d386d8dbcd4d90b37b6b27e3ae2c8d1c4f8055ca088b241496
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

Security for Vexar-MCP 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for vexar_mcp-0.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page