agentcheckup 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentcheckup/__init__.py +27 -0
- agentcheckup/__main__.py +6 -0
- agentcheckup/cli.py +242 -0
- agentcheckup/conflicts.py +253 -0
- agentcheckup/discovery.py +206 -0
- agentcheckup/engine.py +146 -0
- agentcheckup/loader.py +214 -0
- agentcheckup/models.py +166 -0
- agentcheckup/report.py +274 -0
- agentcheckup/rules/__init__.py +12 -0
- agentcheckup/rules/base.py +91 -0
- agentcheckup/rules/config_rules.py +180 -0
- agentcheckup/rules/docs.py +124 -0
- agentcheckup/rules/injection.py +127 -0
- agentcheckup/rules/naming.py +100 -0
- agentcheckup/rules/safety.py +131 -0
- agentcheckup/rules/schema.py +143 -0
- agentcheckup/tokens.py +69 -0
- agentcheckup-0.4.0.dist-info/METADATA +261 -0
- agentcheckup-0.4.0.dist-info/RECORD +23 -0
- agentcheckup-0.4.0.dist-info/WHEEL +4 -0
- agentcheckup-0.4.0.dist-info/entry_points.txt +2 -0
- agentcheckup-0.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Input schema rules.
|
|
2
|
+
|
|
3
|
+
The schema is the contract. A loose contract does not fail loudly - it produces
|
|
4
|
+
a plausible-looking wrong call, which is far more expensive to debug.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ..models import Finding, Severity, Tool
|
|
10
|
+
from .base import tool_rule
|
|
11
|
+
|
|
12
|
+
#: Verbs that imply unbounded input size.
|
|
13
|
+
UNBOUNDED_HINTS = ("query", "sql", "text", "content", "body", "data", "html", "code")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@tool_rule("AGT020", Severity.ERROR, "inputSchema is not an object schema")
|
|
17
|
+
def schema_not_object(tool: Tool):
|
|
18
|
+
"""MCP requires ``{"type": "object", ...}``.
|
|
19
|
+
|
|
20
|
+
Providers that emit ``{"type": "array"}`` or omit ``type`` break client-side
|
|
21
|
+
validation in strict implementations.
|
|
22
|
+
"""
|
|
23
|
+
schema = tool.input_schema
|
|
24
|
+
if not schema:
|
|
25
|
+
return Finding(
|
|
26
|
+
rule="AGT020",
|
|
27
|
+
severity=Severity.ERROR,
|
|
28
|
+
message=f"Tool {tool.name!r} has no inputSchema.",
|
|
29
|
+
tool=tool.name,
|
|
30
|
+
hint='Use {"type": "object", "properties": {}} for a no-argument tool.',
|
|
31
|
+
)
|
|
32
|
+
if schema.get("type") != "object":
|
|
33
|
+
return Finding(
|
|
34
|
+
rule="AGT020",
|
|
35
|
+
severity=Severity.ERROR,
|
|
36
|
+
message=f"Tool {tool.name!r} inputSchema type is {schema.get('type')!r}, expected 'object'.",
|
|
37
|
+
tool=tool.name,
|
|
38
|
+
hint="MCP tool arguments are always a JSON object.",
|
|
39
|
+
)
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@tool_rule("AGT021", Severity.WARNING, "Required references an undeclared property")
|
|
44
|
+
def required_unknown(tool: Tool):
|
|
45
|
+
"""A dangling ``required`` entry makes every call fail validation client-side."""
|
|
46
|
+
props = tool.properties()
|
|
47
|
+
unknown = [r for r in tool.required() if r not in props]
|
|
48
|
+
if unknown:
|
|
49
|
+
return Finding(
|
|
50
|
+
rule="AGT021",
|
|
51
|
+
severity=Severity.WARNING,
|
|
52
|
+
message=f"Tool {tool.name!r} requires undeclared property/properties: {', '.join(unknown)}.",
|
|
53
|
+
tool=tool.name,
|
|
54
|
+
hint="Every name in `required` must exist under `properties`.",
|
|
55
|
+
)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@tool_rule("AGT022", Severity.INFO, "Schema allows undeclared properties")
|
|
60
|
+
def additional_properties_open(tool: Tool):
|
|
61
|
+
"""Models happily invent extra keys when the schema permits it.
|
|
62
|
+
|
|
63
|
+
``additionalProperties: false`` turns a silent wrong-argument bug into a
|
|
64
|
+
visible validation error.
|
|
65
|
+
"""
|
|
66
|
+
if tool.input_schema and tool.input_schema.get("additionalProperties") is not False:
|
|
67
|
+
return Finding(
|
|
68
|
+
rule="AGT022",
|
|
69
|
+
severity=Severity.INFO,
|
|
70
|
+
message=f"Tool {tool.name!r} schema does not set additionalProperties: false.",
|
|
71
|
+
tool=tool.name,
|
|
72
|
+
hint="Closing the schema stops the model from inventing extra keys.",
|
|
73
|
+
)
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@tool_rule("AGT023", Severity.INFO, "Parameter has no enum or format constraint")
|
|
78
|
+
def unconstrained_string(tool: Tool):
|
|
79
|
+
"""Free-form strings are the main source of retry loops.
|
|
80
|
+
|
|
81
|
+
A ``format`` (date-time, uri, email) or an ``enum`` costs one line and
|
|
82
|
+
removes an entire failure class.
|
|
83
|
+
"""
|
|
84
|
+
offenders: list[str] = []
|
|
85
|
+
for name, spec in tool.properties().items():
|
|
86
|
+
if not isinstance(spec, dict):
|
|
87
|
+
continue
|
|
88
|
+
if spec.get("type") != "string":
|
|
89
|
+
continue
|
|
90
|
+
if not any(k in spec for k in ("enum", "format", "pattern", "const")):
|
|
91
|
+
offenders.append(name)
|
|
92
|
+
if offenders:
|
|
93
|
+
preview = ", ".join(sorted(offenders)[:5])
|
|
94
|
+
more = "" if len(offenders) <= 5 else f" (+{len(offenders) - 5} more)"
|
|
95
|
+
return Finding(
|
|
96
|
+
rule="AGT023",
|
|
97
|
+
severity=Severity.INFO,
|
|
98
|
+
message=f"Tool {tool.name!r} has unconstrained string param(s): {preview}{more}.",
|
|
99
|
+
tool=tool.name,
|
|
100
|
+
hint="Add `enum`, `format` or `pattern` where the value space is finite.",
|
|
101
|
+
)
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@tool_rule("AGT024", Severity.INFO, "Array parameter has no size bound")
|
|
106
|
+
def unbounded_array(tool: Tool):
|
|
107
|
+
"""Without ``maxItems`` a single call can blow the context window."""
|
|
108
|
+
offenders = [
|
|
109
|
+
name
|
|
110
|
+
for name, spec in tool.properties().items()
|
|
111
|
+
if isinstance(spec, dict)
|
|
112
|
+
and spec.get("type") == "array"
|
|
113
|
+
and "maxItems" not in spec
|
|
114
|
+
]
|
|
115
|
+
if offenders:
|
|
116
|
+
return Finding(
|
|
117
|
+
rule="AGT024",
|
|
118
|
+
severity=Severity.INFO,
|
|
119
|
+
message=f"Tool {tool.name!r} array param(s) without maxItems: {', '.join(sorted(offenders)[:5])}.",
|
|
120
|
+
tool=tool.name,
|
|
121
|
+
hint="Set `maxItems` and document the cap in the description.",
|
|
122
|
+
)
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@tool_rule("AGT025", Severity.WARNING, "Parameter name is ambiguous")
|
|
127
|
+
def ambiguous_param_name(tool: Tool):
|
|
128
|
+
"""`data`, `value`, `arg`, `input`, `options` tell the model nothing.
|
|
129
|
+
|
|
130
|
+
The model has to infer the expected shape from the type alone, which is
|
|
131
|
+
exactly where hallucinated payloads come from.
|
|
132
|
+
"""
|
|
133
|
+
vague = {"data", "value", "arg", "args", "input", "param", "options", "obj", "thing"}
|
|
134
|
+
hits = [n for n in tool.properties() if n.lower() in vague]
|
|
135
|
+
if hits:
|
|
136
|
+
return Finding(
|
|
137
|
+
rule="AGT025",
|
|
138
|
+
severity=Severity.WARNING,
|
|
139
|
+
message=f"Tool {tool.name!r} has ambiguous param name(s): {', '.join(sorted(hits))}.",
|
|
140
|
+
tool=tool.name,
|
|
141
|
+
hint="Name the payload for what it is: `markdown_body`, `filter_expression`.",
|
|
142
|
+
)
|
|
143
|
+
return None
|
agentcheckup/tokens.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Rough token estimation with no tokenizer dependency.
|
|
2
|
+
|
|
3
|
+
Why not tiktoken: it ships native wheels per platform and pulls a network
|
|
4
|
+
download at first use. For a linter that runs in CI on arbitrary machines that
|
|
5
|
+
is a bad trade. The heuristic below is intentionally conservative and is
|
|
6
|
+
documented as an estimate everywhere it surfaces.
|
|
7
|
+
|
|
8
|
+
Calibration: OpenAI's cl100k / o200k families average ~4 characters per token
|
|
9
|
+
for English prose and ~1 token per CJK character. JSON punctuation is denser
|
|
10
|
+
than prose, so we apply a slight structural discount.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import unicodedata
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
_CJK_RANGES = (
|
|
20
|
+
(0x3040, 0x30FF), # kana
|
|
21
|
+
(0x3400, 0x4DBF), # CJK ext A
|
|
22
|
+
(0x4E00, 0x9FFF), # CJK unified
|
|
23
|
+
(0xAC00, 0xD7AF), # hangul
|
|
24
|
+
(0xF900, 0xFAFF), # CJK compat
|
|
25
|
+
(0xFF00, 0xFFEF), # fullwidth forms
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _is_cjk(ch: str) -> bool:
|
|
30
|
+
cp = ord(ch)
|
|
31
|
+
return any(lo <= cp <= hi for lo, hi in _CJK_RANGES)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def estimate_tokens(text: str) -> int:
|
|
35
|
+
"""Estimate token count for a string."""
|
|
36
|
+
if not text:
|
|
37
|
+
return 0
|
|
38
|
+
cjk = 0
|
|
39
|
+
other = 0
|
|
40
|
+
for ch in text:
|
|
41
|
+
if _is_cjk(ch):
|
|
42
|
+
cjk += 1
|
|
43
|
+
else:
|
|
44
|
+
other += 1
|
|
45
|
+
return cjk + max(1, round(other / 4)) if other else cjk
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def estimate_json_tokens(payload: Any) -> int:
|
|
49
|
+
"""Estimate tokens for a JSON-serialisable structure."""
|
|
50
|
+
dumped = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
|
|
51
|
+
return estimate_tokens(dumped)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def manifest_tokens(tools: list[Any]) -> int:
|
|
55
|
+
"""Estimate how many tokens the whole tool manifest costs in context.
|
|
56
|
+
|
|
57
|
+
This is the number an agent pays on *every* request, so it matters more
|
|
58
|
+
than any single description.
|
|
59
|
+
"""
|
|
60
|
+
return estimate_json_tokens([t.raw if hasattr(t, "raw") else t for t in tools])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def has_invisible_chars(text: str) -> list[str]:
|
|
64
|
+
"""Return code points of characters that hide text from human review."""
|
|
65
|
+
suspects: list[str] = []
|
|
66
|
+
for ch in text:
|
|
67
|
+
if ch in ("\u200b", "\u200c", "\u200d", "\u2060", "\ufeff") or unicodedata.category(ch) == "Cf":
|
|
68
|
+
suspects.append(f"U+{ord(ch):04X}")
|
|
69
|
+
return sorted(set(suspects))
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agentcheckup
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Diagnose your agent setup: tool conflicts, risky config, context budget.
|
|
5
|
+
Project-URL: Homepage, https://github.com/wanjiaqing/agentcheckup
|
|
6
|
+
Project-URL: Issues, https://github.com/wanjiaqing/agentcheckup/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/wanjiaqing/agentcheckup/blob/main/CHANGELOG.md
|
|
8
|
+
Author: wanjiaqing
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,ai,cli,linter,mcp,model-context-protocol,security
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# agentcheckup
|
|
30
|
+
|
|
31
|
+
> Diagnose your agent setup before your agent does.
|
|
32
|
+
|
|
33
|
+
[](https://github.com/wanjiaqing/agentcheckup/actions/workflows/ci.yml)
|
|
34
|
+
[](https://pypi.org/project/agentcheckup/)
|
|
35
|
+
[](https://pypi.org/project/agentcheckup/)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+
|
|
38
|
+
Every other MCP tool looks at **one server**. None of them look at what happens
|
|
39
|
+
when you load five.
|
|
40
|
+
|
|
41
|
+
```console
|
|
42
|
+
$ agentcheckup
|
|
43
|
+
|
|
44
|
+
agentcheckup 0.4.0 | 3 server(s) | 4 tool(s) | ~442 tokens per request
|
|
45
|
+
|
|
46
|
+
Grade C Score 79/100 1 error 2 warning 1 info
|
|
47
|
+
|
|
48
|
+
CONFLICTS (1)
|
|
49
|
+
AGT201 Tool name 'read_file' is exposed by both 'files' and 'git'.
|
|
50
|
+
[files/read_file <-> git/read_file]
|
|
51
|
+
fix: Hard collision: the client keeps one and the other is
|
|
52
|
+
unreachable. Rename one, or use a gateway that prefixes.
|
|
53
|
+
|
|
54
|
+
RISKS (2)
|
|
55
|
+
AGT101 Server 'github' runs unpinned npx package
|
|
56
|
+
'@modelcontextprotocol/server-github'.
|
|
57
|
+
AGT107 Server 'github' stores credential(s) inline:
|
|
58
|
+
GITHUB_PERSONAL_ACCESS_TOKEN.
|
|
59
|
+
|
|
60
|
+
NUDGES (1 across 1 rule(s), no score impact)
|
|
61
|
+
AGT105 Server did not return a tool list (1)
|
|
62
|
+
github
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## The problem nobody is testing for
|
|
66
|
+
|
|
67
|
+
Two servers cannot see each other. Your model can. It gets a flat list of 47
|
|
68
|
+
tools from five servers and picks by description similarity — with no way to know
|
|
69
|
+
that `files.read_file` and `git.read_file` are different things.
|
|
70
|
+
|
|
71
|
+
Three failure modes follow, and no existing tool catches them:
|
|
72
|
+
|
|
73
|
+
**1. Hard collision.** Two servers expose the same tool name. The client keeps
|
|
74
|
+
one, the other is silently unreachable. You spend an afternoon debugging "why
|
|
75
|
+
won't it read the file" when the answer is "it's calling the other one".
|
|
76
|
+
|
|
77
|
+
**2. Soft collision.** `github.search` and `docs.search` with 70% overlapping
|
|
78
|
+
descriptions. Selection becomes a coin flip that looks exactly like model
|
|
79
|
+
flakiness. It isn't — it's your manifest.
|
|
80
|
+
|
|
81
|
+
**3. Context tax.** An agent pays for the full tool manifest on **every** request.
|
|
82
|
+
47 tools with 200-character descriptions is thousands of tokens before the user
|
|
83
|
+
says anything. Nobody budgets for it because nobody measures it.
|
|
84
|
+
|
|
85
|
+
`agentcheckup` measures all three, in one command, from the configs you already
|
|
86
|
+
have.
|
|
87
|
+
|
|
88
|
+
## What it checks
|
|
89
|
+
|
|
90
|
+
| Layer | Question it answers | Rules |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| **Conflicts** | Will my agent pick the wrong tool? | AGT201–AGT203 |
|
|
93
|
+
| **Risks** | Is my setup unsafe? | AGT001–AGT107 |
|
|
94
|
+
| **Budget** | What does my manifest cost per request? | reported on every run |
|
|
95
|
+
|
|
96
|
+
36 rules in total. Full reference: [docs/rules.md](docs/rules.md).
|
|
97
|
+
|
|
98
|
+
## Install
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install agentcheckup
|
|
102
|
+
# or run without installing
|
|
103
|
+
uvx agentcheckup
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Python 3.10+. **Zero runtime dependencies** — no SDK, no tokenizer, no embeddings,
|
|
107
|
+
no network calls. It reads JSON, starts processes, prints text.
|
|
108
|
+
|
|
109
|
+
## Usage
|
|
110
|
+
|
|
111
|
+
### One command, no arguments
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
agentcheckup
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Finds your configs automatically — 27 known locations across Claude Desktop,
|
|
118
|
+
Claude Code (including its per-project `mcpServers`), Cursor, VS Code, Cline,
|
|
119
|
+
Roo Code, Kilo Code, Windsurf, Zed, Codex CLI and Continue, plus project-local
|
|
120
|
+
`.mcp.json` / `.cursor/mcp.json` / `.vscode/mcp.json` — starts each server,
|
|
121
|
+
collects its tools, and reports.
|
|
122
|
+
|
|
123
|
+
Servers that fail to start are reported as `AGT105` and skipped, not fatal. A
|
|
124
|
+
broken server should not stop you auditing the other four.
|
|
125
|
+
|
|
126
|
+
### Common flags
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
agentcheckup --no-spawn # configs only, no processes started
|
|
130
|
+
agentcheckup --config path.json # add a config the discovery missed
|
|
131
|
+
agentcheckup --no-discover # audit only what you passed with --config
|
|
132
|
+
agentcheckup --timeout 40 # slow server? give it longer
|
|
133
|
+
agentcheckup --format json # machine-readable
|
|
134
|
+
agentcheckup --fail-on warning # CI gate
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### One server at a time
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
agentcheckup scan tools.json # {"tools": [...]}, {"result": {...}} or a log
|
|
141
|
+
agentcheckup scan --cmd "python my_server.py" # live stdio handshake
|
|
142
|
+
agentcheckup config ~/.claude.json # config rules only
|
|
143
|
+
agentcheckup rules # list every rule
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### As a GitHub Action
|
|
147
|
+
|
|
148
|
+
The fastest way to keep a manifest from regressing, and the version you pin is
|
|
149
|
+
the version you run — the action installs from its own checkout.
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
- uses: wanjiaqing/agentcheckup@v0.3.0
|
|
153
|
+
with:
|
|
154
|
+
config: .mcp.json
|
|
155
|
+
fail-on: warning
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Full reference, including SARIF upload and score gates: [docs/github-action.md](docs/github-action.md).
|
|
159
|
+
|
|
160
|
+
### Raw CLI in CI
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
- name: Diagnose agent setup
|
|
164
|
+
run: |
|
|
165
|
+
pip install agentcheckup
|
|
166
|
+
agentcheckup --no-spawn --fail-on warning
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
For GitHub code scanning, emit SARIF:
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
- run: agentcheckup --format sarif --fail-on never > agentcheckup.sarif
|
|
173
|
+
- uses: github/codeql-action/upload-sarif@v4
|
|
174
|
+
with:
|
|
175
|
+
sarif_file: agentcheckup.sarif
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
One run can produce all three outputs:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
agentcheckup --format text --json-out report.json --sarif-out report.sarif
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Exit codes: `0` clean, `1` findings at or above `--fail-on`, `2` usage/load error.
|
|
185
|
+
|
|
186
|
+
## How conflicts are detected
|
|
187
|
+
|
|
188
|
+
No embeddings, no model, no network — because a linter that needs a 200MB wheel
|
|
189
|
+
is a linter that stops running on a plane.
|
|
190
|
+
|
|
191
|
+
Each tool pair across servers is scored on two signals:
|
|
192
|
+
|
|
193
|
+
- **name overlap** — normalised token Jaccard (`read_files` ≡ `read_file`)
|
|
194
|
+
- **description overlap** — token Jaccard over stopword-stripped, plural-stemmed words
|
|
195
|
+
|
|
196
|
+
A pair is flagged when the descriptions overlap ≥ 65% **and** the names share a
|
|
197
|
+
head noun, or when the blended score clears 0.55. The shared-noun requirement is
|
|
198
|
+
what keeps `search_repositories` and `search_documents` apart — without it the
|
|
199
|
+
false-positive rate made the report untrustworthy, which is worse than no report.
|
|
200
|
+
|
|
201
|
+
It is a heuristic, and the docs say so. `AGT203` is a warning, not a verdict.
|
|
202
|
+
|
|
203
|
+
## How the score works
|
|
204
|
+
|
|
205
|
+
Most linters fail here: a rule that fires on 18 of 18 tools is a *preference*,
|
|
206
|
+
not a defect, and charging one point each scores the official MCP servers 0/100.
|
|
207
|
+
That number is useless, so the model is calibrated instead of invented:
|
|
208
|
+
|
|
209
|
+
| Severity | Cost | Why |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `error` | 10 | breaks a run, or endangers the host |
|
|
212
|
+
| `warning` | 3 | likely to cause wrong behaviour |
|
|
213
|
+
| `info` | 1, **capped at 10 total** | a human should decide |
|
|
214
|
+
|
|
215
|
+
Budget is **20 points per tool**, so a 200-tool manifest is not punished for
|
|
216
|
+
being large, and a 4-tool manifest with one hard collision is not let off easy.
|
|
217
|
+
|
|
218
|
+
Calibration against four real inputs:
|
|
219
|
+
|
|
220
|
+
| Input | Tools | Result |
|
|
221
|
+
|---|---|---|
|
|
222
|
+
| Clean fixture | 2 | A · 100 |
|
|
223
|
+
| Official `fetch` + `git` + `time` servers | 18 | B · 87 |
|
|
224
|
+
| Hard collision + unpinned package + plaintext token | 4 | C · 79 |
|
|
225
|
+
| Deliberately broken fixture | 7 | F · 36 |
|
|
226
|
+
|
|
227
|
+
Repeated findings are collapsed: 12 tools missing parameter descriptions print
|
|
228
|
+
as one line naming all twelve, and info findings are summarised one line per
|
|
229
|
+
rule. A report nobody can read is a report nobody acts on.
|
|
230
|
+
|
|
231
|
+
## Design principles
|
|
232
|
+
|
|
233
|
+
- **Zero runtime dependencies.** A diagnostic that breaks because a transitive
|
|
234
|
+
dep broke is worse than no diagnostic.
|
|
235
|
+
- **Stable rule IDs.** `AGT201` never changes meaning. Suppressions and CI
|
|
236
|
+
thresholds keep working across releases.
|
|
237
|
+
- **Every finding has a fix.** A rule that only says "this is bad" trains people
|
|
238
|
+
to ignore the output.
|
|
239
|
+
- **Degrade, don't die.** A server that will not start is one finding, not an
|
|
240
|
+
aborted run.
|
|
241
|
+
- **No network calls, ever.** Your config never leaves the machine. This is the
|
|
242
|
+
one thing a commercial scanner structurally cannot promise.
|
|
243
|
+
- **Heuristics are labelled as heuristics.** `info` means "a human should
|
|
244
|
+
decide". The score is a conversation starter, not a certification.
|
|
245
|
+
|
|
246
|
+
## What this is not
|
|
247
|
+
|
|
248
|
+
- Not a sandbox. It tells you the manifest looks risky; it cannot stop the server.
|
|
249
|
+
- Not a runtime guard. Nothing here runs during an agent session.
|
|
250
|
+
- Not an MCP protocol validator. It checks quality and safety, not conformance —
|
|
251
|
+
the official `modelcontextprotocol/conformance` suite does that.
|
|
252
|
+
|
|
253
|
+
## Contributing
|
|
254
|
+
|
|
255
|
+
Adding a rule takes about ten minutes and one small file — see
|
|
256
|
+
[CONTRIBUTING.md](CONTRIBUTING.md). Rules and client adapters are the whole point
|
|
257
|
+
of this project, so those PRs are the most welcome kind.
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
agentcheckup/__init__.py,sha256=7I4aeGtWdYsj3cMwvyxRw6oBYNaUqE2ej7LrGOdwqs0,664
|
|
2
|
+
agentcheckup/__main__.py,sha256=UP0yjf5bUzmplYRElA6c5FuXZNu3sTDMJfOpZsy3Bqg,115
|
|
3
|
+
agentcheckup/cli.py,sha256=e-gEeRx3uu4IXCs64-MaCP7vVfqwgXcNAmJk9PCTolU,8322
|
|
4
|
+
agentcheckup/conflicts.py,sha256=60Q4r5NV1O6Bh-JDAkiNXj2RhNBDhIb8HWeJD9-_BTA,8683
|
|
5
|
+
agentcheckup/discovery.py,sha256=S6kz4FndIydQYcW-Ttyfer8nhidD_NRX3fmw2DokEew,6906
|
|
6
|
+
agentcheckup/engine.py,sha256=MzppSFmp4REQ4jMZYlTrula7eYjNcX8hYQvGI-6NvEk,4625
|
|
7
|
+
agentcheckup/loader.py,sha256=zVxkyWdW9sW1aP9EMSKRKhnLAKb-v3kEqYlxZMuQ3ts,7584
|
|
8
|
+
agentcheckup/models.py,sha256=YzaDPYKTwXPqVtAnSZSp_cAaGLIwKouM_P__R9ZIJZU,5066
|
|
9
|
+
agentcheckup/report.py,sha256=vykhYz_oVQwScRXk-95Ae51jtKON-ZdpuGFmW7BDPd8,9733
|
|
10
|
+
agentcheckup/tokens.py,sha256=BEAWi0iF4CP2BQRjGO3ozIiauMUosJ-OYBu0807wFkE,2190
|
|
11
|
+
agentcheckup/rules/__init__.py,sha256=9N49niQgWcrVUWhrqnDS2KTI8y7T0LwYf7QXdUMlLlE,371
|
|
12
|
+
agentcheckup/rules/base.py,sha256=i3lZ6EutpHKTTlunHeEQ6uV1AefRTMsM6yPBpameAZw,2567
|
|
13
|
+
agentcheckup/rules/config_rules.py,sha256=wpvh6_Exd2srAWr13hnZGTlrtRTBz5wwfpOE_TCVE3g,6537
|
|
14
|
+
agentcheckup/rules/docs.py,sha256=g1cVvz99y87nNYd_qWtsfHZpwELohAf-VXEOCyhR0zI,4434
|
|
15
|
+
agentcheckup/rules/injection.py,sha256=pRvDDff2zZAWeHE1NTh1zmMM36bTI8a9pgnoAJcZuow,5330
|
|
16
|
+
agentcheckup/rules/naming.py,sha256=c9bPgbqmojNVYo1jR7AQ8w9ns0spox6fWt-eBB-qg0Y,3315
|
|
17
|
+
agentcheckup/rules/safety.py,sha256=PTodMfCSvIAkDdqAMQc-Y_ZVxVm1JwxxRzQjCOCW83E,4947
|
|
18
|
+
agentcheckup/rules/schema.py,sha256=b3Ft-DxbKPMJA1eVQwmQ8B5O7I5DqaM0UDRDp1Rn2ks,5374
|
|
19
|
+
agentcheckup-0.4.0.dist-info/METADATA,sha256=VDSo_e-pv-ps7U4QQ-gf0lII75XtA5JyECcvqw0swiU,9702
|
|
20
|
+
agentcheckup-0.4.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
21
|
+
agentcheckup-0.4.0.dist-info/entry_points.txt,sha256=OlI4_uXpibTwRdGpYauao_fpFZ0ddNXq-jnMgROTiHY,55
|
|
22
|
+
agentcheckup-0.4.0.dist-info/licenses/LICENSE,sha256=wPwTNSFI27eV7y5LmMjg1J2GZaOVrpyC5AQB1-6H46o,1067
|
|
23
|
+
agentcheckup-0.4.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wanjiaqing
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|