mcp-api-test 0.1.3__tar.gz → 0.2.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 (29) hide show
  1. {mcp_api_test-0.1.3/src/mcp_api_test.egg-info → mcp_api_test-0.2.0}/PKG-INFO +7 -4
  2. {mcp_api_test-0.1.3 → mcp_api_test-0.2.0}/README.md +4 -2
  3. {mcp_api_test-0.1.3 → mcp_api_test-0.2.0}/pyproject.toml +18 -6
  4. mcp_api_test-0.2.0/src/mcp_api_test/__init__.py +3 -0
  5. {mcp_api_test-0.1.3/src → mcp_api_test-0.2.0/src/mcp_api_test}/__main__.py +1 -1
  6. mcp_api_test-0.2.0/src/mcp_api_test/flags.py +61 -0
  7. {mcp_api_test-0.1.3/src → mcp_api_test-0.2.0/src/mcp_api_test}/logger.py +11 -2
  8. mcp_api_test-0.2.0/src/mcp_api_test/py.typed +0 -0
  9. {mcp_api_test-0.1.3/src → mcp_api_test-0.2.0/src/mcp_api_test}/server.py +32 -22
  10. {mcp_api_test-0.1.3/src → mcp_api_test-0.2.0/src/mcp_api_test}/utils.py +2 -2
  11. {mcp_api_test-0.1.3 → mcp_api_test-0.2.0/src/mcp_api_test.egg-info}/PKG-INFO +7 -4
  12. {mcp_api_test-0.1.3 → mcp_api_test-0.2.0}/src/mcp_api_test.egg-info/SOURCES.txt +12 -6
  13. mcp_api_test-0.2.0/src/mcp_api_test.egg-info/entry_points.txt +2 -0
  14. {mcp_api_test-0.1.3 → mcp_api_test-0.2.0}/src/mcp_api_test.egg-info/requires.txt +2 -1
  15. mcp_api_test-0.2.0/src/mcp_api_test.egg-info/top_level.txt +1 -0
  16. {mcp_api_test-0.1.3 → mcp_api_test-0.2.0}/tests/test_mcp_api_tools.py +1 -1
  17. mcp_api_test-0.2.0/tests/test_mcp_server_compat.py +30 -0
  18. mcp_api_test-0.2.0/tests/test_packaging.py +49 -0
  19. mcp_api_test-0.2.0/tests/test_parse_curl_command.py +69 -0
  20. {mcp_api_test-0.1.3 → mcp_api_test-0.2.0}/tests/test_parse_status_code.py +1 -1
  21. mcp_api_test-0.2.0/tests/test_utils.py +23 -0
  22. mcp_api_test-0.1.3/src/mcp_api_test.egg-info/entry_points.txt +0 -2
  23. mcp_api_test-0.1.3/src/mcp_api_test.egg-info/top_level.txt +0 -5
  24. mcp_api_test-0.1.3/tests/test_parse_curl_command.py +0 -23
  25. {mcp_api_test-0.1.3 → mcp_api_test-0.2.0}/LICENSE +0 -0
  26. {mcp_api_test-0.1.3 → mcp_api_test-0.2.0}/MANIFEST.in +0 -0
  27. {mcp_api_test-0.1.3 → mcp_api_test-0.2.0}/setup.cfg +0 -0
  28. {mcp_api_test-0.1.3/src → mcp_api_test-0.2.0/src/mcp_api_test}/models.py +0 -0
  29. {mcp_api_test-0.1.3 → mcp_api_test-0.2.0}/src/mcp_api_test.egg-info/dependency_links.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-api-test
3
- Version: 0.1.3
3
+ Version: 0.2.0
4
4
  Summary: MCP server for testing REST API with built-in assertions
5
5
  Author: Ryan Febriansyah
6
6
  License-Expression: MIT
@@ -18,20 +18,21 @@ Classifier: Topic :: Software Development :: Testing
18
18
  Requires-Python: >=3.10
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
- Requires-Dist: mcp
21
+ Requires-Dist: mcp<3.0.0,>=2.0.0
22
22
  Requires-Dist: httpx
23
23
  Requires-Dist: python-dotenv
24
24
  Provides-Extra: dev
25
25
  Requires-Dist: pytest; extra == "dev"
26
26
  Requires-Dist: pytest-asyncio; extra == "dev"
27
27
  Requires-Dist: respx; extra == "dev"
28
+ Requires-Dist: mypy; extra == "dev"
28
29
  Provides-Extra: docs
29
30
  Requires-Dist: mkdocs-material; extra == "docs"
30
31
  Dynamic: license-file
31
32
 
32
33
  # mcp-api-test
33
34
 
34
- ![pipeline](https://gitlab.com/ryaneatfood/mcp-api-testing/badges/master/pipeline.svg)
35
+ ![pipeline](https://gitlab.com/ryaneatfood/mcp-api-testing/badges/master/pipeline.svg) ![PyPI License](https://img.shields.io/pypi/l/mcp-api-test) ![PyPI Python Version](https://img.shields.io/pypi/pyversions/mcp-api-test)
35
36
 
36
37
  An AI-native API testing engine that enables LLM agents to execute, validate, and reason about REST APIs using natural language or cURL inputs.
37
38
 
@@ -39,6 +40,8 @@ Unlike traditional testing frameworks, `mcp-api-test` exposes API testing as a *
39
40
 
40
41
  It complements (rather than replaces) tools like pytest, Postman, and Playwright.
41
42
 
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
+
42
45
  ## Use cases
43
46
 
44
47
  - **QA & test engineering**: prompt an agent with natural language instead of crafting Postman requests
@@ -100,7 +103,7 @@ The agent invokes `run_test_api`, validates against the expected contract, and r
100
103
 
101
104
  ## Documentation
102
105
 
103
- Full documentation is hosted on [GitLab Pages](https://ryaneatfood.gitlab.io/mcp-api-testing/) (or build locally with `mkdocs serve`).
106
+ Full documentation is hosted on this [page](https://ryaneatfood.gitlab.io/mcp-api-testing/), and another way to navigate the docs:
104
107
 
105
108
  - **[Getting Started](docs/getting-started/installation.md)**: installation and client setup
106
109
  - **[Usage](docs/usage/request-modes.md)**: cURL mode, structured mode, assertions, response format
@@ -1,6 +1,6 @@
1
1
  # mcp-api-test
2
2
 
3
- ![pipeline](https://gitlab.com/ryaneatfood/mcp-api-testing/badges/master/pipeline.svg)
3
+ ![pipeline](https://gitlab.com/ryaneatfood/mcp-api-testing/badges/master/pipeline.svg) ![PyPI License](https://img.shields.io/pypi/l/mcp-api-test) ![PyPI Python Version](https://img.shields.io/pypi/pyversions/mcp-api-test)
4
4
 
5
5
  An AI-native API testing engine that enables LLM agents to execute, validate, and reason about REST APIs using natural language or cURL inputs.
6
6
 
@@ -8,6 +8,8 @@ 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.
12
+
11
13
  ## Use cases
12
14
 
13
15
  - **QA & test engineering**: prompt an agent with natural language instead of crafting Postman requests
@@ -69,7 +71,7 @@ The agent invokes `run_test_api`, validates against the expected contract, and r
69
71
 
70
72
  ## Documentation
71
73
 
72
- Full documentation is hosted on [GitLab Pages](https://ryaneatfood.gitlab.io/mcp-api-testing/) (or build locally with `mkdocs serve`).
74
+ Full documentation is hosted on this [page](https://ryaneatfood.gitlab.io/mcp-api-testing/), and another way to navigate the docs:
73
75
 
74
76
  - **[Getting Started](docs/getting-started/installation.md)**: installation and client setup
75
77
  - **[Usage](docs/usage/request-modes.md)**: cURL mode, structured mode, assertions, response format
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp-api-test"
3
- version = "0.1.3"
3
+ version = "0.2.0"
4
4
  description = "MCP server for testing REST API with built-in assertions"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -10,7 +10,7 @@ authors = [
10
10
 
11
11
  requires-python = ">=3.10"
12
12
  dependencies = [
13
- "mcp",
13
+ "mcp>=2.0.0,<3.0.0",
14
14
  "httpx",
15
15
  "python-dotenv"
16
16
  ]
@@ -31,7 +31,7 @@ classifiers = [
31
31
  ]
32
32
 
33
33
  [project.scripts]
34
- mcp-api-test = "server:main"
34
+ mcp-api-test = "mcp_api_test.server:main"
35
35
 
36
36
  [build-system]
37
37
  requires = ["setuptools>=61"]
@@ -39,8 +39,20 @@ build-backend = "setuptools.build_meta"
39
39
 
40
40
  [tool.setuptools]
41
41
  package-dir = {"" = "src"}
42
- py-modules = ["server", "__main__", "logger", "models", "utils"]
42
+ packages = ["mcp_api_test"]
43
+
44
+ [tool.setuptools.package-data]
45
+ mcp_api_test = ["py.typed"]
43
46
 
44
47
  [project.optional-dependencies]
45
- dev = ["pytest", "pytest-asyncio", "respx"]
46
- docs = ["mkdocs-material"]
48
+ dev = ["pytest", "pytest-asyncio", "respx", "mypy"]
49
+ docs = ["mkdocs-material"]
50
+
51
+ [tool.mypy]
52
+ python_version = "3.10"
53
+ packages = ["mcp_api_test"]
54
+ check_untyped_defs = true
55
+ disallow_untyped_defs = true
56
+ no_implicit_optional = true
57
+ warn_redundant_casts = true
58
+ warn_unused_ignores = true
@@ -0,0 +1,3 @@
1
+ """Lightweight MCP for REST API testing for Claude Desktop and OpenCode"""
2
+
3
+ __version__ = "0.2.0"
@@ -1,6 +1,6 @@
1
1
  """Entry point for the initialize mcp-api-test package when run as a module"""
2
2
 
3
- from src.server import mcp
3
+ from .server import mcp
4
4
 
5
5
  if __name__ == "__main__":
6
6
  mcp.run()
@@ -0,0 +1,61 @@
1
+ """A collection of curl flag which is being used during HTTP request/inspection"""
2
+
3
+ # flags that consume the next token as their value,
4
+ # maps every recognize consuming flag to its semantic roles
5
+ CURL_VALUE_FLAGS: dict[str, str] = {
6
+ "-X": "method",
7
+ "--request": "method",
8
+ "-H": "header",
9
+ "--header": "header",
10
+ "-d": "data",
11
+ "--data": "data",
12
+ "--data-raw": "data", # browser DevTools "Copy as cURL" uses this
13
+ "--data-binary": "data", # common in API clients
14
+ "--data-urlencode": "data", # URL-encoded payloads
15
+ "-u": "user",
16
+ "--user": "user",
17
+ "--url": "url_flag", # explicit --url form
18
+ "-A": "user_agent",
19
+ "--user-agent": "user_agent",
20
+ "--max-time": "ignored",
21
+ "--connect-timeout": "ignored",
22
+ "-o": "ignored",
23
+ "--output": "ignored",
24
+ "--proxy": "ignored",
25
+ "-x": "ignored",
26
+ "-e": "ignored",
27
+ "--referer": "ignored",
28
+ }
29
+
30
+ # standalone flags with no following value
31
+ CURL_BOOLEAN_FLAGS: set[str] = {
32
+ "-L",
33
+ "--location", # follow redirects, very common in browser exports
34
+ "-k",
35
+ "--insecure", # skip TLS verification
36
+ "-s",
37
+ "--silent",
38
+ "-v",
39
+ "--verbose",
40
+ "-i",
41
+ "--include",
42
+ "-I",
43
+ "--head",
44
+ "--compressed", # added by Chrome DevTools
45
+ "--http1.1",
46
+ "--http2",
47
+ "-G",
48
+ "--get",
49
+ "--no-keepalive",
50
+ "--fail",
51
+ "-f",
52
+ }
53
+
54
+ # flags that we recognize but want to surface with a warning for,
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
+ }
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import os
3
+ from typing import Any
3
4
 
4
5
 
5
6
  class Logger:
@@ -31,7 +32,7 @@ class Logger:
31
32
 
32
33
  self.logger.addHandler(file_handler)
33
34
 
34
- def info(self, message: str, *args, **kwargs) -> None:
35
+ def info(self, message: str, *args: Any, **kwargs: Any) -> None:
35
36
  """
36
37
  Log an info message with particular arguments
37
38
  """
@@ -39,7 +40,15 @@ class Logger:
39
40
  message = message % args
40
41
  self.logger.info(message, **kwargs)
41
42
 
42
- def error(self, message: str, *args, **kwargs) -> None:
43
+ def warning(self, message: str, *args: Any, **kwargs: Any) -> None:
44
+ """
45
+ Log a warning message with particular arguments
46
+ """
47
+ if args:
48
+ message = message % args
49
+ self.logger.warning(message, **kwargs)
50
+
51
+ def error(self, message: str, *args: Any, **kwargs: Any) -> None:
43
52
  """
44
53
  Log an error message with particular arguments
45
54
  """
File without changes
@@ -2,26 +2,26 @@ import json
2
2
  import shlex
3
3
  import time
4
4
  import httpx
5
- from logger import get_logger
6
5
  from dotenv import load_dotenv
7
6
  from urllib.parse import urlparse
8
7
  from typing import Optional, Dict, Any, List
9
- from mcp.server.fastmcp import FastMCP
10
- from models import APITestResult, AssertionResult
11
- from utils import (
8
+ from mcp.server.mcpserver import MCPServer
9
+ from .logger import get_logger
10
+ from .models import APITestResult, AssertionResult
11
+ from .utils import (
12
12
  render_console_template_result,
13
13
  render_error_result,
14
14
  build_fullpath_url,
15
15
  get_nested_fields,
16
16
  )
17
- from flags import CURL_BOOLEAN_FLAGS, CURL_VALUE_FLAGS, CURL_WARN_FLAGS
17
+ from .flags import CURL_BOOLEAN_FLAGS, CURL_VALUE_FLAGS, CURL_WARN_FLAGS
18
18
 
19
19
  load_dotenv()
20
20
 
21
21
  logger = get_logger(name="mcp-api-testing")
22
22
 
23
23
 
24
- mcp = FastMCP("mcp-api-test")
24
+ mcp = MCPServer("mcp-api-test")
25
25
 
26
26
 
27
27
  def parse_status_code_range(range_str: str) -> tuple[int, int]:
@@ -80,7 +80,7 @@ def parse_curl_command(command: str) -> Dict[str, Any]:
80
80
 
81
81
  # emit warning for supported but warnin changing flags
82
82
  if token in CURL_WARN_FLAGS:
83
- logger.warning(message=f"[curl parser] {CURL_WARN_FLAGS[token]}`")
83
+ logger.warning(message=f"[curl parser] {CURL_WARN_FLAGS[token]}")
84
84
 
85
85
  if role == "method":
86
86
  method = value
@@ -96,11 +96,13 @@ def parse_curl_command(command: str) -> Dict[str, Any]:
96
96
  payload = json.loads(raw_data)
97
97
  except json.JSONDecodeError:
98
98
  logger.error(
99
- f"[curl parser] payload for '{token} is not valid JSON"
99
+ f"[curl parser] payload for '{token}' is not valid JSON"
100
100
  )
101
101
  payload = raw_data
102
102
  elif role == "url_flag":
103
103
  url = value
104
+ elif role == "user_agent":
105
+ headers["User-Agent"] = value
104
106
 
105
107
  # everything else is consumed silently
106
108
  index += 2
@@ -109,7 +111,7 @@ def parse_curl_command(command: str) -> Dict[str, Any]:
109
111
  # boolean standalone flags
110
112
  if token in CURL_BOOLEAN_FLAGS:
111
113
  if token in CURL_WARN_FLAGS:
112
- logger.info(f"[curl parser] {CURL_WARN_FLAGS[token]}")
114
+ logger.warning(message=f"[curl parser] {CURL_WARN_FLAGS[token]}")
113
115
  index += 1
114
116
  continue
115
117
 
@@ -160,7 +162,7 @@ async def run_test_api(
160
162
  required_status_code_range: Optional[str] = None,
161
163
  required_response_fields: Optional[List[str]] = None,
162
164
  required_response_contains: Optional[str] = None,
163
- ):
165
+ ) -> str:
164
166
  """
165
167
  Test a given API services, it will supports either:
166
168
 
@@ -190,19 +192,27 @@ async def run_test_api(
190
192
  full_path_url = build_fullpath_url(base_url=base_url, endpoint=endpoint)
191
193
  method = method.upper()
192
194
 
195
+ content: Optional[str] = None
196
+ json_payload: Optional[Dict[str, Any]] = None
197
+ if payload is not None:
198
+ # cURL parser may produce a raw string for non-JSON -d payloads
199
+ # (example: form-encoded data). Use content= to send as-is rather
200
+ # than json= which would double-encode the string.
201
+ if isinstance(payload, str):
202
+ content = payload
203
+ else:
204
+ json_payload = payload
205
+
193
206
  start_time = time.perf_counter()
194
207
  try:
195
208
  async with httpx.AsyncClient(timeout=30) as client:
196
- kwargs = {"method": method, "url": full_path_url, "headers": headers}
197
- if payload is not None:
198
- # cURL parser may produce a raw string for non-JSON -d payloads
199
- # (example: form-encoded data). Use content= to send as-is rather
200
- # than json= which would double-encode the string.
201
- if isinstance(payload, str):
202
- kwargs["content"] = payload
203
- else:
204
- kwargs["json"] = payload
205
- response = await client.request(**kwargs)
209
+ response = await client.request(
210
+ method=method,
211
+ url=full_path_url,
212
+ headers=headers,
213
+ content=content,
214
+ json=json_payload,
215
+ )
206
216
  end_time = time.perf_counter()
207
217
  response_time_ms = (end_time - start_time) * 1000
208
218
  except Exception as e:
@@ -221,7 +231,7 @@ async def run_test_api(
221
231
  )
222
232
  resp_body = response.text
223
233
 
224
- assertions = []
234
+ assertions: List[Dict[str, Any]] = []
225
235
 
226
236
  if required_status_code is not None:
227
237
  passed = response.status_code == required_status_code
@@ -319,7 +329,7 @@ async def run_test_api(
319
329
 
320
330
 
321
331
  # expose this function so it can be called in toml files
322
- def main():
332
+ def main() -> None:
323
333
  mcp.run()
324
334
 
325
335
 
@@ -1,7 +1,7 @@
1
1
  """Utility functions for MCP API testing"""
2
2
 
3
3
  from typing import Any
4
- from models import APITestResult
4
+ from .models import APITestResult
5
5
 
6
6
 
7
7
  def render_console_template_result(result: APITestResult) -> str:
@@ -108,7 +108,7 @@ def build_fullpath_url(base_url: str, endpoint: str) -> str:
108
108
  _endpoint = endpoint.strip()
109
109
 
110
110
  if not _endpoint:
111
- endpoint = "/"
111
+ _endpoint = "/"
112
112
 
113
113
  if not _endpoint.startswith("/"):
114
114
  # automaticaly prepend the '/' before raise an assertion
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-api-test
3
- Version: 0.1.3
3
+ Version: 0.2.0
4
4
  Summary: MCP server for testing REST API with built-in assertions
5
5
  Author: Ryan Febriansyah
6
6
  License-Expression: MIT
@@ -18,20 +18,21 @@ Classifier: Topic :: Software Development :: Testing
18
18
  Requires-Python: >=3.10
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
- Requires-Dist: mcp
21
+ Requires-Dist: mcp<3.0.0,>=2.0.0
22
22
  Requires-Dist: httpx
23
23
  Requires-Dist: python-dotenv
24
24
  Provides-Extra: dev
25
25
  Requires-Dist: pytest; extra == "dev"
26
26
  Requires-Dist: pytest-asyncio; extra == "dev"
27
27
  Requires-Dist: respx; extra == "dev"
28
+ Requires-Dist: mypy; extra == "dev"
28
29
  Provides-Extra: docs
29
30
  Requires-Dist: mkdocs-material; extra == "docs"
30
31
  Dynamic: license-file
31
32
 
32
33
  # mcp-api-test
33
34
 
34
- ![pipeline](https://gitlab.com/ryaneatfood/mcp-api-testing/badges/master/pipeline.svg)
35
+ ![pipeline](https://gitlab.com/ryaneatfood/mcp-api-testing/badges/master/pipeline.svg) ![PyPI License](https://img.shields.io/pypi/l/mcp-api-test) ![PyPI Python Version](https://img.shields.io/pypi/pyversions/mcp-api-test)
35
36
 
36
37
  An AI-native API testing engine that enables LLM agents to execute, validate, and reason about REST APIs using natural language or cURL inputs.
37
38
 
@@ -39,6 +40,8 @@ Unlike traditional testing frameworks, `mcp-api-test` exposes API testing as a *
39
40
 
40
41
  It complements (rather than replaces) tools like pytest, Postman, and Playwright.
41
42
 
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
+
42
45
  ## Use cases
43
46
 
44
47
  - **QA & test engineering**: prompt an agent with natural language instead of crafting Postman requests
@@ -100,7 +103,7 @@ The agent invokes `run_test_api`, validates against the expected contract, and r
100
103
 
101
104
  ## Documentation
102
105
 
103
- Full documentation is hosted on [GitLab Pages](https://ryaneatfood.gitlab.io/mcp-api-testing/) (or build locally with `mkdocs serve`).
106
+ Full documentation is hosted on this [page](https://ryaneatfood.gitlab.io/mcp-api-testing/), and another way to navigate the docs:
104
107
 
105
108
  - **[Getting Started](docs/getting-started/installation.md)**: installation and client setup
106
109
  - **[Usage](docs/usage/request-modes.md)**: cURL mode, structured mode, assertions, response format
@@ -2,11 +2,14 @@ LICENSE
2
2
  MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
- src/__main__.py
6
- src/logger.py
7
- src/models.py
8
- src/server.py
9
- src/utils.py
5
+ src/mcp_api_test/__init__.py
6
+ src/mcp_api_test/__main__.py
7
+ src/mcp_api_test/flags.py
8
+ src/mcp_api_test/logger.py
9
+ src/mcp_api_test/models.py
10
+ src/mcp_api_test/py.typed
11
+ src/mcp_api_test/server.py
12
+ src/mcp_api_test/utils.py
10
13
  src/mcp_api_test.egg-info/PKG-INFO
11
14
  src/mcp_api_test.egg-info/SOURCES.txt
12
15
  src/mcp_api_test.egg-info/dependency_links.txt
@@ -14,5 +17,8 @@ src/mcp_api_test.egg-info/entry_points.txt
14
17
  src/mcp_api_test.egg-info/requires.txt
15
18
  src/mcp_api_test.egg-info/top_level.txt
16
19
  tests/test_mcp_api_tools.py
20
+ tests/test_mcp_server_compat.py
21
+ tests/test_packaging.py
17
22
  tests/test_parse_curl_command.py
18
- tests/test_parse_status_code.py
23
+ tests/test_parse_status_code.py
24
+ tests/test_utils.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mcp-api-test = mcp_api_test.server:main
@@ -1,4 +1,4 @@
1
- mcp
1
+ mcp<3.0.0,>=2.0.0
2
2
  httpx
3
3
  python-dotenv
4
4
 
@@ -6,6 +6,7 @@ python-dotenv
6
6
  pytest
7
7
  pytest-asyncio
8
8
  respx
9
+ mypy
9
10
 
10
11
  [docs]
11
12
  mkdocs-material
@@ -0,0 +1 @@
1
+ mcp_api_test
@@ -3,7 +3,7 @@
3
3
  import pytest
4
4
  import respx
5
5
  import httpx
6
- from src.server import run_test_api
6
+ from mcp_api_test.server import run_test_api
7
7
 
8
8
 
9
9
  @pytest.mark.asyncio
@@ -0,0 +1,30 @@
1
+ """Regression tests for the mcp 2.x migration (FastMCP -> MCPServer)"""
2
+
3
+ import pytest
4
+ from mcp_api_test.server import mcp
5
+
6
+
7
+ def test_server_is_built_on_mcpserver():
8
+ # regression: mcp 2.x removed mcp.server.fastmcp.FastMCP in favor of
9
+ # mcp.server.mcpserver.MCPServer; importing the old path now raises
10
+ # ModuleNotFoundError instead of silently breaking at a later call site
11
+ from mcp.server.mcpserver import MCPServer
12
+
13
+ assert isinstance(mcp, MCPServer)
14
+
15
+
16
+ def test_old_fastmcp_import_path_is_gone():
17
+ # confirms we're actually running against the mcp 2.x line pinned in
18
+ # pyproject.toml, not an mcp 1.x install that would mask this migration
19
+ with pytest.raises(ModuleNotFoundError):
20
+ import mcp.server.fastmcp # noqa: F401
21
+
22
+
23
+ def test_run_test_api_is_registered_as_a_tool():
24
+ # @mcp.tool() kept the same decorator signature across the migration;
25
+ # this would fail if MCPServer's tool manager changed shape
26
+ import asyncio
27
+
28
+ tools = asyncio.run(mcp.list_tools())
29
+ tool_names = {tool.name for tool in tools}
30
+ assert "run_test_api" in tool_names
@@ -0,0 +1,49 @@
1
+ """Static checks that the source layout matches the wheel packaging config"""
2
+
3
+ import tomllib
4
+ from pathlib import Path
5
+
6
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
7
+ PACKAGE_DIR = PROJECT_ROOT / "src" / "mcp_api_test"
8
+
9
+
10
+ def _pyproject() -> dict:
11
+ with open(PROJECT_ROOT / "pyproject.toml", "rb") as f:
12
+ return tomllib.load(f)
13
+
14
+
15
+ def _project_version() -> str:
16
+ return _pyproject()["project"]["version"]
17
+
18
+
19
+ def test_declared_package_matches_src_directory():
20
+ packages = _pyproject()["tool"]["setuptools"]["packages"]
21
+ assert packages == ["mcp_api_test"]
22
+ assert PACKAGE_DIR.is_dir()
23
+
24
+
25
+ def test_all_modules_exist_in_package_dir():
26
+ # regression: flat py-modules layout drifting from the package dir
27
+ expected = {"server", "utils", "models", "logger", "flags", "__init__", "__main__"}
28
+ actual = {p.stem for p in PACKAGE_DIR.glob("*.py")}
29
+ assert expected <= actual
30
+
31
+
32
+ def test_py_typed_marker_exists_and_is_packaged():
33
+ # PEP 561: inline-typed packages must ship a py.typed marker
34
+ assert (PACKAGE_DIR / "py.typed").exists()
35
+
36
+ package_data = _pyproject()["tool"]["setuptools"]["package-data"]
37
+ assert "py.typed" in package_data.get("mcp_api_test", [])
38
+
39
+
40
+ def test_entry_point_targets_the_package():
41
+ scripts = _pyproject()["project"]["scripts"]
42
+ assert scripts["mcp-api-test"] == "mcp_api_test.server:main"
43
+
44
+
45
+ def test_version_matches_pyproject():
46
+ # regression: src/mcp_api_test/__init__.py __version__ drifted from pyproject.toml
47
+ import mcp_api_test
48
+
49
+ assert mcp_api_test.__version__ == _project_version()
@@ -0,0 +1,69 @@
1
+ """Unit tests for the cURL command parser"""
2
+
3
+ import logging
4
+ import pytest
5
+ from mcp_api_test.server import parse_curl_command
6
+
7
+
8
+ def test_basic_curl_command():
9
+ # no network needed, just pure logic
10
+ result = parse_curl_command(command="curl -X POST https://api.example.com/v1/users")
11
+ assert result["method"] == "POST"
12
+ assert result["base_url"] == "https://api.example.com"
13
+ assert result["endpoint"] == "/v1/users"
14
+ assert result["headers"] == {}
15
+ assert result["request_payload"] is None
16
+
17
+
18
+ def test_missing_url_raises():
19
+ with pytest.raises(ValueError, match="URL must be provided"):
20
+ parse_curl_command("curl -X GET -H 'content-type: application/json'")
21
+
22
+
23
+ def test_missing_value_raises():
24
+ # -X at the end with no specific HTTP method
25
+ with pytest.raises((ValueError, IndexError)):
26
+ parse_curl_command("curl https://api.example.com -X")
27
+
28
+
29
+ def test_warn_flag_boolean_does_not_raise():
30
+ # -k is a boolean warn flag: it must not raise even though
31
+ # the parser emits a warning via logger.warning()
32
+ result = parse_curl_command(command="curl -k https://api.example.com/v1/users")
33
+ assert result["method"] == "GET"
34
+ assert result["endpoint"] == "/v1/users"
35
+
36
+
37
+ def test_warn_flag_value_does_not_raise():
38
+ # -u consumes the next token as its value and emits a warning
39
+ result = parse_curl_command(
40
+ command="curl -u user:pass https://api.example.com/v1/users"
41
+ )
42
+ assert result["method"] == "GET"
43
+ assert result["endpoint"] == "/v1/users"
44
+
45
+
46
+ def test_boolean_warn_flag_logs_warning_not_info(caplog):
47
+ # regression: -k must be emitted at WARNING level, not INFO
48
+ with caplog.at_level(logging.WARNING):
49
+ parse_curl_command(command="curl -k https://api.example.com/v1/users")
50
+
51
+ warn_records = [r for r in caplog.records if r.levelno == logging.WARNING]
52
+ assert warn_records, "expected at least one WARNING record"
53
+ assert any("insecure" in r.getMessage() for r in warn_records)
54
+
55
+
56
+ def test_user_agent_flag_sets_header():
57
+ # regression: -A was parsed but silently dropped
58
+ result = parse_curl_command(
59
+ command='curl -A "Mozilla/5.0" https://api.example.com/v1/users'
60
+ )
61
+ assert result["headers"].get("User-Agent") == "Mozilla/5.0"
62
+
63
+
64
+ def test_non_json_payload_kept_as_raw_string():
65
+ # regression: malformed JSON body must be preserved as-is, not crash
66
+ result = parse_curl_command(
67
+ command="curl -d 'not json' https://api.example.com/v1/users"
68
+ )
69
+ assert result["request_payload"] == "not json"
@@ -1,5 +1,5 @@
1
1
  import pytest
2
- from src.server import parse_status_code_range
2
+ from mcp_api_test.server import parse_status_code_range
3
3
 
4
4
 
5
5
  @pytest.mark.parametrize(
@@ -0,0 +1,23 @@
1
+ """Unit tests for URL building helpers"""
2
+
3
+ from mcp_api_test.utils import build_fullpath_url
4
+
5
+
6
+ def test_empty_endpoint_returns_base_with_slash():
7
+ # regression: build_fullpath_url("https://api.example.com", "")
8
+ # must yield "https://api.example.com/" (dead assignment bug)
9
+ assert build_fullpath_url("https://api.example.com", "") == (
10
+ "https://api.example.com/"
11
+ )
12
+
13
+
14
+ def test_bare_endpoint_gets_leading_slash():
15
+ assert build_fullpath_url("https://api.example.com", "v1/users") == (
16
+ "https://api.example.com/v1/users"
17
+ )
18
+
19
+
20
+ def test_fullpath_does_not_double_slash():
21
+ assert build_fullpath_url("https://api.example.com/", "/v1/users") == (
22
+ "https://api.example.com/v1/users"
23
+ )
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- mcp-api-test = server:main
@@ -1,5 +0,0 @@
1
- __main__
2
- logger
3
- models
4
- server
5
- utils
@@ -1,23 +0,0 @@
1
- import pytest
2
- from src.server import parse_curl_command
3
-
4
-
5
- def test_basic_curl_command():
6
- # no network needed, just pure logic
7
- result = parse_curl_command(command="curl -X POST https://api.example.com/v1/users")
8
- assert result["method"] == "POST"
9
- assert result["base_url"] == "https://api.example.com"
10
- assert result["endpoint"] == "/v1/users"
11
- assert result["headers"] == {}
12
- assert result["request_payload"] is None
13
-
14
-
15
- def test_missing_url_raises():
16
- with pytest.raises(ValueError, match="URL must be provided"):
17
- parse_curl_command("curl -X GET -H 'content-type: application/json'")
18
-
19
-
20
- def test_missing_value_raises():
21
- # -X at the end with no specific HTTP method
22
- with pytest.raises((ValueError, IndexError)):
23
- parse_curl_command("curl https://api.example.com -X")
File without changes
File without changes
File without changes