@testomatio/mcp 1.0.0 → 1.0.2
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.
- package/README.md +102 -33
- package/index.js +302 -48
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Testomat.io MCP Server
|
|
2
2
|
|
|
3
|
-
A Model Context Protocol (MCP) server for
|
|
3
|
+
A Model Context Protocol (MCP) server for Testomat.io API integration with AI assistants like Cursor.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,15 +8,9 @@ A Model Context Protocol (MCP) server for Testomatio API integration with AI ass
|
|
|
8
8
|
|
|
9
9
|
- Node.js 18 or higher (with built-in fetch support)
|
|
10
10
|
- npm or yarn package manager
|
|
11
|
-
-
|
|
11
|
+
- Testomat.io account with API access
|
|
12
12
|
|
|
13
|
-
###
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm install -g @testomatio/mcp
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
### Or run directly with npx
|
|
13
|
+
### Run directly with npx
|
|
20
14
|
|
|
21
15
|
```bash
|
|
22
16
|
npx @testomatio/mcp --token <your-token> --project <project-id>
|
|
@@ -37,9 +31,6 @@ npx @testomatio/mcp -t testomat_YOUR_TOKEN_HERE -p your-project-id
|
|
|
37
31
|
# Using long flags
|
|
38
32
|
npx @testomatio/mcp --token testomat_YOUR_TOKEN_HERE --project your-project-id
|
|
39
33
|
|
|
40
|
-
# If installed globally
|
|
41
|
-
testomatio-mcp --token testomat_YOUR_TOKEN_HERE --project your-project-id
|
|
42
|
-
|
|
43
34
|
# With custom base URL
|
|
44
35
|
npx @testomatio/mcp --token testomat_YOUR_TOKEN_HERE --project your-project-id --base-url https://your-instance.testomat.io
|
|
45
36
|
```
|
|
@@ -60,7 +51,7 @@ TESTOMATIO_API_TOKEN=testomat_YOUR_TOKEN_HERE npx @testomatio/mcp --project your
|
|
|
60
51
|
|
|
61
52
|
### Getting Your API Token
|
|
62
53
|
|
|
63
|
-
1. Go to [
|
|
54
|
+
1. Go to [Testomat.io](https://app.testomat.io)
|
|
64
55
|
2. Navigate to user tokens https://app.testomat.io/account/access_tokens
|
|
65
56
|
3. Create and copy **General API token** (starts with `testomat_`)
|
|
66
57
|
|
|
@@ -84,7 +75,7 @@ Add this to your Cursor MCP settings (`cursor-settings.json` or through the Curs
|
|
|
84
75
|
"mcpServers": {
|
|
85
76
|
"testomatio": {
|
|
86
77
|
"command": "npx",
|
|
87
|
-
"args": ["@testomatio/mcp", "--token", "testomat_YOUR_TOKEN_HERE", "--project", "YOUR_PROJECT_ID"]
|
|
78
|
+
"args": ["-y", "@testomatio/mcp@latest", "--token", "testomat_YOUR_TOKEN_HERE", "--project", "YOUR_PROJECT_ID"]
|
|
88
79
|
}
|
|
89
80
|
}
|
|
90
81
|
}
|
|
@@ -105,7 +96,7 @@ Then add this to your Cursor MCP settings:
|
|
|
105
96
|
"mcpServers": {
|
|
106
97
|
"testomatio": {
|
|
107
98
|
"command": "npx",
|
|
108
|
-
"args": ["@testomatio/mcp", "--project", "YOUR_PROJECT_ID"],
|
|
99
|
+
"args": ["-y", "@testomatio/mcp@latest", "--project", "YOUR_PROJECT_ID"],
|
|
109
100
|
"env": {
|
|
110
101
|
"TESTOMATIO_API_TOKEN": "testomat_YOUR_TOKEN_HERE"
|
|
111
102
|
}
|
|
@@ -114,20 +105,29 @@ Then add this to your Cursor MCP settings:
|
|
|
114
105
|
}
|
|
115
106
|
```
|
|
116
107
|
|
|
117
|
-
|
|
108
|
+
## Features
|
|
118
109
|
|
|
119
|
-
|
|
110
|
+
### Tools
|
|
120
111
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
112
|
+
#### Tests
|
|
113
|
+
* `get_tests` – Get all tests (params: `plan`, `query`, `state`, `suite_id`, `tag`, `labels`) — api: GET `/tests`
|
|
114
|
+
* `search_tests` – Search tests (params: `query`, `tql`, `labels`, `state`, `priority`, `filter`, `page`) — api: GET `/tests`
|
|
115
|
+
* `create_test` – Create a new test (params: `suite_id`, `title`, `description`, `code`, `file`, `state`, `tags`, `jira_issues`, `assigned_to`, `labels_ids`) — api: POST `/tests`
|
|
116
|
+
* `update_test` – Update an existing test (params: `test_id`, `suite_id`, `title`, `description`, `code`, `file`, `state`, `tags`, `jira_issues`, `assigned_to`, `labels_ids`) — api: PUT `/tests/{test_id}`
|
|
117
|
+
|
|
118
|
+
#### Test Suites
|
|
119
|
+
* `search_suites` – Search suites (params: `query`, `labels`, `state`, `priority`, `page`) — api: GET `/suites`
|
|
120
|
+
* `get_root_suites` – List root-level suites (no params) — api: GET `/suites`
|
|
121
|
+
* `get_suite` – Get one suite (params: `suite_id`) — api: GET `/suites/{suite_id}`
|
|
122
|
+
|
|
123
|
+
#### Test Runs
|
|
124
|
+
* `get_runs` – List all runs (no params) — api: GET `/runs`
|
|
125
|
+
* `get_run` – Get one run (params: `run_id`, `tree`) — api: GET `/runs/{run_id}`
|
|
126
|
+
* `get_testruns` – Runs for a test (params: `test_id`, `finished_at_date_range`) — api: GET `/testruns`
|
|
127
|
+
|
|
128
|
+
#### Test Plans
|
|
129
|
+
* `get_plans` – List all plans (params: `detail`, `labels`, `page`) — api: GET `/plans`
|
|
130
|
+
* `get_plan` – Get one plan (params: `plan_id`) — api: GET `/plans/{plan_id}`
|
|
131
131
|
|
|
132
132
|
## Example Usage in Cursor
|
|
133
133
|
|
|
@@ -139,6 +139,75 @@ Once configured, you can ask your AI assistant questions like:
|
|
|
139
139
|
- "Show me details for test run xyz789"
|
|
140
140
|
- "List all automated tests with the @smoke tag"
|
|
141
141
|
- "Get all test plans for this project"
|
|
142
|
+
- "Create a new test called 'Login validation' in suite suite-123"
|
|
143
|
+
- "Update test test-456 to change its description and add @regression tag"
|
|
144
|
+
|
|
145
|
+
## Query Patterns
|
|
146
|
+
|
|
147
|
+
### Basic Information Queries
|
|
148
|
+
|
|
149
|
+
These queries retrieve general information without specific filtering:
|
|
150
|
+
|
|
151
|
+
- **"Show me all the tests in the project"** → `get_tests` tool
|
|
152
|
+
- **"What are the root suites in this project?"** → `get_root_suites` tool
|
|
153
|
+
- **"Get all test runs"** → `get_runs` tool
|
|
154
|
+
- **"Get all test plans for this project"** → `get_plans` tool
|
|
155
|
+
|
|
156
|
+
### Test Management Queries
|
|
157
|
+
|
|
158
|
+
These queries allow creating and updating tests:
|
|
159
|
+
|
|
160
|
+
- **"Create a new test called 'Login validation' in suite suite-123"** → `create_test` tool with `title: "Login validation"`, `suite_id: "suite-123"`
|
|
161
|
+
- **"Update test test-456 to change its description"** → `update_test` tool with `test_id: "test-456"`, `description: "new description"`
|
|
162
|
+
- **"Create an automated test with @smoke tag"** → `create_test` tool with `state: "automated"`, `tags: ["smoke"]`
|
|
163
|
+
|
|
164
|
+
### Specific Item Queries
|
|
165
|
+
|
|
166
|
+
These queries target specific entities by ID:
|
|
167
|
+
|
|
168
|
+
- **"Get test runs for test ID abc123"** → `get_testruns` tool with `test_id: "abc123"`
|
|
169
|
+
- **"Show me details for test run xyz789"** → `get_run` tool with `run_id: "xyz789"`
|
|
170
|
+
- **"Get suite details for suite-456"** → `get_suite` tool with `suite_id: "suite-456"`
|
|
171
|
+
|
|
172
|
+
### Search and Filter Queries
|
|
173
|
+
|
|
174
|
+
These queries use advanced filtering capabilities:
|
|
175
|
+
|
|
176
|
+
- **"List all automated tests with the @smoke tag"** → `search_tests` tool with `query: "@smoke"`, `state: "automated"`
|
|
177
|
+
- **"Find tests with priority high"** → `search_tests` tool with `priority: "high"`
|
|
178
|
+
- **"Search for tests containing 'login'"** → `search_tests` tool with `query: "login"`
|
|
179
|
+
- **"List tests tagged @critical or labelled 'ux' with high priority"** → `search_tests` tool with `tql: "tag == 'critical' or label == 'ux' and priority == 'high'"`
|
|
180
|
+
- **"Find tests linked to JIRA-123"** → `search_tests` tool with `tql: jira == 'BDCP-2'`
|
|
181
|
+
|
|
182
|
+
### Advanced Query Syntax
|
|
183
|
+
|
|
184
|
+
#### Test Query Language (TQL)
|
|
185
|
+
|
|
186
|
+
The `search_tests` tool supports TQL for complex filtering:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
"tag == 'smoke' and state == 'manual'"
|
|
190
|
+
"priority == 'high' or label == 'ux'"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### Tag-Based Searches
|
|
194
|
+
|
|
195
|
+
Tags can be searched using the `@` prefix:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
@smoke # Tests tagged with 'smoke'
|
|
199
|
+
@regression # Tests tagged with 'regression'
|
|
200
|
+
@critical # Tests tagged with 'critical'
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### Jira Integration
|
|
204
|
+
|
|
205
|
+
Tests linked to Jira issues can be found using issue keys:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
JIRA-123 # Tests linked to JIRA-123
|
|
209
|
+
PROJ-456 # Tests linked to PROJ-456
|
|
210
|
+
```
|
|
142
211
|
|
|
143
212
|
## Troubleshooting
|
|
144
213
|
|
|
@@ -146,7 +215,7 @@ Once configured, you can ask your AI assistant questions like:
|
|
|
146
215
|
|
|
147
216
|
1. **"API token is required" error**
|
|
148
217
|
- Make sure your token starts with `testomat_`
|
|
149
|
-
- Verify the token is correct in your
|
|
218
|
+
- Verify the token is correct in your Testomat.io project settings
|
|
150
219
|
|
|
151
220
|
2. **"Project ID is required" error**
|
|
152
221
|
- Check that you're passing the correct project ID
|
|
@@ -172,7 +241,7 @@ DEBUG=* npx @testomatio/mcp --token <token> --project <project-id>
|
|
|
172
241
|
|
|
173
242
|
## API Reference
|
|
174
243
|
|
|
175
|
-
For detailed information about the underlying
|
|
244
|
+
For detailed information about the underlying Testomat.io API, refer to the [Testomat.io API Documentation](https://app.testomat.io/docs/api/).
|
|
176
245
|
|
|
177
246
|
## Contributing
|
|
178
247
|
|
|
@@ -185,14 +254,14 @@ This project is licensed under the MIT License - see the LICENSE file for detail
|
|
|
185
254
|
## Support
|
|
186
255
|
|
|
187
256
|
For support, please:
|
|
188
|
-
1. Check the [
|
|
257
|
+
1. Check the [Testomat.io Documentation](https://docs.testomat.io)
|
|
189
258
|
2. Open an issue on GitHub
|
|
190
|
-
3. Contact
|
|
259
|
+
3. Contact Testomat.io support
|
|
191
260
|
|
|
192
261
|
## Changelog
|
|
193
262
|
|
|
194
263
|
### v1.0.0
|
|
195
264
|
- Initial release
|
|
196
|
-
- Support for all major
|
|
265
|
+
- Support for all major Testomat.io API endpoints
|
|
197
266
|
- MCP-compatible tool interface
|
|
198
267
|
- Semantic XML formatting for LLM processing
|
package/index.js
CHANGED
|
@@ -45,13 +45,13 @@ class TestomatioMCPServer {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const data = await response.json();
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
if (!data.jwt) {
|
|
50
50
|
throw new Error('Authentication failed: No JWT token received in response');
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
this.jwtToken = data.jwt;
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
return this.jwtToken;
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -269,6 +269,118 @@ class TestomatioMCPServer {
|
|
|
269
269
|
required: ['plan_id'],
|
|
270
270
|
},
|
|
271
271
|
},
|
|
272
|
+
{
|
|
273
|
+
name: 'create_test',
|
|
274
|
+
description: 'Create a new test in the specified suite',
|
|
275
|
+
inputSchema: {
|
|
276
|
+
type: 'object',
|
|
277
|
+
properties: {
|
|
278
|
+
suite_id: {
|
|
279
|
+
type: 'string',
|
|
280
|
+
description: 'Suite ID where the test will be created',
|
|
281
|
+
},
|
|
282
|
+
title: {
|
|
283
|
+
type: 'string',
|
|
284
|
+
description: 'Test title',
|
|
285
|
+
},
|
|
286
|
+
description: {
|
|
287
|
+
type: 'string',
|
|
288
|
+
description: 'Test description',
|
|
289
|
+
},
|
|
290
|
+
code: {
|
|
291
|
+
type: 'string',
|
|
292
|
+
description: 'Source code of an automated test',
|
|
293
|
+
},
|
|
294
|
+
file: {
|
|
295
|
+
type: 'string',
|
|
296
|
+
description: 'File of an automated test',
|
|
297
|
+
},
|
|
298
|
+
state: {
|
|
299
|
+
type: 'string',
|
|
300
|
+
enum: ['manual', 'automated'],
|
|
301
|
+
description: 'State of the test',
|
|
302
|
+
},
|
|
303
|
+
tags: {
|
|
304
|
+
type: 'array',
|
|
305
|
+
items: { type: 'string' },
|
|
306
|
+
description: 'List of tags for the test',
|
|
307
|
+
},
|
|
308
|
+
jira_issues: {
|
|
309
|
+
type: 'array',
|
|
310
|
+
items: { type: 'string' },
|
|
311
|
+
description: 'List of assigned Jira issues',
|
|
312
|
+
},
|
|
313
|
+
assigned_to: {
|
|
314
|
+
type: 'string',
|
|
315
|
+
description: 'User assigned to this test',
|
|
316
|
+
},
|
|
317
|
+
labels_ids: {
|
|
318
|
+
type: 'array',
|
|
319
|
+
items: { type: 'string' },
|
|
320
|
+
description: 'Slugs of labels to assign to the test',
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
required: ['suite_id', 'title'],
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: 'update_test',
|
|
328
|
+
description: 'Update an existing test',
|
|
329
|
+
inputSchema: {
|
|
330
|
+
type: 'object',
|
|
331
|
+
properties: {
|
|
332
|
+
test_id: {
|
|
333
|
+
type: 'string',
|
|
334
|
+
description: 'ID of the test to update',
|
|
335
|
+
},
|
|
336
|
+
suite_id: {
|
|
337
|
+
type: 'string',
|
|
338
|
+
description: 'Suite ID where the test belongs',
|
|
339
|
+
},
|
|
340
|
+
title: {
|
|
341
|
+
type: 'string',
|
|
342
|
+
description: 'Test title',
|
|
343
|
+
},
|
|
344
|
+
description: {
|
|
345
|
+
type: 'string',
|
|
346
|
+
description: 'Test description',
|
|
347
|
+
},
|
|
348
|
+
code: {
|
|
349
|
+
type: 'string',
|
|
350
|
+
description: 'Source code of an automated test',
|
|
351
|
+
},
|
|
352
|
+
file: {
|
|
353
|
+
type: 'string',
|
|
354
|
+
description: 'File of an automated test',
|
|
355
|
+
},
|
|
356
|
+
state: {
|
|
357
|
+
type: 'string',
|
|
358
|
+
enum: ['manual', 'automated'],
|
|
359
|
+
description: 'State of the test',
|
|
360
|
+
},
|
|
361
|
+
tags: {
|
|
362
|
+
type: 'array',
|
|
363
|
+
items: { type: 'string' },
|
|
364
|
+
description: 'List of tags for the test',
|
|
365
|
+
},
|
|
366
|
+
jira_issues: {
|
|
367
|
+
type: 'array',
|
|
368
|
+
items: { type: 'string' },
|
|
369
|
+
description: 'List of assigned Jira issues',
|
|
370
|
+
},
|
|
371
|
+
assigned_to: {
|
|
372
|
+
type: 'string',
|
|
373
|
+
description: 'User assigned to this test',
|
|
374
|
+
},
|
|
375
|
+
labels_ids: {
|
|
376
|
+
type: 'array',
|
|
377
|
+
items: { type: 'string' },
|
|
378
|
+
description: 'Slugs of labels to assign to the test',
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
required: ['test_id'],
|
|
382
|
+
},
|
|
383
|
+
},
|
|
272
384
|
],
|
|
273
385
|
};
|
|
274
386
|
});
|
|
@@ -298,6 +410,10 @@ class TestomatioMCPServer {
|
|
|
298
410
|
return await this.getPlans(args);
|
|
299
411
|
case 'get_plan':
|
|
300
412
|
return await this.getPlan(args.plan_id);
|
|
413
|
+
case 'create_test':
|
|
414
|
+
return await this.createTest(args);
|
|
415
|
+
case 'update_test':
|
|
416
|
+
return await this.updateTest(args);
|
|
301
417
|
default:
|
|
302
418
|
throw new Error(`Unknown tool: ${name}`);
|
|
303
419
|
}
|
|
@@ -317,9 +433,9 @@ class TestomatioMCPServer {
|
|
|
317
433
|
async makeRequest(path, params = {}) {
|
|
318
434
|
// Ensure we have a valid JWT token
|
|
319
435
|
const jwt = await this.authenticate();
|
|
320
|
-
|
|
436
|
+
|
|
321
437
|
const url = new URL(`${this.config.baseUrl}/api/${this.config.projectId}${path}`);
|
|
322
|
-
|
|
438
|
+
|
|
323
439
|
// Add query parameters with proper array handling
|
|
324
440
|
Object.entries(params).forEach(([key, value]) => {
|
|
325
441
|
if (value !== undefined && value !== null) {
|
|
@@ -352,6 +468,55 @@ class TestomatioMCPServer {
|
|
|
352
468
|
return await response.json();
|
|
353
469
|
}
|
|
354
470
|
|
|
471
|
+
async makePostRequest(path, data) {
|
|
472
|
+
const jwt = await this.authenticate();
|
|
473
|
+
const url = `${this.config.baseUrl}/api/${this.config.projectId}${path}`;
|
|
474
|
+
|
|
475
|
+
const response = await fetch(url, {
|
|
476
|
+
method: 'POST',
|
|
477
|
+
headers: {
|
|
478
|
+
'Authorization': jwt,
|
|
479
|
+
'Content-Type': 'application/json',
|
|
480
|
+
},
|
|
481
|
+
body: JSON.stringify(data),
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
if (!response.ok) {
|
|
485
|
+
if (response.status === 401 && this.jwtToken) {
|
|
486
|
+
this.jwtToken = null;
|
|
487
|
+
return this.makePostRequest(path, data);
|
|
488
|
+
}
|
|
489
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return await response.json();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async makePutRequest(path, data) {
|
|
496
|
+
const jwt = await this.authenticate();
|
|
497
|
+
const url = `${this.config.baseUrl}/api/${this.config.projectId}${path}`;
|
|
498
|
+
|
|
499
|
+
const response = await fetch(url, {
|
|
500
|
+
method: 'PUT',
|
|
501
|
+
headers: {
|
|
502
|
+
'Authorization': jwt,
|
|
503
|
+
'Content-Type': 'application/json',
|
|
504
|
+
},
|
|
505
|
+
body: JSON.stringify(data),
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
if (!response.ok) {
|
|
509
|
+
if (response.status === 401 && this.jwtToken) {
|
|
510
|
+
this.jwtToken = null;
|
|
511
|
+
return this.makePutRequest(path, data);
|
|
512
|
+
}
|
|
513
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return await response.json();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
|
|
355
520
|
escapeXml(text) {
|
|
356
521
|
if (typeof text !== 'string') return text;
|
|
357
522
|
return text
|
|
@@ -415,10 +580,10 @@ class TestomatioMCPServer {
|
|
|
415
580
|
formatModel(model, tagName, fields) {
|
|
416
581
|
const attributes = model.attributes || {};
|
|
417
582
|
const lines = [`<${tagName}>`];
|
|
418
|
-
|
|
583
|
+
|
|
419
584
|
// Always include ID from root level
|
|
420
585
|
lines.push(` <id>${model.id || ''}</id>`);
|
|
421
|
-
|
|
586
|
+
|
|
422
587
|
// Process specified fields
|
|
423
588
|
fields.forEach(field => {
|
|
424
589
|
let value;
|
|
@@ -434,7 +599,7 @@ class TestomatioMCPServer {
|
|
|
434
599
|
}
|
|
435
600
|
|
|
436
601
|
const formattedValue = this.formatValue(value, field);
|
|
437
|
-
|
|
602
|
+
|
|
438
603
|
if (field === 'test' && typeof value === 'object') {
|
|
439
604
|
// Special case for nested test objects
|
|
440
605
|
lines.push(` <test>${formattedValue}\n </test>`);
|
|
@@ -442,7 +607,7 @@ class TestomatioMCPServer {
|
|
|
442
607
|
lines.push(` <${xmlFieldName}>${formattedValue}</${xmlFieldName}>`);
|
|
443
608
|
}
|
|
444
609
|
});
|
|
445
|
-
|
|
610
|
+
|
|
446
611
|
lines.push(`</${tagName}>`);
|
|
447
612
|
return lines.join('\n');
|
|
448
613
|
}
|
|
@@ -450,13 +615,13 @@ class TestomatioMCPServer {
|
|
|
450
615
|
async getTests(filters = {}) {
|
|
451
616
|
const params = this.buildSearchParams(filters);
|
|
452
617
|
const data = await this.makeRequest('/tests', params);
|
|
453
|
-
const formattedTests = data.data.map(test =>
|
|
618
|
+
const formattedTests = data.data.map(test =>
|
|
454
619
|
this.formatModel(test, 'test', [
|
|
455
|
-
'title', 'description', 'code', 'priority',
|
|
620
|
+
'title', 'description', 'code', 'priority',
|
|
456
621
|
'state', 'suite-id', 'tags', 'file'
|
|
457
622
|
])
|
|
458
623
|
).join('\n\n');
|
|
459
|
-
|
|
624
|
+
|
|
460
625
|
return {
|
|
461
626
|
content: [
|
|
462
627
|
{
|
|
@@ -467,19 +632,64 @@ class TestomatioMCPServer {
|
|
|
467
632
|
};
|
|
468
633
|
}
|
|
469
634
|
|
|
635
|
+
buildSearchParams(filters = {}) {
|
|
636
|
+
const params = {};
|
|
637
|
+
|
|
638
|
+
// Handle basic filters
|
|
639
|
+
Object.entries(filters).forEach(([key, value]) => {
|
|
640
|
+
if (value !== undefined && value !== null) {
|
|
641
|
+
if (key === 'labels' && Array.isArray(value)) {
|
|
642
|
+
// Labels need special array handling
|
|
643
|
+
value.forEach(label => {
|
|
644
|
+
if (!params['labels[]']) {
|
|
645
|
+
params['labels[]'] = [];
|
|
646
|
+
}
|
|
647
|
+
if (Array.isArray(params['labels[]'])) {
|
|
648
|
+
params['labels[]'].push(label);
|
|
649
|
+
} else {
|
|
650
|
+
params['labels[]'] = [params['labels[]'], label];
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
} else if (key === 'filter' && typeof value === 'object') {
|
|
654
|
+
// Handle filter hash (e.g., filter[state]=manual)
|
|
655
|
+
Object.entries(value).forEach(([filterKey, filterValue]) => {
|
|
656
|
+
params[`filter[${filterKey}]`] = filterValue;
|
|
657
|
+
});
|
|
658
|
+
} else if (Array.isArray(value)) {
|
|
659
|
+
// Handle other arrays
|
|
660
|
+
value.forEach(v => {
|
|
661
|
+
const paramKey = `${key}[]`;
|
|
662
|
+
if (!params[paramKey]) {
|
|
663
|
+
params[paramKey] = [];
|
|
664
|
+
}
|
|
665
|
+
if (Array.isArray(params[paramKey])) {
|
|
666
|
+
params[paramKey].push(v);
|
|
667
|
+
} else {
|
|
668
|
+
params[paramKey] = [params[paramKey], v];
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
} else {
|
|
672
|
+
params[key] = String(value);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
return params;
|
|
678
|
+
}
|
|
679
|
+
|
|
470
680
|
async searchTests(filters = {}) {
|
|
471
681
|
const params = this.buildSearchParams(filters);
|
|
472
682
|
const data = await this.makeRequest('/tests', params);
|
|
473
|
-
|
|
474
|
-
const formattedTests = data.data.map(test =>
|
|
683
|
+
|
|
684
|
+
const formattedTests = data.data.map(test =>
|
|
475
685
|
this.formatModel(test, 'test', [
|
|
476
|
-
'title', 'description', 'code', 'priority',
|
|
686
|
+
'title', 'description', 'code', 'priority',
|
|
477
687
|
'state', 'suite-id', 'tags', 'file'
|
|
478
688
|
])
|
|
479
689
|
).join('\n\n');
|
|
480
|
-
|
|
690
|
+
|
|
481
691
|
const searchDescription = this.buildSearchDescription(filters);
|
|
482
|
-
|
|
692
|
+
|
|
483
693
|
return {
|
|
484
694
|
content: [
|
|
485
695
|
{
|
|
@@ -494,15 +704,15 @@ class TestomatioMCPServer {
|
|
|
494
704
|
// Add filter=true for suites search to include tests
|
|
495
705
|
const params = this.buildSearchParams({ ...filters, filter: true });
|
|
496
706
|
const data = await this.makeRequest('/suites', params);
|
|
497
|
-
|
|
498
|
-
const formattedSuites = data.data.map(suite =>
|
|
707
|
+
|
|
708
|
+
const formattedSuites = data.data.map(suite =>
|
|
499
709
|
this.formatModel(suite, 'suite', [
|
|
500
710
|
'title', 'description', 'test-count', 'is-root', 'file-type'
|
|
501
711
|
])
|
|
502
712
|
).join('\n\n');
|
|
503
|
-
|
|
713
|
+
|
|
504
714
|
const searchDescription = this.buildSearchDescription(filters);
|
|
505
|
-
|
|
715
|
+
|
|
506
716
|
return {
|
|
507
717
|
content: [
|
|
508
718
|
{
|
|
@@ -515,7 +725,7 @@ class TestomatioMCPServer {
|
|
|
515
725
|
|
|
516
726
|
buildSearchDescription(filters) {
|
|
517
727
|
const descriptions = [];
|
|
518
|
-
|
|
728
|
+
|
|
519
729
|
if (filters.query) {
|
|
520
730
|
if (filters.query.startsWith('@')) {
|
|
521
731
|
descriptions.push(`tagged with "${filters.query}"`);
|
|
@@ -525,41 +735,41 @@ class TestomatioMCPServer {
|
|
|
525
735
|
descriptions.push(`containing "${filters.query}"`);
|
|
526
736
|
}
|
|
527
737
|
}
|
|
528
|
-
|
|
738
|
+
|
|
529
739
|
if (filters.tql) {
|
|
530
740
|
descriptions.push(`matching TQL: "${filters.tql}"`);
|
|
531
741
|
}
|
|
532
|
-
|
|
742
|
+
|
|
533
743
|
if (filters.labels && filters.labels.length > 0) {
|
|
534
744
|
descriptions.push(`with labels: ${filters.labels.join(', ')}`);
|
|
535
745
|
}
|
|
536
|
-
|
|
746
|
+
|
|
537
747
|
if (filters.state) {
|
|
538
748
|
descriptions.push(`state: ${filters.state}`);
|
|
539
749
|
}
|
|
540
|
-
|
|
750
|
+
|
|
541
751
|
if (filters.priority) {
|
|
542
752
|
descriptions.push(`priority: ${filters.priority}`);
|
|
543
753
|
}
|
|
544
|
-
|
|
754
|
+
|
|
545
755
|
if (filters.filter && typeof filters.filter === 'object') {
|
|
546
756
|
const filterDesc = Object.entries(filters.filter)
|
|
547
757
|
.map(([key, value]) => `${key}: ${value}`)
|
|
548
758
|
.join(', ');
|
|
549
759
|
descriptions.push(`filtered by: ${filterDesc}`);
|
|
550
760
|
}
|
|
551
|
-
|
|
761
|
+
|
|
552
762
|
return descriptions.length > 0 ? ` (${descriptions.join(', ')})` : '';
|
|
553
763
|
}
|
|
554
764
|
|
|
555
765
|
async getRootSuites() {
|
|
556
766
|
const data = await this.makeRequest('/suites');
|
|
557
|
-
const formattedSuites = data.data.map(suite =>
|
|
767
|
+
const formattedSuites = data.data.map(suite =>
|
|
558
768
|
this.formatModel(suite, 'suite', [
|
|
559
769
|
'title', 'description', 'test-count', 'is-root', 'file-type'
|
|
560
770
|
])
|
|
561
771
|
).join('\n\n');
|
|
562
|
-
|
|
772
|
+
|
|
563
773
|
return {
|
|
564
774
|
content: [
|
|
565
775
|
{
|
|
@@ -575,7 +785,7 @@ class TestomatioMCPServer {
|
|
|
575
785
|
const formattedSuite = this.formatModel(data.data, 'suite', [
|
|
576
786
|
'title', 'description', 'test-count', 'is-root', 'file-type'
|
|
577
787
|
]);
|
|
578
|
-
|
|
788
|
+
|
|
579
789
|
// Format child suites and tests if they exist
|
|
580
790
|
let childContent = '';
|
|
581
791
|
if (data.data.relationships?.children?.data) {
|
|
@@ -587,18 +797,18 @@ class TestomatioMCPServer {
|
|
|
587
797
|
childContent += `\n\nChild Suites:\n${childSuites}`;
|
|
588
798
|
}
|
|
589
799
|
}
|
|
590
|
-
|
|
800
|
+
|
|
591
801
|
if (data.data.relationships?.tests?.data) {
|
|
592
802
|
const tests = data.data.relationships.tests.data
|
|
593
803
|
.map(test => this.formatModel(test, 'test', [
|
|
594
|
-
'title', 'description', 'code', 'priority',
|
|
804
|
+
'title', 'description', 'code', 'priority',
|
|
595
805
|
'state', 'suite-id', 'tags', 'file'
|
|
596
806
|
])).join('\n\n');
|
|
597
807
|
if (tests) {
|
|
598
808
|
childContent += `\n\nTests:\n${tests}`;
|
|
599
809
|
}
|
|
600
810
|
}
|
|
601
|
-
|
|
811
|
+
|
|
602
812
|
return {
|
|
603
813
|
content: [
|
|
604
814
|
{
|
|
@@ -611,13 +821,13 @@ class TestomatioMCPServer {
|
|
|
611
821
|
|
|
612
822
|
async getRuns() {
|
|
613
823
|
const data = await this.makeRequest('/runs');
|
|
614
|
-
const formattedRuns = data.data.map(run =>
|
|
824
|
+
const formattedRuns = data.data.map(run =>
|
|
615
825
|
this.formatModel(run, 'run', [
|
|
616
826
|
'status', 'title', 'tests-count', 'automated', 'duration',
|
|
617
827
|
'passed', 'failed', 'skipped', 'created-at', 'finished-at'
|
|
618
828
|
])
|
|
619
829
|
).join('\n\n');
|
|
620
|
-
|
|
830
|
+
|
|
621
831
|
return {
|
|
622
832
|
content: [
|
|
623
833
|
{
|
|
@@ -635,7 +845,7 @@ class TestomatioMCPServer {
|
|
|
635
845
|
'status', 'title', 'tests-count', 'automated', 'duration',
|
|
636
846
|
'passed', 'failed', 'skipped', 'created-at', 'finished-at'
|
|
637
847
|
]);
|
|
638
|
-
|
|
848
|
+
|
|
639
849
|
return {
|
|
640
850
|
content: [
|
|
641
851
|
{
|
|
@@ -651,14 +861,14 @@ class TestomatioMCPServer {
|
|
|
651
861
|
if (dateRange) {
|
|
652
862
|
params.finished_at_date_range = dateRange;
|
|
653
863
|
}
|
|
654
|
-
|
|
864
|
+
|
|
655
865
|
const data = await this.makeRequest('/testruns', params);
|
|
656
|
-
const formattedTestruns = data.data.map(testrun =>
|
|
866
|
+
const formattedTestruns = data.data.map(testrun =>
|
|
657
867
|
this.formatModel(testrun, 'testrun', [
|
|
658
868
|
'status', 'run-time', 'message', 'run-id', 'test'
|
|
659
869
|
])
|
|
660
870
|
).join('\n\n');
|
|
661
|
-
|
|
871
|
+
|
|
662
872
|
return {
|
|
663
873
|
content: [
|
|
664
874
|
{
|
|
@@ -671,12 +881,12 @@ class TestomatioMCPServer {
|
|
|
671
881
|
|
|
672
882
|
async getPlans(filters = {}) {
|
|
673
883
|
const data = await this.makeRequest('/plans', filters);
|
|
674
|
-
const formattedPlans = data.data.map(plan =>
|
|
884
|
+
const formattedPlans = data.data.map(plan =>
|
|
675
885
|
this.formatModel(plan, 'plan', [
|
|
676
886
|
'title', 'test-count', 'kind', 'created-at', 'tests-ids', 'labels'
|
|
677
887
|
])
|
|
678
888
|
).join('\n\n');
|
|
679
|
-
|
|
889
|
+
|
|
680
890
|
return {
|
|
681
891
|
content: [
|
|
682
892
|
{
|
|
@@ -692,7 +902,7 @@ class TestomatioMCPServer {
|
|
|
692
902
|
const formattedPlan = this.formatModel(data.data, 'plan', [
|
|
693
903
|
'title', 'test-count', 'kind', 'created-at', 'tests-ids', 'labels'
|
|
694
904
|
]);
|
|
695
|
-
|
|
905
|
+
|
|
696
906
|
return {
|
|
697
907
|
content: [
|
|
698
908
|
{
|
|
@@ -703,6 +913,54 @@ class TestomatioMCPServer {
|
|
|
703
913
|
};
|
|
704
914
|
}
|
|
705
915
|
|
|
916
|
+
async createTest(args) {
|
|
917
|
+
const { test_id, labels_ids, ...attributes } = args;
|
|
918
|
+
const requestData = {
|
|
919
|
+
type: 'test',
|
|
920
|
+
attributes: Object.fromEntries(Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v])),
|
|
921
|
+
};
|
|
922
|
+
if (labels_ids) requestData.labels_ids = labels_ids;
|
|
923
|
+
|
|
924
|
+
const data = await this.makePostRequest('/tests', requestData);
|
|
925
|
+
const formattedTest = this.formatModel(data.data, 'test', [
|
|
926
|
+
'title', 'description', 'code', 'priority',
|
|
927
|
+
'state', 'suite-id', 'tags', 'file'
|
|
928
|
+
]);
|
|
929
|
+
|
|
930
|
+
return {
|
|
931
|
+
content: [
|
|
932
|
+
{
|
|
933
|
+
type: 'text',
|
|
934
|
+
text: `Successfully created test:\n\n${formattedTest}`,
|
|
935
|
+
},
|
|
936
|
+
],
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
async updateTest(args) {
|
|
941
|
+
const { test_id, labels_ids, ...attributes } = args;
|
|
942
|
+
const requestData = {
|
|
943
|
+
type: 'test',
|
|
944
|
+
attributes: Object.fromEntries(Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v])),
|
|
945
|
+
};
|
|
946
|
+
if (labels_ids) requestData.labels_ids = labels_ids;
|
|
947
|
+
|
|
948
|
+
const data = await this.makePutRequest(`/tests/${args.test_id}`, requestData);
|
|
949
|
+
const formattedTest = this.formatModel(data.data, 'test', [
|
|
950
|
+
'title', 'description', 'code', 'priority',
|
|
951
|
+
'state', 'suite-id', 'tags', 'file'
|
|
952
|
+
]);
|
|
953
|
+
|
|
954
|
+
return {
|
|
955
|
+
content: [
|
|
956
|
+
{
|
|
957
|
+
type: 'text',
|
|
958
|
+
text: `Successfully updated test:\n\n${formattedTest}`,
|
|
959
|
+
},
|
|
960
|
+
],
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
706
964
|
async run() {
|
|
707
965
|
// Test authentication on startup
|
|
708
966
|
try {
|
|
@@ -731,7 +989,7 @@ function parseArgs() {
|
|
|
731
989
|
.parse();
|
|
732
990
|
|
|
733
991
|
const options = program.opts();
|
|
734
|
-
|
|
992
|
+
|
|
735
993
|
const token = options.token || process.env.TESTOMATIO_API_TOKEN;
|
|
736
994
|
const projectId = options.project;
|
|
737
995
|
const baseUrl = options.baseUrl || process.env.TESTOMATIO_BASE_URL || 'https://app.testomat.io';
|
|
@@ -761,8 +1019,4 @@ async function main() {
|
|
|
761
1019
|
}
|
|
762
1020
|
}
|
|
763
1021
|
|
|
764
|
-
|
|
765
|
-
main().catch(console.error);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
export { TestomatioMCPServer };
|
|
1022
|
+
main().catch(console.error);
|
package/package.json
CHANGED