codequality-mcp
# codequality-mcp
An MCP server that evaluates Python code against a deterministic quality bar and only says
**review_ready** when every tool ran and found nothing serious.
`review_ready` means the deterministic layer found no blocker; `professional` is a word only
the review pass may use.
Five dimensions: functionality, security, efficiency, maintainability, design (SOLID and
module structure). Deterministic analyzers (ruff, radon, bandit, vulture, mypy, plus in-process
design and hygiene analyzers) produce findings and per-dimension scores. Markdown rubrics are
served to the calling model for the judgement the tools cannot make.
Scoring is conservative by construction: one critical finding caps its dimension at 30, one
high at 60, one medium at 75. That medium cap sits below the professional floor of 85, so a
single medium finding from any analyzer costs the verdict, with no carve-outs. The overall
score is the *minimum* dimension; any analyzer that fails leaves its dimension unscored and
the verdict `incomplete`; targets cannot reconfigure how they are scored.
Scores also disclose how much was read: every analyzer reports the files it examined and the
files it skipped, and a dimension whose weakest analyzer read less than 90% of its eligible
files is left unscored rather than scored on the fraction that was read.
## Install
```bash
uv sync
uv run codequality analyzers # every analyzer should report a version
```
## Use from the command line
```bash
uv run codequality evaluate path/to/project # markdown report, exit 0 only if review_ready
uv run codequality evaluate . --range main..HEAD --json
uv run codequality rules design
```
Exit codes: 0 review_ready, 1 needs_work, 2 incomplete, 3 error.
## Use from Claude Code
Add to `.mcp.json` (project) or `~/.claude.json` (user):
```json
{
"mcpServers": {
"codequality": {
"command": "uv",
"args": ["run", "--directory", "/home/fearsidhe/projects/codequality_mcp", "codequality-mcp"]
}
}
}
```
This repository's own `.mcp.json` uses `"--directory", "."` instead, because a project
`.mcp.json` is resolved relative to the project root; a user-level config, or any config
living outside this repository, needs the absolute path shown above.
Tools: `evaluate(path, git_range?, dimensions?)`, `explain_rule(rule_id)`, `list_analyzers()`.
Resources: `rubric://index`, `rubric://{dimension}`, `rules://{source}`.
Prompt: `review(path, git_range?)` runs an evaluation and returns the report with every checklist.
## Per-target configuration
Everything honoured lives in the target's `pyproject.toml`, and none of it can raise a score:
```toml
[project]
requires-python = ">=3.10" # its lower bound is the Python ruff and mypy are told to assume
[tool.codequality]
layers = ["domain", "application", "infrastructure"] # bottom first; imports may only point downward
exclude = ["migrations/*"] # disclosed in the report; >10% excluded = incomplete
composition_root = "myapp.wiring" # module where the object graph is assembled
io_boundary = "myapp.io" # module or package that owns process and file access
```
**Layers are bottom first.** The first name is the lowest layer and the last is the highest.
The report states the resolved order as a note, `layers: bottom=domain top=infrastructure`,
so a table read the wrong way round shows up in the report rather than in the findings. A
repeated name is dropped with a note and the first occurrence keeps its place.
**Exclude globs are anchored at the target root** and matched against the root-relative
POSIX path, one path segment at a time. A `*` never crosses a `/`, so a pattern reaches
exactly as deep as it says:
| Pattern | Excludes |
|---|---|
| `*.py` | top-level `.py` files only, not `pkg/a.py` |
| `**/*.py` | every `.py` file at any depth |
| `migrations/*` | the files directly under `migrations` |
| `migrations/**` | the whole `migrations` subtree |
| `migrations/` | the whole `migrations` subtree, the short way |
| `migrations` | the same, when `migrations` is a directory at the root |
Two shorthands keep a pattern from quietly covering nothing. A pattern ending in `/` means
the directory and everything under it, whether or not that directory exists. A bare name
with no `/` and no glob characters means the same thing, but only when a directory of that
name exists at the root, and the report says how it was read: `exclude pattern migrations
names a directory; read as migrations/**`. A name that is not a directory still means that
file, so `exclude = ["conftest.py"]` excludes the top-level `conftest.py` and nothing else.
An empty pattern, or an entry that is not a string, is dropped and disclosed as a note
rather than read as a pattern that matches everything. A well-formed pattern that covered
none of the discovered files is disclosed too: `exclude pattern nope/* matched no files`.
Between the two, an exclude setting can never fail silently.
**The Python version comes from `requires-python`.** Its lower bound (`>=3.9`, `>=3.9,<4`,
`~=3.10` and `==3.11.*` all state one) becomes ruff's `--target-version` and mypy's
`--python-version`, so both judge the code as the dialect it claims to support. A specifier
with no lower bound leaves the version unknown and the default 3.12 is used. Either way the
report says which version ran and where it came from: `python version: 3.10 (from
requires-python)` or `python version: 3.12 (default)`. A version outside the range a tool
accepts (ruff `py37` to `py313`, mypy 3.8 to 3.13) is clamped to the nearest one it knows,
with a note naming both.
## Suppression comments
A comment is configuration a target controls, so where a tool can be told to disregard it, it
is. ruff runs with `--ignore-noqa` and bandit with `--ignore-nosec`, so a line carrying
`# noqa` or `# nosec` still reports.
Two markers still work, because their tools offer no flag to disarm them. **`# noqa` hides a
line from vulture**, and **`# type: ignore` hides it from mypy.** Both are counted, and the
hygiene analyzer reports one note across the discovered files, test files included:
```
suppression markers: noqa 3 (ignored by ruff, honoured by vulture),
type: ignore 1 (honoured by mypy), nosec 2 (ignored by bandit)
```
The note is omitted when every count is zero. Read a non-zero `noqa` or `type: ignore` count
as a prompt to go and look: the score is not evidence about the lines those markers cover.
## Known limitations
Two test files with the same name in sibling directories that have no `__init__.py` (say
`tests/unit/test_x.py` and `tests/integration/test_x.py`) make mypy exit with a
duplicate-module error. Functionality is then unscored and the verdict is `incomplete`. The
same layout breaks pytest's default import mode, so the fix is to add `__init__.py` files or
to give the modules distinct names.
Target configuration is neutralised, but host configuration is not: radon reads
`~/.radon.cfg` (and `$RADONCFG`) unconditionally, from the account the server runs as. Keep
the server's host free of both, or its complexity and maintainability results can be changed
for every target at once.
## Development
```bash
uv run pytest -q && uv run ruff check . && uv run mypy
uv run codequality evaluate . # the repo must earn review_ready; tests/calibration/test_dogfood.py enforces it
```
Design: `docs/superpowers/specs/2026-09-11-codequality-mcp-design.md`.
Plan: `docs/superpowers/plans/2026-09-11-codequality-mcp.md`.
TDQS
Scored across 3 tools
Each tool has a clearly distinct purpose: evaluate runs the quality analysis, explain_rule provides details on a specific rule, and list_analyzers checks analyzer health. No overlap or ambiguity in selection.
Two tools follow a verb_noun pattern (explain_rule, list_analyzers), while evaluate is a bare verb. This minor deviation is still readable and consistent in snake_case, but prevents a perfect score.
Three tools are well-scoped for a focused code-quality analysis server; each tool (evaluate, explain_rule, list_analyzers) earns its place without redundancy.
The surface covers running evaluations, explaining rules, and checking analyzer availability, but lacks a way to list all rules or dimensions directly. This minor gap is workable via evaluate findings, but agents cannot discover rules upfront.