gristmill-mcp
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
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:
claude mcp add gristmill -- /absolute/path/to/gristmill-mcp/.venv/bin/gristmill-mcpOr add it directly to your MCP config (.mcp.json in a project, or your global Claude Code config):
{
"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:
.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:
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).venv/bin/gristmill-verify demo/billing.pyOutput 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.pycurrently has the key swapped forNoneto 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, served on demand so verify output can stay terse.
Rules
Rule | Check | Title | Default severity |
| secrets | AWS access key ID | error |
| secrets | AWS secret access key | error |
| secrets | GitHub token | error |
| secrets | Google API key | error |
| secrets | Slack token | error |
| secrets | Stripe live key | error |
| secrets | Private key block | error |
| secrets | JWT | error |
| secrets | Database URI with inline password | error |
| secrets | Generic credential-shaped assignment | error |
| secrets | High-entropy string literal | warning |
| structure | Too many top-level functions (default limit 5) | warning |
| structure | Shared function-name prefix (3+ functions) | warning |
| structure | Repeated first-parameter name (3+ functions) | warning |
| structure | Function too long (default limit 60 lines) | warning |
| structure | Mutable module-level state, mutated elsewhere in the file | warning |
| comment_slop | Conversational address in comment | warning |
| comment_slop | Comment narrates the obvious | info |
| comment_slop | Oversized comment block on a short function | info |
| comment_slop | Placeholder scaffolding left in place | warning |
| comment_slop | Repeated section-divider banners (4+ per file) | info |
Full rationale, false-negative notes, and suppression instructions per rule: docs/RULES.md.
Configuration
.gristmill.toml at the project root, all keys optional:
[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:
SUPPRESSED = "ghp_" + "..." # gristmill: ignore SEC003// gristmill: ignore SEC003
const suppressed = "ghp_" + "...";Language support
Python — full support (stdlib
astandtokenize).JavaScript/TypeScript — full support, via
tree-sitterwith thetree-sitter-javascriptandtree-sitter-typescriptcompiled 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 —structureandcomment_slopwork identically whether or notnodeis onPATH, and it gives a real AST instead of a text-only fallback.Anything else — the
secretscheck still runs (it's regex-based and language-agnostic);structureandcomment_slopare skipped for that file, reported inskipped_paths.
Limitations
Read this before trusting the tool more than it's earned:
secretsonly catches shaped or high-entropy strings. A low-entropy human password likehunter2will 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.
structurelooks 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. Seedocs/RULES.mdfor 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
verifycall 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
verifyfindings)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
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest tests/ -qRegenerate docs/RULES.md after editing src/gristmill/rules.py:
.venv/bin/python3 scripts/generate_rules_doc.pyTests 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.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/mattshuttle/gristmill-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server