AiT-Test MCP server
Commit Nov 12, 2026Performs high-assurance static analysis for Python to trace causation, generate reproduction witnesses, and suggest verified patches.
Also known as xTanTHaix/AiT-Test
- Stars
- 1
- Tools
- โ
- Downloads, 30 days
- โ
- Latest version
- โ
- Last commit
- Nov 12, 2026
- License
- MIT
What is AiT-Test MCP?
AiT-Test performs high-assurance static analysis for Python to trace causation, generate reproduction witnesses, and suggest verified patches.
Summary generated from the project's README
About AiT-Test MCP server
Tracing the exact origin of a NoneType error or a concurrency bug in Python code is what AiT-Test enables for developers. This MCP server provides high-assurance static analysis by using the standard library AST to identify issues without requiring runtime dependencies or network access. It goes beyond standard linting by providing a causation trace that maps every step from the source of a value to its point of failure.
Users can integrate this engine into MCP clients to perform deep code reviews. The server provides specific tools like ait_review for scanning files and directories, ait_slice_context for retrieving token-efficient symbol information, and ait_verify_fix for validating changes. It operates locally on Windows and Linux, supporting Python versions from 3.10 to 3.15. The system uses 13 evidence-based rules to categorize findings as DEFECT, WARNING, or ADVICE, ensuring that high-confidence issues are prioritized while minimizing false positives through strict evidence requirements.
What you can do with it
- 1Trace variable causationIdentify the exact file and line number where a problematic value was originally produced.
- 2Verify automated patchesEnsure that a suggested code fix is syntactically correct and does not introduce new errors.
- 3Analyze symbol contextRetrieve a compact view of variables, calls, and callers for a specific symbol to save tokens.
Written from the project's README. Every use case is checked against a quote from it.
Automated checks
Ran Nov 15, 2026Full report โNot verified yet. A server needs a score of 80, no serious finding, and enough data for us to check. How checks work ยท What to fix for more points
- Source repository linked and reachablextanthaix/ait-test is public and reachablegithub.com/xtanthaix/ait-test โPass
- Package names this repository as its sourceNot checked
- Build provenance (Sigstore attestation)Not checked
- Open source license (OSI)MITspdx.org/licenses/MIT.html โPass
- Commit or release in the last yearLast commit or release 2026-11-12github.com/xtanthaix/ait-test โPass
- OpenSSF ScorecardNot checked
- Package name not close to a popular packageNot checked
- No critical advisory in the package or its dependenciesNot checked
- No hidden instructions to the model0 tool descriptions and the README read; no override phrases, credential paths or hidden charactersPass
- Destructive tools are labelledNot checked
- Remote endpoint answers the MCP handshakeNot checked
README
View on GitHub[!IMPORTANT] Runs on the standard library AST alone: no runtime dependencies and no network access. Witnesses are built as text and syntax-checked only; the code it flags is never executed.
๐ฏ The problem it solves
A regular linter points at a line but not at the path that made the value None.
You open the file, walk backwards to the origin, and guess whether your patch will break something else.
| โ ๏ธ The question left open | โก What AiT-test answers in one result |
|---|---|
Where did this None come from |
Causation trace: every step carries file:line:col from the producer to the point of failure |
| How do I reproduce it | Repro witness: the smallest code that triggers the symptom, plus a safe_to_execute flag |
| Will the fix break something else | Suggested fix: a unified diff that passes ast.parse and is rescanned for new findings |
| Which file should I fix first | A DEFECT needs evidence: without concurrency or type evidence the finding drops to WARNING instead of guessing |
โก Getting started
Python 3.10 or newer. Nothing to install:
python cli.py src
Or install the ait / ait-mcp commands (still no dependencies):
pip install -e .
ait src
The distribution is named ait-test (pip show ait-test). The engine name reported by --version and by MCP serverInfo is ait-engine 1.0.0. Same project at two layers, not two packages.
The full transcript lives in docs/demo.txt, and the GIF above was recorded from the real run.
$ python cli.py demo.py
================== [AIT-N01] Unshielded NoneType Dereference ===================
File: demo.py:10:10
Severity: DEFECT (Confidence: 0.90)
Function: render()
Symbol: value
`value` can be None at this point but is dereferenced without a guard
--- Causation Trace ---
Step 1 | demo.py:9:12
| `lookup()` is declared to return `Optional[str]`, so the result can be None
Step 2 | demo.py:9:4
| `value` receives the return value of `lookup()`
Step 3 | demo.py:10:10
| `value` is dereferenced here without an `is None` guard
--- Suggested Fix ---
--- a/demo.py
+++ b/demo.py
@@ -10,1 +10,3 @@
+ if value is None:
+ raise RuntimeError(f"value is unexpectedly None")
print(value.upper())
========================= 1 failed, 0 passed in 2.59ms =========================
DEFECT 1 ยท WARNING 0 ยท ADVICE 0
exit code: 1
The code used for the demo:
from typing import Optional
def lookup(key: str) -> Optional[str]:
return None
def render() -> None:
value = lookup("alpha")
print(value.upper())
๐งญ The 13 rules
| ID | Level | What it catches | Evidence it requires |
|---|---|---|---|
AIT-Z01 |
DEFECT | Popen(..., stdout=PIPE) followed by wait() without draining the pipe |
a pipe exists and the child is waited on without a prior communicate()/read() |
AIT-Z02 |
DEFECT | a child handle is dropped without wait/poll/returning ownership |
the variable resolves to a process handle |
AIT-Z03 |
WARNING | a process/thread spawned at module scope without if __name__ == "__main__": |
the call really sits at module level |
AIT-R01 |
DEFECT / WARNING | read-modify-write on shared state with no lock around it | concurrency evidence (spawn site, async def, or a lock in the class); otherwise not reported |
AIT-R02 |
WARNING | a condition checked outside the lock and again inside it | a real lock region and the same condition |
AIT-A01 |
DEFECT / WARNING | a blocking call inside async def |
time.sleep/network/process = DEFECT, file I/O = WARNING; offloading through to_thread/executor is not reported |
AIT-A02 |
WARNING | create_task() with nothing holding a reference |
not stored in a variable, not awaited, no done callback |
AIT-A03 |
DEFECT | a coroutine called without await |
the callee resolves to a coroutine in the same file or to a known API |
AIT-M01 |
DEFECT | mutating a caller's parameter in place (append, pop, update, ...) |
the write targets the parameter name directly |
AIT-M02 |
DEFECT | a mutable default argument ([], {}, set(), ...) |
a real mutable literal or constructor |
AIT-N01 |
DEFECT | dereferencing a value that may be None without a guard |
evidence the value is optional (annotation, None literal, a producer returning optional, dict.get) |
AIT-H01 |
WARNING | a bare except: that swallows everything |
except: raise (bare re-raise) is not reported because nothing is swallowed |
AIT-H02 |
ADVICE | more than four levels of nested control flow | counts real control nodes only and never crosses a scope |
[!NOTE] Severity policy: a DEFECT needs supporting evidence. When concurrency or type evidence is missing, the finding drops to WARNING instead of guessing, because one false finding is enough to make people stop reading the report.
๐ฅ๏ธ CLI
python cli.py [target] [options]
| Flag | Effect |
|---|---|
-v, --verbose |
per-file detail |
-q, --quiet |
summary only |
-x, --exitfirst |
stop at the first file carrying a DEFECT |
--lf, --last-failed |
scan only the files that failed last run (needs the cache) |
--fix |
show a diff and ask one finding at a time (needs a TTY) |
--no-cache |
skip both cache reads and writes |
--maxfail N |
show at most N failure blocks |
--no-color |
disable ANSI (or set NO_COLOR=1) |
--version |
print the version |
Exit code: 0 clean ยท 1 DEFECT present ยท 2 interrupted ยท 3 parse or internal error ยท 4 usage error
[!TIP] When
--lffinds a cache it cannot open, no index, or no earlier run, it scans every file and says so on stderr. The alternative is analysing zero files and reporting "clean" without having read anything.
- every patch is passed through
ast.parseagain before it is offered - findings are asked one at a time (
y/N/afor all /qto quit) - before writing, the original is copied to
.ait_backup/<timestamp>-<pid>/ - the file is written to a temporary path and moved with
os.replace(atomic), as bytes, so line endings are preserved - the file is written back with its original codec (
utf-8-sigkeeps the BOM, a BOM-less UTF-8 file stays BOM-less,cp874stayscp874) if a patch contains characters the original codec cannot encode, the write is refused rather than silently converting the file to UTF-8 - that file is rescanned; any new finding triggers an immediate rollback
- the whole target is rescanned at the end to confirm the final state; a Ctrl-C mid-way prints how many fixes were applied and where the backup lives
Inline directives work with or without the AIT- prefix (people tend to remember the rule number from the output):
value.upper() # ait: disable=AIT-N01
except: # ait: disable=H01
pass
except: # ait: disable=ALL
pass
A suppressed finding is not printed but is still counted in the summary line as N suppressed, so nothing disappears quietly.
Or put globs in .aitignore at the project root:
legacy_*.py
build/
๐ค MCP
mcp.py speaks JSON-RPC 2.0 over stdio and works with any MCP client. stdout carries protocol frames only; logs go to stderr.
{
"mcpServers": {
"ait": {
"command": "python",
"args": ["L:\\path\\to\\AiT-Test\\mcp.py"]
}
}
}
opencode (opencode.jsonc) uses mcp.servers.<name> with command as an array, not the mcpServers shape above:
{
"mcp": {
"servers": {
"ait": {
"type": "local",
"command": ["python", "L:\\path\\to\\AiT-Test\\mcp.py"]
}
}
}
}
ait-mcp --help and ait-mcp --version check the install without entering the serve loop.
| Tool | What it does |
|---|---|
ait_review |
scans a file or directory and returns findings with causation, witness and diff |
ait_slice_context |
returns source, variable table, calls and callers for one symbol (token-efficient) |
ait_verify_fix |
compares candidate code against the baseline: what is resolved and what is newly introduced |
| Tool | Arguments |
|---|---|
ait_review |
path (required), severity_min = ADVICE/WARNING/DEFECT, max_findings = 1โ500 (default 50), exitfirst, use_cache |
ait_slice_context |
file_path, symbol_name (required), depth = 0โ3 (default 1) |
ait_verify_fix |
file_path, candidate_code (required) |
isError: falsewhen the tool ran successfully even if it found a DEFECT, because a finding is a usable result, not a tool failure (isError: trueis reserved for the tool genuinely failing, for example a candidate that does not parse)- an argument the schema does not declare, a wrong type, or an out-of-range value gets
-32602instead of being ignored or silently clamped - an unexpected exception (say
RecursionError) is returned as-32603and the process keeps serving requests shutdownandnotifications/exitstop the serve loop;initializedoes not advertise aloggingcapability because none is usedstructuredContentholds the object once;content[0].textis a compact JSON mirror (no indentation) so the payload does not double in size
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
"name":"ait_verify_fix",
"arguments":{
"file_path":"demo.py",
"candidate_code":"from typing import Optional\n\ndef lookup(k: str) -> Optional[str]:\n return None\n\ndef render() -> None:\n value = lookup('a')\n if value is None:\n return\n print(value.upper())\n"
}
}}
ait_verify_fix re-checks AST-level invariants; it is not a mathematical proof.
All it confirms is that the 13 rules find nothing in the new code, which does not mean the program is correct in every case.
๐ Capability and evidence
0 runtime deps ยท offline ยท 13 rules ยท trace + witness on every finding ยท patches only where a deterministic fix exists (A01/A02/H02/M02/R01 have none) ยท digest-aware cache
| Dimension | Status | Evidence in the project |
|---|---|---|
| Runtime dependencies | โ 0 | every import comes from the standard library; python -S still scans |
| Works offline | โ yes | no HTTP client, no telemetry |
| Causation trace | โ every finding | causation[] with file:line:col from producer to failure point |
| Repro witness | โ text + syntax check | never executed; deadlock/recursion demos carry safe_to_execute: false |
| Suggested patch | โ re-parsed and rescanned | a patch that introduces a new finding is rejected |
| Cache | โ sqlite3 WAL | key = path + size + mtime_ns + digest + ruleset bytecode + schema |
| Suppressed finding count | โ | the summary line reports N suppressed |
| Cross-file type knowledge | โ ๏ธ limited | annotations, literals and producers in the same file, plus hard-coded APIs |
| Proves the program correct | โ not the goal | 13 static rules, not formal verification |
๐ Measured on this machine
Measured on Windows 11 (AMD64) and WSL2 Ubuntu across three interpreters.
| Scenario | 3.12.10 (Windows) | 3.14.4 (WSL2 Ubuntu) | 3.15.0rc2 (Windows) |
|---|---|---|---|
Scan src/ (23 files, cold, --no-cache) |
642 ms | 725 ms | 510 ms |
| Rescan (warm cache) | 12.3 ms | 270 ms* | 9.9 ms |
Test suite (unittest, 268 tests) |
6.9 s OK | 17.3 s OK | 6.8 s OK |
DEFECT / WARNING / ADVICE on src/ |
0 / 0 / 0 | 0 / 0 / 0 | 0 / 0 / 0 |
* WSL reads the repo through /mnt/l (drvfs), which slows every sqlite round trip; the engine itself is not the slow part.
Engine size (src/ait, 23 files) 7,083 lines ยท Runtime dependencies 0
Numbers shift with the machine and file sizes; the cold/warm ratio says more than the absolute values.
The code-size badge at the top counts every line of code in the repository as bytes, tests included, so it measures the whole project rather than src/ait.
๐ง How it works
discover โโโบ parse โโโบ scope tree โโโบ call graph โโโบ data flow
โ โ
โโโโโโโโโโโโโโบ 13 rules โโโโโโโโโโโโโโโโโโ
โ
findings โโโโโโโผโโโโโโโบ causation trace
โโโโโโโโบ repro witness
โโโโโโโโบ suggested patch (re-parsed)
- Cache is sqlite3 (WAL), keyed on
path + size + mtime_ns + BLAKE2b digest + ruleset fingerprint + schema. The digest is compared on lookup, so a rewrite that restores the same size and mtime (git checkout,touch -r, a restored backup) does not resurrect results for older content. The ruleset fingerprint hashes the bytecode of every rule, not just its name, so editing a detector invalidates old results. A corrupt DB file (bad magic header) is discarded and recreated once; if that fails too, the cache is switched off instead of crashing or looping. - Regression diff compares finding counts per
(rule, symbol), not line numbers, so shifted lines do not produce phantom findings. - Witness is generated as text and parsed for syntax only; nothing is executed
(deadlock/recursion demos carry
safe_to_execute: false).
๐งฑ Project structure
AiT-Test/
โโโ cli.py # pytest-style entrypoint
โโโ mcp.py # JSON-RPC over stdio entrypoint
โโโ pyproject.toml # packaging (license = MIT)
โโโ LICENSE # MIT license text
โโโ .github/workflows/
โ โโโ ci.yml # 4 jobs: test matrix (12) + package + lint + security
โ โโโ codeql.yml # CodeQL, security-and-quality, weekly
โโโ .github/codeql/
โ โโโ codeql-config.yml # query suite + tests/fixtures excluded
โโโ .github/dependabot.yml # SHA-pinned actions, updated weekly
โโโ src/ait/
โ โโโ engine.py # discover / scan / compare_findings / verify_fix
โ โโโ models.py # Diagnostic, Patch, Witness, ScanResult
โ โโโ context.py # guard detection, suppression, patch builders
โ โโโ cache.py # sqlite3 cache + corruption recovery
โ โโโ reporter.py # terminal renderer
โ โโโ slicer.py # ait_slice_context
โ โโโ flow/ # scope tree, call graph, data flow, witness
โ โโโ rules/ # 13 rules
โโโ tests/ # unittest + fixtures
โโโ docs/ # demo.gif, demo.txt, assets/
| Layer | File | Responsibility |
|---|---|---|
| Frontend | cli.py |
flag parsing, rendering, the --fix session, exit codes |
| Frontend | mcp.py |
JSON-RPC framing, tool schema and validation, stdio loop |
| Core | engine.py |
discover โ parse โ analyze โ cache โ aggregate |
| Core | context.py |
guard/dominance lookup, suppression, evidence and patch builders |
| Flow | flow/scope_tree.py |
symbol table, lexical scopes, binding resolution |
| Flow | flow/call_graph.py |
call sites, lock regions, spawn sites |
| Flow | flow/data_flow.py |
backward tracing of optional/None sources |
| Flow | flow/witness.py |
minimal repro text and syntax validation |
| Rules | rules/*.py |
13 detectors (Z/R/A/M/N/H) |
| Infra | cache.py |
sqlite3 WAL, digest and ruleset keyed, prune, --lf index |
| Infra | models.py |
immutable Diagnostic/Patch/Witness, exit codes, SCHEMA_VERSION |
| CI | .github/workflows/ci.yml |
test matrix, self-scan, stdlib-only run, integration tests |
| CI | .github/workflows/codeql.yml |
CodeQL security-and-quality, tests/fixtures excluded |
| CI | tests/test_cli_integration.py |
drives the real cli.py / mcp.py through subprocess and asserts exit codes and JSON |
๐งช Tests
No pytest required:
python -m unittest discover -s tests -v
The fixtures come in pairs: bad_*.py (must be caught) and clean_*.py (must stay silent).
The suite also asserts that the engine imports nothing outside the standard library (python -S still runs it).
Ran 268 tests in ~7s
OK
Confirmed on 8 interpreters (7 on Windows 11, 1 on WSL2 Ubuntu): every one returns 268 tests OK, a clean src/, and exit 0.
| Interpreter | Source |
|---|---|
| CPython 3.10.21 | uv-managed (Windows 11) |
| CPython 3.11.16 | uv-managed (Windows 11) |
| CPython 3.12.10 | Windows 11 (system) |
| CPython 3.12.14 | L:\.Python312 |
| CPython 3.13.15 | L:\.Python313 |
| CPython 3.14.7 | L:\.Python314 |
| CPython 3.15.0rc2 | L:\.Python315 |
| CPython 3.14.4 | WSL2 Ubuntu |
3.10.21 โ Ran 268 tests in 7.152s OK
3.11.16 โ Ran 268 tests in 6.990s OK
3.12.10 โ Ran 268 tests in 6.928s OK
3.12.14 โ Ran 268 tests in 6.698s OK
3.13.15 โ Ran 268 tests in 6.724s OK
3.14.4 (WSL2 Ubuntu) โ Ran 268 tests in 17.332s OK
3.14.7 โ Ran 268 tests in 7.319s OK
3.15.0rc2 โ Ran 268 tests in 6.769s OK
All 8 scan the 23 files under src/ without a finding and exit 0.
tests/fixtures exits 1 as it should (DEFECT 11 ยท WARNING 4 ยท ADVICE 1).
On 3.15 the suite also passes with -W error::DeprecationWarning for all 268 tests,
which means the engine does not touch APIs scheduled for removal, and python -S (no site) scans normally.
AiT-Test MCP questions
Does AiT-Test require any runtime dependencies?
No, it runs on the standard library AST alone with zero runtime dependencies and no network access.
What happens if a fix breaks the code?
The engine rescans the file after a patch is applied; if any new findings are triggered, it performs an immediate rollback.