@testomatio/mcp 1.0.2 → 1.0.4
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 +99 -3
- package/index.js +318 -60
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -119,6 +119,11 @@ Then add this to your Cursor MCP settings:
|
|
|
119
119
|
* `search_suites` – Search suites (params: `query`, `labels`, `state`, `priority`, `page`) — api: GET `/suites`
|
|
120
120
|
* `get_root_suites` – List root-level suites (no params) — api: GET `/suites`
|
|
121
121
|
* `get_suite` – Get one suite (params: `suite_id`) — api: GET `/suites/{suite_id}`
|
|
122
|
+
* `create_suite` – Create a new suite (params: `title`, `description`, `parent_id`) — api: POST `/suites`
|
|
123
|
+
* `create_folder` – Create a new folder (params: `title`, `description`, `parent_id`) — api: POST `/suites`
|
|
124
|
+
|
|
125
|
+
#### Labels
|
|
126
|
+
* `create_label` – Create a new label with optional custom field (params: `title`, `color`, `scope`, `visibility`, `field`) — api: POST `/labels`
|
|
122
127
|
|
|
123
128
|
#### Test Runs
|
|
124
129
|
* `get_runs` – List all runs (no params) — api: GET `/runs`
|
|
@@ -141,6 +146,9 @@ Once configured, you can ask your AI assistant questions like:
|
|
|
141
146
|
- "Get all test plans for this project"
|
|
142
147
|
- "Create a new test called 'Login validation' in suite suite-123"
|
|
143
148
|
- "Update test test-456 to change its description and add @regression tag"
|
|
149
|
+
- "Create a new suite called 'Authentication Tests' with description 'All login and signup related tests'"
|
|
150
|
+
- "Create a folder called 'API Tests' to organize API-related test suites"
|
|
151
|
+
- "Create a label called 'Severity' with color '#ffe9ad' and predefined values like 'Blocker', 'Critical', 'Major', 'Minor', 'Normal', 'Trivial'"
|
|
144
152
|
|
|
145
153
|
## Query Patterns
|
|
146
154
|
|
|
@@ -161,6 +169,27 @@ These queries allow creating and updating tests:
|
|
|
161
169
|
- **"Update test test-456 to change its description"** → `update_test` tool with `test_id: "test-456"`, `description: "new description"`
|
|
162
170
|
- **"Create an automated test with @smoke tag"** → `create_test` tool with `state: "automated"`, `tags: ["smoke"]`
|
|
163
171
|
|
|
172
|
+
### Suite and Folder Management Queries
|
|
173
|
+
|
|
174
|
+
These queries help organize your test structure:
|
|
175
|
+
|
|
176
|
+
- **"Create a new suite called 'Authentication Tests'"** → `create_suite` tool with `title: "Authentication Tests"`
|
|
177
|
+
- **"Create a suite for login tests with description"** → `create_suite` tool with `title: "Login Tests"`, `description: "All login related test cases"`
|
|
178
|
+
- **"Create a folder called 'API Tests' under parent suite-123"** → `create_folder` tool with `title: "API Tests"`, `parent_id: "suite-123"`
|
|
179
|
+
- **"Create a test suite for payment features"** → `create_suite` tool with `title: "Payment Features", description: "Tests covering payment processing"`
|
|
180
|
+
- **"Create a folder to organize integration tests"** → `create_folder` tool with `title: "Integration Tests"`
|
|
181
|
+
|
|
182
|
+
**Note**: Suites can only contain other suites, while folders can contain both suites and folders (but no tests).
|
|
183
|
+
|
|
184
|
+
### Label Creation Queries
|
|
185
|
+
|
|
186
|
+
These queries help create custom labels for better test categorization:
|
|
187
|
+
|
|
188
|
+
- **"Create a label called 'Severity' with red color"** → `create_label` tool with `title: "Severity"`, `color: "#ff6b6b"`
|
|
189
|
+
- **"Create a severity label with predefined values"** → `create_label` tool with `title: "Severity"`, `color: "#ffe9ad"`, `field: { "type": "list", "short": true, "value": "Blocker\nCritical\nMajor\nNormal\nMinor\nTrivial" }`
|
|
190
|
+
- **"Create a simple label for test types"** → `create_label` tool with `title: "Test Type"`, `scope: ["tests", "suites"]`
|
|
191
|
+
- **"Create a label visible in test lists"** → `create_label` tool with `title: "Category"`, `visibility: ["list"]`
|
|
192
|
+
|
|
164
193
|
### Specific Item Queries
|
|
165
194
|
|
|
166
195
|
These queries target specific entities by ID:
|
|
@@ -174,9 +203,8 @@ These queries target specific entities by ID:
|
|
|
174
203
|
These queries use advanced filtering capabilities:
|
|
175
204
|
|
|
176
205
|
- **"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
206
|
- **"Search for tests containing 'login'"** → `search_tests` tool with `query: "login"`
|
|
179
|
-
- **"List tests tagged @critical or labelled 'ux' with
|
|
207
|
+
- **"List tests tagged @critical or labelled 'ux' with critical severity"** → `search_tests` tool with `tql: "tag == 'critical' or label == 'ux' and severity == 'critical'"`
|
|
180
208
|
- **"Find tests linked to JIRA-123"** → `search_tests` tool with `tql: jira == 'BDCP-2'`
|
|
181
209
|
|
|
182
210
|
### Advanced Query Syntax
|
|
@@ -187,7 +215,7 @@ The `search_tests` tool supports TQL for complex filtering:
|
|
|
187
215
|
|
|
188
216
|
```
|
|
189
217
|
"tag == 'smoke' and state == 'manual'"
|
|
190
|
-
"
|
|
218
|
+
"severity == 'critical' or label == 'ux'"
|
|
191
219
|
```
|
|
192
220
|
|
|
193
221
|
#### Tag-Based Searches
|
|
@@ -247,6 +275,74 @@ For detailed information about the underlying Testomat.io API, refer to the [Tes
|
|
|
247
275
|
|
|
248
276
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
249
277
|
|
|
278
|
+
### Development Setup
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
# Clone the repository
|
|
282
|
+
git clone https://github.com/testomatio/mcp.git
|
|
283
|
+
cd mcp
|
|
284
|
+
|
|
285
|
+
# Install dependencies
|
|
286
|
+
npm install
|
|
287
|
+
|
|
288
|
+
# Run unit tests
|
|
289
|
+
npm test
|
|
290
|
+
|
|
291
|
+
# Run integration tests (requires environment variables)
|
|
292
|
+
npm run test:integration
|
|
293
|
+
|
|
294
|
+
# Run all tests
|
|
295
|
+
npm run test:all
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Testing
|
|
299
|
+
|
|
300
|
+
The project includes comprehensive test coverage:
|
|
301
|
+
|
|
302
|
+
- **Unit Tests**: Fast tests with mocked dependencies
|
|
303
|
+
- **Integration Tests**: Real API tests against Testomat.io
|
|
304
|
+
|
|
305
|
+
#### Running Tests Locally
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
# Unit tests only
|
|
309
|
+
npm run test:unit
|
|
310
|
+
|
|
311
|
+
# Integration tests (requires .env file)
|
|
312
|
+
npm run test:integration
|
|
313
|
+
|
|
314
|
+
# With coverage
|
|
315
|
+
npm run test:coverage
|
|
316
|
+
npm run test:coverage:integration
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### Environment Setup for Integration Tests
|
|
320
|
+
|
|
321
|
+
Create a `.env` file:
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
TESTOMATIO_API_TOKEN=testomat_your_token_here
|
|
325
|
+
TESTOMATIO_PROJECT_ID=your_project_id
|
|
326
|
+
TESTOMATIO_BASE_URL=https://app.testomat.io # optional
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### CI/CD
|
|
330
|
+
|
|
331
|
+
This project uses GitHub Actions for continuous integration:
|
|
332
|
+
|
|
333
|
+
- ✅ **Unit Tests**: Run on every push/PR across Node.js 18, 20, 22
|
|
334
|
+
- ✅ **Integration Tests**: Run daily and on main branch merges
|
|
335
|
+
- ✅ **Coverage Reports**: Automatic upload to Codecov
|
|
336
|
+
- ✅ **Security**: Secrets management for API credentials
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
### Code Quality
|
|
340
|
+
|
|
341
|
+
- Follow existing code style patterns
|
|
342
|
+
- Add tests for new functionality
|
|
343
|
+
- Update documentation when needed
|
|
344
|
+
- Ensure all tests pass before submitting PRs
|
|
345
|
+
|
|
250
346
|
## License
|
|
251
347
|
|
|
252
348
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
package/index.js
CHANGED
|
@@ -41,17 +41,18 @@ class TestomatioMCPServer {
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
if (!response.ok) {
|
|
44
|
-
|
|
44
|
+
const errorText = await response.text();
|
|
45
|
+
throw new Error(`Authentication failed: HTTP ${response.status}: ${response.statusText}. Response: ${errorText}`);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
const data = await response.json();
|
|
48
|
-
|
|
49
|
+
|
|
49
50
|
if (!data.jwt) {
|
|
50
51
|
throw new Error('Authentication failed: No JWT token received in response');
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
this.jwtToken = data.jwt;
|
|
54
|
-
|
|
55
|
+
|
|
55
56
|
return this.jwtToken;
|
|
56
57
|
}
|
|
57
58
|
|
|
@@ -358,6 +359,11 @@ class TestomatioMCPServer {
|
|
|
358
359
|
enum: ['manual', 'automated'],
|
|
359
360
|
description: 'State of the test',
|
|
360
361
|
},
|
|
362
|
+
priority: {
|
|
363
|
+
type: 'string',
|
|
364
|
+
enum: ['low', 'normal', 'high', 'critical'],
|
|
365
|
+
description: 'Priority level of the test',
|
|
366
|
+
},
|
|
361
367
|
tags: {
|
|
362
368
|
type: 'array',
|
|
363
369
|
items: { type: 'string' },
|
|
@@ -381,6 +387,102 @@ class TestomatioMCPServer {
|
|
|
381
387
|
required: ['test_id'],
|
|
382
388
|
},
|
|
383
389
|
},
|
|
390
|
+
{
|
|
391
|
+
name: 'create_suite',
|
|
392
|
+
description: 'Create a new suite. Suites can only contain other suites (no tests or folders)',
|
|
393
|
+
inputSchema: {
|
|
394
|
+
type: 'object',
|
|
395
|
+
properties: {
|
|
396
|
+
title: {
|
|
397
|
+
type: 'string',
|
|
398
|
+
description: 'Suite title',
|
|
399
|
+
},
|
|
400
|
+
description: {
|
|
401
|
+
type: 'string',
|
|
402
|
+
description: 'Suite description',
|
|
403
|
+
},
|
|
404
|
+
parent_id: {
|
|
405
|
+
type: 'string',
|
|
406
|
+
description: 'Parent suite ID to create this suite under',
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
required: ['title'],
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: 'create_folder',
|
|
414
|
+
description: 'Create a new folder. Folders can contain suites and folders (but no tests)',
|
|
415
|
+
inputSchema: {
|
|
416
|
+
type: 'object',
|
|
417
|
+
properties: {
|
|
418
|
+
title: {
|
|
419
|
+
type: 'string',
|
|
420
|
+
description: 'Folder title',
|
|
421
|
+
},
|
|
422
|
+
description: {
|
|
423
|
+
type: 'string',
|
|
424
|
+
description: 'Folder description',
|
|
425
|
+
},
|
|
426
|
+
parent_id: {
|
|
427
|
+
type: 'string',
|
|
428
|
+
description: 'Parent folder or suite ID to create this folder under',
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
required: ['title'],
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
name: 'create_label',
|
|
436
|
+
description: 'Create a new label with optional custom field configuration. Labels can be used to tag and categorize tests and suites',
|
|
437
|
+
inputSchema: {
|
|
438
|
+
type: 'object',
|
|
439
|
+
properties: {
|
|
440
|
+
title: {
|
|
441
|
+
type: 'string',
|
|
442
|
+
description: 'Label title (e.g., "Severity", "Priority", "Type")',
|
|
443
|
+
},
|
|
444
|
+
color: {
|
|
445
|
+
type: 'string',
|
|
446
|
+
description: 'Label color in hex format (e.g., "#ffe9ad")',
|
|
447
|
+
},
|
|
448
|
+
scope: {
|
|
449
|
+
type: 'array',
|
|
450
|
+
items: {
|
|
451
|
+
type: 'string',
|
|
452
|
+
enum: ['tests', 'suites']
|
|
453
|
+
},
|
|
454
|
+
description: 'Where this label can be used (e.g., ["tests", "suites"])',
|
|
455
|
+
},
|
|
456
|
+
visibility: {
|
|
457
|
+
type: 'array',
|
|
458
|
+
items: {
|
|
459
|
+
type: 'string'
|
|
460
|
+
},
|
|
461
|
+
description: 'Where the label is visible (e.g., ["list"])',
|
|
462
|
+
},
|
|
463
|
+
field: {
|
|
464
|
+
type: 'object',
|
|
465
|
+
description: 'Custom field configuration for labels with predefined values',
|
|
466
|
+
properties: {
|
|
467
|
+
type: {
|
|
468
|
+
type: 'string',
|
|
469
|
+
description: 'Field type (e.g., "list", "string", "number")',
|
|
470
|
+
},
|
|
471
|
+
short: {
|
|
472
|
+
type: 'boolean',
|
|
473
|
+
description: 'Whether to display short version',
|
|
474
|
+
},
|
|
475
|
+
value: {
|
|
476
|
+
type: 'string',
|
|
477
|
+
description: 'Predefined values for the field (newline separated for list type)',
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
required: ['type'],
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
required: ['title'],
|
|
484
|
+
},
|
|
485
|
+
},
|
|
384
486
|
],
|
|
385
487
|
};
|
|
386
488
|
});
|
|
@@ -414,6 +516,12 @@ class TestomatioMCPServer {
|
|
|
414
516
|
return await this.createTest(args);
|
|
415
517
|
case 'update_test':
|
|
416
518
|
return await this.updateTest(args);
|
|
519
|
+
case 'create_suite':
|
|
520
|
+
return await this.createSuite(args);
|
|
521
|
+
case 'create_folder':
|
|
522
|
+
return await this.createFolder(args);
|
|
523
|
+
case 'create_label':
|
|
524
|
+
return await this.createLabel(args);
|
|
417
525
|
default:
|
|
418
526
|
throw new Error(`Unknown tool: ${name}`);
|
|
419
527
|
}
|
|
@@ -433,9 +541,9 @@ class TestomatioMCPServer {
|
|
|
433
541
|
async makeRequest(path, params = {}) {
|
|
434
542
|
// Ensure we have a valid JWT token
|
|
435
543
|
const jwt = await this.authenticate();
|
|
436
|
-
|
|
544
|
+
|
|
437
545
|
const url = new URL(`${this.config.baseUrl}/api/${this.config.projectId}${path}`);
|
|
438
|
-
|
|
546
|
+
|
|
439
547
|
// Add query parameters with proper array handling
|
|
440
548
|
Object.entries(params).forEach(([key, value]) => {
|
|
441
549
|
if (value !== undefined && value !== null) {
|
|
@@ -486,7 +594,7 @@ class TestomatioMCPServer {
|
|
|
486
594
|
this.jwtToken = null;
|
|
487
595
|
return this.makePostRequest(path, data);
|
|
488
596
|
}
|
|
489
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
597
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}; ${await response.text()}`);
|
|
490
598
|
}
|
|
491
599
|
|
|
492
600
|
return await response.json();
|
|
@@ -580,10 +688,10 @@ class TestomatioMCPServer {
|
|
|
580
688
|
formatModel(model, tagName, fields) {
|
|
581
689
|
const attributes = model.attributes || {};
|
|
582
690
|
const lines = [`<${tagName}>`];
|
|
583
|
-
|
|
691
|
+
|
|
584
692
|
// Always include ID from root level
|
|
585
693
|
lines.push(` <id>${model.id || ''}</id>`);
|
|
586
|
-
|
|
694
|
+
|
|
587
695
|
// Process specified fields
|
|
588
696
|
fields.forEach(field => {
|
|
589
697
|
let value;
|
|
@@ -599,7 +707,7 @@ class TestomatioMCPServer {
|
|
|
599
707
|
}
|
|
600
708
|
|
|
601
709
|
const formattedValue = this.formatValue(value, field);
|
|
602
|
-
|
|
710
|
+
|
|
603
711
|
if (field === 'test' && typeof value === 'object') {
|
|
604
712
|
// Special case for nested test objects
|
|
605
713
|
lines.push(` <test>${formattedValue}\n </test>`);
|
|
@@ -607,7 +715,7 @@ class TestomatioMCPServer {
|
|
|
607
715
|
lines.push(` <${xmlFieldName}>${formattedValue}</${xmlFieldName}>`);
|
|
608
716
|
}
|
|
609
717
|
});
|
|
610
|
-
|
|
718
|
+
|
|
611
719
|
lines.push(`</${tagName}>`);
|
|
612
720
|
return lines.join('\n');
|
|
613
721
|
}
|
|
@@ -615,13 +723,13 @@ class TestomatioMCPServer {
|
|
|
615
723
|
async getTests(filters = {}) {
|
|
616
724
|
const params = this.buildSearchParams(filters);
|
|
617
725
|
const data = await this.makeRequest('/tests', params);
|
|
618
|
-
const formattedTests = data.data.map(test =>
|
|
726
|
+
const formattedTests = data.data.map(test =>
|
|
619
727
|
this.formatModel(test, 'test', [
|
|
620
|
-
'title', 'description', 'code', 'priority',
|
|
728
|
+
'title', 'description', 'code', 'priority',
|
|
621
729
|
'state', 'suite-id', 'tags', 'file'
|
|
622
730
|
])
|
|
623
731
|
).join('\n\n');
|
|
624
|
-
|
|
732
|
+
|
|
625
733
|
return {
|
|
626
734
|
content: [
|
|
627
735
|
{
|
|
@@ -634,7 +742,7 @@ class TestomatioMCPServer {
|
|
|
634
742
|
|
|
635
743
|
buildSearchParams(filters = {}) {
|
|
636
744
|
const params = {};
|
|
637
|
-
|
|
745
|
+
|
|
638
746
|
// Handle basic filters
|
|
639
747
|
Object.entries(filters).forEach(([key, value]) => {
|
|
640
748
|
if (value !== undefined && value !== null) {
|
|
@@ -680,16 +788,16 @@ class TestomatioMCPServer {
|
|
|
680
788
|
async searchTests(filters = {}) {
|
|
681
789
|
const params = this.buildSearchParams(filters);
|
|
682
790
|
const data = await this.makeRequest('/tests', params);
|
|
683
|
-
|
|
684
|
-
const formattedTests = data.data.map(test =>
|
|
791
|
+
|
|
792
|
+
const formattedTests = data.data.map(test =>
|
|
685
793
|
this.formatModel(test, 'test', [
|
|
686
|
-
'title', 'description', 'code', 'priority',
|
|
794
|
+
'title', 'description', 'code', 'priority',
|
|
687
795
|
'state', 'suite-id', 'tags', 'file'
|
|
688
796
|
])
|
|
689
797
|
).join('\n\n');
|
|
690
|
-
|
|
798
|
+
|
|
691
799
|
const searchDescription = this.buildSearchDescription(filters);
|
|
692
|
-
|
|
800
|
+
|
|
693
801
|
return {
|
|
694
802
|
content: [
|
|
695
803
|
{
|
|
@@ -704,15 +812,15 @@ class TestomatioMCPServer {
|
|
|
704
812
|
// Add filter=true for suites search to include tests
|
|
705
813
|
const params = this.buildSearchParams({ ...filters, filter: true });
|
|
706
814
|
const data = await this.makeRequest('/suites', params);
|
|
707
|
-
|
|
708
|
-
const formattedSuites = data.data.map(suite =>
|
|
815
|
+
|
|
816
|
+
const formattedSuites = data.data.map(suite =>
|
|
709
817
|
this.formatModel(suite, 'suite', [
|
|
710
818
|
'title', 'description', 'test-count', 'is-root', 'file-type'
|
|
711
819
|
])
|
|
712
820
|
).join('\n\n');
|
|
713
|
-
|
|
821
|
+
|
|
714
822
|
const searchDescription = this.buildSearchDescription(filters);
|
|
715
|
-
|
|
823
|
+
|
|
716
824
|
return {
|
|
717
825
|
content: [
|
|
718
826
|
{
|
|
@@ -725,7 +833,7 @@ class TestomatioMCPServer {
|
|
|
725
833
|
|
|
726
834
|
buildSearchDescription(filters) {
|
|
727
835
|
const descriptions = [];
|
|
728
|
-
|
|
836
|
+
|
|
729
837
|
if (filters.query) {
|
|
730
838
|
if (filters.query.startsWith('@')) {
|
|
731
839
|
descriptions.push(`tagged with "${filters.query}"`);
|
|
@@ -735,41 +843,41 @@ class TestomatioMCPServer {
|
|
|
735
843
|
descriptions.push(`containing "${filters.query}"`);
|
|
736
844
|
}
|
|
737
845
|
}
|
|
738
|
-
|
|
846
|
+
|
|
739
847
|
if (filters.tql) {
|
|
740
848
|
descriptions.push(`matching TQL: "${filters.tql}"`);
|
|
741
849
|
}
|
|
742
|
-
|
|
850
|
+
|
|
743
851
|
if (filters.labels && filters.labels.length > 0) {
|
|
744
852
|
descriptions.push(`with labels: ${filters.labels.join(', ')}`);
|
|
745
853
|
}
|
|
746
|
-
|
|
854
|
+
|
|
747
855
|
if (filters.state) {
|
|
748
856
|
descriptions.push(`state: ${filters.state}`);
|
|
749
857
|
}
|
|
750
|
-
|
|
858
|
+
|
|
751
859
|
if (filters.priority) {
|
|
752
860
|
descriptions.push(`priority: ${filters.priority}`);
|
|
753
861
|
}
|
|
754
|
-
|
|
862
|
+
|
|
755
863
|
if (filters.filter && typeof filters.filter === 'object') {
|
|
756
864
|
const filterDesc = Object.entries(filters.filter)
|
|
757
865
|
.map(([key, value]) => `${key}: ${value}`)
|
|
758
866
|
.join(', ');
|
|
759
867
|
descriptions.push(`filtered by: ${filterDesc}`);
|
|
760
868
|
}
|
|
761
|
-
|
|
869
|
+
|
|
762
870
|
return descriptions.length > 0 ? ` (${descriptions.join(', ')})` : '';
|
|
763
871
|
}
|
|
764
872
|
|
|
765
873
|
async getRootSuites() {
|
|
766
874
|
const data = await this.makeRequest('/suites');
|
|
767
|
-
const formattedSuites = data.data.map(suite =>
|
|
875
|
+
const formattedSuites = data.data.map(suite =>
|
|
768
876
|
this.formatModel(suite, 'suite', [
|
|
769
877
|
'title', 'description', 'test-count', 'is-root', 'file-type'
|
|
770
878
|
])
|
|
771
879
|
).join('\n\n');
|
|
772
|
-
|
|
880
|
+
|
|
773
881
|
return {
|
|
774
882
|
content: [
|
|
775
883
|
{
|
|
@@ -785,7 +893,7 @@ class TestomatioMCPServer {
|
|
|
785
893
|
const formattedSuite = this.formatModel(data.data, 'suite', [
|
|
786
894
|
'title', 'description', 'test-count', 'is-root', 'file-type'
|
|
787
895
|
]);
|
|
788
|
-
|
|
896
|
+
|
|
789
897
|
// Format child suites and tests if they exist
|
|
790
898
|
let childContent = '';
|
|
791
899
|
if (data.data.relationships?.children?.data) {
|
|
@@ -797,18 +905,18 @@ class TestomatioMCPServer {
|
|
|
797
905
|
childContent += `\n\nChild Suites:\n${childSuites}`;
|
|
798
906
|
}
|
|
799
907
|
}
|
|
800
|
-
|
|
908
|
+
|
|
801
909
|
if (data.data.relationships?.tests?.data) {
|
|
802
910
|
const tests = data.data.relationships.tests.data
|
|
803
911
|
.map(test => this.formatModel(test, 'test', [
|
|
804
|
-
'title', 'description', 'code', 'priority',
|
|
912
|
+
'title', 'description', 'code', 'priority',
|
|
805
913
|
'state', 'suite-id', 'tags', 'file'
|
|
806
914
|
])).join('\n\n');
|
|
807
915
|
if (tests) {
|
|
808
916
|
childContent += `\n\nTests:\n${tests}`;
|
|
809
917
|
}
|
|
810
918
|
}
|
|
811
|
-
|
|
919
|
+
|
|
812
920
|
return {
|
|
813
921
|
content: [
|
|
814
922
|
{
|
|
@@ -821,13 +929,13 @@ class TestomatioMCPServer {
|
|
|
821
929
|
|
|
822
930
|
async getRuns() {
|
|
823
931
|
const data = await this.makeRequest('/runs');
|
|
824
|
-
const formattedRuns = data.data.map(run =>
|
|
932
|
+
const formattedRuns = data.data.map(run =>
|
|
825
933
|
this.formatModel(run, 'run', [
|
|
826
934
|
'status', 'title', 'tests-count', 'automated', 'duration',
|
|
827
935
|
'passed', 'failed', 'skipped', 'created-at', 'finished-at'
|
|
828
936
|
])
|
|
829
937
|
).join('\n\n');
|
|
830
|
-
|
|
938
|
+
|
|
831
939
|
return {
|
|
832
940
|
content: [
|
|
833
941
|
{
|
|
@@ -845,7 +953,7 @@ class TestomatioMCPServer {
|
|
|
845
953
|
'status', 'title', 'tests-count', 'automated', 'duration',
|
|
846
954
|
'passed', 'failed', 'skipped', 'created-at', 'finished-at'
|
|
847
955
|
]);
|
|
848
|
-
|
|
956
|
+
|
|
849
957
|
return {
|
|
850
958
|
content: [
|
|
851
959
|
{
|
|
@@ -861,14 +969,14 @@ class TestomatioMCPServer {
|
|
|
861
969
|
if (dateRange) {
|
|
862
970
|
params.finished_at_date_range = dateRange;
|
|
863
971
|
}
|
|
864
|
-
|
|
972
|
+
|
|
865
973
|
const data = await this.makeRequest('/testruns', params);
|
|
866
|
-
const formattedTestruns = data.data.map(testrun =>
|
|
974
|
+
const formattedTestruns = data.data.map(testrun =>
|
|
867
975
|
this.formatModel(testrun, 'testrun', [
|
|
868
976
|
'status', 'run-time', 'message', 'run-id', 'test'
|
|
869
977
|
])
|
|
870
978
|
).join('\n\n');
|
|
871
|
-
|
|
979
|
+
|
|
872
980
|
return {
|
|
873
981
|
content: [
|
|
874
982
|
{
|
|
@@ -881,12 +989,12 @@ class TestomatioMCPServer {
|
|
|
881
989
|
|
|
882
990
|
async getPlans(filters = {}) {
|
|
883
991
|
const data = await this.makeRequest('/plans', filters);
|
|
884
|
-
const formattedPlans = data.data.map(plan =>
|
|
992
|
+
const formattedPlans = data.data.map(plan =>
|
|
885
993
|
this.formatModel(plan, 'plan', [
|
|
886
994
|
'title', 'test-count', 'kind', 'created-at', 'tests-ids', 'labels'
|
|
887
995
|
])
|
|
888
996
|
).join('\n\n');
|
|
889
|
-
|
|
997
|
+
|
|
890
998
|
return {
|
|
891
999
|
content: [
|
|
892
1000
|
{
|
|
@@ -902,7 +1010,7 @@ class TestomatioMCPServer {
|
|
|
902
1010
|
const formattedPlan = this.formatModel(data.data, 'plan', [
|
|
903
1011
|
'title', 'test-count', 'kind', 'created-at', 'tests-ids', 'labels'
|
|
904
1012
|
]);
|
|
905
|
-
|
|
1013
|
+
|
|
906
1014
|
return {
|
|
907
1015
|
content: [
|
|
908
1016
|
{
|
|
@@ -914,19 +1022,33 @@ class TestomatioMCPServer {
|
|
|
914
1022
|
}
|
|
915
1023
|
|
|
916
1024
|
async createTest(args) {
|
|
917
|
-
const {
|
|
1025
|
+
const { suite_id, labels_ids, ...attributes } = args;
|
|
918
1026
|
const requestData = {
|
|
919
|
-
|
|
920
|
-
|
|
1027
|
+
data: {
|
|
1028
|
+
type: 'tests',
|
|
1029
|
+
attributes: Object.fromEntries(Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v]))
|
|
1030
|
+
}
|
|
921
1031
|
};
|
|
1032
|
+
|
|
1033
|
+
if (suite_id) {
|
|
1034
|
+
requestData.data.relationships = {
|
|
1035
|
+
suite: {
|
|
1036
|
+
data: {
|
|
1037
|
+
type: 'suites',
|
|
1038
|
+
id: suite_id
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
922
1044
|
if (labels_ids) requestData.labels_ids = labels_ids;
|
|
923
1045
|
|
|
924
1046
|
const data = await this.makePostRequest('/tests', requestData);
|
|
925
1047
|
const formattedTest = this.formatModel(data.data, 'test', [
|
|
926
|
-
'title', 'description', 'code', 'priority',
|
|
1048
|
+
'title', 'description', 'code', 'priority',
|
|
927
1049
|
'state', 'suite-id', 'tags', 'file'
|
|
928
1050
|
]);
|
|
929
|
-
|
|
1051
|
+
|
|
930
1052
|
return {
|
|
931
1053
|
content: [
|
|
932
1054
|
{
|
|
@@ -938,19 +1060,33 @@ class TestomatioMCPServer {
|
|
|
938
1060
|
}
|
|
939
1061
|
|
|
940
1062
|
async updateTest(args) {
|
|
941
|
-
const { test_id, labels_ids, ...attributes } = args;
|
|
1063
|
+
const { test_id, suite_id, labels_ids, ...attributes } = args;
|
|
942
1064
|
const requestData = {
|
|
943
|
-
|
|
944
|
-
|
|
1065
|
+
data: {
|
|
1066
|
+
type: 'tests',
|
|
1067
|
+
attributes: Object.fromEntries(Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v]))
|
|
1068
|
+
}
|
|
945
1069
|
};
|
|
1070
|
+
|
|
1071
|
+
if (suite_id) {
|
|
1072
|
+
requestData.data.relationships = {
|
|
1073
|
+
suite: {
|
|
1074
|
+
data: {
|
|
1075
|
+
type: 'suites',
|
|
1076
|
+
id: suite_id
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
|
|
946
1082
|
if (labels_ids) requestData.labels_ids = labels_ids;
|
|
947
1083
|
|
|
948
|
-
const data = await this.makePutRequest(`/tests/${
|
|
1084
|
+
const data = await this.makePutRequest(`/tests/${test_id}`, requestData);
|
|
949
1085
|
const formattedTest = this.formatModel(data.data, 'test', [
|
|
950
|
-
'title', 'description', 'code', 'priority',
|
|
1086
|
+
'title', 'description', 'code', 'priority',
|
|
951
1087
|
'state', 'suite-id', 'tags', 'file'
|
|
952
1088
|
]);
|
|
953
|
-
|
|
1089
|
+
|
|
954
1090
|
return {
|
|
955
1091
|
content: [
|
|
956
1092
|
{
|
|
@@ -961,6 +1097,122 @@ class TestomatioMCPServer {
|
|
|
961
1097
|
};
|
|
962
1098
|
}
|
|
963
1099
|
|
|
1100
|
+
async createSuite(args) {
|
|
1101
|
+
const { parent_id, ...attributes } = args;
|
|
1102
|
+
const requestData = {
|
|
1103
|
+
data: {
|
|
1104
|
+
type: 'suites',
|
|
1105
|
+
attributes: {
|
|
1106
|
+
...Object.fromEntries(Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v])),
|
|
1107
|
+
'file-type': 'file'
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
if (parent_id) {
|
|
1113
|
+
requestData.data.relationships = {
|
|
1114
|
+
parent: {
|
|
1115
|
+
data: {
|
|
1116
|
+
type: 'suites',
|
|
1117
|
+
id: parent_id
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const data = await this.makePostRequest('/suites', requestData);
|
|
1124
|
+
const formattedSuite = this.formatModel(data.data, 'suite', [
|
|
1125
|
+
'title', 'description', 'test-count', 'is-root', 'file-type'
|
|
1126
|
+
]);
|
|
1127
|
+
|
|
1128
|
+
return {
|
|
1129
|
+
content: [
|
|
1130
|
+
{
|
|
1131
|
+
type: 'text',
|
|
1132
|
+
text: `Successfully created suite:\n\n${formattedSuite}`,
|
|
1133
|
+
},
|
|
1134
|
+
],
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
async createFolder(args) {
|
|
1139
|
+
const { parent_id, ...attributes } = args;
|
|
1140
|
+
const requestData = {
|
|
1141
|
+
data: {
|
|
1142
|
+
type: 'suites',
|
|
1143
|
+
attributes: {
|
|
1144
|
+
...Object.fromEntries(Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v])),
|
|
1145
|
+
'file-type': 'folder'
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
if (parent_id) {
|
|
1151
|
+
requestData.data.relationships = {
|
|
1152
|
+
parent: {
|
|
1153
|
+
data: {
|
|
1154
|
+
type: 'suites',
|
|
1155
|
+
id: parent_id
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const data = await this.makePostRequest('/suites', requestData);
|
|
1162
|
+
const formattedSuite = this.formatModel(data.data, 'suite', [
|
|
1163
|
+
'title', 'description', 'test-count', 'is-root', 'file-type'
|
|
1164
|
+
]);
|
|
1165
|
+
|
|
1166
|
+
return {
|
|
1167
|
+
content: [
|
|
1168
|
+
{
|
|
1169
|
+
type: 'text',
|
|
1170
|
+
text: `Successfully created folder:\n\n${formattedSuite}`,
|
|
1171
|
+
},
|
|
1172
|
+
],
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
async createLabel(args) {
|
|
1177
|
+
const { title, color, scope, visibility, field, ...otherAttributes } = args;
|
|
1178
|
+
|
|
1179
|
+
const requestData = {
|
|
1180
|
+
data: {
|
|
1181
|
+
type: 'labels',
|
|
1182
|
+
attributes: {
|
|
1183
|
+
...Object.fromEntries(Object.entries(otherAttributes).map(([k, v]) => [k.replace(/_/g, '-'), v])),
|
|
1184
|
+
title,
|
|
1185
|
+
color,
|
|
1186
|
+
scope,
|
|
1187
|
+
visibility
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
// Add field configuration if provided
|
|
1193
|
+
if (field) {
|
|
1194
|
+
requestData.data.attributes.field = {
|
|
1195
|
+
type: field.type,
|
|
1196
|
+
...(field.short !== undefined && { short: field.short }),
|
|
1197
|
+
...(field.value && { value: field.value })
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const data = await this.makePostRequest('/labels', requestData);
|
|
1202
|
+
const formattedLabel = this.formatModel(data.data, 'label', [
|
|
1203
|
+
'title', 'color', 'scope', 'visibility', 'field'
|
|
1204
|
+
]);
|
|
1205
|
+
|
|
1206
|
+
return {
|
|
1207
|
+
content: [
|
|
1208
|
+
{
|
|
1209
|
+
type: 'text',
|
|
1210
|
+
text: `Successfully created label:\n\n${formattedLabel}`,
|
|
1211
|
+
},
|
|
1212
|
+
],
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
|
|
964
1216
|
async run() {
|
|
965
1217
|
// Test authentication on startup
|
|
966
1218
|
try {
|
|
@@ -977,6 +1229,9 @@ class TestomatioMCPServer {
|
|
|
977
1229
|
}
|
|
978
1230
|
}
|
|
979
1231
|
|
|
1232
|
+
// Export the class for testing
|
|
1233
|
+
export { TestomatioMCPServer };
|
|
1234
|
+
|
|
980
1235
|
// Parse command line arguments using commander
|
|
981
1236
|
function parseArgs() {
|
|
982
1237
|
program
|
|
@@ -989,9 +1244,9 @@ function parseArgs() {
|
|
|
989
1244
|
.parse();
|
|
990
1245
|
|
|
991
1246
|
const options = program.opts();
|
|
992
|
-
|
|
1247
|
+
|
|
993
1248
|
const token = options.token || process.env.TESTOMATIO_API_TOKEN;
|
|
994
|
-
const projectId = options.project;
|
|
1249
|
+
const projectId = options.project || process.env.TESTOMATIO_PROJECT_ID;
|
|
995
1250
|
const baseUrl = options.baseUrl || process.env.TESTOMATIO_BASE_URL || 'https://app.testomat.io';
|
|
996
1251
|
|
|
997
1252
|
if (!token) {
|
|
@@ -1000,7 +1255,7 @@ function parseArgs() {
|
|
|
1000
1255
|
}
|
|
1001
1256
|
|
|
1002
1257
|
if (!projectId) {
|
|
1003
|
-
console.error('Error: Project ID is required. Use --project <project_id>');
|
|
1258
|
+
console.error('Error: Project ID is required. Use --project <project_id> or set TESTOMATIO_PROJECT_ID environment variable');
|
|
1004
1259
|
process.exit(1);
|
|
1005
1260
|
}
|
|
1006
1261
|
|
|
@@ -1019,4 +1274,7 @@ async function main() {
|
|
|
1019
1274
|
}
|
|
1020
1275
|
}
|
|
1021
1276
|
|
|
1022
|
-
main()
|
|
1277
|
+
// Only run main() if this file is executed directly (not imported)
|
|
1278
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
1279
|
+
main().catch(console.error);
|
|
1280
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Model Context Protocol server for Testomatio API",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,14 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node index.js",
|
|
12
12
|
"dev": "node index.js",
|
|
13
|
-
"test": "
|
|
13
|
+
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
|
14
|
+
"test:unit": "node --experimental-vm-modules node_modules/.bin/jest",
|
|
15
|
+
"test:integration": "node --experimental-vm-modules node_modules/.bin/jest --config jest.integration.config.js",
|
|
16
|
+
"test:watch": "node --experimental-vm-modules node_modules/.bin/jest --watch",
|
|
17
|
+
"test:integration:watch": "node --experimental-vm-modules node_modules/.bin/jest --config jest.integration.config.js --watch",
|
|
18
|
+
"test:coverage": "node --experimental-vm-modules node_modules/.bin/jest --coverage",
|
|
19
|
+
"test:coverage:integration": "node --experimental-vm-modules node_modules/.bin/jest --config jest.integration.config.js --coverage",
|
|
20
|
+
"test:all": "node --experimental-vm-modules node_modules/.bin/jest --config jest.integration.config.js"
|
|
14
21
|
},
|
|
15
22
|
"keywords": [
|
|
16
23
|
"testomatio",
|
|
@@ -24,7 +31,8 @@
|
|
|
24
31
|
"license": "MIT",
|
|
25
32
|
"dependencies": {
|
|
26
33
|
"@modelcontextprotocol/sdk": "^0.4.0",
|
|
27
|
-
"commander": "^12.0.0"
|
|
34
|
+
"commander": "^12.0.0",
|
|
35
|
+
"dotenv": "^17.2.3"
|
|
28
36
|
},
|
|
29
37
|
"files": [
|
|
30
38
|
"index.js",
|
|
@@ -32,5 +40,8 @@
|
|
|
32
40
|
],
|
|
33
41
|
"engines": {
|
|
34
42
|
"node": ">=18.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"jest": "^30.2.0"
|
|
35
46
|
}
|
|
36
47
|
}
|