pytest-evidence-mcp
MCP server that gives an AI agent access to structured evidence about pytest test failures.
The server delivers deterministic data — it does not diagnose. Diagnosis is the agent's job. It exposes four tools (list_failed_tests, get_test_failure, inspect_fixture, parse_pytest_output), one documentation resource per tool, and one prompt template per tool.
Prerequisites
- Python 3.10+.
- pytest installed in the target project's own environment (the project being investigated), not in this server's environment. This server does not depend on pytest as a package — it locates and calls the pytest already installed where the code under investigation lives (its
.venv, by default). If pytest isn't installed there,list_failed_tests/get_test_failureraisePytestNotFoundErrorwith a clean message instead of crashing. - An MCP client that supports stdio transport (Claude Code, VS Code with the Copilot Chat MCP integration, or any other MCP-compatible client).
uvinstalled, if you're running the server viauvx(recommended, see below) rather than from a local clone. Any reasonably recent version works (tested with 0.12.x).
Quick start: running from PyPI (recommended)
No clone, no virtualenv to manage — uvx downloads the package and runs it in an isolated, disposable environment on demand:
uvx pytest-evidence-mcp
It should log Starting MCP Server. to stderr and wait on stdin (this is a stdio server — running it standalone in a terminal is only useful to sanity-check that it starts; stop it with Ctrl+C). This is also the command your MCP client will run under the hood (see "Registering with an MCP client" below).
Updating to a new version: uvx resolves the latest version compatible with your Python at each run, but may reuse a cached resolution. To force it to pick up a version you just published:
uvx --refresh-package pytest-evidence-mcp pytest-evidence-mcp
Alternative: running from a local clone (for development)
Only needed if you're modifying this server itself, not to investigate a target project's tests.
git clone https://github.com/wilcsonaraujo/pytest-evidence-mcp
cd pytest-evidence-mcp
python -m venv .venv
.venv/bin/pip install -e ".[dev]" # Linux/macOS
.venv\Scripts\pip install -e ".[dev]" # Windows
To confirm it starts correctly:
.venv/bin/python -m pytest_evidence_mcp
Registering with an MCP client
Claude Code, from PyPI:
claude mcp add --transport stdio --scope project pytest-evidence-mcp -- uvx pytest-evidence-mcp
Claude Code only loads MCP servers when a session starts — if you register the server while a session is already open, restart the session (or run /mcp) before using the tools.
VS Code (Copilot Chat), from PyPI, .vscode/mcp.json in the workspace you'll investigate:
{
"servers": {
"pytest-evidence-mcp": {
"type": "stdio",
"command": "uvx",
"args": ["pytest-evidence-mcp"]
}
}
}
From a local clone instead (development), point command/the executable at the .venv created above rather than at uvx:
claude mcp add --transport stdio --scope project pytest-evidence-mcp -- "/absolute/path/to/pytest-evidence-mcp/.venv/bin/python" -m pytest_evidence_mcp
{
"servers": {
"pytest-evidence-mcp": {
"type": "stdio",
"command": "/absolute/path/to/pytest-evidence-mcp/.venv/bin/python",
"args": ["-m", "pytest_evidence_mcp"]
}
}
}
(On Windows, use .venv\Scripts\python.exe.)
Any other MCP client that supports a generic stdio server definition (command + args) can be registered the same way, from either source.
How it decides where the data comes from
list_failed_tests and get_test_failure resolve the most recent test run for a project through a priority chain, tried in order:
.report.json(pytest-json-report) — read if it exists, at the path declared in the project's own pytest config (addoptsinpytest.ini,pyproject.toml,tox.iniorsetup.cfg, in that precedence order) or at the default.report.json.junit.xml— same lookup, falling back to the defaultjunit.xml.- Run pytest now, via subprocess, if neither report exists. This is a real, fresh execution every time — the temporary report it generates is deleted right after parsing, so there is no caching in this branch, and a repeated call without a persisted report re-runs the whole suite from scratch.
The source field in list_failed_tests' output tells you which branch was used, and age_seconds tells you how old that data is.
The four tools
list_failed_tests(path: str)
Summarizes the most recent pytest run for a project. Always the starting point of an investigation.
Input:
{ "path": "/home/dev/my-project" }
Output:
{
"source": "json_report",
"generated_at": "2026-08-28T14:35:30.180000",
"age_seconds": 12.4,
"total": 42,
"passed": 40,
"failed": 2,
"skipped": 0,
"failed_tests": [
{
"nodeid": "tests/test_checkout.py::test_apply_discount",
"name": "test_apply_discount",
"error_type": "AssertionError"
},
{
"nodeid": "tests/test_checkout.py::test_apply_discount_negative",
"name": "test_apply_discount_negative",
"error_type": "AssertionError"
}
]
}
A project where nothing fails returns failed_tests: [], not an error.
get_test_failure(test_name: str, path: str, max_output_chars: int = 10000)
Returns the full evidence pytest already collected for one failing test: error, traceback, captured output. No diagnosis.
Input:
{ "test_name": "test_apply_discount", "path": "/home/dev/my-project" }
Output:
{
"error_type": "AssertionError",
"message": "assert 90 == 9.0",
"traceback": "def test_apply_discount():\n> assert apply_discount(100, 10) == 9.0\nE AssertionError: assert 90 == 9.0\n\ntests/test_checkout.py:14: AssertionError",
"actual": "90",
"expected": "9.0",
"stdout": null,
"stderr": null,
"log": null,
"duration_ms": 3
}
test_name accepts either the short name (test_apply_discount) or a full nodeid (tests/test_checkout.py::test_apply_discount). If the short name matches more than one test, the call raises AmbiguousTestNameError listing every matching nodeid — retry with the full nodeid from that list.
Large fields (traceback, actual, expected, stdout, stderr, log) are truncated to max_output_chars, keeping the tail (where the relevant part usually is) and prefixing a ...[N chars omitted]... note.
inspect_fixture(path: str)
Inspects a JSON or YAML fixture file used by a test, to check whether a failure comes from the input data itself rather than the code. path points to the fixture file, not to the project root.
Input:
{ "path": "/home/dev/my-project/fixtures/orders.json" }
Output:
{
"valid": true,
"field_count": 6,
"null_fields": [],
"types": {
"orders[].id": "integer",
"orders[].customer": "string",
"orders[].total": "float",
"orders[23].id": "integer",
"orders[23].customer": "string",
"orders[23].total": "string"
},
"collapsed_lists": {
"orders": 49
}
}
Lists longer than 10 items get their majority shape collapsed into a single path[] entry (collapsed_lists records how many items share that shape), so the output stays bounded on large fixtures. Any item whose shape doesn't match the majority — like orders[23] above, where total is a string instead of a float — is still reported individually, in full, by its real index. That is usually the actual cause of the failure.
An invalid or unparsable file returns {"valid": false, "message": "..."} instead of raising.
parse_pytest_output(raw_text: str, max_output_chars: int = 10000)
Last-resort fallback: extracts failure evidence directly from raw pytest terminal output, for when pytest can't be run again and no .report.json/junit.xml is available (e.g. output pasted from a CI log).
Input:
{ "raw_text": "=================== FAILURES ===================\n_____ test_apply_discount _____\n\n def test_apply_discount():\n> assert apply_discount(100, 10) == 9.0\nE AssertionError: assert 90 == 9.0\n\ntests/test_checkout.py:14: AssertionError\n=============== 1 failed, 41 passed in 0.42s ===============" }
Output:
{
"confidence": "low",
"failures": [
{
"test_name": "test_apply_discount",
"outcome": "failed",
"error_type": "AssertionError",
"message": "assert 90 == 9.0",
"traceback": "...",
"actual": "90",
"expected": "9.0",
"captured_stdout": null,
"captured_stderr": null,
"captured_log": null,
"duration_ms": null
}
]
}
Known limitation: this parser recognizes pytest's own default output format (the === FAILURES === / ==== ERRORS ==== sections and their ----- Captured stdout call ----- subsections). It does not understand output reformatted by a different plugin — most commonly pytest-sugar, which changes the layout enough that these sections stop being recognizable. If the target project has pytest-sugar (or a similar output plugin) installed and active, either disable it for the run you're capturing (pytest -p no:sugar) or use one of the structured sources (branches 1/2 above) instead. confidence: "low" on every response from this tool is a permanent reminder that, unlike the other three tools, this one is reconstructing evidence from free text rather than reading a structured report — treat it as a fallback, not a primary source.
Error handling
Every domain error (PytestNotFoundError, TestNotFoundError, AmbiguousTestNameError, IncompleteEvidenceError, and others in core/errors.py) reaches the calling agent as a clean MCP tool error with a descriptive message, not as a crash or a silently swallowed exception. If a tool call fails, the message itself usually says exactly what to do next (e.g. "install pytest", "use one of these full nodeids").
Resources and prompts
Each tool also has a matching documentation resource (docs://tools/<tool_name>) with its machine-readable contract, and a prompt template (<tool_name>_prompt) that fills in a ready-to-send instruction for that tool given the same arguments. These exist so a client can introspect a tool's real input/output shape, or start an investigation, without leaving the chat.
Development
After the local clone install above (pip install -e ".[dev]"), you can launch the MCP Inspector against the server directly:
mcp dev src/pytest_evidence_mcp/server.py
Run the test suite and checks:
pytest
mypy src/
ruff check .
Build distributable artifacts (wheel + sdist):
uv build
Metadata
Release files for pytest-evidence-mcp 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 | Provenance | Actions |
|---|---|---|---|---|
| pytest_evidence_mcp-0.1.0.tar.gz | 139.1 kB | Without provenance | Details for pytest_evidence_mcp-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pytest_evidence_mcp-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for pytest_evidence_mcp-0.1.0-py3-none-any.whl |
Total release size: 175.2 kB
Release files / pytest_evidence_mcp-0.1.0.tar.gz
| Download URL | pytest_evidence_mcp-0.1.0.tar.gz |
|---|---|
| Size | 139.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
658aed7f8de1c9ac1b75c6f1598b055094438dcd16f1edeaaee70be328273e03
|
|
BLAKE2b-256 checksum How to use checksums |
a6ff892f24f7ed783cd4b440d5c9ce7ad481eb069d41cddab155be4d2edd08d8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.9 {"installer":{"name":"uv","version":"0.11.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / pytest_evidence_mcp-0.1.0-py3-none-any.whl
| Download URL | pytest_evidence_mcp-0.1.0-py3-none-any.whl |
|---|---|
| Size | 36.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
857b428c4c2ef9c4c7e9f17437cc514c1fcd7885556f54b275696050b35c53a4
|
|
BLAKE2b-256 checksum How to use checksums |
b61b1382e9c6fb46a18f3770c399ed78566b895a713010da50e6be75a94008e3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.9 {"installer":{"name":"uv","version":"0.11.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for pytest-evidence-mcp 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.