mcp-reportcard 0.1.0__tar.gz
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.
- mcp_reportcard-0.1.0/.env.example +2 -0
- mcp_reportcard-0.1.0/.github/workflows/ci.yml +24 -0
- mcp_reportcard-0.1.0/.github/workflows/release.yml +40 -0
- mcp_reportcard-0.1.0/.gitignore +10 -0
- mcp_reportcard-0.1.0/FINDINGS.md +35 -0
- mcp_reportcard-0.1.0/LICENSE +21 -0
- mcp_reportcard-0.1.0/PKG-INFO +198 -0
- mcp_reportcard-0.1.0/README.md +167 -0
- mcp_reportcard-0.1.0/examples/bad_server.py +65 -0
- mcp_reportcard-0.1.0/examples/bad_tools.json +215 -0
- mcp_reportcard-0.1.0/examples/good_server.py +57 -0
- mcp_reportcard-0.1.0/examples/good_tools.json +118 -0
- mcp_reportcard-0.1.0/pyproject.toml +47 -0
- mcp_reportcard-0.1.0/space/README.md +28 -0
- mcp_reportcard-0.1.0/space/build.sh +16 -0
- mcp_reportcard-0.1.0/space/deploy.sh +13 -0
- mcp_reportcard-0.1.0/space/index.html +197 -0
- mcp_reportcard-0.1.0/src/mcp_reportcard/__init__.py +13 -0
- mcp_reportcard-0.1.0/src/mcp_reportcard/analyze.py +102 -0
- mcp_reportcard-0.1.0/src/mcp_reportcard/cli.py +193 -0
- mcp_reportcard-0.1.0/src/mcp_reportcard/connect.py +70 -0
- mcp_reportcard-0.1.0/src/mcp_reportcard/fuzz.py +207 -0
- mcp_reportcard-0.1.0/src/mcp_reportcard/loader.py +49 -0
- mcp_reportcard-0.1.0/src/mcp_reportcard/models.py +109 -0
- mcp_reportcard-0.1.0/src/mcp_reportcard/report.py +127 -0
- mcp_reportcard-0.1.0/src/mcp_reportcard/rules.py +317 -0
- mcp_reportcard-0.1.0/src/mcp_reportcard/tokens.py +48 -0
- mcp_reportcard-0.1.0/tests/test_cli_web.py +54 -0
- mcp_reportcard-0.1.0/tests/test_fuzz.py +86 -0
- mcp_reportcard-0.1.0/tests/test_rules.py +120 -0
- mcp_reportcard-0.1.0/web/app.py +65 -0
- mcp_reportcard-0.1.0/web/passenger_wsgi.py +7 -0
- mcp_reportcard-0.1.0/web/requirements.txt +1 -0
- mcp_reportcard-0.1.0/web/templates/index.html +131 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
15
|
+
python: ["3.10", "3.12", "3.13"]
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python }}
|
|
21
|
+
- run: pip install -e ".[dev]"
|
|
22
|
+
- run: pytest -q
|
|
23
|
+
- name: Dogfood on the good example
|
|
24
|
+
run: mcp-reportcard check "python examples/good_server.py" --fuzz --fail-under 90 -f markdown
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Release to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishes when a GitHub Release is published (e.g. tag v0.1.0).
|
|
4
|
+
# Uses PyPI Trusted Publishing (OIDC): no API token is stored anywhere.
|
|
5
|
+
# One-time setup on pypi.org: add a "pending publisher" for this repo,
|
|
6
|
+
# workflow file `release.yml`, environment `pypi`.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
release:
|
|
10
|
+
types: [published]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
- run: pip install build twine
|
|
21
|
+
- run: python -m build
|
|
22
|
+
- run: twine check dist/*
|
|
23
|
+
- uses: actions/upload-artifact@v4
|
|
24
|
+
with:
|
|
25
|
+
name: dist
|
|
26
|
+
path: dist/
|
|
27
|
+
|
|
28
|
+
publish:
|
|
29
|
+
needs: build
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
environment:
|
|
32
|
+
name: pypi
|
|
33
|
+
permissions:
|
|
34
|
+
id-token: write # required for Trusted Publishing
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/download-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: dist
|
|
39
|
+
path: dist/
|
|
40
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# FINDINGS — mcp-reportcard field test
|
|
2
|
+
|
|
3
|
+
Date: 2026-11-14 · mcp-reportcard v0.1.0 · Windows 11, Python 3.13, Node 22 · every run used `--fuzz` (read-only tools only)
|
|
4
|
+
|
|
5
|
+
## Servers tested
|
|
6
|
+
| Server | Version | Language | Transport |
|
|
7
|
+
|---|---|---|---|
|
|
8
|
+
| @modelcontextprotocol/server-everything | 2.3.0 | Node | stdio **and** Streamable HTTP |
|
|
9
|
+
| @modelcontextprotocol/server-filesystem | 2.3.0 | Node | stdio |
|
|
10
|
+
| @modelcontextprotocol/server-memory | 2.1.1 | Node | stdio |
|
|
11
|
+
| mcp-server-fetch | 2026.11.11 | Python | stdio |
|
|
12
|
+
| mcp-server-time | 2026.11.11 | Python | stdio |
|
|
13
|
+
| mcp-server-git | 2026.11.11 | Python | stdio |
|
|
14
|
+
|
|
15
|
+
All six connected and completed without crashes. 159 fuzz cases were sent in total.
|
|
16
|
+
|
|
17
|
+
## False positives found and fixed
|
|
18
|
+
1. **F002 "leaks internals" fired on file paths.** The filesystem and git servers return messages like `Repository path 'x' is outside the allowed repository 'C:\...\repo'`. That's helpful, not a leak. F002 now matches only real stack traces (Python, Node, JVM, Go, Rust). The filesystem server went from B 88 to A 100.
|
|
19
|
+
2. **D005 "no when-to-use guidance" was noise.** It fired on 30 tools across well-written servers, e.g. fetch's single tool. It now fires only when a tool has a similar sibling (≥35% word overlap) the model could confuse it with, and it names that sibling.
|
|
20
|
+
3. **Connection failures printed an unreadable `ExceptionGroup`.** The CLI now digs out the root cause, e.g. "Connection closed (the server exited during startup; rerun with --server-logs to see its output)".
|
|
21
|
+
|
|
22
|
+
## Final results (after fixes)
|
|
23
|
+
| Server | Grade | Score | Tools | Tokens/turn | Fuzz cases | Fuzz problems | Remaining issues |
|
|
24
|
+
|---|---|---:|---:|---:|---:|---:|---|
|
|
25
|
+
| everything | A | 94 | 22 | 2,797 | 32 | 0 | X001 (two async trigger tools described almost identically), D005 ×6 |
|
|
26
|
+
| fetch | A | 100 | 1 | 296 | 6 | 0 | none |
|
|
27
|
+
| filesystem | A | 100 | 16 | 2,925 | 46 | 0 | none |
|
|
28
|
+
| memory | A | 99 | 9 | 1,736 | 10 | 0 | D005 ×2 |
|
|
29
|
+
| time | A | 98 | 3 | 500 | 20 | 0 | D005 ×2 |
|
|
30
|
+
| git | B | 83 | 14 | 1,435 | 45 | 0 | S003 (parameters have a `title` but no `description`), D002, D005 |
|
|
31
|
+
|
|
32
|
+
## Assessment
|
|
33
|
+
- **The remaining findings look legitimate.** git's undocumented parameters are a real, common Python issue: pydantic generates `title` but not `description`. The everything server's two async trigger tools really are described almost word for word alike.
|
|
34
|
+
- **Fuzzing:** every server rejected bad input cleanly. Skipped tools were exactly the ones not annotated read-only, as designed.
|
|
35
|
+
- **Not covered yet:** CI on Linux and macOS (runs once pushed to GitHub), community servers outside the official set, and servers that need authentication.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Scaleup Solutions
|
|
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.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mcp-reportcard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A health check for MCP servers: lint tool descriptions and schemas, estimate token cost, fuzz error handling, and get a graded report card.
|
|
5
|
+
Project-URL: Homepage, https://scaleup-solutions.in/
|
|
6
|
+
Project-URL: Repository, https://github.com/builditwithgk/mcp-reportcard
|
|
7
|
+
Project-URL: Issues, https://github.com/builditwithgk/mcp-reportcard/issues
|
|
8
|
+
Author: Gopi K Aitham (builditwithgk)
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-agents,fuzzing,lint,llm,mcp,model-context-protocol
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
+
Classifier: Topic :: Software Development :: Testing
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: mcp>=2.0
|
|
20
|
+
Requires-Dist: rich>=13
|
|
21
|
+
Requires-Dist: typer>=0.12
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: flask>=3.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
25
|
+
Requires-Dist: tiktoken>=0.7; extra == 'dev'
|
|
26
|
+
Provides-Extra: tokens
|
|
27
|
+
Requires-Dist: tiktoken>=0.7; extra == 'tokens'
|
|
28
|
+
Provides-Extra: web
|
|
29
|
+
Requires-Dist: flask>=3.0; extra == 'web'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# mcp-reportcard
|
|
33
|
+
|
|
34
|
+
> **A health check for MCP servers.** Lint your tools, measure their token cost, fuzz their error handling — and get a graded report card with a concrete fix for every issue.
|
|
35
|
+
|
|
36
|
+
    [](https://github.com/builditwithgk/mcp-reportcard/actions/workflows/ci.yml) [](https://huggingface.co/spaces/builditwithgk/mcp-reportcard)
|
|
37
|
+
|
|
38
|
+
When an agent picks the wrong tool, sends malformed arguments, or burns context on bloated definitions, the cause is usually in the server — and the model only ever sees what your `tools/list` says. `mcp-reportcard` checks exactly that, in seconds, for any MCP server in any language.
|
|
39
|
+
|
|
40
|
+
**Try it in the browser:** [huggingface.co/spaces/builditwithgk/mcp-reportcard](https://huggingface.co/spaces/builditwithgk/mcp-reportcard) — paste a `tools/list` JSON, get the report card.
|
|
41
|
+
|
|
42
|
+
## Why it's different
|
|
43
|
+
|
|
44
|
+
**It grades what the model sees, not what your code does.**
|
|
45
|
+
|
|
46
|
+
The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) is for poking at a server by hand. `mcp-reportcard` is automated and opinionated: it scores tool names, descriptions, schemas and safety annotations against the things that actually make models misbehave — vague descriptions, undocumented parameters, near-duplicate tools, missing `readOnlyHint`s, stack traces in error replies — and it runs in CI.
|
|
47
|
+
|
|
48
|
+
Every finding comes with a rule ID, a severity and a specific fix. No LLM is involved in the checks, so results are deterministic and safe to gate a merge on.
|
|
49
|
+
|
|
50
|
+
## Example output
|
|
51
|
+
|
|
52
|
+
Running against the deliberately sloppy server in [`examples/bad_server.py`](https://github.com/builditwithgk/mcp-reportcard/blob/main/examples/bad_server.py):
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
┌──────── mcp-reportcard · acme-orders (bad example) ─────────┐
|
|
56
|
+
│ Grade D · score 67/100 │
|
|
57
|
+
│ 7 tools · 1 errors · 16 warnings · 5 info │
|
|
58
|
+
│ ~351 tokens per turn (tiktoken o200k_base (approximate)) │
|
|
59
|
+
└──────────────────────────────────────────────────────────┘
|
|
60
|
+
F002 lookup_invoice Error output exposes internals: `Traceback (most recent call last)`
|
|
61
|
+
fix: Log stack traces server-side; return a short, safe message to the client.
|
|
62
|
+
X001 server `find_items` and `search_items` are described almost the same way (88% word overlap)
|
|
63
|
+
fix: Say in each description when to use it instead of the other, or merge them.
|
|
64
|
+
...
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The same service written properly, [`examples/good_server.py`](https://github.com/builditwithgk/mcp-reportcard/blob/main/examples/good_server.py), scores **A (100)**.
|
|
68
|
+
|
|
69
|
+
**Field-tested:** run against six official MCP reference servers (Node and Python, stdio and HTTP) — 159 fuzz cases, zero crashes, and two over-eager rules fixed as a result. Numbers and details in [`FINDINGS.md`](https://github.com/builditwithgk/mcp-reportcard/blob/main/FINDINGS.md).
|
|
70
|
+
|
|
71
|
+
## Install & run
|
|
72
|
+
|
|
73
|
+
Requires Python 3.10+.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install "mcp-reportcard[tokens] @ git+https://github.com/builditwithgk/mcp-reportcard"
|
|
77
|
+
|
|
78
|
+
# …or from source
|
|
79
|
+
git clone https://github.com/builditwithgk/mcp-reportcard
|
|
80
|
+
cd mcp-reportcard
|
|
81
|
+
pip install -e ".[dev]"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A PyPI release (`pip install mcp-reportcard`) is coming. The `[tokens]` extra adds tiktoken for more accurate token counts; without it, a characters/4 estimate is used.
|
|
85
|
+
|
|
86
|
+
## Command line
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# A stdio server (any language): pass the command that starts it
|
|
90
|
+
mcp-reportcard check "python server.py"
|
|
91
|
+
mcp-reportcard check "npx -y @modelcontextprotocol/server-everything"
|
|
92
|
+
|
|
93
|
+
# A Streamable HTTP server
|
|
94
|
+
mcp-reportcard check http://localhost:8000/mcp
|
|
95
|
+
|
|
96
|
+
# A saved tools/list JSON (offline, no server needed)
|
|
97
|
+
mcp-reportcard check tools.json
|
|
98
|
+
|
|
99
|
+
# Also test error handling with bad inputs (read-only tools only — see below)
|
|
100
|
+
mcp-reportcard check "python server.py" --fuzz
|
|
101
|
+
|
|
102
|
+
# Reports for PRs and CI
|
|
103
|
+
mcp-reportcard check "python server.py" -f markdown -o report.md
|
|
104
|
+
mcp-reportcard check "python server.py" -f json --fail-under 80
|
|
105
|
+
|
|
106
|
+
# Save a server's tool list (for offline checks or the web demo)
|
|
107
|
+
mcp-reportcard dump "python server.py" -o tools.json
|
|
108
|
+
|
|
109
|
+
# List every check
|
|
110
|
+
mcp-reportcard rules
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Use in CI
|
|
114
|
+
|
|
115
|
+
`--fail-under` exits 1 when the score drops below the threshold:
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
# .github/workflows/mcp-reportcard.yml
|
|
119
|
+
on: pull_request
|
|
120
|
+
jobs:
|
|
121
|
+
mcp-reportcard:
|
|
122
|
+
runs-on: ubuntu-latest
|
|
123
|
+
steps:
|
|
124
|
+
- uses: actions/checkout@v4
|
|
125
|
+
- uses: actions/setup-python@v5
|
|
126
|
+
with: { python-version: "3.12" }
|
|
127
|
+
- run: pip install "mcp-reportcard @ git+https://github.com/builditwithgk/mcp-reportcard"
|
|
128
|
+
- run: mcp-reportcard check "python server.py" --fuzz -f markdown -o mcp-reportcard.md --fail-under 80
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
This repo dogfoods it: CI runs `mcp-reportcard` against [`examples/good_server.py`](https://github.com/builditwithgk/mcp-reportcard/blob/main/examples/good_server.py) with `--fail-under 90` on Linux, macOS and Windows.
|
|
132
|
+
|
|
133
|
+
## What it checks
|
|
134
|
+
|
|
135
|
+
| ID | Severity | Check |
|
|
136
|
+
| --- | --- | --- |
|
|
137
|
+
| N001–N005 | error / warning / info | Missing, duplicate, invalid or over-long names; mixed naming styles |
|
|
138
|
+
| D001–D005 | error / warning / info | Missing, too short, too long, or name-restating descriptions; no "when to use" guidance when a similar tool exists |
|
|
139
|
+
| S001–S008 | error / warning / info | Missing object schema, untyped or undocumented parameters, broken `required`, free-form objects, deep nesting, missing `enum`, too many parameters |
|
|
140
|
+
| A001–A002 | warning | Missing or contradictory `readOnlyHint` / `destructiveHint` |
|
|
141
|
+
| T001, X002 | info / warning | Heavy tool definitions; total token cost per turn |
|
|
142
|
+
| X001, X003, X004 | warning / error | Tools with overlapping descriptions; too many tools; no tools |
|
|
143
|
+
| F001–F003 | warning / error | (fuzz) Accepts schema-violating input; leaks stack traces; crashes or hangs |
|
|
144
|
+
|
|
145
|
+
**Scoring:** each tool starts at 100 and loses 25 per error, 10 per warning and 3 per info. The server score is the average tool score minus server-level penalties (capped at 40). Grades: A ≥ 90, B ≥ 80, C ≥ 70, D ≥ 60, F below.
|
|
146
|
+
|
|
147
|
+
## Fuzzing safety
|
|
148
|
+
|
|
149
|
+
`--fuzz` calls your tools with missing arguments, wrong types, out-of-enum values, 10,000-character strings and tricky Unicode, then checks for clean `isError` responses.
|
|
150
|
+
|
|
151
|
+
- By default it **only calls tools annotated `readOnlyHint: true`** (and not `destructiveHint: true`). Everything else is skipped — and listed in the report, never silently.
|
|
152
|
+
- `--allow-unsafe` fuzzes every tool. **Only use it against a test instance**, never production data.
|
|
153
|
+
- A stdio server's own stderr is hidden to keep the report readable; add `--server-logs` to see it.
|
|
154
|
+
|
|
155
|
+
## Web demo
|
|
156
|
+
|
|
157
|
+
The demo runs the static checks only — it never connects to servers, calls tools or contacts an LLM, and nothing pasted is stored. Because the core is pure standard-library Python, it ships two ways from the same code:
|
|
158
|
+
|
|
159
|
+
- **In the browser** — [`space/`](https://github.com/builditwithgk/mcp-reportcard/tree/main/space) is a static page that runs `src/mcp_reportcard` client-side with [Pyodide](https://pyodide.org). That's the [Hugging Face Space](https://huggingface.co/spaces/builditwithgk/mcp-reportcard): no server, free tier. `bash space/build.sh` assembles it into `space/_site/`; `bash space/deploy.sh` publishes it. The same folder works on GitHub Pages or any static host.
|
|
160
|
+
- **As a Flask app** — [`web/`](https://github.com/builditwithgk/mcp-reportcard/tree/main/web), for Python hosting such as cPanel: create a Python app with `web/` as the root and `passenger_wsgi.py` as the startup file, install `web/requirements.txt`, and upload `src/` and `examples/` alongside.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
pip install flask
|
|
164
|
+
python web/app.py # http://localhost:5000
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Limitations
|
|
168
|
+
|
|
169
|
+
- Token counts are **estimates**. Each provider wraps tool definitions in its own format; use the numbers to compare tools and track changes, not as a bill.
|
|
170
|
+
- Description checks are heuristics. A D005 ("no usage guidance") is a nudge, not a hard rule.
|
|
171
|
+
- Leak detection (F002) looks for stack traces only. File paths in error messages are not flagged — file and git tools legitimately name paths.
|
|
172
|
+
- Fuzzing can't prove a tool is correct; it shows how it fails on bad input.
|
|
173
|
+
- v1 checks tools only, not resources or prompts.
|
|
174
|
+
|
|
175
|
+
## Roadmap
|
|
176
|
+
|
|
177
|
+
- Tool-selection eval (bring your own API key): generate realistic tasks per tool and measure how often a model picks the right one.
|
|
178
|
+
- README badge (`mcp-reportcard: A`).
|
|
179
|
+
- Resource and prompt checks.
|
|
180
|
+
- PyPI release.
|
|
181
|
+
|
|
182
|
+
## Development
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
186
|
+
pip install -e ".[dev]"
|
|
187
|
+
pytest
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Tests cover every lint rule, scoring, leak detection, fuzz-case generation, the CLI and the web demo — plus end-to-end runs against both example servers, in-process and as real stdio subprocesses.
|
|
191
|
+
|
|
192
|
+
## Author
|
|
193
|
+
|
|
194
|
+
Built by **Gopi K Aitham** ([builditwithgk](https://github.com/builditwithgk)) — see [scaleup-solutions.in](https://scaleup-solutions.in/). Available for freelance and contract work on AI, agent, and MCP tooling.
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# mcp-reportcard
|
|
2
|
+
|
|
3
|
+
> **A health check for MCP servers.** Lint your tools, measure their token cost, fuzz their error handling — and get a graded report card with a concrete fix for every issue.
|
|
4
|
+
|
|
5
|
+
    [](https://github.com/builditwithgk/mcp-reportcard/actions/workflows/ci.yml) [](https://huggingface.co/spaces/builditwithgk/mcp-reportcard)
|
|
6
|
+
|
|
7
|
+
When an agent picks the wrong tool, sends malformed arguments, or burns context on bloated definitions, the cause is usually in the server — and the model only ever sees what your `tools/list` says. `mcp-reportcard` checks exactly that, in seconds, for any MCP server in any language.
|
|
8
|
+
|
|
9
|
+
**Try it in the browser:** [huggingface.co/spaces/builditwithgk/mcp-reportcard](https://huggingface.co/spaces/builditwithgk/mcp-reportcard) — paste a `tools/list` JSON, get the report card.
|
|
10
|
+
|
|
11
|
+
## Why it's different
|
|
12
|
+
|
|
13
|
+
**It grades what the model sees, not what your code does.**
|
|
14
|
+
|
|
15
|
+
The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) is for poking at a server by hand. `mcp-reportcard` is automated and opinionated: it scores tool names, descriptions, schemas and safety annotations against the things that actually make models misbehave — vague descriptions, undocumented parameters, near-duplicate tools, missing `readOnlyHint`s, stack traces in error replies — and it runs in CI.
|
|
16
|
+
|
|
17
|
+
Every finding comes with a rule ID, a severity and a specific fix. No LLM is involved in the checks, so results are deterministic and safe to gate a merge on.
|
|
18
|
+
|
|
19
|
+
## Example output
|
|
20
|
+
|
|
21
|
+
Running against the deliberately sloppy server in [`examples/bad_server.py`](https://github.com/builditwithgk/mcp-reportcard/blob/main/examples/bad_server.py):
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
┌──────── mcp-reportcard · acme-orders (bad example) ─────────┐
|
|
25
|
+
│ Grade D · score 67/100 │
|
|
26
|
+
│ 7 tools · 1 errors · 16 warnings · 5 info │
|
|
27
|
+
│ ~351 tokens per turn (tiktoken o200k_base (approximate)) │
|
|
28
|
+
└──────────────────────────────────────────────────────────┘
|
|
29
|
+
F002 lookup_invoice Error output exposes internals: `Traceback (most recent call last)`
|
|
30
|
+
fix: Log stack traces server-side; return a short, safe message to the client.
|
|
31
|
+
X001 server `find_items` and `search_items` are described almost the same way (88% word overlap)
|
|
32
|
+
fix: Say in each description when to use it instead of the other, or merge them.
|
|
33
|
+
...
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The same service written properly, [`examples/good_server.py`](https://github.com/builditwithgk/mcp-reportcard/blob/main/examples/good_server.py), scores **A (100)**.
|
|
37
|
+
|
|
38
|
+
**Field-tested:** run against six official MCP reference servers (Node and Python, stdio and HTTP) — 159 fuzz cases, zero crashes, and two over-eager rules fixed as a result. Numbers and details in [`FINDINGS.md`](https://github.com/builditwithgk/mcp-reportcard/blob/main/FINDINGS.md).
|
|
39
|
+
|
|
40
|
+
## Install & run
|
|
41
|
+
|
|
42
|
+
Requires Python 3.10+.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install "mcp-reportcard[tokens] @ git+https://github.com/builditwithgk/mcp-reportcard"
|
|
46
|
+
|
|
47
|
+
# …or from source
|
|
48
|
+
git clone https://github.com/builditwithgk/mcp-reportcard
|
|
49
|
+
cd mcp-reportcard
|
|
50
|
+
pip install -e ".[dev]"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A PyPI release (`pip install mcp-reportcard`) is coming. The `[tokens]` extra adds tiktoken for more accurate token counts; without it, a characters/4 estimate is used.
|
|
54
|
+
|
|
55
|
+
## Command line
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# A stdio server (any language): pass the command that starts it
|
|
59
|
+
mcp-reportcard check "python server.py"
|
|
60
|
+
mcp-reportcard check "npx -y @modelcontextprotocol/server-everything"
|
|
61
|
+
|
|
62
|
+
# A Streamable HTTP server
|
|
63
|
+
mcp-reportcard check http://localhost:8000/mcp
|
|
64
|
+
|
|
65
|
+
# A saved tools/list JSON (offline, no server needed)
|
|
66
|
+
mcp-reportcard check tools.json
|
|
67
|
+
|
|
68
|
+
# Also test error handling with bad inputs (read-only tools only — see below)
|
|
69
|
+
mcp-reportcard check "python server.py" --fuzz
|
|
70
|
+
|
|
71
|
+
# Reports for PRs and CI
|
|
72
|
+
mcp-reportcard check "python server.py" -f markdown -o report.md
|
|
73
|
+
mcp-reportcard check "python server.py" -f json --fail-under 80
|
|
74
|
+
|
|
75
|
+
# Save a server's tool list (for offline checks or the web demo)
|
|
76
|
+
mcp-reportcard dump "python server.py" -o tools.json
|
|
77
|
+
|
|
78
|
+
# List every check
|
|
79
|
+
mcp-reportcard rules
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Use in CI
|
|
83
|
+
|
|
84
|
+
`--fail-under` exits 1 when the score drops below the threshold:
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
# .github/workflows/mcp-reportcard.yml
|
|
88
|
+
on: pull_request
|
|
89
|
+
jobs:
|
|
90
|
+
mcp-reportcard:
|
|
91
|
+
runs-on: ubuntu-latest
|
|
92
|
+
steps:
|
|
93
|
+
- uses: actions/checkout@v4
|
|
94
|
+
- uses: actions/setup-python@v5
|
|
95
|
+
with: { python-version: "3.12" }
|
|
96
|
+
- run: pip install "mcp-reportcard @ git+https://github.com/builditwithgk/mcp-reportcard"
|
|
97
|
+
- run: mcp-reportcard check "python server.py" --fuzz -f markdown -o mcp-reportcard.md --fail-under 80
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This repo dogfoods it: CI runs `mcp-reportcard` against [`examples/good_server.py`](https://github.com/builditwithgk/mcp-reportcard/blob/main/examples/good_server.py) with `--fail-under 90` on Linux, macOS and Windows.
|
|
101
|
+
|
|
102
|
+
## What it checks
|
|
103
|
+
|
|
104
|
+
| ID | Severity | Check |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| N001–N005 | error / warning / info | Missing, duplicate, invalid or over-long names; mixed naming styles |
|
|
107
|
+
| D001–D005 | error / warning / info | Missing, too short, too long, or name-restating descriptions; no "when to use" guidance when a similar tool exists |
|
|
108
|
+
| S001–S008 | error / warning / info | Missing object schema, untyped or undocumented parameters, broken `required`, free-form objects, deep nesting, missing `enum`, too many parameters |
|
|
109
|
+
| A001–A002 | warning | Missing or contradictory `readOnlyHint` / `destructiveHint` |
|
|
110
|
+
| T001, X002 | info / warning | Heavy tool definitions; total token cost per turn |
|
|
111
|
+
| X001, X003, X004 | warning / error | Tools with overlapping descriptions; too many tools; no tools |
|
|
112
|
+
| F001–F003 | warning / error | (fuzz) Accepts schema-violating input; leaks stack traces; crashes or hangs |
|
|
113
|
+
|
|
114
|
+
**Scoring:** each tool starts at 100 and loses 25 per error, 10 per warning and 3 per info. The server score is the average tool score minus server-level penalties (capped at 40). Grades: A ≥ 90, B ≥ 80, C ≥ 70, D ≥ 60, F below.
|
|
115
|
+
|
|
116
|
+
## Fuzzing safety
|
|
117
|
+
|
|
118
|
+
`--fuzz` calls your tools with missing arguments, wrong types, out-of-enum values, 10,000-character strings and tricky Unicode, then checks for clean `isError` responses.
|
|
119
|
+
|
|
120
|
+
- By default it **only calls tools annotated `readOnlyHint: true`** (and not `destructiveHint: true`). Everything else is skipped — and listed in the report, never silently.
|
|
121
|
+
- `--allow-unsafe` fuzzes every tool. **Only use it against a test instance**, never production data.
|
|
122
|
+
- A stdio server's own stderr is hidden to keep the report readable; add `--server-logs` to see it.
|
|
123
|
+
|
|
124
|
+
## Web demo
|
|
125
|
+
|
|
126
|
+
The demo runs the static checks only — it never connects to servers, calls tools or contacts an LLM, and nothing pasted is stored. Because the core is pure standard-library Python, it ships two ways from the same code:
|
|
127
|
+
|
|
128
|
+
- **In the browser** — [`space/`](https://github.com/builditwithgk/mcp-reportcard/tree/main/space) is a static page that runs `src/mcp_reportcard` client-side with [Pyodide](https://pyodide.org). That's the [Hugging Face Space](https://huggingface.co/spaces/builditwithgk/mcp-reportcard): no server, free tier. `bash space/build.sh` assembles it into `space/_site/`; `bash space/deploy.sh` publishes it. The same folder works on GitHub Pages or any static host.
|
|
129
|
+
- **As a Flask app** — [`web/`](https://github.com/builditwithgk/mcp-reportcard/tree/main/web), for Python hosting such as cPanel: create a Python app with `web/` as the root and `passenger_wsgi.py` as the startup file, install `web/requirements.txt`, and upload `src/` and `examples/` alongside.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pip install flask
|
|
133
|
+
python web/app.py # http://localhost:5000
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Limitations
|
|
137
|
+
|
|
138
|
+
- Token counts are **estimates**. Each provider wraps tool definitions in its own format; use the numbers to compare tools and track changes, not as a bill.
|
|
139
|
+
- Description checks are heuristics. A D005 ("no usage guidance") is a nudge, not a hard rule.
|
|
140
|
+
- Leak detection (F002) looks for stack traces only. File paths in error messages are not flagged — file and git tools legitimately name paths.
|
|
141
|
+
- Fuzzing can't prove a tool is correct; it shows how it fails on bad input.
|
|
142
|
+
- v1 checks tools only, not resources or prompts.
|
|
143
|
+
|
|
144
|
+
## Roadmap
|
|
145
|
+
|
|
146
|
+
- Tool-selection eval (bring your own API key): generate realistic tasks per tool and measure how often a model picks the right one.
|
|
147
|
+
- README badge (`mcp-reportcard: A`).
|
|
148
|
+
- Resource and prompt checks.
|
|
149
|
+
- PyPI release.
|
|
150
|
+
|
|
151
|
+
## Development
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
155
|
+
pip install -e ".[dev]"
|
|
156
|
+
pytest
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Tests cover every lint rule, scoring, leak detection, fuzz-case generation, the CLI and the web demo — plus end-to-end runs against both example servers, in-process and as real stdio subprocesses.
|
|
160
|
+
|
|
161
|
+
## Author
|
|
162
|
+
|
|
163
|
+
Built by **Gopi K Aitham** ([builditwithgk](https://github.com/builditwithgk)) — see [scaleup-solutions.in](https://scaleup-solutions.in/). Available for freelance and contract work on AI, agent, and MCP tooling.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""A deliberately sloppy MCP server for demos and tests. Every tool has a common mistake.
|
|
2
|
+
|
|
3
|
+
Run: mcp-reportcard check "python examples/bad_server.py" --fuzz
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import traceback
|
|
7
|
+
|
|
8
|
+
from mcp.server.mcpserver import MCPServer
|
|
9
|
+
from mcp.server.mcpserver.exceptions import ToolError
|
|
10
|
+
from mcp_types import ToolAnnotations
|
|
11
|
+
|
|
12
|
+
server = MCPServer("acme-orders (bad example)")
|
|
13
|
+
|
|
14
|
+
ORDERS = {"1001": {"status": "shipped", "total": 49.0}, "1002": {"status": "pending", "total": 12.5}}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@server.tool(annotations=ToolAnnotations(read_only_hint=True))
|
|
18
|
+
def search(query: str, mode: str = "fuzzy") -> list[str]:
|
|
19
|
+
"""Search.""" # too short, `mode` should be an enum, no parameter descriptions
|
|
20
|
+
return [oid for oid in ORDERS if query in oid]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@server.tool(annotations=ToolAnnotations(read_only_hint=True))
|
|
24
|
+
def find_items(keyword: str) -> list[str]:
|
|
25
|
+
"""Search the store catalogue items by keyword and return the matching items."""
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@server.tool(annotations=ToolAnnotations(read_only_hint=True))
|
|
30
|
+
def search_items(keyword: str) -> list[str]:
|
|
31
|
+
"""Search store catalogue items by keyword and return matching catalogue items."""
|
|
32
|
+
return [] # near-duplicate of find_items: the model can't tell which to use
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@server.tool()
|
|
36
|
+
def getCustomer(id: str) -> dict: # noqa: N802 camelCase mixed with snake_case, no annotations
|
|
37
|
+
"""Get customer"""
|
|
38
|
+
return {"id": id}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@server.tool(annotations=ToolAnnotations(read_only_hint=True))
|
|
42
|
+
def lookup_invoice(invoice_id: str) -> dict:
|
|
43
|
+
"""Look up an invoice by its numeric ID and return the amount due."""
|
|
44
|
+
try:
|
|
45
|
+
n = int(invoice_id)
|
|
46
|
+
return {"invoice": n, "due": 0}
|
|
47
|
+
except Exception:
|
|
48
|
+
raise ToolError(traceback.format_exc()) # leaks a stack trace and file paths
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@server.tool()
|
|
52
|
+
def delete_order(order_id: str) -> str:
|
|
53
|
+
"""Deletes an order permanently.""" # destructive, but not annotated as such
|
|
54
|
+
ORDERS.pop(order_id, None)
|
|
55
|
+
return "deleted"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@server.tool()
|
|
59
|
+
def update_settings(settings: dict) -> str:
|
|
60
|
+
"""Update account settings.""" # free-form object: the model must guess the keys
|
|
61
|
+
return "ok"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
server.run()
|