aihealth
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
aihealth
Code health estimates (1–10, CodeScene-style) for Python, Dart/Flutter, Swift/SwiftUI and Makefiles, built as feedback for AI coding agents.
delta: did this change make the code healthier or worse? Verdict, score before → after, new/worsened smells with
path:lineand fix hints. Exit code 1 when health degraded.scan: overview of a codebase. Project score, worst files, top refactoring targets.
MCP server: the same, as tools an agent calls itself.
Scores come from 13 explainable code smells: Brain Method, Large/Complex Method, Cognitive Complexity, Deep Nesting, Bumpy Road, Excess Arguments, Complex Conditional, Deep Widget Tree (Flutter, SwiftUI), Large File, Low Cohesion, Primitive Obsession and Code Duplication. Bands: healthy ≥ 9, warning 4–9, alert < 4.
Install
Needs Python 3.11+ and git 2.24+.
uv tool install "git+https://github.com/epatel/ai-health" # or: pipx install "git+..."
python3 -m venv .venv && .venv/bin/pip install -e . # from a clone (or: make setup)Full setup (MCP server, hooks, for you or for a project): AGENT-INSTALL.md lists the exact commands and file changes, plus a short prompt that has your agent do the same steps.
Related MCP server: ARGUS
CLI
aihealth delta # working tree (incl. untracked) vs HEAD
aihealth delta --staged # staged changes only
aihealth delta --base main --head HEAD lib/ # a commit range, limited to lib/
aihealth delta --format markdown # agent-oriented output (also: text, json)
aihealth scan path/to/repo --format markdown --max-findings 20
aihealth scan . --fail-under 7 # CI gate on the project score
aihealth scan src/app.py # one file (also: Makefile, lib/x.dart, App.swift)History
aihealth scan . --save # store in .aihealth/health.db (self-gitignored)
aihealth history # score per saved scan, Δ, config changesDashboard
aihealth serve --repo path/to/repo # http://127.0.0.1:8765It shows an overview (score, trend, where the penalty comes from, worst files), a treemap (area = lines of code, colour = health), and per-file findings and function metrics. "Scan now" runs and saves a scan. Charts load ECharts from a CDN.
MCP server (Claude Code and other agents)
Register it for a project (run inside that project; the server analyses its working directory):
claude mcp add aihealth -- /path/to/ai-health/.venv/bin/aihealth mcpTools:
Tool | Use |
| Delta of uncommitted (or |
| Score proposed |
| All smells in one file |
| Project overview |
| What a smell measures and how to fix it |
All tools return markdown by default (output: "json" for machine use).
Automatic feedback in Claude Code (hooks)
Instead of relying on the agent to remember, let Claude Code run aihealth itself. Add to the
project's .claude/settings.json (use the absolute path to your aihealth install):
{
"hooks": {
"PostToolUse": [
{ "matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "/path/to/ai-health/.venv/bin/aihealth hook post-edit" }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "/path/to/ai-health/.venv/bin/aihealth hook stop" }] }
]
}
}post-edit: after each edit of a supported file, checks that file against HEAD. Silent unless it got worse; then Claude gets the regressions with fix hints next to the edit result.
stop: when Claude is about to finish, checks all uncommitted changes. If health degraded, Claude gets one more turn to fix them (or to say why they are intended). It never fires twice in a row, so it cannot loop.
Both fail open: outside a git repo, for other languages, or on any error they do nothing.
Swift
*.swift files are scored with the generic smells; nothing is Swift-only.
Units: functions, methods,
init/deinit, operators, subscripts, computed-property getters/setters andwillSet/didSetobservers. Local functions and local types are scored on their own; closures stay part of their function.#Preview { }blocks and Quick-styledescribe/context/itclosures are units too (configurable).Types: struct, class, enum, actor and extension are all classes for Low Cohesion (stored, non-
staticproperties are the state).Parameters with a call-site label count half for Excess Arguments;
_ xcounts fully.SwiftUI: nesting of view calls (
VStack { HStack { Text(...) } }) is the Deep Widget Tree depth. View-builder closures don't add cognitive nesting (they are layout, not control flow);Task { }and.map { }closures do.bodyuses the normal Large Method limit (measured below).Skipped:
.build/,Pods/,DerivedData/,*.docc/(tutorial step files),*.generated.swiftand SwiftGen/Sourcery output.Very new or stdlib-internal syntax (
@_lifetime,__owned) andf(())are not in the grammar yet: such files report "syntax errors", and the rest of the file is still scored.
Makefiles
Makefile, makefile, GNUmakefile and *.mk files are scored with the same smells,
mapped onto make:
Rules with a recipe are the "functions". The recipe is parsed as shell (bash), so long recipes (Large Method), nested
if/forshell logic (Deep Nesting, Cognitive Complexity) and&&/||chains in conditions are measured on the real Makefile lines.defineblocks are scored as canned shell recipes, or as make templates when they contain make syntax.Top-level
ifeq/ifdefblocks, and variables built with$(if),$(or),$(and)or$(foreach), are scored for their make-level branching.Copied recipes show up as Code Duplication. Fix hints suggest make remedies: pattern rules,
define+$(call), moving shell into a script file.Missing .PHONY (Makefile-only smell): targets that never create a file of their name (
clean,test,all) but aren't listed in.PHONY. If a file or directory with that name ever exists,make testsilently does nothing. The check is conservative: targets writing$@or mentioning their own name, names with.///%/$, emptyFORCE:rules and special targets are skipped, and.PHONY: $(VARS)is expanded from the file's own assignments (nothing is reported if it can't be expanded).
Generated files (automake, CMake, qmake output, and in every language anything marked "DO NOT EDIT" or "@generated") are skipped.
Configuration
Optional .aihealth.toml at the repository root (all keys in src/aihealth/config.py):
[thresholds]
large_method = 100
[weights]
primitive_obsession = 0 # 0 disables a smell
[scan]
exclude = ["legacy/*"] # added to defaults (*.g.dart, *.freezed.dart, build/, ...)
[dart]
test_closure_calls = ["group", "test", "testWidgets", "patrolTest"]
[swift]
test_closure_calls = ["describe", "context", "it"] # default: Quick's full setTurn smells off for one file where they are imposed by an interface (e.g. CLI/JSON/MCP boundaries) with a comment (strings and docstrings don't count):
# aihealth: disable=primitive_obsession, excess_argumentsAdding a language
All metrics, smells and scoring are language-neutral. A language is one adapter file
that tells the engine what the syntax tree means, plus tests that pin that meaning. Python
(src/aihealth/lang/python.py), Dart (lang/dart.py) and Swift (lang/swift.py) are the
reference adapters. Copy the one whose language is closer.
How the pieces fit
flowchart LR
SRC[source file] -->|tree-sitter grammar| TREE[syntax tree]
TREE --> EX["adapter._extract()<br/>Module / Class / Function / Param"]
TREE --> ROLE["adapter.role(node)<br/>IF, LOOP, CATCH, LAMBDA, ..."]
EX --> MET[metrics.py]
ROLE --> MET
MET --> SM[smells.py] --> SC[scoring.py]The adapter answers two questions. What are the functions, classes and parameters?
That's _extract, which builds the model in model.py. What does each node mean for
complexity? That's the roles table plus a few hooks. Nothing else in the engine knows
about syntax.
Steps
I'd do it in this order. Each step has a check before you move on.
Spike the grammar first. Grammars come from
tree-sitter-language-pack(the name inlanguagemust match its grammar name). Write a fixture with modern syntax for the language, parse it, and dump the node types. Check that there are zeroERRORnodes, and note quirks: Dart, for example, puts a function body next to its signature instead of inside it. Add a test liketests/test_parser_spike.pythat pins the node names you'll rely on, so a grammar upgrade fails loudly. Record the quirks incards/parser-choice.md.Write the adapter (
src/aihealth/lang/<name>.py, subclassLanguageAdapter):Part
What to provide
language,extensionsgrammar name, file extensions
_extract(module)every scorable function as a
Function: unique dottedqualname,kind, line span,params, and the body node (metrics walk only this). Classes, and their analogues (Go receiver types, Rustimplblocks, Swift extensions), becomeClass. Named nested functions become their ownFunctionand are given theSTOProle, so they aren't counted twice.Paramnamed=Truefor parameters that are named at the call site (Kotlin/Swift labels, Python keyword-only, Dart{}), which count half for Excess Arguments.type_textas written, orNoneif untyped.rolesnode type →
Role:IF,LOOP,SWITCH,CASE(non-default arms),CATCH,TERNARY,LAMBDA(closures),STOP,COMPREHENSION,JUMP_LABEL,BOOL_EXPRrole(node)overrideonly where meaning depends on context: an else-if, a
default/_arm, a catch body that is a sibling block, labeled jumpsbool_token(node)"and"/"or"for&&/||/and/ortokens. Count tokens, don't trust the tree shape (grammars disagree on precedence).comment_types,block_types,bool_transparentfor line counts, statement lists and condition detection
primitive_types,base_types()for Primitive Obsession; generics are judged by their type arguments (
List<Order>is a domain type)literal_typesliteral nodes that duplication detection normalises
member_refs(cls)which fields/methods each method touches (LCOM4 cohesion). It can return
{}at first, which simply disables the smell.widget_depth(body)optional UI-tree depth. It suits JSX, SwiftUI and Compose as well as Flutter.
Register it: add the class to
ADAPTERSinsrc/aihealth/lang/__init__.py. Add generated-file patterns toDEFAULT_EXCLUDESinconfig.py. If the language needs options (like Dart'stest_closure_calls), add a config section and map it inConfig.adapter_options.Golden metric tests. Port the SonarSource cognitive-complexity examples (
sumOfPrimes= 7,getWords= 1,a && b && c || d || e && f= +3), as intests/test_metrics.py. Then add cases for the language's own constructs: else-if, default arms, closures, labeled breaks, pattern matching, error handling.Coverage check on real code. Run the adapter over a large, well-known codebase in the language. There should be no crashes, every function-body node should be extracted, and it should be reasonably fast (Dart reached 100 % of 60,461 bodies across the Flutter SDK). Fix what's missed. The misses are usually unusual declaration forms.
Calibrate. Add a few well-regarded libraries and a few big, complex codebases to
CORPORAinscripts/calibrate.py, then run it. Good libraries should land mostly healthy and complex code around 5–7. If a language idiom trips a threshold everywhere (as Flutterbuild()methods did for Large Method), add a targeted, measured exception rather than a global change, and record the numbers incards/code-health-model.md.Dogfood and document. Run
aihealth deltaon the adapter itself. Update this README and the language list at the top.
Things to decide per language
What counts as a function: arrow functions assigned to a name (
const f = () => {}in TS/JS) should be scored as named functions, while inline callbacks stay part of their parent. Test-framework closures (describe/it, XCTest) likely deserve the Dart treatment, so a test file isn't one giant function.Idiomatic branching: Go's
if err != nil { return err }adds a branch to every call. Check in calibration whether it should get the same exemption as asserts.Named arguments: Kotlin, Swift and C# have them, and they should count half, consistent with Dart and Python.
UI trees: JSX, SwiftUI
bodyand Compose@Composablefunctions map ontowidget_depthand the Deep Widget Tree smell.Operator precedence: grammars can nest operators wrongly. Swift's puts
||insidea > b, and parsesn * f(x)as(n * f)(x). Look through comparison nodes (bool_transparent) and look for callees in the rightmost operand, then pin both in tests.
License
MIT, see LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Query your team's AI coding history and act on HypAware recommendations from any MCP client.
GitHub Copilot MCP: code, repo, PR, workflow tools for AI assistants.
Read-only AI coding tools for change verification, release readiness, capacity, and guidance.
AI-native git hosting — repos, PRs, issues, CI gates, and AI code review over MCP (60 tools).
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceAnalyzes repositories, explains architecture, calculates change impact, and enforces guardrails for AI Agents like Claude Code, Cursor, and Codex via MCP tools.-
- FlicenseBqualityCmaintenanceEnables AI agents to perform comprehensive, zero-infrastructure codebase analysis through 24 MCP tools, covering security, quality, architecture, type safety, git history, and dead code detection with high precision and local privacy.45-
- AlicenseBqualityCmaintenanceEnables automated pull request review, security vulnerability analysis, and code quality improvement suggestions through any MCP client.315 npmMIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to query a repository's living, versioned knowledge graph through 11 MCP tools, so they can search code, inspect node, file and community context, trace paths between routes and tables, run impact analysis, and review what changed between versions.98 npm2MIT