Skip to main content
Glama

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

uv sync
uv run codequality analyzers     # every analyzer should report a version

Related MCP server: io.github.neogeweb3/code-health-suite

Use from the command line

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):

{
  "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:

[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

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.

Available Tools

3 tools
evaluateA

Evaluate a file or directory against the deterministic quality bar.

Returns findings, per-dimension scores, hotspots, and a verdict. With git_range (for example main..HEAD) findings on touched lines are marked in_scope; scores always cover the whole target. Restricting dimensions leaves the others unscored, so the verdict is incomplete.

Inputs: path (required), git_range, dimensions. Returns: The complete result of one evaluation.

explain_ruleB

What a rule checks, why it matters, and how to fix it (e.g. design:CYCLE, ruff:F821).

Inputs: rule_id (required). Returns: What a rule checks, why it matters, and how to fix a violation.

list_analyzersB

Health check: each analyzer tool version and availability.

Inputs: no named parameters. Returns: result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.6/5.0

    Scored across 3 tools

    Disambiguation5/5

    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.

    Naming Consistency4/5

    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.

    Tool Count5/5

    Three tools are well-scoped for a focused code-quality analysis server; each tool (evaluate, explain_rule, list_analyzers) earns its place without redundancy.

    Completeness4/5

    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.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Evaluates RAG outputs on faithfulness, answer relevancy, and context precision using an LLM-as-a-Judge backend. Exposes tools for running evaluations, scoring individual samples, and checking thresholds, enabling CI gating and on-demand assessment via MCP.
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables MCP clients to evaluate text or local files for LLM-output quality issues, returning 0-100 scores, grades, and precise line/column findings. It also lets clients list all available lint rules and severities.
      MIT