pytest-mcp-assert 0.5.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.
@@ -0,0 +1,3 @@
1
+ """pytest plugin for mcp-assert."""
2
+
3
+ __version__ = "0.5.0"
@@ -0,0 +1,226 @@
1
+ """pytest plugin that runs mcp-assert YAML assertions as test items.
2
+
3
+ Each .yaml file in the configured suite directory becomes a pytest test item.
4
+ The plugin calls the mcp-assert binary with --json output and maps the result
5
+ to pytest pass/fail/skip.
6
+
7
+ Usage:
8
+ pytest --mcp-suite evals/
9
+ pytest --mcp-suite evals/ --mcp-fixture /path/to/fixtures
10
+ pytest --mcp-suite evals/ --mcp-server "agent-lsp go:gopls"
11
+ pytest --mcp-suite evals/ --mcp-timeout 60s
12
+
13
+ Configuration via pyproject.toml:
14
+ [tool.pytest.ini_options]
15
+ mcp_suite = "evals/"
16
+ mcp_fixture = "fixtures/"
17
+ mcp_timeout = "30s"
18
+ """
19
+
20
+ import json
21
+ import os
22
+ import shutil
23
+ import subprocess
24
+
25
+ import pytest
26
+
27
+
28
+ def pytest_addoption(parser):
29
+ """Register mcp-assert CLI options."""
30
+ group = parser.getgroup("mcp-assert", "MCP server assertion testing")
31
+ group.addoption(
32
+ "--mcp-suite",
33
+ action="store",
34
+ default=None,
35
+ help="Directory containing mcp-assert YAML assertion files",
36
+ )
37
+ group.addoption(
38
+ "--mcp-fixture",
39
+ action="store",
40
+ default=None,
41
+ help="Fixture directory (substituted for {{fixture}} in assertions)",
42
+ )
43
+ group.addoption(
44
+ "--mcp-server",
45
+ action="store",
46
+ default=None,
47
+ help="Override server command for all assertions",
48
+ )
49
+ group.addoption(
50
+ "--mcp-timeout",
51
+ action="store",
52
+ default="30s",
53
+ help="Per-assertion timeout (default: 30s)",
54
+ )
55
+ group.addoption(
56
+ "--mcp-binary",
57
+ action="store",
58
+ default=None,
59
+ help="Path to mcp-assert binary (default: auto-detect from PATH or pip package)",
60
+ )
61
+ parser.addini(
62
+ "mcp_suite", "Directory containing mcp-assert YAML assertion files"
63
+ )
64
+ parser.addini("mcp_fixture", "Fixture directory for assertions")
65
+ parser.addini("mcp_timeout", "Per-assertion timeout")
66
+
67
+
68
+ def _find_binary(config):
69
+ """Find the mcp-assert binary."""
70
+ # 1. Explicit --mcp-binary flag
71
+ explicit = config.getoption("--mcp-binary")
72
+ if explicit:
73
+ return explicit
74
+
75
+ # 2. PATH lookup
76
+ found = shutil.which("mcp-assert")
77
+ if found:
78
+ return found
79
+
80
+ # 3. PyPI package (mcp_assert/bin/mcp-assert)
81
+ try:
82
+ import mcp_assert # noqa: F811
83
+
84
+ pkg_dir = os.path.dirname(os.path.abspath(mcp_assert.__file__))
85
+ bin_path = os.path.join(pkg_dir, "bin", "mcp-assert")
86
+ if os.path.isfile(bin_path):
87
+ return bin_path
88
+ except ImportError:
89
+ pass
90
+
91
+ return None
92
+
93
+
94
+ def pytest_configure(config):
95
+ """Add the mcp-suite directory to pytest's collection paths."""
96
+ suite_dir = _get_suite_dir(config)
97
+ if suite_dir and os.path.isdir(suite_dir):
98
+ # Only add if no explicit test paths were given (avoid overriding user intent)
99
+ if not config.args or config.args == ["."]:
100
+ config.args = [suite_dir]
101
+ elif suite_dir not in config.args:
102
+ config.args.append(suite_dir)
103
+
104
+
105
+ def pytest_collect_file(parent, file_path):
106
+ """Collect .yaml files from the mcp-suite directory as test items."""
107
+ suite_dir = _get_suite_dir(parent.config)
108
+ if suite_dir is None:
109
+ return None
110
+
111
+ suite_path = os.path.abspath(suite_dir)
112
+ file_abs = str(file_path)
113
+
114
+ if file_abs.startswith(suite_path) and file_path.suffix == ".yaml":
115
+ return McpAssertFile.from_parent(parent, path=file_path)
116
+
117
+ return None
118
+
119
+
120
+ def _get_suite_dir(config):
121
+ """Get suite directory from CLI option or ini."""
122
+ suite = config.getoption("--mcp-suite")
123
+ if suite:
124
+ return suite
125
+ ini = config.getini("mcp_suite")
126
+ if ini:
127
+ return ini
128
+ return None
129
+
130
+
131
+ class McpAssertFile(pytest.File):
132
+ """Collector for a single mcp-assert YAML file."""
133
+
134
+ def collect(self):
135
+ """Yield a single test item for this YAML assertion file."""
136
+ # Extract assertion name from YAML (first "name:" line)
137
+ name = self.path.stem
138
+ try:
139
+ with open(self.path) as f:
140
+ for line in f:
141
+ line = line.strip()
142
+ if line.startswith("name:"):
143
+ name = line[5:].strip().strip('"').strip("'")
144
+ break
145
+ except OSError:
146
+ pass
147
+
148
+ yield McpAssertItem.from_parent(self, name=name, yaml_path=self.path)
149
+
150
+
151
+ class McpAssertItem(pytest.Item):
152
+ """A single mcp-assert YAML assertion as a pytest test item."""
153
+
154
+ def __init__(self, name, parent, yaml_path):
155
+ super().__init__(name, parent)
156
+ self.yaml_path = yaml_path
157
+ self._result = None
158
+
159
+ def runtest(self):
160
+ """Run the mcp-assert binary on this YAML file."""
161
+ config = self.config
162
+ binary = _find_binary(config)
163
+ if binary is None:
164
+ pytest.skip(
165
+ "mcp-assert binary not found. Install via: "
166
+ "brew install blackwell-systems/tap/mcp-assert, "
167
+ "pip install mcp-assert, or "
168
+ "npm install -g @blackwell-systems/mcp-assert"
169
+ )
170
+
171
+ cmd = [
172
+ binary,
173
+ "run",
174
+ "--suite",
175
+ str(self.yaml_path),
176
+ "--json",
177
+ "--timeout",
178
+ config.getoption("--mcp-timeout"),
179
+ ]
180
+
181
+ fixture = config.getoption("--mcp-fixture") or config.getini("mcp_fixture")
182
+ if fixture:
183
+ cmd.extend(["--fixture", fixture])
184
+
185
+ server = config.getoption("--mcp-server")
186
+ if server:
187
+ cmd.extend(["--server", server])
188
+
189
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
190
+
191
+ # Parse JSON output
192
+ try:
193
+ results = json.loads(result.stdout)
194
+ except json.JSONDecodeError:
195
+ if result.returncode != 0:
196
+ raise McpAssertFailure(
197
+ f"mcp-assert failed (exit {result.returncode}): {result.stderr.strip()}"
198
+ )
199
+ raise McpAssertFailure(f"Could not parse mcp-assert output: {result.stdout[:500]}")
200
+
201
+ if not results:
202
+ raise McpAssertFailure("mcp-assert returned no results")
203
+
204
+ # Each YAML file produces one result
205
+ r = results[0] if isinstance(results, list) else results
206
+
207
+ self._result = r
208
+ status = r.get("status", "").lower()
209
+
210
+ if status == "skip":
211
+ pytest.skip(r.get("detail", "skipped"))
212
+ elif status == "fail":
213
+ raise McpAssertFailure(r.get("detail", "assertion failed"))
214
+
215
+ def repr_failure(self, excinfo):
216
+ """Format assertion failure for pytest output."""
217
+ if isinstance(excinfo.value, McpAssertFailure):
218
+ return str(excinfo.value)
219
+ return super().repr_failure(excinfo)
220
+
221
+ def reportinfo(self):
222
+ return self.path, None, f"mcp-assert: {self.name}"
223
+
224
+
225
+ class McpAssertFailure(Exception):
226
+ """Raised when an mcp-assert assertion fails."""
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-mcp-assert
3
+ Version: 0.5.0
4
+ Summary: pytest plugin for mcp-assert: run MCP server assertions as pytest test items
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/blackwell-systems/mcp-assert
7
+ Project-URL: Documentation, https://blackwell-systems.github.io/mcp-assert
8
+ Project-URL: Repository, https://github.com/blackwell-systems/mcp-assert
9
+ Keywords: pytest,mcp,testing,assertions,model-context-protocol
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Framework :: Pytest
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Testing
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: pytest>=7.0
18
+
19
+ # pytest-mcp-assert
20
+
21
+ pytest plugin for [mcp-assert](https://github.com/blackwell-systems/mcp-assert). Run MCP server assertions as pytest test items.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install pytest-mcp-assert
27
+ ```
28
+
29
+ You also need the mcp-assert binary. Any of these:
30
+
31
+ ```bash
32
+ brew install blackwell-systems/tap/mcp-assert
33
+ pip install mcp-assert
34
+ npm install -g @blackwell-systems/mcp-assert
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ Write assertion YAML files (see [mcp-assert docs](https://blackwell-systems.github.io/mcp-assert)):
40
+
41
+ ```yaml
42
+ # evals/echo.yaml
43
+ name: echo returns the message
44
+ server:
45
+ command: my-mcp-server
46
+ assert:
47
+ tool: echo
48
+ args:
49
+ message: hello
50
+ expect:
51
+ not_error: true
52
+ contains: ["hello"]
53
+ timeout: 30s
54
+ ```
55
+
56
+ Run with pytest:
57
+
58
+ ```bash
59
+ pytest --mcp-suite evals/
60
+ ```
61
+
62
+ Each YAML file becomes a pytest test item with pass/fail/skip semantics.
63
+
64
+ ## Options
65
+
66
+ | Option | pyproject.toml | Description |
67
+ |--------|---------------|-------------|
68
+ | `--mcp-suite DIR` | `mcp_suite` | Directory containing assertion YAML files |
69
+ | `--mcp-fixture DIR` | `mcp_fixture` | Fixture directory (substituted for `{{fixture}}`) |
70
+ | `--mcp-server CMD` | | Override server command for all assertions |
71
+ | `--mcp-timeout DUR` | `mcp_timeout` | Per-assertion timeout (default: 30s) |
72
+ | `--mcp-binary PATH` | | Path to mcp-assert binary |
73
+
74
+ ## Configuration
75
+
76
+ ```toml
77
+ # pyproject.toml
78
+ [tool.pytest.ini_options]
79
+ mcp_suite = "evals/"
80
+ mcp_fixture = "fixtures/"
81
+ mcp_timeout = "30s"
82
+ ```
83
+
84
+ Then just run `pytest` with no extra flags.
85
+
86
+ ## How it works
87
+
88
+ The plugin discovers `.yaml` files in the suite directory, creates a pytest Item for each one, and calls `mcp-assert run --suite <file> --json` to execute it. The JSON result is mapped to pytest outcomes:
89
+
90
+ - `status: "pass"` becomes a pytest pass
91
+ - `status: "fail"` becomes a pytest failure with the detail as the message
92
+ - `status: "skip"` becomes a pytest skip
93
+ - `skip: true` in the YAML skips the test (for known bugs)
94
+
95
+ The Go binary handles all MCP protocol interaction. The plugin is a thin bridge.
@@ -0,0 +1,7 @@
1
+ pytest_mcp_assert/__init__.py,sha256=AwkOYbGTNYY9Ozwld4txKf8_PNGCdPr_5-2m9dCVPoQ,59
2
+ pytest_mcp_assert/plugin.py,sha256=LmDt-TKqmP7tD_Wk7ixNK8xf9zPFrKChNoiU7h2Broc,6902
3
+ pytest_mcp_assert-0.5.0.dist-info/METADATA,sha256=1GWVmM5VhwCt1aeOWDGa20NVtzzW6QTPCh2zEsMo1H8,2779
4
+ pytest_mcp_assert-0.5.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ pytest_mcp_assert-0.5.0.dist-info/entry_points.txt,sha256=Jcttm0yzYDldIGc1u9Hv5UkUh8HgTkGu7u579oCx_Lk,49
6
+ pytest_mcp_assert-0.5.0.dist-info/top_level.txt,sha256=5aFjvITEKhfPOe4eYFqStY3uToxqBsnKvPTe2kJQeL4,18
7
+ pytest_mcp_assert-0.5.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ mcp_assert = pytest_mcp_assert.plugin
@@ -0,0 +1 @@
1
+ pytest_mcp_assert