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.
- pytest_mcp_tools-0.1.0/LICENSE +21 -0
- pytest_mcp_tools-0.1.0/PKG-INFO +121 -0
- pytest_mcp_tools-0.1.0/README.md +66 -0
- pytest_mcp_tools-0.1.0/pyproject.toml +73 -0
- pytest_mcp_tools-0.1.0/setup.cfg +4 -0
- pytest_mcp_tools-0.1.0/src/pytest_mcp_tools/__init__.py +1 -0
- pytest_mcp_tools-0.1.0/src/pytest_mcp_tools/plugin.py +236 -0
- pytest_mcp_tools-0.1.0/src/pytest_mcp_tools.egg-info/PKG-INFO +121 -0
- pytest_mcp_tools-0.1.0/src/pytest_mcp_tools.egg-info/SOURCES.txt +13 -0
- pytest_mcp_tools-0.1.0/src/pytest_mcp_tools.egg-info/dependency_links.txt +1 -0
- pytest_mcp_tools-0.1.0/src/pytest_mcp_tools.egg-info/entry_points.txt +2 -0
- pytest_mcp_tools-0.1.0/src/pytest_mcp_tools.egg-info/requires.txt +23 -0
- pytest_mcp_tools-0.1.0/src/pytest_mcp_tools.egg-info/top_level.txt +1 -0
- pytest_mcp_tools-0.1.0/tests/test_integration.py +191 -0
- pytest_mcp_tools-0.1.0/tests/test_unit.py +9 -0
|
@@ -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
|
+

|
|
57
|
+

|
|
58
|
+

|
|
59
|
+

|
|
60
|
+

|
|
61
|
+
[](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
|
+

|
|
2
|
+

|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
[](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 @@
|
|
|
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
|
+

|
|
57
|
+

|
|
58
|
+

|
|
59
|
+

|
|
60
|
+

|
|
61
|
+
[](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 @@
|
|
|
1
|
+
|
|
@@ -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)
|