mcp-api-test 0.2.0__tar.gz → 0.3.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.
Files changed (32) hide show
  1. {mcp_api_test-0.2.0/src/mcp_api_test.egg-info → mcp_api_test-0.3.0}/PKG-INFO +7 -6
  2. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/README.md +5 -5
  3. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/pyproject.toml +2 -1
  4. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/src/mcp_api_test/__init__.py +1 -1
  5. mcp_api_test-0.3.0/src/mcp_api_test/assertions.py +192 -0
  6. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/src/mcp_api_test/flags.py +2 -7
  7. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/src/mcp_api_test/models.py +2 -1
  8. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/src/mcp_api_test/server.py +143 -106
  9. mcp_api_test-0.3.0/src/mcp_api_test/utils.py +206 -0
  10. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0/src/mcp_api_test.egg-info}/PKG-INFO +7 -6
  11. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/src/mcp_api_test.egg-info/SOURCES.txt +4 -0
  12. mcp_api_test-0.3.0/tests/test_assertions.py +191 -0
  13. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/tests/test_mcp_api_tools.py +1 -1
  14. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/tests/test_packaging.py +9 -0
  15. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/tests/test_parse_curl_command.py +58 -2
  16. mcp_api_test-0.3.0/tests/test_run_test_api_behavior.py +278 -0
  17. mcp_api_test-0.3.0/tests/test_run_test_api_options.py +363 -0
  18. mcp_api_test-0.3.0/tests/test_utils.py +178 -0
  19. mcp_api_test-0.2.0/src/mcp_api_test/utils.py +0 -117
  20. mcp_api_test-0.2.0/tests/test_utils.py +0 -23
  21. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/LICENSE +0 -0
  22. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/MANIFEST.in +0 -0
  23. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/setup.cfg +0 -0
  24. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/src/mcp_api_test/__main__.py +0 -0
  25. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/src/mcp_api_test/logger.py +0 -0
  26. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/src/mcp_api_test/py.typed +0 -0
  27. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/src/mcp_api_test.egg-info/dependency_links.txt +0 -0
  28. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/src/mcp_api_test.egg-info/entry_points.txt +0 -0
  29. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/src/mcp_api_test.egg-info/requires.txt +0 -0
  30. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/src/mcp_api_test.egg-info/top_level.txt +0 -0
  31. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/tests/test_mcp_server_compat.py +0 -0
  32. {mcp_api_test-0.2.0 → mcp_api_test-0.3.0}/tests/test_parse_status_code.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-api-test
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: MCP server for testing REST API with built-in assertions
5
5
  Author: Ryan Febriansyah
6
6
  License-Expression: MIT
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Topic :: Software Development :: Quality Assurance
17
17
  Classifier: Topic :: Software Development :: Testing
18
+ Classifier: Typing :: Typed
18
19
  Requires-Python: >=3.10
19
20
  Description-Content-Type: text/markdown
20
21
  License-File: LICENSE
@@ -40,7 +41,7 @@ Unlike traditional testing frameworks, `mcp-api-test` exposes API testing as a *
40
41
 
41
42
  It complements (rather than replaces) tools like pytest, Postman, and Playwright.
42
43
 
43
- > **Breaking changes in v0.2.0.** The server now runs on `mcp` 2.x, and the package layout moved from flat top-level modules to a proper `mcp_api_test` package. If you import internals directly, you may update to `from mcp_api_test.server import ...`. MCP client configuration (OpenCode, Claude Desktop, Claude Code) is unaffected. See the [changelog](docs/changelog.md) for details.
44
+ > **Breaking changes in v0.2.0.** The server now runs on `mcp` 2.x, and the package layout moved from flat top-level modules to a proper `mcp_api_test` package. If you import internals directly, you may update to `from mcp_api_test.server import ...`. MCP client configuration (OpenCode, Claude Desktop, Claude Code) is unaffected. See the [changelog](https://ryaneatfood.gitlab.io/mcp-api-testing/changelog/) for details.
44
45
 
45
46
  ## Use cases
46
47
 
@@ -105,10 +106,10 @@ The agent invokes `run_test_api`, validates against the expected contract, and r
105
106
 
106
107
  Full documentation is hosted on this [page](https://ryaneatfood.gitlab.io/mcp-api-testing/), and another way to navigate the docs:
107
108
 
108
- - **[Getting Started](docs/getting-started/installation.md)**: installation and client setup
109
- - **[Usage](docs/usage/request-modes.md)**: cURL mode, structured mode, assertions, response format
110
- - **[Guides](docs/guides/troubleshooting.md)**: troubleshooting, development, configuration
111
- - **[Reference](docs/reference/tool-parameters.md)**: full parameter reference
109
+ - **[Getting Started](https://ryaneatfood.gitlab.io/mcp-api-testing/getting-started/installation/)**: installation and client setup
110
+ - **[Usage](https://ryaneatfood.gitlab.io/mcp-api-testing/usage/request-modes/)**: cURL mode, structured mode, assertions, response format
111
+ - **[Guides](https://ryaneatfood.gitlab.io/mcp-api-testing/guides/troubleshooting/)**: troubleshooting, development, configuration
112
+ - **[Reference](https://ryaneatfood.gitlab.io/mcp-api-testing/reference/tool-parameters/)**: full parameter reference
112
113
 
113
114
  ## License
114
115
 
@@ -8,7 +8,7 @@ Unlike traditional testing frameworks, `mcp-api-test` exposes API testing as a *
8
8
 
9
9
  It complements (rather than replaces) tools like pytest, Postman, and Playwright.
10
10
 
11
- > **Breaking changes in v0.2.0.** The server now runs on `mcp` 2.x, and the package layout moved from flat top-level modules to a proper `mcp_api_test` package. If you import internals directly, you may update to `from mcp_api_test.server import ...`. MCP client configuration (OpenCode, Claude Desktop, Claude Code) is unaffected. See the [changelog](docs/changelog.md) for details.
11
+ > **Breaking changes in v0.2.0.** The server now runs on `mcp` 2.x, and the package layout moved from flat top-level modules to a proper `mcp_api_test` package. If you import internals directly, you may update to `from mcp_api_test.server import ...`. MCP client configuration (OpenCode, Claude Desktop, Claude Code) is unaffected. See the [changelog](https://ryaneatfood.gitlab.io/mcp-api-testing/changelog/) for details.
12
12
 
13
13
  ## Use cases
14
14
 
@@ -73,10 +73,10 @@ The agent invokes `run_test_api`, validates against the expected contract, and r
73
73
 
74
74
  Full documentation is hosted on this [page](https://ryaneatfood.gitlab.io/mcp-api-testing/), and another way to navigate the docs:
75
75
 
76
- - **[Getting Started](docs/getting-started/installation.md)**: installation and client setup
77
- - **[Usage](docs/usage/request-modes.md)**: cURL mode, structured mode, assertions, response format
78
- - **[Guides](docs/guides/troubleshooting.md)**: troubleshooting, development, configuration
79
- - **[Reference](docs/reference/tool-parameters.md)**: full parameter reference
76
+ - **[Getting Started](https://ryaneatfood.gitlab.io/mcp-api-testing/getting-started/installation/)**: installation and client setup
77
+ - **[Usage](https://ryaneatfood.gitlab.io/mcp-api-testing/usage/request-modes/)**: cURL mode, structured mode, assertions, response format
78
+ - **[Guides](https://ryaneatfood.gitlab.io/mcp-api-testing/guides/troubleshooting/)**: troubleshooting, development, configuration
79
+ - **[Reference](https://ryaneatfood.gitlab.io/mcp-api-testing/reference/tool-parameters/)**: full parameter reference
80
80
 
81
81
  ## License
82
82
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp-api-test"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "MCP server for testing REST API with built-in assertions"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -28,6 +28,7 @@ classifiers = [
28
28
  "Programming Language :: Python :: 3.12",
29
29
  "Topic :: Software Development :: Quality Assurance",
30
30
  "Topic :: Software Development :: Testing",
31
+ "Typing :: Typed",
31
32
  ]
32
33
 
33
34
  [project.scripts]
@@ -1,3 +1,3 @@
1
1
  """Lightweight MCP for REST API testing for Claude Desktop and OpenCode"""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
@@ -0,0 +1,192 @@
1
+ """
2
+ Assertion evaluation for a completed HTTP response.
3
+
4
+ ``evaluate_assertions`` is the single entry point: it takes the response, the
5
+ parsed body and the measured time, and returns one ``AssertionResult`` per
6
+ requested check. It never raises for a failed check, a mismatch is reported as
7
+ ``passed=False`` with the expected and actual values, so the caller can render
8
+ every failure at once instead of stopping at the first one.
9
+ """
10
+
11
+ import json
12
+ import httpx
13
+ from typing import Any, Dict, List, Optional, Tuple
14
+ from .models import AssertionResult
15
+ from .utils import get_nested_fields, get_nested_value
16
+
17
+
18
+ def values_equal(expected: Any, actual: Any) -> bool:
19
+ """
20
+ Compare an expected JSON value with the actual one.
21
+
22
+ Same as ``==`` except that a boolean never equals a number: in Python
23
+ ``True == 1`` and ``False == 0``, but in JSON they are different types, so
24
+ an assertion of ``{"ok": true}`` must not pass against ``{"ok": 1}``.
25
+ Nested dicts and lists are compared with plain ``==``.
26
+ """
27
+ if isinstance(expected, bool) != isinstance(actual, bool):
28
+ return False
29
+ return bool(expected == actual)
30
+
31
+
32
+ def body_as_text(body: Any) -> str:
33
+ """
34
+ Serialize a response body into the text the substring assertions search.
35
+
36
+ Dicts and lists become JSON (with real double quotes, and non-ASCII
37
+ characters kept as-is rather than ``\\uXXXX`` escapes). Anything else, such
38
+ as a plain-text body, is returned as ``str``.
39
+ """
40
+ return (
41
+ json.dumps(body, ensure_ascii=False)
42
+ if isinstance(body, (dict, list))
43
+ else str(body)
44
+ )
45
+
46
+
47
+ def evaluate_assertions(
48
+ response: httpx.Response,
49
+ body: Any,
50
+ response_time_ms: float,
51
+ *,
52
+ status_code: Optional[int] = None,
53
+ status_range: Optional[Tuple[str, Tuple[int, int]]] = None,
54
+ fields: Optional[List[str]] = None,
55
+ values: Optional[Dict[str, Any]] = None,
56
+ headers: Optional[Dict[str, str]] = None,
57
+ contains: Optional[str] = None,
58
+ not_contains: Optional[str] = None,
59
+ max_time_ms: Optional[float] = None,
60
+ ) -> List[AssertionResult]:
61
+ """
62
+ Run every requested assertion independently and return one result each.
63
+
64
+ Only the checks whose argument is given are run, so with no keyword
65
+ arguments the result is an empty list. Results keep a stable order:
66
+ status code, status range, fields, values, headers, contains,
67
+ not-contains, then response time.
68
+
69
+ Args:
70
+ response: The completed response, used for the status code and headers.
71
+ body: The parsed response body, JSON when it parsed, otherwise raw text.
72
+ response_time_ms: How long the request took, in milliseconds.
73
+ status_code: Exact status code the response must have.
74
+ status_range: ``(label, (low, high))`` inclusive range the status code
75
+ must fall in. The label (for example ``"2xx"``) is only used for
76
+ reporting.
77
+ fields: Dot-notation paths that must exist. For a JSON array body, a
78
+ path passes when at least one item has it. For a non-JSON body the
79
+ entry is matched as a plain substring.
80
+ values: Dot-notation path to exact expected value. Numeric path parts
81
+ index into lists (``items.0.id``). A missing path fails, even when
82
+ the expected value is ``None``. One result is produced per path.
83
+ headers: Header name to expected text. Names are case-insensitive and
84
+ the value passes when it *contains* the expected text, ignoring
85
+ case, so ``"application/json"`` matches
86
+ ``"application/json; charset=utf-8"``. An empty string only checks
87
+ that the header is present. One result is produced per header.
88
+ contains: Text the serialized body must contain.
89
+ not_contains: Text the serialized body must not contain.
90
+ max_time_ms: Upper limit, inclusive, for ``response_time_ms``.
91
+
92
+ Returns:
93
+ One ``AssertionResult`` per check. ``expected`` and ``actual`` are set
94
+ so a failure can be explained without re-running the request.
95
+ """
96
+ results: List[AssertionResult] = []
97
+ actual_status = response.status_code
98
+
99
+ if status_code is not None:
100
+ results.append(
101
+ AssertionResult(
102
+ "status_code", actual_status == status_code, status_code, actual_status
103
+ )
104
+ )
105
+
106
+ if status_range is not None:
107
+ label, (low, high) = status_range
108
+ results.append(
109
+ AssertionResult(
110
+ "status_code_range", low <= actual_status <= high, label, actual_status
111
+ )
112
+ )
113
+
114
+ for field in fields or []:
115
+ if isinstance(body, dict):
116
+ exists = get_nested_fields(body=body, field_path=field)
117
+ elif isinstance(body, list):
118
+ # For JSON arrays, check whether at least one item
119
+ # (if dict) contains the expected field.
120
+ exists = any(
121
+ (
122
+ get_nested_fields(body=item, field_path=field)
123
+ if isinstance(item, dict)
124
+ else field in str(item)
125
+ )
126
+ for item in body
127
+ )
128
+ else:
129
+ # Scalar or plain-text response like substring match.
130
+ exists = field in str(body)
131
+ results.append(
132
+ AssertionResult(
133
+ "response_field_exist",
134
+ exists,
135
+ field,
136
+ "field is present" if exists else "field is missing",
137
+ )
138
+ )
139
+
140
+ for path, expected in (values or {}).items():
141
+ found, actual = get_nested_value(body, path)
142
+ results.append(
143
+ AssertionResult(
144
+ f"response_value:{path}",
145
+ found and values_equal(expected, actual),
146
+ expected,
147
+ actual if found else "field is missing",
148
+ )
149
+ )
150
+
151
+ for name, expected in (headers or {}).items():
152
+ actual_header = response.headers.get(name)
153
+ # header exists and its value contains the expected text, so that
154
+ # "application/json" also matches "application/json; charset=utf-8"
155
+ ok = actual_header is not None and expected.lower() in actual_header.lower()
156
+ results.append(
157
+ AssertionResult(
158
+ f"response_header:{name.lower()}",
159
+ ok,
160
+ expected,
161
+ actual_header if actual_header is not None else "header is missing",
162
+ )
163
+ )
164
+
165
+ if contains is not None:
166
+ results.append(
167
+ AssertionResult(
168
+ "response_contains", contains in body_as_text(body), contains, None
169
+ )
170
+ )
171
+
172
+ if not_contains is not None:
173
+ results.append(
174
+ AssertionResult(
175
+ "response_not_contains",
176
+ not_contains not in body_as_text(body),
177
+ not_contains,
178
+ None,
179
+ )
180
+ )
181
+
182
+ if max_time_ms is not None:
183
+ results.append(
184
+ AssertionResult(
185
+ "max_response_time_ms",
186
+ response_time_ms <= max_time_ms,
187
+ f"<= {max_time_ms:g}",
188
+ round(response_time_ms, 2),
189
+ )
190
+ )
191
+
192
+ return results
@@ -12,7 +12,7 @@ CURL_VALUE_FLAGS: dict[str, str] = {
12
12
  "--data-raw": "data", # browser DevTools "Copy as cURL" uses this
13
13
  "--data-binary": "data", # common in API clients
14
14
  "--data-urlencode": "data", # URL-encoded payloads
15
- "-u": "user",
15
+ "-u": "user", # basic auth, applied via httpx.BasicAuth
16
16
  "--user": "user",
17
17
  "--url": "url_flag", # explicit --url form
18
18
  "-A": "user_agent",
@@ -53,9 +53,4 @@ CURL_BOOLEAN_FLAGS: set[str] = {
53
53
 
54
54
  # flags that we recognize but want to surface with a warning for,
55
55
  # emit a logger.warning with a human-readable explanation
56
- CURL_WARN_FLAGS: dict[str, str] = {
57
- "-k": "--insecure is not supported: TLS verification will still be enforced",
58
- "--insecure": "--insecure is not supported: TLS verification will still be enforced",
59
- "-u": "-u/--user (basic auth) is not supported: add an Authorization header explicitly",
60
- "--user": "-u/--user (basic auth) is not supported: add an Authorization header explicitly",
61
- }
56
+ CURL_WARN_FLAGS: dict[str, str] = {}
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass, field
2
- from typing import List, Any
2
+ from typing import Dict, List, Any, Optional
3
3
 
4
4
 
5
5
  @dataclass
@@ -20,3 +20,4 @@ class APITestResult:
20
20
  response_time_ms: float
21
21
  assertions: List[AssertionResult] = field(default_factory=list)
22
22
  response_body: Any = None
23
+ response_headers: Optional[Dict[str, str]] = None