ABC complexity gate for AI-written code with an MCP inspection tool.
A must-have gate for AI-written code. Agents ship faster than humans can re-read; abcop keeps that code understandable by gating function and module ABC complexity so every unit fits a human head and an LLM context window. Diagnostics are also a hook for automated refactoring — extract a method, split a module — so CI rejects bad growth and points agents at concrete maintainability fixes.
One self-contained binary across Ruby, Rust, Python, Go, PHP, Java, C#, Dart, JavaScript, TypeScript, C, C++, Objective-C, Swift, Solidity, Zig and Haskell — no runtimes, no plugins, no per-language installs. Written in Rust for speed: one parse per file, one walk per metric, grammars compiled in; whole trees in milliseconds.
brew install adrianov/abcop/abcop
cargo install abcop
macOS (.tar.gz from GitHub Releases; binary + man):
# Apple Silicon — use the *-x86_64-apple-darwin.tar.gz asset on Intel Macs
curl -LO https://github.com/adrianov/abcop/releases/download/v0.19.1/abcop-0.19.1-aarch64-apple-darwin.tar.gz
tar -xzf abcop-0.19.1-aarch64-apple-darwin.tar.gz
sudo cp abcop-0.19.1-aarch64-apple-darwin/abcop /usr/local/bin/
sudo mkdir -p /usr/local/share/man/man1
sudo cp abcop-0.19.1-aarch64-apple-darwin/abcop.1 /usr/local/share/man/man1/
Ubuntu / Debian (.deb from GitHub Releases; amd64, Ubuntu 22.04+ / Debian bookworm+):
# example for v0.19.1 — use the asset name from the release page
curl -LO https://github.com/adrianov/abcop/releases/download/v0.19.1/abcop_0.19.1-1_amd64.deb
sudo dpkg -i abcop_0.19.1-1_amd64.deb
man abcop
lib/sinatra/base.rb:1254:0: C: Metrics/AbcSize: Assignment Branch Condition size for `error_block!` is too high. [<7, 14, 9> 18.06/17]
src/main.rs: W: Metrics/ModuleAbcSize: Assignment Branch Condition size for module is too high. [<80, 200, 60> 228.04/120] -- extract a coherent subunit
132 files analysed in 0.09s, 7 abc offenses, 0 used-once offenses, 0 never-used warnings, 14 module-abc warnings
Line counts lie. The ABC metric
(Jerry Fitzpatrick, C++ Report, June 1997) counts what does work —
assignments (A), branches (B), conditions (C) — as sqrt(A² + B² + C²).
Fitzpatrick defined both method and module scope; abcop gates both:
| Rule | Severity | Meaning |
|---|---|---|
Metrics/AbcSize | C | function ABC above --max-abc (default 17) |
Metrics/ModuleAbcSize | W | module ABC above --max-module-abc (default 120) |
UsedOnce | W | local written once, read once — consider inlining |
NeverUsed | W | local written, never read |
A sparse wrapper and a dense god-object can share a line count; ABC separates them. Complexity is the gate — never a line budget. UsedOnce / NeverUsed are secondary.
1 plus stable JSON/JSONL diagnostics
(rule, score, vector, message) feed agents and scripts: split
oversized modules, extract hot methods. Each finding is an actionable
maintainability step, not a style nit.--max-abc and
--max-module-abc.abcop --mcp is the best way to use abcop from
an agent: the model gets offenses as soon as it writes, so it can fix
complexity and dead locals in the same turn and ship effective code
right away — not after a later CLI/CI pass. Official Rust
rmcp SDK; tool abcop_inspection.
Listed on the
MCP Registry as
io.github.adrianov/abcop (crates.io package + each GitHub v* tag).Inspired by RuboCop (Ruby AbcSize
parity and # rubocop:disable directives) and
lizard (one tool, many languages).
Ruby's counting matches RuboCop 1.89 byte-for-byte
(scripts/compare_parity.py).
rubocop --only Metrics/AbcSize (cache off), ~140× a full rubocop run.cache.redb) compiled in; works on clean CI images with no language
toolchains.--sort-by-score buffers for worst-first emit.abcop [OPTIONS] PATH...
| Option | Default | Meaning |
|---|---|---|
[PATH]... | auto | targets; omitted → scope selection |
--max-abc N | 17 | function ABC ceiling |
--max-module-abc N | 120 | module ABC ceiling |
--only abc|used-once|never-used | all | single check |
--full | off | whole production tree (default skips stay on) |
--everything | off | no gitignore / hidden / vendored pruning |
--format text|json|jsonl | text | CI-friendly output |
--sort-by-score | off | highest ABC first |
--mr | off | MR scope (uncommitted + branch vs base) |
--uncommitted | off | working-tree + index + untracked vs HEAD only |
--no-cache | off | skip on-disk cache |
--mcp | off | MCP server on stdio (for AI clients) |
--dump-tree FILE | — | debug syntax tree |
Exit codes: 0 clean, 1 findings, 2 usage error.
abcop app lib # two trees
abcop --format jsonl lib > abcop.jsonl # streaming CI / agent input
abcop --only used-once src # inline candidates
abcop --max-abc 12 --only abc lib # stricter function budget
abcop --max-module-abc 80 lib # stricter module budget
abcop --sort-by-score --only abc lib # worst first
abcop --uncommitted # pre-commit / agent loop
abcop --mr --only abc # this branch's touched units
abcop --mcp # MCP server on stdio (for AI clients)
JSON diagnostics include file, line, column, severity, rule,
message, plus score / vector for ABC rules:
{"rule":"Metrics/AbcSize","score":10.0,"vector":"<6, 8, 0>"}
{"rule":"Metrics/ModuleAbcSize","score":120.5,"vector":"<40, 100, 40>"}
Named paths — those targets only.
Omitted — narrowest useful scope, announced on stderr:
HEAD if the tree is dirty--mr forces this)Default walks prune test/fixture trees, vendored/build output
(vendor/, node_modules/, target/, …), db/migrate/, route tables
(config/routes.rb, config/routes/*.rb), and generated names
(*.min.js, *_pb.go, …). Name a path explicitly to scan it anyway.
Third-party, route-table, and fixture paths are never scoped review
surface — a diff through vendor/ or tests/fixtures/ does not make
that material owned code.
Scoped ModuleAbcSize re-sums only methods that intersect the diff and
compares that total to --max-module-abc (default 120; untracked =
every method). A small patch into an oversized legacy file stays quiet
unless the touched methods themselves exceed the ceiling; AbcSize still
reports any changed method over --max-abc. Full scans (--full,
--everything) report every production module over the ceiling;
ModuleAbcSize still exempts test trees on full scans (scoped runs can
flag them when changed methods sum over the limit). UsedOnce /
NeverUsed always follow the changed lines.
On a dirty tree the bare default is uncommitted-only; --mr takes the
full branch union. Commits straight to main use a 36-hour window when no
branch base applies. --uncommitted fails outside a repository instead
of silently widening.
Everywhere except Rust, RuboCop-style # / // suppressions work
(trailing and block; bare Metrics allowed). rubocop:disable-next is
ignored, matching rubocop.
def legacy_path # rubocop:disable Metrics/AbcSize
...
end
Preferred for LLM / agent workflows. Wire abcop as an MCP server so the
model can call abcop_inspection while it edits: feedback arrives in the
same turn, the agent corrects soon, and it writes maintainable code on the
first pass instead of discovering ABC / UsedOnce / NeverUsed only in CI.
abcop --mcp runs a long-lived MCP server on stdio — same idea as
RuboCop’s MCP
and rrubocop, with no Ruby mcp
gem. Tool:
| Tool | Purpose |
|---|---|
abcop_inspection | Analyse via path / paths (string or array) and/or inline source_code; returns compact offense JSON (code, line, column, message; score / vector for ABC rules). Always pass an explicit project path — omitting targets errors out (avoids scanning $HOME when MCP cwd is mis-set). |
mcp-name: io.github.adrianov/abcopMetadata lives in server.json, which is part of the
Cargo package on crates.io. Each GitHub
v* release tag publishes that listing to the
MCP Registry (after the
crates.io upload).
Example client config (Cursor / VS Code / Windsurf):
{
"mcpServers": {
"abcop": {
"type": "stdio",
"command": "abcop",
"args": ["--mcp"],
"cwd": "/path/to/your/project"
}
}
}
Claude Code:
claude mcp add abcop -- abcop --mcp
Intended for MCP clients, not interactive use.
Content-addressed cache under $XDG_CACHE_HOME/abcop (or ~/.cache/abcop;
override with ABCOP_CACHE_DIR). Warm reruns ~5× faster. Keys cover
contents, version, rule revision, threshold, checks and path — no
cross-project collisions. Auto-pruned to 20 000 entries; --no-cache
disables. Nothing is written inside the project.
Seventeen languages plus embedded JS in HTML/templates, four rules each, one CI gate.
| Language | Files | Notes |
|---|---|---|
| Ruby | .rb .rake .ru .gemspec, Gemfile, Rakefile, … | RuboCop-parity AbcSize |
| Rust | .rs | |
| Python | .py .pyi .pyw | |
| Go | .go | |
| PHP | .php | |
| Java | .java | |
| C# | .cs | |
| Solidity | .sol | |
| Dart | .dart | |
| Zig | .zig | |
| Haskell | .hs .lhs | |
| JavaScript | .js .mjs .cjs .jsx | also <script> in HTML/templates |
| TypeScript | .ts .tsx .mts .cts | lang="ts" / typescript script types |
| Embedded JS | .html .htm .xhtml .erb .slim .haml .pug .vue .svelte .ejs .njk .jinja .j2 .twig .liquid .hbs …, *.js.erb | extract scripts; blank <% %>, {{ }}, {% %} holes |
| C / C++ | .c .h .cc .cpp .cxx .hpp .hxx .hh | .h via C++ grammar |
| Objective-C | .m .mm | |
| Swift | .swift |
Named declarations are measured units; anonymous function-likes roll into
the enclosing unit; nested units never double-count. C-family: file-scope
globals are out of single-file reach; loop-head locals are protocol;
field writes also read the object; export-macro class forms
(class UTIL_EXPORT Foo) are skipped for variable rules.
Apple M1 Pro, warm cache:
| Corpus | Size | abcop | rubocop --only AbcSize¹ | Full rubocop |
|---|---|---|---|---|
| rubocop/lib | 943 files, 110k LOC | 0.13 s | 5.1 s (~39×) | 18.3 s |
| cargo registry sample | 6,603 files, 2.6M LOC | ~2.8 s | — | — |
¹ --cache false — rubocop's own cache otherwise makes repeats look free.
cargo test
cargo build --all-targets # zero warnings
abcop src # dogfood
Shared engine: src/scope_model/ (backend, walk, eval); language
collectors sit beside each backend (clike/, sollang/, …). Probe
grammar nodes with abcop --dump-tree FILE. Parity:
scripts/compare_parity.py.
GNU GPL v3 or later (SPDX: GPL-3.0-or-later). See LICENSE.
Copyright © 2026 Peter Adrianov. All rights reserved.
This listing does not have a supported local package template. Use the maintainer’s documentation for its hosted endpoint, authentication, and client-specific setup. No install command has been inferred.
abcop works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.