mcp-api-test 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,8 @@
1
+ Copyright 2026 Ryan Febriansyah
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+
@@ -0,0 +1 @@
1
+ prune log
@@ -0,0 +1,257 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-api-test
3
+ Version: 0.1.0
4
+ Summary: MCP server for testing REST API with built-in assertions
5
+ Author: Ryan Febriansyah
6
+ Keywords: testing,mcp,llm
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Natural Language :: English
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Topic :: Software Development :: Quality Assurance
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: mcp
22
+ Requires-Dist: httpx
23
+ Requires-Dist: python-dotenv
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest; extra == "dev"
26
+ Requires-Dist: pytest-asyncio; extra == "dev"
27
+ Requires-Dist: respx; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # api-test-mcp
31
+
32
+ ![image](https://gitlab.com/ryaneatfood/mcp-api-testing/badges/master/pipeline.svg)
33
+
34
+ MCP server for REST API testing with assertions. Provides a `test_api` tool that accepts either a cURL command or structured request fields, runs the request, and validates against expected status codes and response fields
35
+
36
+ ## Prerequisites
37
+
38
+ - Python >= 3.10
39
+ - pip or uv for package installation
40
+
41
+ ## Install
42
+
43
+ ### From PyPI
44
+
45
+ ```bash
46
+ pip install mcp-api-test
47
+ ```
48
+
49
+ ### From source
50
+
51
+ ```bash
52
+ git clone git@gitlab.com:ryaneatfood/mcp-api-testing.git
53
+
54
+ # change the directory
55
+ cd api-test-mcp
56
+
57
+ # install from source
58
+ pip install -e .
59
+ ```
60
+
61
+ This registers the `api-test-mcp` CLI command on your PATH. Verify with:
62
+
63
+ ```bash
64
+ which api-test-mcp
65
+ ```
66
+
67
+ ## Testing with MCP Inspector
68
+
69
+ The [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) provides a browser-based UI to verify your server connects and responds correctly before wiring it into a client.
70
+
71
+ Make sure `api-test-mcp` is installed on your PATH (see [Install](#install)), then run:
72
+
73
+ ```bash
74
+ npx @modelcontextprotocol/inspector
75
+ ```
76
+
77
+ This launches the inspector and opens `http://localhost:5173` in your browser. From the UI:
78
+
79
+ 1. Set **Transport Type** to `STDIO`
80
+ 2. Set **Command** to `api-test-mcp`
81
+ 3. Leave **Arguments** empty (your server needs none)
82
+ 4. Click **Connect**
83
+
84
+ When the connection succeeds, the status badge changes to **Connected** and the inspector lists the `test_api` tool under the Tools tab. You can click the tool to open a form and run test calls interactively. This is useful for rapid iteration without restarting your MCP client.
85
+
86
+ If the status shows "Failed to connect" double-check that `which api-test-mcp` resolves and that `pip install -e .` completed without errors
87
+
88
+ ## Running Tests
89
+
90
+ Install test dependencies and run the suite:
91
+
92
+ ```bash
93
+ # install test dependencies
94
+ pip install pytest pytest-asyncio respx
95
+
96
+ # run the whole test suite
97
+ pytest -v
98
+ ```
99
+
100
+ All integration tests use `respx` to mock HTTP responses. No real network calls are made, so the suite runs fast and offline. The test suite also consisted of unit test and integration test
101
+
102
+ ## OpenCode Setup
103
+
104
+ Edit `opencode.json` (already configured at project root):
105
+
106
+ ```json
107
+ {
108
+ "$schema": "https://opencode.ai/config.json",
109
+ "mcp": {
110
+ "api-test-mcp": {
111
+ "type": "local",
112
+ "command": ["api-test-mcp"],
113
+ "enabled": true
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ ## Claude Desktop Setup
120
+
121
+ Edit `claude_desktop_config.json` (usually at `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
122
+
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "api-test-mcp": {
127
+ "command": "api-test-mcp"
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ Restart Claude Desktop after saving.
134
+
135
+ ## Usage
136
+
137
+ The `test_api` tool supports two modes:
138
+
139
+ ### cURL mode
140
+
141
+ Pass a raw cURL command, and then the server parses method, URL, headers, and payload automatically into restructured data. Example prompt that you might be used:
142
+
143
+ > "Please test this API using this cURL: `curl -X POST https://api.example.com/v1/users -H 'Authorization: Bearer xxx' -d '{"name":"test"}'`, ensure status code is 201 and response contains 'id'"
144
+
145
+ ### Structured mode
146
+
147
+ Provide individual fields rather than used a cURL command. Example prompt:
148
+
149
+ > "Please test this API at https://api.example.com/v1/users with method GET, and the request header is {"Content-Type":"application/json"}, ensure status code is 200 and response body contains 'data'" attribute
150
+
151
+ ### Assertions
152
+
153
+ At the moment, this library provides built-in assertions that proved useful for testing the API, such as:
154
+
155
+ | Parameter | Type | Description |
156
+ |-----------|------|-------------|
157
+ | `required_status_code` | `int` | Assert exact status code |
158
+ | `required_status_code_range` | `str` | Assert status code is within a range (e.g. `"2xx"`, `"200-299"`) |
159
+ | `required_response_fields` | `list[str]` | Assert fields exist in JSON response |
160
+ | `required_response_contains` | `str` | Assert response body contains a substring |
161
+
162
+ ## Response
163
+
164
+ The tool returns a formatted console string with box-drawing, pass/fail symbols, and a summary:
165
+
166
+ ### Passing (with assertions)
167
+
168
+ ```
169
+ ┌──────────────────────────────────────────────┐
170
+ │ Api Test Result │
171
+ └──────────────────────────────────────────────┘
172
+
173
+ API Name : create-user
174
+ Method : POST
175
+ URL : https://api.example.com/v1/users
176
+ Status Code : 201
177
+ Response Time : 142.35 ms
178
+ Overall Result : Passed
179
+
180
+ Assertions
181
+ --------------------------------------------------
182
+ ✓ status_code
183
+ ✓ response_field_exist
184
+
185
+ Summary
186
+ --------------------------------------------------
187
+ Total Assertions : 2
188
+ Passed : 2
189
+ Failed : 0
190
+ ```
191
+
192
+ ### Failing (assertion mismatch)
193
+
194
+ ```
195
+ ┌──────────────────────────────────────────────┐
196
+ │ Api Test Result │
197
+ └──────────────────────────────────────────────┘
198
+
199
+ API Name : create-user
200
+ Method : POST
201
+ URL : https://api.example.com/v1/users
202
+ Status Code : 500
203
+ Response Time : 89.12 ms
204
+ Overall Result : Failed
205
+
206
+ Assertions
207
+ --------------------------------------------------
208
+ ✗ status_code
209
+
210
+ Summary
211
+ --------------------------------------------------
212
+ Total Assertions : 1
213
+ Passed : 0
214
+ Failed : 1
215
+ ```
216
+
217
+ ### Network / runtime error
218
+
219
+ ```
220
+ ┌──────────────────────────────────────────────┐
221
+ │ Api Test Result │
222
+ └──────────────────────────────────────────────┘
223
+
224
+ API Name : check-service
225
+ Method : GET
226
+ URL : https://api.internal.example.com/v1/status
227
+ Overall Result : Failed
228
+
229
+ Error
230
+ --------------------------------------------------
231
+ ConnectError: [Errno 8] nodename nor servname provided, or not known
232
+ ```
233
+
234
+ ## Logging
235
+
236
+ The server writes logs to `log/mcp-api-test.log` at the project root. Useful for debugging failed requests or tracing tool calls.
237
+
238
+ ## Environment Variables
239
+
240
+ The server loads a `.env` file via `python-dotenv` on startup. Useful for storing API keys, base URLs, etc. without hardcoding them in prompts.
241
+
242
+ ## Development
243
+
244
+ ```bash
245
+ # Re-install after pulling changes
246
+ pip install -e .
247
+
248
+ # Run the MCP server directly
249
+ api-test-mcp
250
+ ```
251
+
252
+ To run the test suite:
253
+
254
+ ```bash
255
+ pip install -e ".[dev]"
256
+ pytest -v
257
+ ```
@@ -0,0 +1,228 @@
1
+ # api-test-mcp
2
+
3
+ ![image](https://gitlab.com/ryaneatfood/mcp-api-testing/badges/master/pipeline.svg)
4
+
5
+ MCP server for REST API testing with assertions. Provides a `test_api` tool that accepts either a cURL command or structured request fields, runs the request, and validates against expected status codes and response fields
6
+
7
+ ## Prerequisites
8
+
9
+ - Python >= 3.10
10
+ - pip or uv for package installation
11
+
12
+ ## Install
13
+
14
+ ### From PyPI
15
+
16
+ ```bash
17
+ pip install mcp-api-test
18
+ ```
19
+
20
+ ### From source
21
+
22
+ ```bash
23
+ git clone git@gitlab.com:ryaneatfood/mcp-api-testing.git
24
+
25
+ # change the directory
26
+ cd api-test-mcp
27
+
28
+ # install from source
29
+ pip install -e .
30
+ ```
31
+
32
+ This registers the `api-test-mcp` CLI command on your PATH. Verify with:
33
+
34
+ ```bash
35
+ which api-test-mcp
36
+ ```
37
+
38
+ ## Testing with MCP Inspector
39
+
40
+ The [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) provides a browser-based UI to verify your server connects and responds correctly before wiring it into a client.
41
+
42
+ Make sure `api-test-mcp` is installed on your PATH (see [Install](#install)), then run:
43
+
44
+ ```bash
45
+ npx @modelcontextprotocol/inspector
46
+ ```
47
+
48
+ This launches the inspector and opens `http://localhost:5173` in your browser. From the UI:
49
+
50
+ 1. Set **Transport Type** to `STDIO`
51
+ 2. Set **Command** to `api-test-mcp`
52
+ 3. Leave **Arguments** empty (your server needs none)
53
+ 4. Click **Connect**
54
+
55
+ When the connection succeeds, the status badge changes to **Connected** and the inspector lists the `test_api` tool under the Tools tab. You can click the tool to open a form and run test calls interactively. This is useful for rapid iteration without restarting your MCP client.
56
+
57
+ If the status shows "Failed to connect" double-check that `which api-test-mcp` resolves and that `pip install -e .` completed without errors
58
+
59
+ ## Running Tests
60
+
61
+ Install test dependencies and run the suite:
62
+
63
+ ```bash
64
+ # install test dependencies
65
+ pip install pytest pytest-asyncio respx
66
+
67
+ # run the whole test suite
68
+ pytest -v
69
+ ```
70
+
71
+ All integration tests use `respx` to mock HTTP responses. No real network calls are made, so the suite runs fast and offline. The test suite also consisted of unit test and integration test
72
+
73
+ ## OpenCode Setup
74
+
75
+ Edit `opencode.json` (already configured at project root):
76
+
77
+ ```json
78
+ {
79
+ "$schema": "https://opencode.ai/config.json",
80
+ "mcp": {
81
+ "api-test-mcp": {
82
+ "type": "local",
83
+ "command": ["api-test-mcp"],
84
+ "enabled": true
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ ## Claude Desktop Setup
91
+
92
+ Edit `claude_desktop_config.json` (usually at `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
93
+
94
+ ```json
95
+ {
96
+ "mcpServers": {
97
+ "api-test-mcp": {
98
+ "command": "api-test-mcp"
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ Restart Claude Desktop after saving.
105
+
106
+ ## Usage
107
+
108
+ The `test_api` tool supports two modes:
109
+
110
+ ### cURL mode
111
+
112
+ Pass a raw cURL command, and then the server parses method, URL, headers, and payload automatically into restructured data. Example prompt that you might be used:
113
+
114
+ > "Please test this API using this cURL: `curl -X POST https://api.example.com/v1/users -H 'Authorization: Bearer xxx' -d '{"name":"test"}'`, ensure status code is 201 and response contains 'id'"
115
+
116
+ ### Structured mode
117
+
118
+ Provide individual fields rather than used a cURL command. Example prompt:
119
+
120
+ > "Please test this API at https://api.example.com/v1/users with method GET, and the request header is {"Content-Type":"application/json"}, ensure status code is 200 and response body contains 'data'" attribute
121
+
122
+ ### Assertions
123
+
124
+ At the moment, this library provides built-in assertions that proved useful for testing the API, such as:
125
+
126
+ | Parameter | Type | Description |
127
+ |-----------|------|-------------|
128
+ | `required_status_code` | `int` | Assert exact status code |
129
+ | `required_status_code_range` | `str` | Assert status code is within a range (e.g. `"2xx"`, `"200-299"`) |
130
+ | `required_response_fields` | `list[str]` | Assert fields exist in JSON response |
131
+ | `required_response_contains` | `str` | Assert response body contains a substring |
132
+
133
+ ## Response
134
+
135
+ The tool returns a formatted console string with box-drawing, pass/fail symbols, and a summary:
136
+
137
+ ### Passing (with assertions)
138
+
139
+ ```
140
+ ┌──────────────────────────────────────────────┐
141
+ │ Api Test Result │
142
+ └──────────────────────────────────────────────┘
143
+
144
+ API Name : create-user
145
+ Method : POST
146
+ URL : https://api.example.com/v1/users
147
+ Status Code : 201
148
+ Response Time : 142.35 ms
149
+ Overall Result : Passed
150
+
151
+ Assertions
152
+ --------------------------------------------------
153
+ ✓ status_code
154
+ ✓ response_field_exist
155
+
156
+ Summary
157
+ --------------------------------------------------
158
+ Total Assertions : 2
159
+ Passed : 2
160
+ Failed : 0
161
+ ```
162
+
163
+ ### Failing (assertion mismatch)
164
+
165
+ ```
166
+ ┌──────────────────────────────────────────────┐
167
+ │ Api Test Result │
168
+ └──────────────────────────────────────────────┘
169
+
170
+ API Name : create-user
171
+ Method : POST
172
+ URL : https://api.example.com/v1/users
173
+ Status Code : 500
174
+ Response Time : 89.12 ms
175
+ Overall Result : Failed
176
+
177
+ Assertions
178
+ --------------------------------------------------
179
+ ✗ status_code
180
+
181
+ Summary
182
+ --------------------------------------------------
183
+ Total Assertions : 1
184
+ Passed : 0
185
+ Failed : 1
186
+ ```
187
+
188
+ ### Network / runtime error
189
+
190
+ ```
191
+ ┌──────────────────────────────────────────────┐
192
+ │ Api Test Result │
193
+ └──────────────────────────────────────────────┘
194
+
195
+ API Name : check-service
196
+ Method : GET
197
+ URL : https://api.internal.example.com/v1/status
198
+ Overall Result : Failed
199
+
200
+ Error
201
+ --------------------------------------------------
202
+ ConnectError: [Errno 8] nodename nor servname provided, or not known
203
+ ```
204
+
205
+ ## Logging
206
+
207
+ The server writes logs to `log/mcp-api-test.log` at the project root. Useful for debugging failed requests or tracing tool calls.
208
+
209
+ ## Environment Variables
210
+
211
+ The server loads a `.env` file via `python-dotenv` on startup. Useful for storing API keys, base URLs, etc. without hardcoding them in prompts.
212
+
213
+ ## Development
214
+
215
+ ```bash
216
+ # Re-install after pulling changes
217
+ pip install -e .
218
+
219
+ # Run the MCP server directly
220
+ api-test-mcp
221
+ ```
222
+
223
+ To run the test suite:
224
+
225
+ ```bash
226
+ pip install -e ".[dev]"
227
+ pytest -v
228
+ ```
@@ -0,0 +1,45 @@
1
+ [project]
2
+ name = "mcp-api-test"
3
+ version = "0.1.0"
4
+ description = "MCP server for testing REST API with built-in assertions"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Ryan Febriansyah" }
8
+ ]
9
+
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "mcp",
13
+ "httpx",
14
+ "python-dotenv"
15
+ ]
16
+
17
+ keywords = ["testing", "mcp", "llm"]
18
+
19
+ classifiers = [
20
+ "Development Status :: 3 - Alpha",
21
+ "Intended Audience :: Developers",
22
+ "Operating System :: OS Independent",
23
+ "Natural Language :: English",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Topic :: Software Development :: Quality Assurance",
30
+ "Topic :: Software Development :: Testing",
31
+ ]
32
+
33
+ [project.scripts]
34
+ api-test-mcp = "server:main"
35
+
36
+ [build-system]
37
+ requires = ["setuptools>=61"]
38
+ build-backend = "setuptools.build_meta"
39
+
40
+ [tool.setuptools]
41
+ package-dir = {"" = "src"}
42
+ py-modules = ["server", "__main__", "logger", "models", "utils"]
43
+
44
+ [project.optional-dependencies]
45
+ dev = ["pytest", "pytest-asyncio", "respx"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """Entry point for the initialize api-test-mcp package when run as a module"""
2
+
3
+ from server import mcp
4
+
5
+ if __name__ == "__main__":
6
+ mcp.run()
@@ -0,0 +1,51 @@
1
+ import logging
2
+ import os
3
+
4
+
5
+ class Logger:
6
+ """
7
+ A simple logger class that initialize a logger instance
8
+ """
9
+
10
+ def __init__(self, name: str) -> None:
11
+ self.name = name
12
+ self.logger = logging.getLogger(name)
13
+
14
+ if not self.logger.handlers:
15
+ self.logger.setLevel(logging.INFO)
16
+ formatter = logging.Formatter(
17
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
18
+ )
19
+
20
+ # only create for file handler, no need to display on console
21
+ log_directory = os.path.expanduser("log")
22
+ os.makedirs(log_directory, exist_ok=True)
23
+ log_file = os.path.join(log_directory, "mcp-api-test.log")
24
+ file_handler = logging.FileHandler(log_file)
25
+ file_handler.setLevel(logging.INFO)
26
+ file_handler.setFormatter(formatter)
27
+
28
+ self.logger.addHandler(file_handler)
29
+
30
+ def info(self, message: str, *args, **kwargs) -> None:
31
+ """
32
+ Log an info message with particular arguments
33
+ """
34
+ if args:
35
+ message = message % args
36
+ self.logger.info(message, **kwargs)
37
+
38
+ def error(self, message: str, *args, **kwargs) -> None:
39
+ """
40
+ Log an error message with particular arguments
41
+ """
42
+ if args:
43
+ message = message % args
44
+ self.logger.error(message, **kwargs)
45
+
46
+
47
+ def get_logger(name: str) -> Logger:
48
+ """
49
+ Get a logger instance with the given name
50
+ """
51
+ return Logger(name=name)
@@ -0,0 +1,257 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-api-test
3
+ Version: 0.1.0
4
+ Summary: MCP server for testing REST API with built-in assertions
5
+ Author: Ryan Febriansyah
6
+ Keywords: testing,mcp,llm
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Natural Language :: English
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Topic :: Software Development :: Quality Assurance
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: mcp
22
+ Requires-Dist: httpx
23
+ Requires-Dist: python-dotenv
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest; extra == "dev"
26
+ Requires-Dist: pytest-asyncio; extra == "dev"
27
+ Requires-Dist: respx; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # api-test-mcp
31
+
32
+ ![image](https://gitlab.com/ryaneatfood/mcp-api-testing/badges/master/pipeline.svg)
33
+
34
+ MCP server for REST API testing with assertions. Provides a `test_api` tool that accepts either a cURL command or structured request fields, runs the request, and validates against expected status codes and response fields
35
+
36
+ ## Prerequisites
37
+
38
+ - Python >= 3.10
39
+ - pip or uv for package installation
40
+
41
+ ## Install
42
+
43
+ ### From PyPI
44
+
45
+ ```bash
46
+ pip install mcp-api-test
47
+ ```
48
+
49
+ ### From source
50
+
51
+ ```bash
52
+ git clone git@gitlab.com:ryaneatfood/mcp-api-testing.git
53
+
54
+ # change the directory
55
+ cd api-test-mcp
56
+
57
+ # install from source
58
+ pip install -e .
59
+ ```
60
+
61
+ This registers the `api-test-mcp` CLI command on your PATH. Verify with:
62
+
63
+ ```bash
64
+ which api-test-mcp
65
+ ```
66
+
67
+ ## Testing with MCP Inspector
68
+
69
+ The [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) provides a browser-based UI to verify your server connects and responds correctly before wiring it into a client.
70
+
71
+ Make sure `api-test-mcp` is installed on your PATH (see [Install](#install)), then run:
72
+
73
+ ```bash
74
+ npx @modelcontextprotocol/inspector
75
+ ```
76
+
77
+ This launches the inspector and opens `http://localhost:5173` in your browser. From the UI:
78
+
79
+ 1. Set **Transport Type** to `STDIO`
80
+ 2. Set **Command** to `api-test-mcp`
81
+ 3. Leave **Arguments** empty (your server needs none)
82
+ 4. Click **Connect**
83
+
84
+ When the connection succeeds, the status badge changes to **Connected** and the inspector lists the `test_api` tool under the Tools tab. You can click the tool to open a form and run test calls interactively. This is useful for rapid iteration without restarting your MCP client.
85
+
86
+ If the status shows "Failed to connect" double-check that `which api-test-mcp` resolves and that `pip install -e .` completed without errors
87
+
88
+ ## Running Tests
89
+
90
+ Install test dependencies and run the suite:
91
+
92
+ ```bash
93
+ # install test dependencies
94
+ pip install pytest pytest-asyncio respx
95
+
96
+ # run the whole test suite
97
+ pytest -v
98
+ ```
99
+
100
+ All integration tests use `respx` to mock HTTP responses. No real network calls are made, so the suite runs fast and offline. The test suite also consisted of unit test and integration test
101
+
102
+ ## OpenCode Setup
103
+
104
+ Edit `opencode.json` (already configured at project root):
105
+
106
+ ```json
107
+ {
108
+ "$schema": "https://opencode.ai/config.json",
109
+ "mcp": {
110
+ "api-test-mcp": {
111
+ "type": "local",
112
+ "command": ["api-test-mcp"],
113
+ "enabled": true
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ ## Claude Desktop Setup
120
+
121
+ Edit `claude_desktop_config.json` (usually at `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
122
+
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "api-test-mcp": {
127
+ "command": "api-test-mcp"
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ Restart Claude Desktop after saving.
134
+
135
+ ## Usage
136
+
137
+ The `test_api` tool supports two modes:
138
+
139
+ ### cURL mode
140
+
141
+ Pass a raw cURL command, and then the server parses method, URL, headers, and payload automatically into restructured data. Example prompt that you might be used:
142
+
143
+ > "Please test this API using this cURL: `curl -X POST https://api.example.com/v1/users -H 'Authorization: Bearer xxx' -d '{"name":"test"}'`, ensure status code is 201 and response contains 'id'"
144
+
145
+ ### Structured mode
146
+
147
+ Provide individual fields rather than used a cURL command. Example prompt:
148
+
149
+ > "Please test this API at https://api.example.com/v1/users with method GET, and the request header is {"Content-Type":"application/json"}, ensure status code is 200 and response body contains 'data'" attribute
150
+
151
+ ### Assertions
152
+
153
+ At the moment, this library provides built-in assertions that proved useful for testing the API, such as:
154
+
155
+ | Parameter | Type | Description |
156
+ |-----------|------|-------------|
157
+ | `required_status_code` | `int` | Assert exact status code |
158
+ | `required_status_code_range` | `str` | Assert status code is within a range (e.g. `"2xx"`, `"200-299"`) |
159
+ | `required_response_fields` | `list[str]` | Assert fields exist in JSON response |
160
+ | `required_response_contains` | `str` | Assert response body contains a substring |
161
+
162
+ ## Response
163
+
164
+ The tool returns a formatted console string with box-drawing, pass/fail symbols, and a summary:
165
+
166
+ ### Passing (with assertions)
167
+
168
+ ```
169
+ ┌──────────────────────────────────────────────┐
170
+ │ Api Test Result │
171
+ └──────────────────────────────────────────────┘
172
+
173
+ API Name : create-user
174
+ Method : POST
175
+ URL : https://api.example.com/v1/users
176
+ Status Code : 201
177
+ Response Time : 142.35 ms
178
+ Overall Result : Passed
179
+
180
+ Assertions
181
+ --------------------------------------------------
182
+ ✓ status_code
183
+ ✓ response_field_exist
184
+
185
+ Summary
186
+ --------------------------------------------------
187
+ Total Assertions : 2
188
+ Passed : 2
189
+ Failed : 0
190
+ ```
191
+
192
+ ### Failing (assertion mismatch)
193
+
194
+ ```
195
+ ┌──────────────────────────────────────────────┐
196
+ │ Api Test Result │
197
+ └──────────────────────────────────────────────┘
198
+
199
+ API Name : create-user
200
+ Method : POST
201
+ URL : https://api.example.com/v1/users
202
+ Status Code : 500
203
+ Response Time : 89.12 ms
204
+ Overall Result : Failed
205
+
206
+ Assertions
207
+ --------------------------------------------------
208
+ ✗ status_code
209
+
210
+ Summary
211
+ --------------------------------------------------
212
+ Total Assertions : 1
213
+ Passed : 0
214
+ Failed : 1
215
+ ```
216
+
217
+ ### Network / runtime error
218
+
219
+ ```
220
+ ┌──────────────────────────────────────────────┐
221
+ │ Api Test Result │
222
+ └──────────────────────────────────────────────┘
223
+
224
+ API Name : check-service
225
+ Method : GET
226
+ URL : https://api.internal.example.com/v1/status
227
+ Overall Result : Failed
228
+
229
+ Error
230
+ --------------------------------------------------
231
+ ConnectError: [Errno 8] nodename nor servname provided, or not known
232
+ ```
233
+
234
+ ## Logging
235
+
236
+ The server writes logs to `log/mcp-api-test.log` at the project root. Useful for debugging failed requests or tracing tool calls.
237
+
238
+ ## Environment Variables
239
+
240
+ The server loads a `.env` file via `python-dotenv` on startup. Useful for storing API keys, base URLs, etc. without hardcoding them in prompts.
241
+
242
+ ## Development
243
+
244
+ ```bash
245
+ # Re-install after pulling changes
246
+ pip install -e .
247
+
248
+ # Run the MCP server directly
249
+ api-test-mcp
250
+ ```
251
+
252
+ To run the test suite:
253
+
254
+ ```bash
255
+ pip install -e ".[dev]"
256
+ pytest -v
257
+ ```
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ src/__main__.py
6
+ src/logger.py
7
+ src/models.py
8
+ src/server.py
9
+ src/utils.py
10
+ src/mcp_api_test.egg-info/PKG-INFO
11
+ src/mcp_api_test.egg-info/SOURCES.txt
12
+ src/mcp_api_test.egg-info/dependency_links.txt
13
+ src/mcp_api_test.egg-info/entry_points.txt
14
+ src/mcp_api_test.egg-info/requires.txt
15
+ src/mcp_api_test.egg-info/top_level.txt
16
+ tests/test_mcp_api_tools.py
17
+ tests/test_parse_curl_command.py
18
+ tests/test_parse_status_code.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ api-test-mcp = server:main
@@ -0,0 +1,8 @@
1
+ mcp
2
+ httpx
3
+ python-dotenv
4
+
5
+ [dev]
6
+ pytest
7
+ pytest-asyncio
8
+ respx
@@ -0,0 +1,5 @@
1
+ __main__
2
+ logger
3
+ models
4
+ server
5
+ utils
@@ -0,0 +1,22 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import List, Dict, Any
3
+
4
+
5
+ @dataclass
6
+ class AssertionResult:
7
+ assertion: str
8
+ passed: bool
9
+ expected: Any = None
10
+ actual: Any = None
11
+
12
+
13
+ @dataclass
14
+ class APITestResult:
15
+ api_name: str
16
+ passed: bool
17
+ method: str
18
+ url: str
19
+ status_code: int
20
+ response_time_ms: float
21
+ assertions: List[AssertionResult] = field(default_factory=list)
22
+ response_body: Any = None
@@ -0,0 +1,284 @@
1
+ import sys
2
+ import json
3
+ import shlex
4
+ import time
5
+ import httpx
6
+ from logger import get_logger
7
+ from dotenv import load_dotenv
8
+ from urllib.parse import urlparse
9
+ from typing import Optional, Dict, Any, List
10
+ from mcp.server.fastmcp import FastMCP
11
+ from models import APITestResult, AssertionResult
12
+ from utils import render_console_template_result, render_error_result
13
+
14
+ load_dotenv()
15
+
16
+ logger = get_logger(name="mcp-api-testing")
17
+
18
+
19
+ mcp = FastMCP("api-test-mcp")
20
+
21
+
22
+ def parse_status_code_range(range_str: str) -> tuple[int, int]:
23
+ """
24
+ Parse a status code range like '2xx' or '200-299' into (min, max) inclusive
25
+ """
26
+ range_str = range_str.strip()
27
+
28
+ if "xx" in range_str:
29
+ hundred = int(range_str[0])
30
+ return (hundred * 100, hundred * 100 + 99)
31
+
32
+ if "-" in range_str:
33
+ parts = range_str.split("-", 1)
34
+ return (int(parts[0]), int(parts[1]))
35
+
36
+ return (int(range_str), int(range_str))
37
+
38
+
39
+ def parse_curl_command(command: str) -> Dict[str, Any]:
40
+ """
41
+ Parse a curl command into structured request data.
42
+ It will supports HTTP method, headers, request payload,
43
+ and base URL
44
+ """
45
+
46
+ tokens = shlex.split(command)
47
+
48
+ method = "GET" # initial HTTP methods
49
+ headers = {}
50
+ payload = None
51
+ url = None
52
+
53
+ index = 0
54
+
55
+ while index < len(tokens):
56
+ token = tokens[index]
57
+
58
+ # moved from a global position into each specific branch,
59
+ # otherwise it will triggers a missing value error because
60
+ # it always trigger against every token including URLs
61
+ if token in ["-X", "--request"]:
62
+ if index + 1 >= len(tokens):
63
+ raise ValueError(
64
+ f"Invalid curl command, option {token} is missing its value"
65
+ )
66
+ method = tokens[index + 1]
67
+ index += 2
68
+ continue
69
+
70
+ if token in ["-H", "--header"]:
71
+ if index + 1 >= len(tokens):
72
+ raise ValueError(
73
+ f"Invalid curl command, option {token} is missing its value"
74
+ )
75
+ header_line = tokens[index + 1]
76
+ if ":" in header_line:
77
+ key, value = header_line.split(":", 1)
78
+ headers[key.strip()] = value.strip()
79
+
80
+ index += 2
81
+ continue
82
+
83
+ if token in ["-d", "--data"]:
84
+ if index + 1 >= len(tokens):
85
+ raise ValueError(
86
+ f"Invalid curl command, option {token} is missing its value"
87
+ )
88
+ raw_data = tokens[index + 1]
89
+
90
+ try:
91
+ payload = json.loads(raw_data)
92
+ except Exception:
93
+ logger.error(
94
+ message=f"Failed to parse request payload as JSON, using raw string instead. Error: {sys.exc_info()[0]}"
95
+ )
96
+ payload = raw_data
97
+
98
+ index += 2
99
+ continue
100
+
101
+ if token.startswith("http://") or token.startswith("https://"):
102
+ url = token
103
+
104
+ index += 1
105
+
106
+ if not url:
107
+ raise ValueError("URL must be provided in curl command")
108
+
109
+ parsed = urlparse(url=url)
110
+ base_url = f"{parsed.scheme}://{parsed.netloc}"
111
+ endpoint = parsed.path
112
+
113
+ if parsed.query:
114
+ endpoint += "?" + parsed.query
115
+
116
+ return {
117
+ "method": method.upper(),
118
+ "base_url": base_url,
119
+ "endpoint": endpoint,
120
+ "headers": headers,
121
+ "request_payload": payload,
122
+ }
123
+
124
+
125
+ @mcp.tool()
126
+ async def run_test_api(
127
+ api_name: str,
128
+ curl_command: Optional[str] = None,
129
+ base_url: Optional[str] = None,
130
+ endpoint: Optional[str] = None,
131
+ method: str = "GET",
132
+ headers: Optional[Dict[str, str]] = None,
133
+ payload: Optional[Dict[str, Any]] = None,
134
+ required_status_code: Optional[int] = None,
135
+ required_status_code_range: Optional[str] = None,
136
+ required_response_fields: Optional[List[str]] = None,
137
+ required_response_contains: Optional[str] = None,
138
+ ):
139
+ """
140
+ Test a given API services, it will supports either:
141
+
142
+ 1. Shared cURL command and it will be structured
143
+ 2. Or, pre-defined structured requests data
144
+
145
+ Example prompt:
146
+ `Please test this API based on this cURL, ensure status
147
+ code is 2xx, and response contains 'id'`
148
+ """
149
+ logger.info(message=f"Testing API: {api_name} with cURL: {curl_command}")
150
+ if curl_command:
151
+ parsed = parse_curl_command(command=curl_command)
152
+
153
+ method = parsed["method"]
154
+ base_url = parsed["base_url"]
155
+ endpoint = parsed["endpoint"]
156
+ headers = parsed["headers"]
157
+ payload = parsed["request_payload"]
158
+
159
+ if not base_url:
160
+ raise ValueError("`base_url` is required unless cURL command is used")
161
+
162
+ if not endpoint:
163
+ raise ValueError("`endpoint` is required unless cURL command is used")
164
+
165
+ full_path_url = f"{base_url.rstrip('/')}/{endpoint.lstrip('/')}"
166
+ method = method.upper()
167
+
168
+ start_time = time.perf_counter()
169
+ try:
170
+ async with httpx.AsyncClient(timeout=30) as client:
171
+ # prevent requests payload with None-type object or empty body
172
+ kwargs = {"method": method, "url": full_path_url, "headers": headers}
173
+ if payload is not None:
174
+ kwargs["json"] = payload
175
+ response = await client.request(**kwargs)
176
+ end_time = time.perf_counter()
177
+ response_time_ms = (end_time - start_time) * 1000
178
+ except Exception as e:
179
+ logger.error(
180
+ message=f"Error occurred while testing API: {api_name}, error: {str(e)}"
181
+ )
182
+ return render_error_result(
183
+ api_name=api_name, method=method, url=full_path_url, error=str(e)
184
+ )
185
+
186
+ try:
187
+ resp_body = response.json()
188
+ except Exception:
189
+ logger.error(
190
+ message=f"Failed to parse response body as JSON for API: {api_name}, using raw text instead."
191
+ )
192
+ resp_body = response.text
193
+
194
+ assertions = []
195
+
196
+ if required_status_code is not None:
197
+ passed = response.status_code == required_status_code
198
+ assertions.append(
199
+ {
200
+ "assertion": "status_code",
201
+ "expected": required_status_code,
202
+ "actual": response.status_code,
203
+ "passed": passed,
204
+ }
205
+ )
206
+
207
+ if required_status_code_range is not None:
208
+ # low refers to 1xx - 2xx and high refers to 3xx - 5xx
209
+ low, high = parse_status_code_range(required_status_code_range)
210
+ passed = low <= response.status_code <= high
211
+ assertions.append(
212
+ {
213
+ "assertion": "status_code_range",
214
+ "expected": required_status_code_range,
215
+ "actual": response.status_code,
216
+ "passed": passed,
217
+ }
218
+ )
219
+
220
+ if required_response_fields:
221
+ for field in required_response_fields:
222
+ if isinstance(resp_body, dict):
223
+ is_exist = field in resp_body
224
+ else:
225
+ is_exist = field in str(resp_body)
226
+ assertions.append(
227
+ {
228
+ "assertion": "response_field_exist",
229
+ "field": field,
230
+ "passed": is_exist,
231
+ }
232
+ )
233
+
234
+ if required_response_contains is not None:
235
+ body_str = (
236
+ json.dumps(resp_body) if isinstance(resp_body, dict) else str(resp_body)
237
+ )
238
+ found = required_response_contains in body_str
239
+ assertions.append(
240
+ {
241
+ "assertion": "response_contains",
242
+ "expected": required_response_contains,
243
+ "passed": found,
244
+ }
245
+ )
246
+
247
+ assertion_objects = []
248
+ for a in assertions:
249
+ assertion_objects.append(
250
+ AssertionResult(
251
+ assertion=a["assertion"],
252
+ passed=a["passed"],
253
+ expected=a.get("expected"),
254
+ actual=a.get("actual"),
255
+ )
256
+ )
257
+
258
+ overall_result = (
259
+ all(a["passed"] for a in assertions) if assertions else response.is_success
260
+ )
261
+
262
+ result = APITestResult(
263
+ api_name=api_name,
264
+ passed=overall_result,
265
+ method=method,
266
+ url=full_path_url,
267
+ status_code=response.status_code,
268
+ response_time_ms=response_time_ms,
269
+ assertions=assertion_objects,
270
+ response_body=resp_body,
271
+ )
272
+
273
+ logger.info(message="Finished testing API, now returning the result to MCP core...")
274
+ return render_console_template_result(result=result)
275
+
276
+
277
+ # expose this function so it can be called in toml files
278
+ def main():
279
+ mcp.run()
280
+
281
+
282
+ if __name__ == "__main__":
283
+ logger.info(message="Initialize the mcp-api-testing...")
284
+ main()
@@ -0,0 +1,65 @@
1
+ """Utility functions for MCP API testing"""
2
+
3
+ from models import APITestResult
4
+
5
+
6
+ def render_console_template_result(result: APITestResult) -> str:
7
+ """
8
+ Render the test result into a human-readable format for console output
9
+ """
10
+
11
+ divider = "-" * 50
12
+
13
+ passed_assertions = sum(1 for a in result.assertions if a.passed)
14
+ failed_assertions = len(result.assertions) - passed_assertions
15
+
16
+ lines = []
17
+
18
+ lines.append("┌──────────────────────────────────────────────┐")
19
+ lines.append("│ Api Test Result │")
20
+ lines.append("└──────────────────────────────────────────────┘")
21
+ lines.append("")
22
+ lines.append(f"API Name : {result.api_name}")
23
+ lines.append(f"Method : {result.method}")
24
+ lines.append(f"URL : {result.url}")
25
+ lines.append(f"Status Code : {result.status_code}")
26
+ lines.append(f"Response Time : {result.response_time_ms} ms")
27
+ lines.append(f"Overall Result : {'Passed' if result.passed else 'Failed'}")
28
+
29
+ lines.append("")
30
+ lines.append("Assertions")
31
+ lines.append(divider)
32
+
33
+ for assertion in result.assertions:
34
+ symbol = "✓" if assertion.passed else "x"
35
+ lines.append(f"{symbol} {assertion.assertion}")
36
+
37
+ lines.append("")
38
+ lines.append("Summary")
39
+ lines.append(divider)
40
+ lines.append(f"Total Assertions : {len(result.assertions)}")
41
+ lines.append(f"Passed : {passed_assertions}")
42
+ lines.append(f"Failed : {failed_assertions}")
43
+
44
+ return "\n".join(lines)
45
+
46
+
47
+ def render_error_result(api_name: str, method: str, url: str, error: str) -> str:
48
+ """
49
+ Render a runtime error into the same console format as successful results
50
+ """
51
+ lines = [
52
+ "┌──────────────────────────────────────────────┐",
53
+ "│ Api Test Result │",
54
+ "└──────────────────────────────────────────────┘",
55
+ "",
56
+ f"API Name : {api_name}",
57
+ f"Method : {method}",
58
+ f"URL : {url}",
59
+ f"Overall Result : Failed",
60
+ "",
61
+ "Error",
62
+ "-" * 50,
63
+ error,
64
+ ]
65
+ return "\n".join(lines)
@@ -0,0 +1,24 @@
1
+ """Integration tests with mocked HTTP, without hitting a real network"""
2
+
3
+ import pytest
4
+ import respx
5
+ import httpx
6
+ from src.server import run_test_api
7
+
8
+
9
+ @pytest.mark.asyncio
10
+ @respx.mock
11
+ async def test_api_return_on_200():
12
+ respx.get("https://api.example.com/v1/users").mock(
13
+ return_value=httpx.Response(200, json={"userID": 1})
14
+ )
15
+
16
+ result = await run_test_api(
17
+ api_name="fetch-user-id",
18
+ base_url="https://api.example.com/",
19
+ endpoint="/v1/users",
20
+ method="GET",
21
+ required_status_code=200,
22
+ )
23
+
24
+ assert "passed" in str(result).lower() or "PASSED" in result
@@ -0,0 +1,23 @@
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")
@@ -0,0 +1,16 @@
1
+ import pytest
2
+ from src.server import parse_status_code_range
3
+
4
+
5
+ @pytest.mark.parametrize(
6
+ "range_str, low, high",
7
+ [
8
+ ("2xx", 200, 299),
9
+ ("4xx", 400, 499),
10
+ ("5xx", 500, 599),
11
+ ("200-204", 200, 204),
12
+ ("201", 201, 201),
13
+ ],
14
+ )
15
+ def test_parse_range_status_code(range_str, low, high):
16
+ assert parse_status_code_range(range_str=range_str) == (low, high)