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.
Files changed (34) hide show
  1. mcp_reportcard-0.1.0/.env.example +2 -0
  2. mcp_reportcard-0.1.0/.github/workflows/ci.yml +24 -0
  3. mcp_reportcard-0.1.0/.github/workflows/release.yml +40 -0
  4. mcp_reportcard-0.1.0/.gitignore +10 -0
  5. mcp_reportcard-0.1.0/FINDINGS.md +35 -0
  6. mcp_reportcard-0.1.0/LICENSE +21 -0
  7. mcp_reportcard-0.1.0/PKG-INFO +198 -0
  8. mcp_reportcard-0.1.0/README.md +167 -0
  9. mcp_reportcard-0.1.0/examples/bad_server.py +65 -0
  10. mcp_reportcard-0.1.0/examples/bad_tools.json +215 -0
  11. mcp_reportcard-0.1.0/examples/good_server.py +57 -0
  12. mcp_reportcard-0.1.0/examples/good_tools.json +118 -0
  13. mcp_reportcard-0.1.0/pyproject.toml +47 -0
  14. mcp_reportcard-0.1.0/space/README.md +28 -0
  15. mcp_reportcard-0.1.0/space/build.sh +16 -0
  16. mcp_reportcard-0.1.0/space/deploy.sh +13 -0
  17. mcp_reportcard-0.1.0/space/index.html +197 -0
  18. mcp_reportcard-0.1.0/src/mcp_reportcard/__init__.py +13 -0
  19. mcp_reportcard-0.1.0/src/mcp_reportcard/analyze.py +102 -0
  20. mcp_reportcard-0.1.0/src/mcp_reportcard/cli.py +193 -0
  21. mcp_reportcard-0.1.0/src/mcp_reportcard/connect.py +70 -0
  22. mcp_reportcard-0.1.0/src/mcp_reportcard/fuzz.py +207 -0
  23. mcp_reportcard-0.1.0/src/mcp_reportcard/loader.py +49 -0
  24. mcp_reportcard-0.1.0/src/mcp_reportcard/models.py +109 -0
  25. mcp_reportcard-0.1.0/src/mcp_reportcard/report.py +127 -0
  26. mcp_reportcard-0.1.0/src/mcp_reportcard/rules.py +317 -0
  27. mcp_reportcard-0.1.0/src/mcp_reportcard/tokens.py +48 -0
  28. mcp_reportcard-0.1.0/tests/test_cli_web.py +54 -0
  29. mcp_reportcard-0.1.0/tests/test_fuzz.py +86 -0
  30. mcp_reportcard-0.1.0/tests/test_rules.py +120 -0
  31. mcp_reportcard-0.1.0/web/app.py +65 -0
  32. mcp_reportcard-0.1.0/web/passenger_wsgi.py +7 -0
  33. mcp_reportcard-0.1.0/web/requirements.txt +1 -0
  34. mcp_reportcard-0.1.0/web/templates/index.html +131 -0
@@ -0,0 +1,2 @@
1
+ # mcp-reportcard v1 needs no environment variables or API keys.
2
+ # (The planned tool-selection eval will read a model API key from here.)
@@ -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,10 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .env
9
+ reports/
10
+ space/_site/
@@ -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
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue) ![License](https://img.shields.io/badge/license-MIT-green) ![Version](https://img.shields.io/badge/version-0.1.0-blue) ![MCP](https://img.shields.io/badge/MCP-any%20server%2C%20any%20language-brightgreen) [![CI](https://github.com/builditwithgk/mcp-reportcard/actions/workflows/ci.yml/badge.svg)](https://github.com/builditwithgk/mcp-reportcard/actions/workflows/ci.yml) [![Hugging Face Space](https://img.shields.io/badge/%F0%9F%A4%97%20Space-live%20demo-yellow)](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
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue) ![License](https://img.shields.io/badge/license-MIT-green) ![Version](https://img.shields.io/badge/version-0.1.0-blue) ![MCP](https://img.shields.io/badge/MCP-any%20server%2C%20any%20language-brightgreen) [![CI](https://github.com/builditwithgk/mcp-reportcard/actions/workflows/ci.yml/badge.svg)](https://github.com/builditwithgk/mcp-reportcard/actions/workflows/ci.yml) [![Hugging Face Space](https://img.shields.io/badge/%F0%9F%A4%97%20Space-live%20demo-yellow)](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()