pytest-mcp-honesty
Bidirectional contract testing for FastMCP servers, in your own pytest suite.
A tool's schema is a contract, and servers break it in both directions:
- The schema promises less than the implementation demands. A param is
str | None = Nonein the signature — so the generated schema calls it optional — but the handler errors without it. Every agent that trusts the schema burns a failed call to learn the truth. (Found in the wild: a server whoseresolve_teamdeclaredrequired: []and then errored "provide club_name or org_id".) - The implementation enforces less than the schema promises. Required params can be omitted, or structurally wrong values are accepted, and garbage flows into your handler.
check_contracts probes every tool over FastMCP's in-memory client — no network, no auth,
hermetic — so a failed probe is a finding about your contract, not your environment:
from pytest_mcp_honesty import check_contracts
from my_app import server # your FastMCP instance
async def test_tool_contracts():
report = await check_contracts(server)
assert not report.violations, report.summary()
Three probe families, all deterministic (same schema → same probe):
| Probe | Catches |
|---|---|
| minimal valid call (declared-required args only) | schemas that under-declare what the tool needs |
| drop one required arg | schemas that over-declare, or missing validation |
| structurally wrong value (uncoercible — a dict where a scalar belongs) | declared types that are not enforced |
The wrong-value probe deliberately avoids values Pydantic's lax coercion legally repairs
("5" → 5 is not a violation); it sends values nothing can coerce.
What this is not
- Not protocol conformance — the transport/spec layer is a different job (see the official MCP Inspector, or mcp-java-testkit on the JVM).
- Not a fuzzer — probes are deterministic transcriptions of the contract, not random inputs (see mcp-fuzzer for security-style fuzzing).
- Not schema portability — whether your schema survives specific clients is mcp-portability-lint's job; this checks whether your schema tells the truth about your own server.
Development
Maintained by one person with AI assistance; where AI tooling contributed is logged per commit in docs/ai-maintenance-log.md.
License
Apache-2.0
Release files for pytest-mcp-honesty 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pytest_mcp_honesty-0.1.0.tar.gz | 11.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytest_mcp_honesty-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 22.7 kB
Release files / pytest_mcp_honesty-0.1.0.tar.gz
| Download URL | pytest_mcp_honesty-0.1.0.tar.gz |
|---|---|
| Size | 11.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b823b0b645bfd9a90a32bd2d6180938aa6afc1d1345db4db9a51c34d9f48d151
|
|
BLAKE2b-256 checksum How to use checksums |
0db213cd5b05e48342212c3bdfe2f7677630ed0d58e0be913f40a7304890e99b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Release files / pytest_mcp_honesty-0.1.0-py3-none-any.whl
| Download URL | pytest_mcp_honesty-0.1.0-py3-none-any.whl |
|---|---|
| Size | 11.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dde194261e9ac759b4c18845da7b0550e7f8272151e22d414caf54ced95e23a3
|
|
BLAKE2b-256 checksum How to use checksums |
3de96281f7dd56f30c4d94d9a18703c2f50efda09181de408e20e4139e1e8634
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|