Repository navigation
Home
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.
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 allVexar-MCPnow. The Python import pathmcp_doctoris unchanged, and legacy.mcp-doctor.tomlconfigs still load.
| 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.
# 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 warningTry 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 0python -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).
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.
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-boundsorMCP012was 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 | 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.
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 bareexec(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) —
fetchhas no nativetimeoutoption, so the loader only counts a bound when an abort signal is used;requests/httpx-styletimeout=keywords are honoured where present. -
Zero execution — no
eval,execorsubprocessin 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).
| 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/turnToken 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.
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.sarifCLI 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 errorNeither needs a token, a network call at scan time, or an upload. Full recipes including SARIF upload: docs/github-actions.md.
-
No code execution during a scan. Python targets are parsed with
ast.parseonly — 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, orignore = [...]in config. A bare# noqasuppresses 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.
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
mainbydashboard-deploy.yml, which buildsindex.html+ a freshaudit-report.jsonfromexamples/. 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 blockfetch()onfile://.
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.
- 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.
Standards this repository enforces (see CONTRIBUTING.md):
-
Rules — a function decorated with
@register_rule(...); never a hand-maintained list. -
Loaders —
load_*(path) -> list[ToolSpec], registered inengine.scan_paths. -
Reporters — pure
ScanReport -> str, exported fromreporters/__init__.py, wired intocli.py. -
Tests — fixture-based where possible (
tests/fixtures/), plus positive and negative cases intests/test_rules.py. -
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=85Good entry points: good first issue · help wanted · .github/roadmap.json
| 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 |
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.