Skip to main content
Glama
mattshuttle

gristmill-mcp

by mattshuttle
README.md
# gristmill-mcp

An MCP server that inspects AI-generated code and returns a deterministic list of structural and safety violations, so an AI coding agent can fix its own output before the code lands.

*Grist* is grain brought to a mill for grinding. AI output is grist — genuinely valuable raw material, but unprocessed. The mill gives it structure.

**AI writes the grist. Gristmill makes it code you can ship.**

## Why an MCP server, not a skill

A skill is text loaded into a model's context — it changes what the model *knows*. An MCP server is a program the model *executes* — it changes what the model *can do*.

Style guidance ("prefer classes over loose functions") belongs in a skill. Verification ("this file has 7 top-level functions at lines 12, 40, 66…") requires running code against the file. A model reading its own output and reasoning "this looks like it has too many functions" is a guess dressed up as an observation — it has no ground truth for what "too many" means in *this* file, and no reliable way to count. Gristmill parses the AST and counts. That distinction — instruction versus execution — is why this exists as a server instead of a paragraph of advice.

The server never calls an LLM, never varies between runs on the same input, and never emits a confidence score. Same input → byte-identical output, every time. That determinism is the entire product. The AI layer sits *above* this server, consuming its findings and deciding what to do about them — the server's job stops at reporting facts with line numbers.

## Install

```bash
git clone <this repo> gristmill-mcp
cd gristmill-mcp
python3 -m venv .venv
.venv/bin/pip install -e .
```

### Claude Code

Register it with the CLI, pointing at the venv's console script:

```bash
claude mcp add gristmill -- /absolute/path/to/gristmill-mcp/.venv/bin/gristmill-mcp
```

Or add it directly to your MCP config (`.mcp.json` in a project, or your global Claude Code config):

```json
{
  "mcpServers": {
    "gristmill": {
      "command": "/absolute/path/to/gristmill-mcp/.venv/bin/gristmill-mcp"
    }
  }
}
```

### Other MCP clients

Any stdio-based MCP client can launch the same binary — `gristmill-mcp` (or `python3 -m gristmill.server` inside the venv) speaks the standard MCP stdio transport with no client-specific configuration.

### Command line (no MCP client)

For local testing, or to reproduce the worked example below, a thin CLI wraps the same engine:

```bash
.venv/bin/gristmill-verify path/to/file_or_dir [--checks secrets structure comment_slop] [--severity-floor warning] [--json]
```

## Worked example

`demo/billing.py`, an unedited first draft of a Stripe billing helper:

```python
import stripe

# I've added this as you requested — sets up the Stripe client
STRIPE_SECRET_KEY = None  # was a literal sk_live_... key — see note below

stripe.api_key = STRIPE_SECRET_KEY


def customer_create(config):
    return stripe.Customer.create(**config)


def customer_delete(config):
    return stripe.Customer.delete(config["id"])


def customer_find(config):
    return stripe.Customer.retrieve(config["id"])


def customer_update(config):
    return stripe.Customer.modify(config["id"], **config)
```

```bash
.venv/bin/gristmill-verify demo/billing.py
```

Output with a real Stripe-live-key-shaped literal in place of the `None` above:

```
gristmill: 1 files scanned, 0 skipped (2 error, 4 warning, 0 info) in 1ms
  [WARNING] STR002  billing.py:1  4 top-level functions share the prefix `customer_` — consider a `Customer` class or module
  [WARNING] STR003  billing.py:1  4 top-level functions take a first parameter named `config` — consider making it instance state
  [WARNING] CMT001  billing.py:3  Comment addresses the reader conversationally ('as you requested')
  [ERROR  ] SEC006  billing.py:4:22  Stripe live key assigned to `STRIPE_SECRET_KEY`
  [ERROR  ] SEC010  billing.py:4:22  String literal assigned to `STRIPE_SECRET_KEY`, which looks credential-shaped
  [WARNING] SEC011  billing.py:4:22  High-entropy string literal (5.1 bits/char) assigned to `STRIPE_SECRET_KEY`
```

(File paths are shown relative to the nearest `.gristmill.toml` — `demo/` carries its own so this example's output stays stable independent of the top-level project config.)

> **Note:** GitHub's push protection blocks any pushed file containing a real-format secret — including in a comment or a markdown code block, this README included. `demo/billing.py` currently has the key swapped for `None` to unblock the initial push; this is a TODO to restore (via an allow-listed secret-scanning exception) so the demo is live again.

The `--json` flag (or the `verify` MCP tool, which returns both) gives the full structured form — file, line, column, a static suggestion string, and a redacted `evidence` field (`sk_l… (49 chars)`, never the key itself).

## Tools

### `verify`

> Inspect source files for secrets, structural problems, and low-quality comments. Returns deterministic findings with file paths and line numbers. Call this after generating or editing code, before presenting it as finished.

Input: `paths` (files or directories, required), `checks` (optional subset of `secrets`/`structure`/`comment_slop`, default all), `severity_floor` (optional, default `info`).

Output: a compact human-readable summary, followed by the full structured JSON — file, line, column, message, redacted evidence, and a static suggestion string per rule. Findings are sorted by `path`, then `line`, then `rule_id`, always — that stability is what makes runs byte-identical and lets a model navigate straight to the problem.

### `explain_rule`

Takes a `rule_id` (e.g. `SEC001`) and returns its rationale, what it catches, what it misses, and how to suppress it — the same content as [`docs/RULES.md`](docs/RULES.md), served on demand so `verify` output can stay terse.

## Rules

| Rule | Check | Title | Default severity |
|---|---|---|---|
| `SEC001` | secrets | AWS access key ID | error |
| `SEC002` | secrets | AWS secret access key | error |
| `SEC003` | secrets | GitHub token | error |
| `SEC004` | secrets | Google API key | error |
| `SEC005` | secrets | Slack token | error |
| `SEC006` | secrets | Stripe live key | error |
| `SEC007` | secrets | Private key block | error |
| `SEC008` | secrets | JWT | error |
| `SEC009` | secrets | Database URI with inline password | error |
| `SEC010` | secrets | Generic credential-shaped assignment | error |
| `SEC011` | secrets | High-entropy string literal | warning |
| `STR001` | structure | Too many top-level functions (default limit 5) | warning |
| `STR002` | structure | Shared function-name prefix (3+ functions) | warning |
| `STR003` | structure | Repeated first-parameter name (3+ functions) | warning |
| `STR004` | structure | Function too long (default limit 60 lines) | warning |
| `STR005` | structure | Mutable module-level state, mutated elsewhere in the file | warning |
| `CMT001` | comment_slop | Conversational address in comment | warning |
| `CMT002` | comment_slop | Comment narrates the obvious | info |
| `CMT003` | comment_slop | Oversized comment block on a short function | info |
| `CMT004` | comment_slop | Placeholder scaffolding left in place | warning |
| `CMT005` | comment_slop | Repeated section-divider banners (4+ per file) | info |

Full rationale, false-negative notes, and suppression instructions per rule: [`docs/RULES.md`](docs/RULES.md).

## Configuration

`.gristmill.toml` at the project root, all keys optional:

```toml
[checks]
enabled = ["secrets", "structure", "comment_slop"]

[structure]
max_top_level_functions = 5
max_function_lines = 60

[secrets]
entropy_threshold = 4.5

[ignore]
paths = ["legacy/**", "vendor/**"]
rules = ["CMT003"]
```

A `.gristmillignore` file (gitignore syntax) works alongside `[ignore] paths`. Inline suppression is also honored on the flagged line or the line above it:

```python
SUPPRESSED = "ghp_" + "..."  # gristmill: ignore SEC003
```

```javascript
// gristmill: ignore SEC003
const suppressed = "ghp_" + "...";
```

## Language support

- **Python** — full support (stdlib `ast` and `tokenize`).
- **JavaScript/TypeScript** — full support, via `tree-sitter` with the `tree-sitter-javascript` and `tree-sitter-typescript` compiled grammars, rather than shelling out to a Node-based parser. This trades a compiled Python dependency for independence from the host having Node installed at all — `structure` and `comment_slop` work identically whether or not `node` is on `PATH`, and it gives a real AST instead of a text-only fallback.
- **Anything else** — the `secrets` check still runs (it's regex-based and language-agnostic); `structure` and `comment_slop` are skipped for that file, reported in `skipped_paths`.

## Limitations

Read this before trusting the tool more than it's earned:

- **`secrets` only catches shaped or high-entropy strings.** A low-entropy human password like `hunter2` will never be flagged — there is no reliable way to distinguish it from an ordinary short string. Credentials assembled at runtime (string concatenation, `os.environ.get(...) or "fallback"`, base64-decoded pieces) are invisible to a regex/entropy pass over static text.
- **Structural problems that span files are invisible.** `structure` looks at one file at a time; a class that should be split across files, or duplicated logic in two different modules, is out of scope.
- **`comment_slop`'s CMT002 is deliberately narrow.** It's the highest false-positive-risk rule in the set, so it's implemented to bias hard toward silence — it will miss real narration far more often than it over-flags. See `docs/RULES.md` for the exact subset-match rule.
- **Languages outside Python and JS/TS get secrets-only coverage.** No structural or comment analysis for Go, Rust, Ruby, etc. in v1.
- **This is not a secrets-in-git-history scanner.** It inspects the working tree as given. A key that was committed and later removed from the current file is not this tool's concern (a git-history scanner is a different, complementary tool).
- **No auto-fix.** Gristmill reports; the calling model decides what and how to change. That split is intentional (see "Why an MCP server, not a skill" above), but it means a `verify` call alone never fixes anything.

A tool that oversells its coverage is worse than one that's upfront about its blind spots — silence beats false confidence here as much as it beats noisy findings.

## Roadmap

Explicitly out of scope for v1, in rough priority order:

- Auto-fix / patch generation (the calling model does this today, using `verify` findings)
- Dependency freshness and CVE checking (needs network calls to package registries — a natural v2)
- Language support beyond Python and JavaScript/TypeScript
- Git history scanning for secrets that were committed and later removed
- A hosted service, web UI, or dashboard

## Development

```bash
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest tests/ -q
```

Regenerate `docs/RULES.md` after editing `src/gristmill/rules.py`:

```bash
.venv/bin/python3 scripts/generate_rules_doc.py
```

Tests cover (`tests/`): golden-file output for a known-dirty fixture directory, 10x determinism with and without parallelism, a false-positive corpus that must produce zero findings, redaction (no raw secret ever reaches any output field), and resilience (invalid syntax, binary, empty, and oversized files never crash a run).

## License

MIT — see [`LICENSE`](LICENSE).

TDQS

A3.5/5.0

Scored across 2 tools

Disambiguation4/5

The two tools have clearly distinct purposes: one is for static analysis of code, the other for documentation of rules. An agent would not confuse them.

Naming Consistency3/5

The naming is inconsistent: 'verify' uses a plain verb while 'explain_rule' uses verb_noun pattern. Both are clear in isolation, but the lack of a unified pattern (e.g., 'verify_code' vs 'explain_rule') makes the set feel ad-hoc.

Tool Count2/5

With only 2 tools, the server feels thin for a tool suite called 'gristmill-mcp'. A code analysis server typically needs more tools like listing rules or scanning for specific categories to feel properly scoped.

Completeness2/5

The server only provides a scan tool and a rule lookup tool, but is missing operations like listing all rules, skipping specific rules, or generating reports. Users cannot discover available rules without knowing their IDs, creating a dead end.

Maintenance

ActivitySlowing
ResponsivenessNo issues