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.
- mcp_api_test-0.1.0/LICENSE +8 -0
- mcp_api_test-0.1.0/MANIFEST.in +1 -0
- mcp_api_test-0.1.0/PKG-INFO +257 -0
- mcp_api_test-0.1.0/README.md +228 -0
- mcp_api_test-0.1.0/pyproject.toml +45 -0
- mcp_api_test-0.1.0/setup.cfg +4 -0
- mcp_api_test-0.1.0/src/__main__.py +6 -0
- mcp_api_test-0.1.0/src/logger.py +51 -0
- mcp_api_test-0.1.0/src/mcp_api_test.egg-info/PKG-INFO +257 -0
- mcp_api_test-0.1.0/src/mcp_api_test.egg-info/SOURCES.txt +18 -0
- mcp_api_test-0.1.0/src/mcp_api_test.egg-info/dependency_links.txt +1 -0
- mcp_api_test-0.1.0/src/mcp_api_test.egg-info/entry_points.txt +2 -0
- mcp_api_test-0.1.0/src/mcp_api_test.egg-info/requires.txt +8 -0
- mcp_api_test-0.1.0/src/mcp_api_test.egg-info/top_level.txt +5 -0
- mcp_api_test-0.1.0/src/models.py +22 -0
- mcp_api_test-0.1.0/src/server.py +284 -0
- mcp_api_test-0.1.0/src/utils.py +65 -0
- mcp_api_test-0.1.0/tests/test_mcp_api_tools.py +24 -0
- mcp_api_test-0.1.0/tests/test_parse_curl_command.py +23 -0
- mcp_api_test-0.1.0/tests/test_parse_status_code.py +16 -0
|
@@ -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
|
+

|
|
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
|
+

|
|
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,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
|
+

|
|
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 @@
|
|
|
1
|
+
|
|
@@ -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)
|