Skip to content
Ranjith Rosan edited this page Oct 27, 2026 · 3 revisions

Vexar-MCP — security and quality checks for MCP servers

Static analysis for Model Context Protocol servers. Reads Python, TypeScript/JavaScript and JSON tool definitions and reports security sinks, schema drift, and context-token cost — without executing the code it inspects.

Vexar-MCP — security and quality checks for MCP servers

CI Release Python License

Repository: github.com/RANJITHROSAN17/Vexar-MCP · Dashboard: ranjithrosan17.github.io/Vexar-MCP · Docs: docs/

Formerly published as mcp-doctor. The executable, package metadata, config file and reports are all Vexar-MCP now. The Python import path mcp_doctor is unchanged, and legacy .mcp-doctor.toml configs still load.


What it checks

Area Rules What you get
Security sinks MCP005, MCP006, MCP007, MCP009, MCP010, MCP013 Shell execution, SSRF, path traversal, hardcoded credentials, missing timeouts, unsafe deserialization — with the file, line, and the parameter that flows in
Tool contract & schema MCP001, MCP003, MCP004, MCP008 Missing descriptions, untyped/Any parameters, unbounded pagination, generic or duplicate tool names
Token budget MCP002 Per-tool context-window cost, plus a token-report leaderboard

Every finding is a static signal, not a confirmed vulnerability. What it cannot detect is documented in Limitations.


Quick Start

# Install (no PyPI release yet — install from the repository)
pip install "git+https://github.com/RANJITHROSAN17/Vexar-MCP.git@main"

# Scan a server, a directory, or a tool manifest
vexar-mcp scan path/to/your_mcp_server

# Machine-readable report for CI
vexar-mcp scan . --format sarif --fail-on warning

Try the bundled examples:

git clone https://github.com/RANJITHROSAN17/Vexar-MCP.git && cd Vexar-MCP
python -m venv .venv && . .venv/bin/activate && pip install -e .
vexar-mcp scan examples/vulnerable_server.py                            # 10 findings, exit 1
vexar-mcp scan examples/clean_server.py examples/tools_manifest.json     # 100/100, exit 0

python -m mcp_doctor scan <path> works as an alternative to the console script. A subcommand is required — bare python -m mcp_doctor <path> is a usage error.

Requirements: Python 3.10+. No runtime dependencies on 3.11+ (tomli is installed automatically on 3.10).


Actual output

Real run against examples/vulnerable_server.py in this repository (findings elided, marked ...):

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

[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] 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.
...

The reference implementation scores clean:

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

Scoring: 100 − errors×15 − warnings×6 − infos×2, floored at 0. Grades: A+ ≥ 97, A ≥ 90, B ≥ 80, C ≥ 70, D ≥ 55, else F.


Rule reference — all 11 built-in rules

Run vexar-mcp rules (or --format json) for this list straight from the registry.

ID Slug Sev Category Checks
MCP001 missing-tool-description error contract Tool description missing or < 12 chars — degrades tool selection
MCP002 excessive-tool-tokens warning tokens One tool definition exceeds the per-tool budget (default 250)
MCP003 untyped-or-any-parameter warning schema Parameter unannotated, or Any / bare dict
MCP004 unbounded-collection-or-limit info schema limit / pagination parameter with no upper bound
MCP005 dangerous-shell-execution error security shell=True, os.system, os.popen, eval, exec from a tool handler
MCP006 unvalidated-ssrf-sink warning security Tool input → outbound HTTP without visible host/scheme validation
MCP007 unconfined-filesystem-access warning security Tool input → filesystem path without base-directory confinement
MCP008 ambiguous-or-duplicate-tool-name error contract Duplicate, generic, or non-conforming tool name
MCP009 hardcoded-secret-in-tool error security Credential literal in a tool body (provider prefixes, bearer headers)
MCP010 missing-network-timeout warning security HTTP call with no timeout= (or timeout=None)
MCP013 unsafe-deserialization error security pickle.loads, yaml.load without SafeLoader

MCP011 / MCP012 are not implemented. The IDs are reserved for queued roadmap items and skipped so ignore lists and SARIF results stay stable if they land. Older documentation that listed MCP008 unvalidated-file-upload, MCP009 protocol, MCP011 missing-parameter-bounds or MCP012 was describing rules that do not exist in this codebase.

Per-rule detail with bad/good examples: docs/rules.md.

Privacy note: MCP009 records only the shape of a credential match — pattern name, variable name, line. The literal value is never stored in the diagnostic, JSON report, or SARIF output, so a report cannot become a second copy of a leaked key.


Source coverage

Source Loader What is extracted
.py loaders/python_ast.py (ast.parse) @mcp.tool() / @server.tool(), Tool(...), FastMCP builders; parameter types, defaults, bounds; sink calls in the handler
.ts .js .jsx loaders/typescript_loader.py (static scan, no Node runtime) server.tool(...) registrations, Zod schemas, import-tracked sinks
.json loaders/json_manifest.py MCP tools/list, OpenAI and Anthropic tool manifests

Directories are walked recursively; .git, .venv, venv, node_modules, __pycache__, dist, build are skipped by default, and *.d.ts files are ignored.


TypeScript / JavaScript loader

A static text-and-structure scan of TS/JS sources — no Node runtime, no TypeScript compiler, no code execution.

File Role
loaders/typescript_loader.py Tokenizes server.tool(...) calls, brace/paren-balanced argument split, Zod field parsing, sink + timeout audit
engine.py Registers .ts / .js / .jsx in discovery and skips *.d.ts
tests/fixtures/clean_ts_server.ts Clean: z.string(), .optional(), .default(), .max(), nested z.object()
tests/fixtures/vulnerable_ts_server.ts Vulnerable: import-tracked exec(, eval(, fetch(url) with no bound
tests/test_typescript_loader.py Loader + fixture assertions

Design notes

  • Balanced argument split — commas inside { ... }, [ ... ] and nested calls do not split arguments wrongly.
  • Import tracking — import { exec } from "child_process" is tracked, so a bare exec( is only flagged when it actually resolves to a dangerous module.
  • Line-accurate — parameters and sinks report real source lines.
  • Zod-aware — .describe(), .optional(), .default(), .min() / .max() are parsed; nested objects stay visible to the audit.
  • Timeout audit (MCP010) — fetch has no native timeout option, so the loader only counts a bound when an abort signal is used; requests/httpx-style timeout= keywords are honoured where present.
  • Zero execution — no eval, exec or subprocess in the loader itself, so hostile fixtures are safe to scan.

Scanning the vulnerable fixture shows it working end to end (3 tools, 3 errors, 2 warnings, score 43/F).


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 SARIF upload or Advanced Security needed

Plus vexar-mcp token-report . for a per-tool context-window leaderboard:

$ 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

Token counts are a deterministic offline estimate (character/structure based), not a tokenizer round-trip — compare tools against each other and track change over time rather than treating the number as an exact billed figure.


GitHub Actions

Composite action — one line, exposes score, grade, total-diagnostics as step outputs, writes SARIF and a job summary:

- uses: RANJITHROSAN17/Vexar-MCP@main
  with:
    path: .
    fail-on: error              # error | warning | info | never
    sarif-output: vexar-mcp.sarif

CLI in your own steps — full control, and the only option that produces inline PR annotations:

- run: pip install "git+https://github.com/RANJITHROSAN17/Vexar-MCP.git@main"
- run: vexar-mcp scan . --format github --fail-on error

Neither needs a token, a network call at scan time, or an upload. Full recipes including SARIF upload: docs/github-actions.md.


Safety model

  • No code execution during a scan. Python targets are parsed with ast.parse only — no imports, no subprocesses, no plugin loading of scanned files.
  • No network calls. Scanning is fully offline.
  • No secrets in output. MCP009 stores the match shape, never the literal (see above).
  • Read-only. The tool writes to stdout or a file you name; scanned files are never modified.
  • Suppression is explicit. # noqa: MCP001 (or # noqa: MCP001,MCP008, # noqa: ALL) on the reported line, or ignore = [...] in config. A bare # noqa suppresses nothing.

A clean scan means "none of the 11 rules matched the code that could be parsed" — not "this server is secure". Please read Limitations before treating either a finding or a clean result as a verdict.


Dashboard

A single-file static viewer with no CDN, no build step, and no third-party requests:

  • Live: ranjithrosan17.github.io/Vexar-MCP — republished on every push to main by dashboard-deploy.yml, which builds index.html + a fresh audit-report.json from examples/. That report scores 8/100 (F) because the fixtures are intentionally vulnerable; it demonstrates what findings look like, not the project's own health.
  • Local: vexar-mcp scan examples/ --format json > assets/audit-report.json, then serve the folder over HTTP (python -m http.server 8000) — browsers block fetch() on file://.

The viewer validates the report before rendering and has distinct states for loading, loaded, empty, missing, invalid, and request-failure. Any fallback is labelled Sample preview with the reason; it never presents invented data as a live scan. All report values are rendered with textContent, so a crafted finding message cannot inject markup. Details: docs/dashboard.md.


Documented limitations (short version)

  • No cross-module data flow — each file is analysed independently.
  • Dynamic registration (getattr, runtime decorators, factory-built tool registries) is invisible.
  • No taint propagation through helper functions, which inflates MCP006/MCP007 false positives.
  • Token counts are approximations, not tokenizer output.
  • Configuration is discovered from the first scan target's directory, not per package.

Full list with the false-positive table: docs/limitations.md.


Contributing

Standards this repository enforces (see CONTRIBUTING.md):

  1. Rules — a function decorated with @register_rule(...); never a hand-maintained list.
  2. Loaders — load_*(path) -> list[ToolSpec], registered in engine.scan_paths.
  3. Reporters — pure ScanReport -> str, exported from reporters/__init__.py, wired into cli.py.
  4. Tests — fixture-based where possible (tests/fixtures/), plus positive and negative cases in tests/test_rules.py.
  5. Commits — Conventional Commits: feat(rules): ..., fix(ast): ..., docs: ....

Run the same gate CI runs before pushing:

pip install -e ".[dev]"
ruff check . && ruff format --check . && mypy src \
  && pytest --cov=mcp_doctor --cov-fail-under=85

Good entry points: good first issue · help wanted · .github/roadmap.json


Repository map

Path Contents
src/mcp_doctor/loaders/ Python AST, TypeScript/JS, JSON manifest loaders
src/mcp_doctor/rules/ Rule registry and the 11 diagnostics
src/mcp_doctor/reporters/ Terminal, JSON, SARIF, Markdown, Actions annotations
src/mcp_doctor/engine.py Discovery, suppression, scoring, report assembly
src/mcp_doctor/config.py vexar-mcp.toml / pyproject.toml discovery
docs/ usage, rules, github-actions, limitations, dashboard
ARCHITECTURE.md Pipeline, module responsibilities, design principles
action.yml Composite GitHub Action
.pre-commit-hooks.yaml Pre-commit hook entry

Project status

0.1.x — usable, pre-1.0. Rule IDs and the JSON schema are stable; CLI flags may still change with a changelog note.

Verified signal Value Source
Diagnostic rules 11 (MCP001–MCP010, MCP013) vexar-mcp rules
Report formats 5 src/mcp_doctor/reporters/
Tests 151 passing pytest
Statement coverage 92.87% pytest --cov=mcp_doctor
Coverage gate 85% required --cov-fail-under=85 in ci.yml (threshold, not a result)
Runtime dependencies 0 on Python 3.11+ pyproject.toml

Installing: there is no PyPI release. pip install vexar-mcp does not work, and the mcp-doctor package on PyPI is an unrelated project by a different author. Install from the repository as shown above.