pytest-mcp-tools 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sinan Ozel
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,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-mcp-tools
3
+ Version: 0.1.0
4
+ Author-email: Sinan Ozel <coding@sinan.slmail.me>
5
+ License: MIT License
6
+
7
+ Copyright (c) 2025 Sinan Ozel
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Project-URL: Homepage, https://github.com/<ORGANIZATION>/<MODULE-NAME>
28
+ Project-URL: Issues, https://github.com/<ORGANIZATION>/<MODULE-NAME>/issues
29
+ Classifier: Programming Language :: Python :: 3
30
+ Classifier: License :: OSI Approved :: MIT License
31
+ Classifier: Operating System :: OS Independent
32
+ Classifier: Framework :: Pytest
33
+ Description-Content-Type: text/markdown
34
+ License-File: LICENSE
35
+ Requires-Dist: art>=6.0
36
+ Requires-Dist: requests>=2.31.0
37
+ Provides-Extra: test
38
+ Requires-Dist: pytest>=7.0.0; extra == "test"
39
+ Requires-Dist: pytest-cov>=3.0.0; extra == "test"
40
+ Requires-Dist: pytest-depends>=1.0.1; extra == "test"
41
+ Requires-Dist: pytest-mock>=3.14.0; extra == "test"
42
+ Requires-Dist: httpx>=0.28.1; extra == "test"
43
+ Provides-Extra: dev
44
+ Requires-Dist: isort>=5.12.0; extra == "dev"
45
+ Requires-Dist: ruff>=0.12.11; extra == "dev"
46
+ Requires-Dist: black>=24.0.0; extra == "dev"
47
+ Requires-Dist: docformatter>=1.7.5; extra == "dev"
48
+ Provides-Extra: docs
49
+ Requires-Dist: mkdocs<2.0.0,>=1.5.0; extra == "docs"
50
+ Requires-Dist: mkdocs-material>=9.0.0; extra == "docs"
51
+ Requires-Dist: mkdocstrings[python]>=0.24.0; extra == "docs"
52
+ Provides-Extra: publish
53
+ Requires-Dist: packaging>=25.0; extra == "publish"
54
+ Dynamic: license-file
55
+
56
+ ![Tests & Lint](https://github.com/<ORGANIZATION>/pytest-mcp-tools/actions/workflows/ci.yaml/badge.svg?branch=main)
57
+ ![PyPI](https://img.shields.io/pypi/v/pytest-mcp-tools.svg)
58
+ ![Downloads](https://static.pepy.tech/badge/pytest-mcp-tools)
59
+ ![Monthly Downloads](https://static.pepy.tech/badge/pytest-mcp-tools/month)
60
+ ![License](https://img.shields.io/github/license/sinan-ozel/pypi-publish-with-cicd.svg)
61
+ [![Documentation](https://img.shields.io/badge/docs-github--pages-blue)](https://sinan-ozel.github.io/pytest-mcp-tools/)
62
+
63
+ # Introduction
64
+
65
+ # ✨ Introduction
66
+
67
+ I created this repository to automatically test my MCP tool servers.
68
+ ```
69
+ pytest --mcp-tools=http://localhost:8000
70
+ ```
71
+
72
+ Will create some tests, automatically, and you will get an output that looks like this:
73
+ ```
74
+ šŸ” MCP Tools: Discovering endpoints at http://test-server:8000...
75
+ Retry 1/10: Checking http://test-server:8000...
76
+ āœ“ Server reachable (status: 404)
77
+ āœ“ Found endpoint: /mcp (status: 406)
78
+ āœ— Endpoint /sse not found (status: 404)
79
+ āœ— Endpoint /messages not found (status: 404)
80
+ āœ… MCP Tools: Discovered endpoints: /mcp
81
+
82
+ ============================= test session starts ==============================
83
+ platform linux -- Python 3.11.14, pytest-9.0.2, pluggy-1.6.0 -- /usr/local/bin/python3.11
84
+ cachedir: .pytest_cache
85
+ rootdir: /app
86
+ configfile: pyproject.toml
87
+ plugins: cov-7.0.0, anyio-4.12.1, depends-1.0.1, mock-3.15.1, mcp-tools-0.1.0
88
+ collecting ... collected 3 items
89
+
90
+ āœ… MCP tools test created for discovered endpoints: /mcp
91
+ šŸ“” HTTP streaming support detected
92
+
93
+ test_samples/test_sample_math.py::test_sample_addition PASSED [ 25%]
94
+ test_samples/test_sample_math.py::test_sample_multiplication PASSED [ 50%]
95
+ .::test_mcp_tools[POST /mcp] PASSED [ 75%]
96
+ test_samples/test_sample_math.py::test_sample_string_operations PASSED [100%]
97
+
98
+ ============================== 4 passed in 0.03s ===============================
99
+ ```
100
+
101
+ Note the test called `.::test_mcp_tools[POST /mcp] PASSED [ 75%]`.
102
+ This is automatically generated by the plugin, and the plan is to make more of these automatically-generated tests based on descriptions of the tools.
103
+
104
+
105
+ # Reporting Issues
106
+ If you tested this on your server, and think that there is an issue, just give me the docker image of your server in the issue, and tell me what you are expecting, what you got.
107
+
108
+ If you don't have a docker hub image, give me a minimal example. That's all I need.
109
+
110
+ # šŸ› ļø Development
111
+
112
+ The only requirement is 🐳 Docker.
113
+ (The `.devcontainer` and `tasks.json` are prepared assuming a *nix system, but if you know the commands, this will work on Windows, too.)
114
+
115
+ 1. Clone the repo.
116
+ 2. Branch out.
117
+ 3. Open in "devcontainer" on VS Code and start developing. Run `pytest` under `tests` to test.
118
+ 4. Akternatively, if you are a fan of Test-Driven Development like me, you can run the tests without getting on a container. `.vscode/tasks.json` has the command to do so, but it's also listed here:
119
+ ```
120
+ docker compose -f tests/docker-compose.yaml up --build --abort-on-container-exit --exit-code-from test
121
+ ```
@@ -0,0 +1,66 @@
1
+ ![Tests & Lint](https://github.com/<ORGANIZATION>/pytest-mcp-tools/actions/workflows/ci.yaml/badge.svg?branch=main)
2
+ ![PyPI](https://img.shields.io/pypi/v/pytest-mcp-tools.svg)
3
+ ![Downloads](https://static.pepy.tech/badge/pytest-mcp-tools)
4
+ ![Monthly Downloads](https://static.pepy.tech/badge/pytest-mcp-tools/month)
5
+ ![License](https://img.shields.io/github/license/sinan-ozel/pypi-publish-with-cicd.svg)
6
+ [![Documentation](https://img.shields.io/badge/docs-github--pages-blue)](https://sinan-ozel.github.io/pytest-mcp-tools/)
7
+
8
+ # Introduction
9
+
10
+ # ✨ Introduction
11
+
12
+ I created this repository to automatically test my MCP tool servers.
13
+ ```
14
+ pytest --mcp-tools=http://localhost:8000
15
+ ```
16
+
17
+ Will create some tests, automatically, and you will get an output that looks like this:
18
+ ```
19
+ šŸ” MCP Tools: Discovering endpoints at http://test-server:8000...
20
+ Retry 1/10: Checking http://test-server:8000...
21
+ āœ“ Server reachable (status: 404)
22
+ āœ“ Found endpoint: /mcp (status: 406)
23
+ āœ— Endpoint /sse not found (status: 404)
24
+ āœ— Endpoint /messages not found (status: 404)
25
+ āœ… MCP Tools: Discovered endpoints: /mcp
26
+
27
+ ============================= test session starts ==============================
28
+ platform linux -- Python 3.11.14, pytest-9.0.2, pluggy-1.6.0 -- /usr/local/bin/python3.11
29
+ cachedir: .pytest_cache
30
+ rootdir: /app
31
+ configfile: pyproject.toml
32
+ plugins: cov-7.0.0, anyio-4.12.1, depends-1.0.1, mock-3.15.1, mcp-tools-0.1.0
33
+ collecting ... collected 3 items
34
+
35
+ āœ… MCP tools test created for discovered endpoints: /mcp
36
+ šŸ“” HTTP streaming support detected
37
+
38
+ test_samples/test_sample_math.py::test_sample_addition PASSED [ 25%]
39
+ test_samples/test_sample_math.py::test_sample_multiplication PASSED [ 50%]
40
+ .::test_mcp_tools[POST /mcp] PASSED [ 75%]
41
+ test_samples/test_sample_math.py::test_sample_string_operations PASSED [100%]
42
+
43
+ ============================== 4 passed in 0.03s ===============================
44
+ ```
45
+
46
+ Note the test called `.::test_mcp_tools[POST /mcp] PASSED [ 75%]`.
47
+ This is automatically generated by the plugin, and the plan is to make more of these automatically-generated tests based on descriptions of the tools.
48
+
49
+
50
+ # Reporting Issues
51
+ If you tested this on your server, and think that there is an issue, just give me the docker image of your server in the issue, and tell me what you are expecting, what you got.
52
+
53
+ If you don't have a docker hub image, give me a minimal example. That's all I need.
54
+
55
+ # šŸ› ļø Development
56
+
57
+ The only requirement is 🐳 Docker.
58
+ (The `.devcontainer` and `tasks.json` are prepared assuming a *nix system, but if you know the commands, this will work on Windows, too.)
59
+
60
+ 1. Clone the repo.
61
+ 2. Branch out.
62
+ 3. Open in "devcontainer" on VS Code and start developing. Run `pytest` under `tests` to test.
63
+ 4. Akternatively, if you are a fan of Test-Driven Development like me, you can run the tests without getting on a container. `.vscode/tasks.json` has the command to do so, but it's also listed here:
64
+ ```
65
+ docker compose -f tests/docker-compose.yaml up --build --abort-on-container-exit --exit-code-from test
66
+ ```
@@ -0,0 +1,73 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pytest-mcp-tools"
7
+ dynamic = ["version"]
8
+ description = ""
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Sinan Ozel", email = "coding@sinan.slmail.me" },
12
+ ]
13
+ license = { file = "LICENSE" }
14
+ dependencies = [
15
+ "art>=6.0",
16
+ "requests>=2.31.0",
17
+ ]
18
+ classifiers = [
19
+ "Programming Language :: Python :: 3",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: OS Independent",
22
+ "Framework :: Pytest",
23
+ ]
24
+
25
+ [project.entry-points.pytest11]
26
+ pytest_mcp_tools = "pytest_mcp_tools.plugin"
27
+
28
+ [project.optional-dependencies]
29
+ test = [
30
+ "pytest>=7.0.0", # For running tests
31
+ "pytest-cov>=3.0.0", # For test coverage reporting
32
+ "pytest-depends>=1.0.1", # For test dependency management
33
+ "pytest-mock>=3.14.0", # For mocking in tests
34
+ "httpx>=0.28.1", # To make more complex calls, such as streaming.
35
+ ]
36
+
37
+ dev = [
38
+ "isort>=5.12.0", # For import sorting
39
+ "ruff>=0.12.11", # For linting
40
+ "black>=24.0.0", # For code formatting (Black)
41
+ "docformatter>=1.7.5", # For formatting docstrings
42
+ ]
43
+
44
+ docs = [
45
+ "mkdocs>=1.5.0,<2.0.0", # Pin to MkDocs 1.x for Material compatibility
46
+ "mkdocs-material>=9.0.0", # Material theme for MkDocs
47
+ "mkdocstrings[python]>=0.24.0", # API documentation from docstrings
48
+ ]
49
+
50
+ publish = [
51
+ "packaging>=25.0",
52
+ ]
53
+
54
+ [tool.ruff]
55
+ line-length = 80
56
+
57
+ [tool.ruff.lint.per-file-ignores]
58
+ "**/test_*.py" = ["RUF001"]
59
+
60
+ [tool.black]
61
+ line-length = 80
62
+ target-version = ['py311']
63
+
64
+ [tool.isort]
65
+ profile = "black"
66
+ line_length = 80
67
+
68
+ [tool.setuptools.dynamic]
69
+ version = { attr = "pytest_mcp_tools.__version__" }
70
+
71
+ [project.urls]
72
+ Homepage = "https://github.com/<ORGANIZATION>/<MODULE-NAME>"
73
+ Issues = "https://github.com/<ORGANIZATION>/<MODULE-NAME>/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,236 @@
1
+ """Pytest plugin for MCP tools testing."""
2
+
3
+ import time
4
+
5
+ import pytest
6
+ import requests
7
+ from _pytest.python import Module
8
+
9
+
10
+ def pytest_addoption(parser):
11
+ """Add --mcp-tools CLI option."""
12
+ group = parser.getgroup("mcp-tools")
13
+ group.addoption(
14
+ "--mcp-tools",
15
+ action="store",
16
+ metavar="BASE_URL",
17
+ help="Run MCP tools tests against the specified base URL",
18
+ )
19
+
20
+
21
+ def pytest_configure(config):
22
+ """Configure pytest with MCP tools marker."""
23
+ config.addinivalue_line(
24
+ "markers",
25
+ "mcp_tools: MCP tools tests",
26
+ )
27
+
28
+ # If --mcp-tools flag is provided, discover endpoints
29
+ base_url = config.getoption("--mcp-tools")
30
+
31
+ if base_url:
32
+ # Store configuration
33
+ config._mcp_tools_base_url = base_url
34
+ config._mcp_tools_http_streaming = False
35
+ config._mcp_tools_sse = False
36
+
37
+ # Debug logging
38
+ print(f"\nšŸ” MCP Tools: Discovering endpoints at {base_url}...")
39
+
40
+ # Wait for server to be ready and discover which endpoints exist
41
+ endpoints_found = []
42
+ endpoints_404 = [] # Track which endpoints returned 404
43
+ max_retries = 10
44
+ retry_delay = 1.0
45
+
46
+ for retry in range(max_retries):
47
+ # First, check if the server is reachable at all
48
+ server_reachable = False
49
+ try:
50
+ # Try root endpoint first
51
+ print(f" Retry {retry + 1}/{max_retries}: Checking {base_url}...")
52
+ response = requests.get(
53
+ base_url, timeout=2, allow_redirects=False
54
+ )
55
+ if response.status_code < 500:
56
+ server_reachable = True
57
+ print(f" āœ“ Server reachable (status: {response.status_code})")
58
+ except Exception as e:
59
+ print(f" āœ— Server not reachable: {e}")
60
+
61
+ # If server is reachable, try specific endpoints
62
+ if server_reachable or retry > 3: # Give server extra time on early retries
63
+ for endpoint in ["/mcp", "/sse", "/messages"]:
64
+ # Skip this endpoint if we already got 404 for it
65
+ if endpoint in endpoints_404:
66
+ continue
67
+
68
+ try:
69
+ # Use POST for /mcp (JSON-RPC), GET for others
70
+ if endpoint == "/mcp":
71
+ response = requests.post(
72
+ f"{base_url}{endpoint}",
73
+ json={},
74
+ timeout=2,
75
+ allow_redirects=False,
76
+ )
77
+ else:
78
+ response = requests.get(
79
+ f"{base_url}{endpoint}",
80
+ timeout=2,
81
+ allow_redirects=False,
82
+ )
83
+
84
+ # Only consider endpoint as existing if we get a response that indicates
85
+ # the endpoint exists (not 404). Acceptable codes:
86
+ # - 200-299: Success
87
+ # - 400-499 except 404: Client error (endpoint exists but request invalid)
88
+ # - 405: Method not allowed (endpoint exists, wrong method)
89
+ if response.status_code < 500 and response.status_code != 404:
90
+ if endpoint not in endpoints_found:
91
+ endpoints_found.append(endpoint)
92
+ print(f" āœ“ Found endpoint: {endpoint} (status: {response.status_code})")
93
+
94
+ # Set internal toggles
95
+ if endpoint == "/mcp":
96
+ config._mcp_tools_http_streaming = True
97
+ elif endpoint == "/sse":
98
+ config._mcp_tools_sse = True
99
+ elif endpoint == "/messages":
100
+ config._mcp_tools_http_streaming = True
101
+ elif response.status_code == 404:
102
+ # Mark this endpoint as 404 so we don't retry it
103
+ if endpoint not in endpoints_404:
104
+ endpoints_404.append(endpoint)
105
+ print(f" āœ— Endpoint {endpoint} not found (status: 404)")
106
+
107
+ except Exception as e:
108
+ print(f" āœ— Endpoint {endpoint} not found: {e}")
109
+
110
+ # If we found at least one endpoint, we're done
111
+ if endpoints_found:
112
+ break
113
+
114
+ # If all endpoints returned 404, we're done (no need to retry)
115
+ if len(endpoints_404) == 3: # All three endpoints returned 404
116
+ break
117
+
118
+ # Wait before retrying
119
+ if retry < max_retries - 1:
120
+ time.sleep(retry_delay)
121
+
122
+ # Store discovered endpoints
123
+ config._mcp_tools_endpoints = endpoints_found
124
+
125
+ if endpoints_found:
126
+ print(f"āœ… MCP Tools: Discovered endpoints: {', '.join(endpoints_found)}\n")
127
+ else:
128
+ print("āŒ MCP Tools: No endpoints discovered! Server is not a valid MCP server.\n")
129
+
130
+ # Raise warning if SSE is used (legacy)
131
+ if config._mcp_tools_sse:
132
+ print(
133
+ "āš ļø Warning: SSE endpoint detected. SSE is legacy and should be migrated to HTTP streaming."
134
+ )
135
+
136
+
137
+ def pytest_collection_modifyitems(session, config, items):
138
+ """Inject MCP tools test items dynamically into the test collection.
139
+
140
+ This hook allows us to add MCP tools tests without requiring a test file.
141
+ """
142
+ # Check if --mcp-tools flag was provided
143
+ base_url = config.getoption("--mcp-tools", default=None)
144
+ if not base_url:
145
+ return
146
+
147
+ # Get discovered endpoints
148
+ endpoints = getattr(config, "_mcp_tools_endpoints", [])
149
+
150
+ # Create a virtual module to be parent of all MCP tools test items
151
+ module = Module.from_parent(session, path=session.path)
152
+ module._mcp_tools_virtual_module = True
153
+
154
+ # Create a single test that checks if at least one endpoint exists
155
+ test_items = []
156
+
157
+ if not endpoints:
158
+ # No endpoints found - create a failing test
159
+ test_id = "test_mcp_tools[NO ENDPOINTS FOUND]"
160
+
161
+ def make_failing_test(url):
162
+ def test_func():
163
+ pytest.fail(
164
+ f"No MCP endpoints found at {url}. Expected at least one of: /mcp, /sse, /messages"
165
+ )
166
+ return test_func
167
+
168
+ test_func = make_failing_test(base_url)
169
+ test_func.__name__ = test_id
170
+
171
+ # Create pytest Function item
172
+ item = pytest.Function.from_parent(
173
+ module,
174
+ name=test_id,
175
+ callobj=test_func,
176
+ )
177
+ item.add_marker(pytest.mark.mcp_tools)
178
+ test_items.append(item)
179
+ else:
180
+ # Format endpoint list for test name
181
+ endpoint_names = "|".join(endpoints)
182
+ test_id = f"test_mcp_tools[POST {endpoint_names}]"
183
+
184
+ def make_test_func(url, eps):
185
+ def test_func():
186
+ # Test passes if at least one endpoint was discovered
187
+ if not eps:
188
+ pytest.fail(
189
+ f"No MCP endpoints found at {url}. Expected at least one of: /mcp, /sse, /messages"
190
+ )
191
+
192
+ return test_func
193
+
194
+ test_func = make_test_func(base_url, endpoints)
195
+ test_func.__name__ = test_id
196
+
197
+ # Create pytest Function item
198
+ item = pytest.Function.from_parent(
199
+ module,
200
+ name=test_id,
201
+ callobj=test_func,
202
+ )
203
+ item.add_marker(pytest.mark.mcp_tools)
204
+ test_items.append(item)
205
+
206
+ # Add all MCP tools test items to the collection
207
+ items.extend(test_items)
208
+
209
+
210
+ def pytest_collection_finish(session):
211
+ """Print message about MCP tools tests after collection."""
212
+ config = session.config
213
+
214
+ # Only print if we added MCP tools tests
215
+ if hasattr(config, "_mcp_tools_endpoints"):
216
+ endpoints = config._mcp_tools_endpoints
217
+ if endpoints:
218
+ endpoint_list = ", ".join(endpoints)
219
+ print(
220
+ f"\nāœ… MCP tools test created for discovered endpoints: {endpoint_list}"
221
+ )
222
+
223
+ # Print status of toggles
224
+ http_streaming = getattr(
225
+ config, "_mcp_tools_http_streaming", False
226
+ )
227
+ sse = getattr(config, "_mcp_tools_sse", False)
228
+
229
+ if http_streaming:
230
+ print(" šŸ“” HTTP streaming support detected")
231
+ if sse:
232
+ print(" šŸ“” SSE support detected (legacy)")
233
+
234
+
235
+ # Use hookimpl with trylast to ensure this runs after terminal reporter
236
+ pytest_collection_finish.trylast = True
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-mcp-tools
3
+ Version: 0.1.0
4
+ Author-email: Sinan Ozel <coding@sinan.slmail.me>
5
+ License: MIT License
6
+
7
+ Copyright (c) 2025 Sinan Ozel
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Project-URL: Homepage, https://github.com/<ORGANIZATION>/<MODULE-NAME>
28
+ Project-URL: Issues, https://github.com/<ORGANIZATION>/<MODULE-NAME>/issues
29
+ Classifier: Programming Language :: Python :: 3
30
+ Classifier: License :: OSI Approved :: MIT License
31
+ Classifier: Operating System :: OS Independent
32
+ Classifier: Framework :: Pytest
33
+ Description-Content-Type: text/markdown
34
+ License-File: LICENSE
35
+ Requires-Dist: art>=6.0
36
+ Requires-Dist: requests>=2.31.0
37
+ Provides-Extra: test
38
+ Requires-Dist: pytest>=7.0.0; extra == "test"
39
+ Requires-Dist: pytest-cov>=3.0.0; extra == "test"
40
+ Requires-Dist: pytest-depends>=1.0.1; extra == "test"
41
+ Requires-Dist: pytest-mock>=3.14.0; extra == "test"
42
+ Requires-Dist: httpx>=0.28.1; extra == "test"
43
+ Provides-Extra: dev
44
+ Requires-Dist: isort>=5.12.0; extra == "dev"
45
+ Requires-Dist: ruff>=0.12.11; extra == "dev"
46
+ Requires-Dist: black>=24.0.0; extra == "dev"
47
+ Requires-Dist: docformatter>=1.7.5; extra == "dev"
48
+ Provides-Extra: docs
49
+ Requires-Dist: mkdocs<2.0.0,>=1.5.0; extra == "docs"
50
+ Requires-Dist: mkdocs-material>=9.0.0; extra == "docs"
51
+ Requires-Dist: mkdocstrings[python]>=0.24.0; extra == "docs"
52
+ Provides-Extra: publish
53
+ Requires-Dist: packaging>=25.0; extra == "publish"
54
+ Dynamic: license-file
55
+
56
+ ![Tests & Lint](https://github.com/<ORGANIZATION>/pytest-mcp-tools/actions/workflows/ci.yaml/badge.svg?branch=main)
57
+ ![PyPI](https://img.shields.io/pypi/v/pytest-mcp-tools.svg)
58
+ ![Downloads](https://static.pepy.tech/badge/pytest-mcp-tools)
59
+ ![Monthly Downloads](https://static.pepy.tech/badge/pytest-mcp-tools/month)
60
+ ![License](https://img.shields.io/github/license/sinan-ozel/pypi-publish-with-cicd.svg)
61
+ [![Documentation](https://img.shields.io/badge/docs-github--pages-blue)](https://sinan-ozel.github.io/pytest-mcp-tools/)
62
+
63
+ # Introduction
64
+
65
+ # ✨ Introduction
66
+
67
+ I created this repository to automatically test my MCP tool servers.
68
+ ```
69
+ pytest --mcp-tools=http://localhost:8000
70
+ ```
71
+
72
+ Will create some tests, automatically, and you will get an output that looks like this:
73
+ ```
74
+ šŸ” MCP Tools: Discovering endpoints at http://test-server:8000...
75
+ Retry 1/10: Checking http://test-server:8000...
76
+ āœ“ Server reachable (status: 404)
77
+ āœ“ Found endpoint: /mcp (status: 406)
78
+ āœ— Endpoint /sse not found (status: 404)
79
+ āœ— Endpoint /messages not found (status: 404)
80
+ āœ… MCP Tools: Discovered endpoints: /mcp
81
+
82
+ ============================= test session starts ==============================
83
+ platform linux -- Python 3.11.14, pytest-9.0.2, pluggy-1.6.0 -- /usr/local/bin/python3.11
84
+ cachedir: .pytest_cache
85
+ rootdir: /app
86
+ configfile: pyproject.toml
87
+ plugins: cov-7.0.0, anyio-4.12.1, depends-1.0.1, mock-3.15.1, mcp-tools-0.1.0
88
+ collecting ... collected 3 items
89
+
90
+ āœ… MCP tools test created for discovered endpoints: /mcp
91
+ šŸ“” HTTP streaming support detected
92
+
93
+ test_samples/test_sample_math.py::test_sample_addition PASSED [ 25%]
94
+ test_samples/test_sample_math.py::test_sample_multiplication PASSED [ 50%]
95
+ .::test_mcp_tools[POST /mcp] PASSED [ 75%]
96
+ test_samples/test_sample_math.py::test_sample_string_operations PASSED [100%]
97
+
98
+ ============================== 4 passed in 0.03s ===============================
99
+ ```
100
+
101
+ Note the test called `.::test_mcp_tools[POST /mcp] PASSED [ 75%]`.
102
+ This is automatically generated by the plugin, and the plan is to make more of these automatically-generated tests based on descriptions of the tools.
103
+
104
+
105
+ # Reporting Issues
106
+ If you tested this on your server, and think that there is an issue, just give me the docker image of your server in the issue, and tell me what you are expecting, what you got.
107
+
108
+ If you don't have a docker hub image, give me a minimal example. That's all I need.
109
+
110
+ # šŸ› ļø Development
111
+
112
+ The only requirement is 🐳 Docker.
113
+ (The `.devcontainer` and `tasks.json` are prepared assuming a *nix system, but if you know the commands, this will work on Windows, too.)
114
+
115
+ 1. Clone the repo.
116
+ 2. Branch out.
117
+ 3. Open in "devcontainer" on VS Code and start developing. Run `pytest` under `tests` to test.
118
+ 4. Akternatively, if you are a fan of Test-Driven Development like me, you can run the tests without getting on a container. `.vscode/tasks.json` has the command to do so, but it's also listed here:
119
+ ```
120
+ docker compose -f tests/docker-compose.yaml up --build --abort-on-container-exit --exit-code-from test
121
+ ```
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/pytest_mcp_tools/__init__.py
5
+ src/pytest_mcp_tools/plugin.py
6
+ src/pytest_mcp_tools.egg-info/PKG-INFO
7
+ src/pytest_mcp_tools.egg-info/SOURCES.txt
8
+ src/pytest_mcp_tools.egg-info/dependency_links.txt
9
+ src/pytest_mcp_tools.egg-info/entry_points.txt
10
+ src/pytest_mcp_tools.egg-info/requires.txt
11
+ src/pytest_mcp_tools.egg-info/top_level.txt
12
+ tests/test_integration.py
13
+ tests/test_unit.py
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ pytest_mcp_tools = pytest_mcp_tools.plugin
@@ -0,0 +1,23 @@
1
+ art>=6.0
2
+ requests>=2.31.0
3
+
4
+ [dev]
5
+ isort>=5.12.0
6
+ ruff>=0.12.11
7
+ black>=24.0.0
8
+ docformatter>=1.7.5
9
+
10
+ [docs]
11
+ mkdocs<2.0.0,>=1.5.0
12
+ mkdocs-material>=9.0.0
13
+ mkdocstrings[python]>=0.24.0
14
+
15
+ [publish]
16
+ packaging>=25.0
17
+
18
+ [test]
19
+ pytest>=7.0.0
20
+ pytest-cov>=3.0.0
21
+ pytest-depends>=1.0.1
22
+ pytest-mock>=3.14.0
23
+ httpx>=0.28.1
@@ -0,0 +1 @@
1
+ pytest_mcp_tools
@@ -0,0 +1,191 @@
1
+ """Integration tests for pytest-mcp-tools CLI functionality."""
2
+
3
+ import subprocess
4
+ import time
5
+ import pytest
6
+
7
+
8
+ def test_mcp_tools_flag_is_recognized():
9
+ """Test that --mcp-tools flag is recognized by pytest (plugin is loaded).
10
+
11
+ This test verifies that the pytest-mcp-tools plugin is properly installed
12
+ and the --mcp-tools flag is available.
13
+ """
14
+ print("\nšŸ” Testing if --mcp-tools flag is recognized...", flush=True)
15
+
16
+ # Give the MCP server time to start up
17
+ time.sleep(2)
18
+
19
+ # Run pytest with --mcp-tools flag
20
+ result = subprocess.run(
21
+ ["pytest", "--mcp-tools=http://basic-server:8000", "-v"],
22
+ capture_output=True,
23
+ text=True,
24
+ cwd="/app",
25
+ )
26
+
27
+ # Check that the flag is NOT unrecognized
28
+ output = result.stdout
29
+
30
+ assert (
31
+ "unrecognized arguments: --mcp-tools" not in output
32
+ ), f"Plugin not loaded: --mcp-tools flag not recognized. Output:\n{output}"
33
+
34
+ print("āœ… --mcp-tools flag is recognized", flush=True)
35
+
36
+
37
+ @pytest.mark.depends(on=["test_mcp_tools_flag_is_recognized"])
38
+ def test_basic_mcp_server_tools_discovered():
39
+ """Test that MCP tools are discovered and tests are generated.
40
+
41
+ This test verifies that the plugin:
42
+ 1. Discovers MCP tools from the server
43
+ 2. Generates test items for each tool
44
+ 3. Tests appear in pytest's output with the expected format
45
+
46
+ Expected test format: test_mcp_tools[POST /mcp|/sse|/messages]
47
+ """
48
+ print("\nšŸ” Testing MCP tools discovery and test generation...", flush=True)
49
+ time.sleep(0.5)
50
+
51
+ result = subprocess.run(
52
+ ["pytest", "--mcp-tools=http://basic-server:8000", "-v", "-s"],
53
+ capture_output=True,
54
+ text=True,
55
+ cwd="/app",
56
+ )
57
+
58
+ output = result.stdout
59
+ stderr = result.stderr
60
+
61
+ # Debug: print both stdout and stderr
62
+ print(f"STDOUT:\n{output}\n")
63
+ print(f"STDERR:\n{stderr}\n")
64
+
65
+ # Check that MCP tools test was discovered and appears in output
66
+ # Accept any endpoint pattern
67
+ assert (
68
+ "test_mcp_tools[POST" in output
69
+ ), f"Expected test_mcp_tools[POST ...] in output, got:\n{output}\n\nSTDERR:\n{stderr}"
70
+
71
+ # Check that regular tests also ran
72
+ assert (
73
+ "test_samples" in output
74
+ ), f"Expected regular test files to be collected, got:\n{output}"
75
+
76
+ # Check that some tests passed
77
+ assert (
78
+ "passed" in output.lower() or "PASSED" in output
79
+ ), f"Expected some tests to pass, got:\n{output}"
80
+
81
+ print("āœ… MCP tools discovered and tests generated", flush=True)
82
+
83
+
84
+ @pytest.mark.depends(on=["test_mcp_tools_flag_is_recognized"])
85
+ def test_mcp_tools_run_alongside_regular_tests():
86
+ """Test that --mcp-tools flag allows regular pytest tests to run alongside MCP tests.
87
+
88
+ This ensures the plugin integrates properly with pytest's test collection
89
+ and doesn't interfere with normal pytest operation.
90
+ """
91
+ print(
92
+ "\nšŸ” Testing MCP tools plugin with regular pytest tests...", flush=True
93
+ )
94
+ time.sleep(0.5)
95
+
96
+ # This test expects that there are regular test files in /app/test_samples/
97
+ # The plugin should:
98
+ # 1. Run MCP tools tests
99
+ # 2. Then allow pytest to continue and run regular tests
100
+ result = subprocess.run(
101
+ [
102
+ "pytest",
103
+ "--mcp-tools=http://basic-server:8000",
104
+ "/app/test_samples/",
105
+ "-v",
106
+ ],
107
+ capture_output=True,
108
+ text=True,
109
+ cwd="/app",
110
+ )
111
+
112
+ output = result.stdout
113
+
114
+ # Check that /mcp endpoint is found
115
+ assert (
116
+ "Found endpoint: /mcp" in output
117
+ ), f"Expected /mcp endpoint to be found, got:\n{output}"
118
+
119
+ # Check that /sse endpoint is not found (404)
120
+ assert (
121
+ "Endpoint /sse not found" in output
122
+ ), f"Expected /sse endpoint not found, got:\n{output}"
123
+
124
+ # Check that regular tests were collected and ran
125
+ assert (
126
+ "test_sample_addition" in output or "test_samples" in output
127
+ ), f"Expected regular test files to be collected, got:\n{output}"
128
+
129
+ # Check that regular tests passed
130
+ assert (
131
+ "test_sample_addition" in output or "PASSED" in output
132
+ ), f"Expected regular tests to pass, got:\n{output}"
133
+
134
+ print("āœ… MCP tools and regular tests run together", flush=True)
135
+
136
+
137
+ @pytest.mark.depends(on=["test_mcp_tools_flag_is_recognized"])
138
+ def test_empty_server_all_endpoints_404():
139
+ """Test that a server with no MCP endpoints returns 404 for all endpoint checks.
140
+
141
+ This test verifies that when a server has no MCP endpoints:
142
+ 1. All 3 MCP endpoints (POST /mcp, /sse, /messages) return 404
143
+ 2. The plugin reports no endpoints discovered
144
+ 3. No MCP test is created
145
+ """
146
+ print("\nšŸ” Testing server with no MCP endpoints (all 404)...", flush=True)
147
+ time.sleep(0.5)
148
+
149
+ result = subprocess.run(
150
+ ["pytest", "--mcp-tools=http://empty-server:8000", "-v", "-s"],
151
+ capture_output=True,
152
+ text=True,
153
+ cwd="/app",
154
+ )
155
+
156
+ output = result.stdout
157
+ stderr = result.stderr
158
+
159
+ # Debug: print both stdout and stderr
160
+ print(f"STDOUT:\n{output}\n")
161
+ print(f"STDERR:\n{stderr}\n")
162
+
163
+ # Check that all 3 endpoints were not found (404)
164
+ assert (
165
+ "Endpoint /mcp not found" in output
166
+ ), f"Expected /mcp endpoint not found, got:\n{output}\n\nSTDERR:\n{stderr}"
167
+
168
+ assert (
169
+ "Endpoint /sse not found" in output
170
+ ), f"Expected /sse endpoint not found, got:\n{output}\n\nSTDERR:\n{stderr}"
171
+
172
+ assert (
173
+ "Endpoint /messages not found" in output
174
+ ), f"Expected /messages endpoint not found, got:\n{output}\n\nSTDERR:\n{stderr}"
175
+
176
+ # Check that no endpoints were discovered
177
+ assert (
178
+ "No endpoints discovered" in output
179
+ ), f"Expected 'No endpoints discovered' message, got:\n{output}"
180
+
181
+ # Check that a failing test was created
182
+ assert (
183
+ "test_mcp_tools[NO ENDPOINTS FOUND]" in output or "FAILED" in output
184
+ ), f"Expected failing test to be created, got:\n{output}"
185
+
186
+ # Check that pytest exited with error code when no endpoints found
187
+ assert (
188
+ result.returncode != 0
189
+ ), f"Expected pytest to fail (exit code != 0) when no endpoints found, got exit code: {result.returncode}"
190
+
191
+ print("āœ… Empty server test shows all endpoints 404, pytest failed as expected", flush=True)
@@ -0,0 +1,9 @@
1
+ import os
2
+ import json
3
+ import time
4
+
5
+ import pytest
6
+
7
+
8
+ def test_unit():
9
+ assert True