@testomatio/mcp 1.0.8 → 1.0.11
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 +30 -12
- package/index.js +295 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A Model Context Protocol (MCP) server for Testomat.io API integration with AI assistants like Cursor.
|
|
4
4
|
|
|
5
|
+
<img width="1920" height="1085" alt="image" src="https://github.com/user-attachments/assets/cf823e8b-1305-4ed2-a7c5-712efec12ceb" />
|
|
6
|
+
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
7
9
|
### Prerequisites
|
|
@@ -62,6 +64,17 @@ Your project ID can be found in the URL when you're viewing your project:
|
|
|
62
64
|
https://app.testomat.io/projects/YOUR_PROJECT_ID
|
|
63
65
|
```
|
|
64
66
|
|
|
67
|
+
### ID Formats
|
|
68
|
+
|
|
69
|
+
Testomatio uses specific ID formats for different resources:
|
|
70
|
+
|
|
71
|
+
- **Test IDs**: Start with `T` prefix followed by 7 alphanumeric characters (e.g., `Ta1b2c3d4`)
|
|
72
|
+
- **Suite IDs**: Start with `S` prefix followed by 7 alphanumeric characters (e.g., `Sx9y8z7w6`)
|
|
73
|
+
- **Total length**: 8 characters including the prefix
|
|
74
|
+
- **Format**: `[Prefix][7 alphanumeric characters]`
|
|
75
|
+
|
|
76
|
+
When working with tools that require specific IDs, ensure you use the correct format with the appropriate prefix.
|
|
77
|
+
|
|
65
78
|
## Integration with Cursor
|
|
66
79
|
|
|
67
80
|
To use this MCP server with Cursor, add the following configuration to your Cursor settings:
|
|
@@ -111,6 +124,7 @@ Then add this to your Cursor MCP settings:
|
|
|
111
124
|
|
|
112
125
|
#### Tests
|
|
113
126
|
* `get_tests` – Get all tests (params: `plan`, `query`, `state`, `suite_id`, `tag`, `labels`) — api: GET `/tests`
|
|
127
|
+
* `get_test` – Get a specific test by ID with all information including labels, tags, and metadata (params: `test_id`) — api: GET `/tests/{test_id}`
|
|
114
128
|
* `search_tests` – Search tests (params: `query`, `tql`, `labels`, `state`, `priority`, `filter`, `page`) — api: GET `/tests`
|
|
115
129
|
* `create_test` – Create a new test (params: `suite_id`, `title`, `description`, `code`, `file`, `state`, `tags`, `jira_issues`, `assigned_to`, `labels_ids`, `fields`) — api: POST `/tests`
|
|
116
130
|
* `update_test` – Update an existing test (params: `test_id`, `suite_id`, `title`, `description`, `code`, `file`, `state`, `tags`, `jira_issues`, `assigned_to`, `labels_ids`, `fields`) — api: PUT `/tests/{test_id}`
|
|
@@ -123,7 +137,9 @@ Then add this to your Cursor MCP settings:
|
|
|
123
137
|
* `create_folder` – Create a new folder (params: `title`, `description`, `parent_id`, `fields`) — api: POST `/suites`
|
|
124
138
|
|
|
125
139
|
#### Labels
|
|
126
|
-
* `
|
|
140
|
+
* `get_labels` – Get all available labels with their IDs and configurations
|
|
141
|
+
* `create_label` – Create a new label with optional custom field
|
|
142
|
+
* `unlink_label` – Remove a label from a test or suite
|
|
127
143
|
|
|
128
144
|
### Custom Fields and Labels
|
|
129
145
|
|
|
@@ -200,15 +216,16 @@ The MCP server provides two distinct ways to assign values to tests, suites, and
|
|
|
200
216
|
Once configured, you can ask your AI assistant questions like:
|
|
201
217
|
|
|
202
218
|
- "Show me all the tests in the project"
|
|
203
|
-
- "Get
|
|
219
|
+
- "Get details for test ID Ta1b2c3d4"
|
|
220
|
+
- "Get the test runs for test ID Ta1b2c3d4"
|
|
204
221
|
- "What are the root suites in this project?"
|
|
205
222
|
- "Show me details for test run xyz789"
|
|
206
223
|
- "List all automated tests with the @smoke tag"
|
|
207
224
|
- "Get all test plans for this project"
|
|
208
|
-
- "Create a new test called 'Login validation' in suite
|
|
209
|
-
- "Update test
|
|
225
|
+
- "Create a new test called 'Login validation' in suite Sx9y8z7w6"
|
|
226
|
+
- "Update test Ta1b2c3d4 to change its description and add @regression tag"
|
|
210
227
|
- "Create a test with custom fields: priority='high', severity='critical', team='backend'"
|
|
211
|
-
- "Update test
|
|
228
|
+
- "Update test Tb2c3d4e5 to set custom fields for risk score and assigned team"
|
|
212
229
|
- "Create a new suite called 'Authentication Tests' with description 'All login and signup related tests'"
|
|
213
230
|
- "Create a suite with custom fields for team ownership and priority level"
|
|
214
231
|
- "Create a folder called 'API Tests' to organize API-related test suites with custom fields"
|
|
@@ -229,11 +246,11 @@ These queries retrieve general information without specific filtering:
|
|
|
229
246
|
|
|
230
247
|
These queries allow creating and updating tests:
|
|
231
248
|
|
|
232
|
-
- **"Create a new test called 'Login validation' in suite
|
|
233
|
-
- **"Update test
|
|
249
|
+
- **"Create a new test called 'Login validation' in suite Sx9y8z7w6"** → `create_test` tool with `title: "Login validation"`, `suite_id: "Sx9y8z7w6"`
|
|
250
|
+
- **"Update test Ta1b2c3d4 to change its description"** → `update_test` tool with `test_id: "Ta1b2c3d4"`, `description: "new description"`
|
|
234
251
|
- **"Create an automated test with @smoke tag"** → `create_test` tool with `state: "automated"`, `tags: ["smoke"]`
|
|
235
|
-
- **"Create a test with custom fields: priority='high', severity='critical'"** → `create_test` tool with `title: "Test Title"`, `suite_id: "
|
|
236
|
-
- **"Update test
|
|
252
|
+
- **"Create a test with custom fields: priority='high', severity='critical'"** → `create_test` tool with `title: "Test Title"`, `suite_id: "Sx9y8z7w6"`, `fields: { "priority": "high", "severity": "critical" }`
|
|
253
|
+
- **"Update test Tb2c3d4e5 to set custom fields for risk score and team"** → `update_test` tool with `test_id: "Tb2c3d4e5"`, `fields: { "risk_score": "8.5", "team": "backend" }`
|
|
237
254
|
|
|
238
255
|
### Suite and Folder Management Queries
|
|
239
256
|
|
|
@@ -242,7 +259,7 @@ These queries help organize your test structure:
|
|
|
242
259
|
- **"Create a new suite called 'Authentication Tests'"** → `create_suite` tool with `title: "Authentication Tests"`
|
|
243
260
|
- **"Create a suite for login tests with description"** → `create_suite` tool with `title: "Login Tests"`, `description: "All login related test cases"`
|
|
244
261
|
- **"Create a suite with custom fields for team ownership and priority level"** → `create_suite` tool with `title: "Backend Tests"`, `fields: { "team": "backend", "priority": "high" }`
|
|
245
|
-
- **"Create a folder called 'API Tests' under parent suite
|
|
262
|
+
- **"Create a folder called 'API Tests' under parent suite Sx9y8z7w6"** → `create_folder` tool with `title: "API Tests"`, `parent_id: "Sx9y8z7w6"`
|
|
246
263
|
- **"Create a folder with custom fields for team and project"** → `create_folder` tool with `title: "Integration Tests"`, `fields: { "team": "qa", "project": "mobile-app" }`
|
|
247
264
|
- **"Create a test suite for payment features"** → `create_suite` tool with `title: "Payment Features", description: "Tests covering payment processing"`
|
|
248
265
|
- **"Create a folder to organize integration tests"** → `create_folder` tool with `title: "Integration Tests"`
|
|
@@ -262,9 +279,10 @@ These queries help create custom labels for better test categorization:
|
|
|
262
279
|
|
|
263
280
|
These queries target specific entities by ID:
|
|
264
281
|
|
|
265
|
-
- **"Get
|
|
282
|
+
- **"Get details for test ID Ta1b2c3d4"** → `get_test` tool with `test_id: "Ta1b2c3d4"`
|
|
283
|
+
- **"Get test runs for test ID Ta1b2c3d4"** → `get_testruns` tool with `test_id: "Ta1b2c3d4"`
|
|
266
284
|
- **"Show me details for test run xyz789"** → `get_run` tool with `run_id: "xyz789"`
|
|
267
|
-
- **"Get suite details for suite
|
|
285
|
+
- **"Get suite details for suite Sx9y8z7w6"** → `get_suite` tool with `suite_id: "Sx9y8z7w6"`
|
|
268
286
|
|
|
269
287
|
### Search and Filter Queries
|
|
270
288
|
|
package/index.js
CHANGED
|
@@ -116,6 +116,20 @@ class TestomatioMCPServer {
|
|
|
116
116
|
},
|
|
117
117
|
},
|
|
118
118
|
},
|
|
119
|
+
{
|
|
120
|
+
name: 'get_test',
|
|
121
|
+
description: 'Get a specific test by its ID with all information including labels, tags, and metadata',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
test_id: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
description: 'The ID of the test to retrieve',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
required: ['test_id'],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
119
133
|
{
|
|
120
134
|
name: 'search_tests',
|
|
121
135
|
description: 'Search tests by keywords, tags, labels, TQL queries, and other filters',
|
|
@@ -303,7 +317,7 @@ class TestomatioMCPServer {
|
|
|
303
317
|
},
|
|
304
318
|
title: {
|
|
305
319
|
type: 'string',
|
|
306
|
-
description: 'Test title',
|
|
320
|
+
description: 'Test title. @tags in the title (e.g., "@smoke test") will be automatically extracted as tags',
|
|
307
321
|
},
|
|
308
322
|
description: {
|
|
309
323
|
type: 'string',
|
|
@@ -325,7 +339,7 @@ class TestomatioMCPServer {
|
|
|
325
339
|
tags: {
|
|
326
340
|
type: 'array',
|
|
327
341
|
items: { type: 'string' },
|
|
328
|
-
description: 'List of tags for the test',
|
|
342
|
+
description: 'List of @tags for the test. Tags are automatically extracted from @ mentions in the title (e.g., @smoke, @regression). Can also be provided as an array of tag names (without @ prefix).',
|
|
329
343
|
},
|
|
330
344
|
jira_issues: {
|
|
331
345
|
type: 'array',
|
|
@@ -368,7 +382,7 @@ class TestomatioMCPServer {
|
|
|
368
382
|
},
|
|
369
383
|
title: {
|
|
370
384
|
type: 'string',
|
|
371
|
-
description: 'Test title',
|
|
385
|
+
description: 'Test title. @tags in the title (e.g., "@smoke test") will be automatically extracted as tags',
|
|
372
386
|
},
|
|
373
387
|
description: {
|
|
374
388
|
type: 'string',
|
|
@@ -395,7 +409,7 @@ class TestomatioMCPServer {
|
|
|
395
409
|
tags: {
|
|
396
410
|
type: 'array',
|
|
397
411
|
items: { type: 'string' },
|
|
398
|
-
description: 'List of tags for the test',
|
|
412
|
+
description: 'List of @tags for the test. Tags are automatically extracted from @ mentions in the title (e.g., @smoke, @regression). Can also be provided as an array of tag names (without @ prefix).',
|
|
399
413
|
},
|
|
400
414
|
jira_issues: {
|
|
401
415
|
type: 'array',
|
|
@@ -480,6 +494,57 @@ class TestomatioMCPServer {
|
|
|
480
494
|
required: ['title'],
|
|
481
495
|
},
|
|
482
496
|
},
|
|
497
|
+
{
|
|
498
|
+
name: 'get_labels',
|
|
499
|
+
description: 'Get all available labels for the project with their IDs and configurations',
|
|
500
|
+
inputSchema: {
|
|
501
|
+
type: 'object',
|
|
502
|
+
properties: {
|
|
503
|
+
scope: {
|
|
504
|
+
type: 'array',
|
|
505
|
+
items: {
|
|
506
|
+
type: 'string',
|
|
507
|
+
enum: ['tests', 'suites']
|
|
508
|
+
},
|
|
509
|
+
description: 'Filter labels by scope (e.g., ["tests"], ["suites"], or ["tests", "suites"])',
|
|
510
|
+
},
|
|
511
|
+
page: {
|
|
512
|
+
type: 'number',
|
|
513
|
+
description: 'Page number for pagination',
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
name: 'unlink_label',
|
|
520
|
+
description: 'Remove a label from a test or suite. Can remove specific label values or all instances of the label',
|
|
521
|
+
inputSchema: {
|
|
522
|
+
type: 'object',
|
|
523
|
+
properties: {
|
|
524
|
+
label_id: {
|
|
525
|
+
type: 'string',
|
|
526
|
+
description: 'Label ID to remove (e.g., "priority", "severity")',
|
|
527
|
+
},
|
|
528
|
+
test_id: {
|
|
529
|
+
type: 'string',
|
|
530
|
+
description: 'Test ID to remove label from (use either test_id or suite_id)',
|
|
531
|
+
},
|
|
532
|
+
suite_id: {
|
|
533
|
+
type: 'string',
|
|
534
|
+
description: 'Suite ID to remove label from (use either test_id or suite_id)',
|
|
535
|
+
},
|
|
536
|
+
value: {
|
|
537
|
+
type: 'string',
|
|
538
|
+
description: 'Specific label value to remove (e.g., "high", "critical"). If omitted, all instances are removed',
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
required: ['label_id'],
|
|
542
|
+
oneOf: [
|
|
543
|
+
{ required: ['test_id'] },
|
|
544
|
+
{ required: ['suite_id'] }
|
|
545
|
+
],
|
|
546
|
+
},
|
|
547
|
+
},
|
|
483
548
|
{
|
|
484
549
|
name: 'create_label',
|
|
485
550
|
description: 'Create a new label with optional custom field configuration. Labels can be used to tag and categorize tests and suites',
|
|
@@ -543,6 +608,8 @@ class TestomatioMCPServer {
|
|
|
543
608
|
switch (name) {
|
|
544
609
|
case 'get_tests':
|
|
545
610
|
return await this.getTests(args);
|
|
611
|
+
case 'get_test':
|
|
612
|
+
return await this.getTest(args.test_id);
|
|
546
613
|
case 'search_tests':
|
|
547
614
|
return await this.searchTests(args);
|
|
548
615
|
case 'search_suites':
|
|
@@ -561,6 +628,10 @@ class TestomatioMCPServer {
|
|
|
561
628
|
return await this.getPlans(args);
|
|
562
629
|
case 'get_plan':
|
|
563
630
|
return await this.getPlan(args.plan_id);
|
|
631
|
+
case 'get_labels':
|
|
632
|
+
return await this.getLabels(args);
|
|
633
|
+
case 'unlink_label':
|
|
634
|
+
return await this.unlinkLabel(args);
|
|
564
635
|
case 'create_test':
|
|
565
636
|
return await this.createTest(args);
|
|
566
637
|
case 'update_test':
|
|
@@ -773,11 +844,16 @@ class TestomatioMCPServer {
|
|
|
773
844
|
|
|
774
845
|
async getTests(filters = {}) {
|
|
775
846
|
const params = this.buildSearchParams(filters);
|
|
847
|
+
// Add labels=true and detail=true for comprehensive test information
|
|
848
|
+
params.labels = 'true';
|
|
849
|
+
params.detail = 'true';
|
|
850
|
+
|
|
776
851
|
const data = await this.makeRequest('/tests', params);
|
|
777
852
|
const formattedTests = data.data.map(test =>
|
|
778
853
|
this.formatModel(test, 'test', [
|
|
779
|
-
'title', 'description', 'code', 'priority',
|
|
780
|
-
'
|
|
854
|
+
'title', 'description', 'code', 'priority', 'state',
|
|
855
|
+
'suite-id', 'tags', 'file', 'jira-issues', 'assigned-to',
|
|
856
|
+
'created-at', 'updated-at', 'labels'
|
|
781
857
|
])
|
|
782
858
|
).join('\n\n');
|
|
783
859
|
|
|
@@ -791,6 +867,25 @@ class TestomatioMCPServer {
|
|
|
791
867
|
};
|
|
792
868
|
}
|
|
793
869
|
|
|
870
|
+
async getTest(testId) {
|
|
871
|
+
// Add labels=true and detail=true for comprehensive test information
|
|
872
|
+
const data = await this.makeRequest(`/tests/${testId}`, { labels: 'true', detail: 'true' });
|
|
873
|
+
const formattedTest = this.formatModel(data.data, 'test', [
|
|
874
|
+
'title', 'description', 'code', 'priority', 'state',
|
|
875
|
+
'suite-id', 'tags', 'file', 'jira-issues', 'assigned-to',
|
|
876
|
+
'created-at', 'updated-at', 'labels'
|
|
877
|
+
]);
|
|
878
|
+
|
|
879
|
+
return {
|
|
880
|
+
content: [
|
|
881
|
+
{
|
|
882
|
+
type: 'text',
|
|
883
|
+
text: `Test ${testId}:\n\n${formattedTest}`,
|
|
884
|
+
},
|
|
885
|
+
],
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
794
889
|
buildSearchParams(filters = {}) {
|
|
795
890
|
const params = {};
|
|
796
891
|
|
|
@@ -838,12 +933,17 @@ class TestomatioMCPServer {
|
|
|
838
933
|
|
|
839
934
|
async searchTests(filters = {}) {
|
|
840
935
|
const params = this.buildSearchParams(filters);
|
|
936
|
+
// Add labels=true and detail=true for comprehensive test information
|
|
937
|
+
params.labels = 'true';
|
|
938
|
+
params.detail = 'true';
|
|
939
|
+
|
|
841
940
|
const data = await this.makeRequest('/tests', params);
|
|
842
941
|
|
|
843
942
|
const formattedTests = data.data.map(test =>
|
|
844
943
|
this.formatModel(test, 'test', [
|
|
845
|
-
'title', 'description', 'code', 'priority',
|
|
846
|
-
'
|
|
944
|
+
'title', 'description', 'code', 'priority', 'state',
|
|
945
|
+
'suite-id', 'tags', 'file', 'jira-issues', 'assigned-to',
|
|
946
|
+
'created-at', 'updated-at', 'labels'
|
|
847
947
|
])
|
|
848
948
|
).join('\n\n');
|
|
849
949
|
|
|
@@ -1072,9 +1172,91 @@ class TestomatioMCPServer {
|
|
|
1072
1172
|
};
|
|
1073
1173
|
}
|
|
1074
1174
|
|
|
1175
|
+
async getLabels(filters = {}) {
|
|
1176
|
+
const params = {};
|
|
1177
|
+
|
|
1178
|
+
// Handle scope filter
|
|
1179
|
+
if (filters.scope && Array.isArray(filters.scope)) {
|
|
1180
|
+
if (!params['scope[]']) {
|
|
1181
|
+
params['scope[]'] = [];
|
|
1182
|
+
}
|
|
1183
|
+
filters.scope.forEach(scope => {
|
|
1184
|
+
params['scope[]'].push(scope);
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Handle pagination
|
|
1189
|
+
if (filters.page) {
|
|
1190
|
+
params.page = filters.page;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const data = await this.makeRequest('/labels', params);
|
|
1194
|
+
const formattedLabels = data.data.map(label =>
|
|
1195
|
+
this.formatModel(label, 'label', [
|
|
1196
|
+
'title', 'color', 'scope', 'visibility', 'field'
|
|
1197
|
+
])
|
|
1198
|
+
).join('\n\n');
|
|
1199
|
+
|
|
1200
|
+
return {
|
|
1201
|
+
content: [
|
|
1202
|
+
{
|
|
1203
|
+
type: 'text',
|
|
1204
|
+
text: `Available labels for project ${this.config.projectId}:\n\n${formattedLabels || 'No labels found matching the criteria.'}`,
|
|
1205
|
+
},
|
|
1206
|
+
],
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
extractTagsFromTitle(title) {
|
|
1211
|
+
if (!title) return [];
|
|
1212
|
+
|
|
1213
|
+
// Find all @tags in the title
|
|
1214
|
+
const tagMatches = title.match(/@([a-zA-Z0-9_\-]+)/g);
|
|
1215
|
+
|
|
1216
|
+
if (!tagMatches) return [];
|
|
1217
|
+
|
|
1218
|
+
// Remove @ prefix and deduplicate
|
|
1219
|
+
const tags = tagMatches
|
|
1220
|
+
.map(tag => tag.substring(1)) // Remove @
|
|
1221
|
+
.filter(tag => tag.length > 0); // Filter out empty tags
|
|
1222
|
+
|
|
1223
|
+
return [...new Set(tags)]; // Remove duplicates
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
mergeTags(explicitTags, titleTags) {
|
|
1227
|
+
const allTags = [];
|
|
1228
|
+
|
|
1229
|
+
// Add explicit tags (remove @ prefix if present)
|
|
1230
|
+
if (explicitTags && Array.isArray(explicitTags)) {
|
|
1231
|
+
allTags.push(...explicitTags.map(tag => tag.replace(/^@/, '')));
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Add tags extracted from title
|
|
1235
|
+
if (titleTags && Array.isArray(titleTags)) {
|
|
1236
|
+
allTags.push(...titleTags);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Remove duplicates while preserving order
|
|
1240
|
+
const uniqueTags = [];
|
|
1241
|
+
const seen = new Set();
|
|
1242
|
+
|
|
1243
|
+
for (const tag of allTags) {
|
|
1244
|
+
if (!seen.has(tag) && tag.length > 0) {
|
|
1245
|
+
seen.add(tag);
|
|
1246
|
+
uniqueTags.push(tag);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
return uniqueTags;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1075
1253
|
async createTest(args) {
|
|
1076
1254
|
const { suite_id, labels_ids, fields, ...attributes } = args;
|
|
1077
1255
|
|
|
1256
|
+
// Extract tags from title if provided
|
|
1257
|
+
const titleTags = this.extractTagsFromTitle(attributes.title);
|
|
1258
|
+
const mergedTags = this.mergeTags(attributes.tags, titleTags);
|
|
1259
|
+
|
|
1078
1260
|
// Convert attributes to use hyphens instead of underscores for API compatibility
|
|
1079
1261
|
const apiAttributes = Object.fromEntries(
|
|
1080
1262
|
Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v])
|
|
@@ -1085,12 +1267,16 @@ class TestomatioMCPServer {
|
|
|
1085
1267
|
apiAttributes['suite_id'] = suite_id;
|
|
1086
1268
|
}
|
|
1087
1269
|
|
|
1088
|
-
//
|
|
1270
|
+
// Build JSON-API request data
|
|
1089
1271
|
const requestData = {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1272
|
+
data: {
|
|
1273
|
+
type: 'tests',
|
|
1274
|
+
attributes: {
|
|
1275
|
+
...apiAttributes,
|
|
1276
|
+
...(mergedTags.length > 0 && { tags: mergedTags }),
|
|
1277
|
+
...(labels_ids && { labels_ids: labels_ids }),
|
|
1278
|
+
...(fields && { 'custom-fields': fields })
|
|
1279
|
+
}
|
|
1094
1280
|
}
|
|
1095
1281
|
};
|
|
1096
1282
|
|
|
@@ -1110,9 +1296,87 @@ class TestomatioMCPServer {
|
|
|
1110
1296
|
};
|
|
1111
1297
|
}
|
|
1112
1298
|
|
|
1299
|
+
async linkLabels(testId, labelsIds) {
|
|
1300
|
+
if (!labelsIds || labelsIds.length === 0) {
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Process each label individually using the label linking API
|
|
1305
|
+
for (const labelId of labelsIds) {
|
|
1306
|
+
// Parse label:value format if present
|
|
1307
|
+
let labelUid = labelId;
|
|
1308
|
+
let value = null;
|
|
1309
|
+
|
|
1310
|
+
if (labelId.includes(':')) {
|
|
1311
|
+
[labelUid, value] = labelId.split(':', 2);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Build URL with test_id query parameter
|
|
1315
|
+
let url = `/labels/${labelUid}/link?test_id=${testId}`;
|
|
1316
|
+
|
|
1317
|
+
// Add value as query parameter if present
|
|
1318
|
+
if (value) {
|
|
1319
|
+
url += `&value=${encodeURIComponent(value)}`;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
await this.makePostRequest(url, {});
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
async unlinkLabel(args) {
|
|
1327
|
+
const { label_id, test_id, suite_id, value } = args;
|
|
1328
|
+
|
|
1329
|
+
// Validate that either test_id or suite_id is provided
|
|
1330
|
+
if (!test_id && !suite_id) {
|
|
1331
|
+
throw new Error('Either test_id or suite_id must be provided');
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
if (test_id && suite_id) {
|
|
1335
|
+
throw new Error('Cannot specify both test_id and suite_id. Use one or the other.');
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Build URL with base label link endpoint
|
|
1339
|
+
let url = `/labels/${label_id}/link`;
|
|
1340
|
+
|
|
1341
|
+
// Add appropriate query parameters
|
|
1342
|
+
if (test_id) {
|
|
1343
|
+
url += `?test_id=${test_id}`;
|
|
1344
|
+
} else if (suite_id) {
|
|
1345
|
+
url += `?suite_id=${suite_id}`;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Add event=remove parameter
|
|
1349
|
+
url += `&event=remove`;
|
|
1350
|
+
|
|
1351
|
+
// Add value as query parameter if present
|
|
1352
|
+
if (value) {
|
|
1353
|
+
url += `&value=${encodeURIComponent(value)}`;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Make the API request
|
|
1357
|
+
await this.makePostRequest(url, {});
|
|
1358
|
+
|
|
1359
|
+
const itemType = test_id ? 'test' : 'suite';
|
|
1360
|
+
const itemId = test_id || suite_id;
|
|
1361
|
+
const removeDescription = value ? `value "${value}"` : 'all instances';
|
|
1362
|
+
|
|
1363
|
+
return {
|
|
1364
|
+
content: [
|
|
1365
|
+
{
|
|
1366
|
+
type: 'text',
|
|
1367
|
+
text: `Successfully removed label "${label_id}" (${removeDescription}) from ${itemType} "${itemId}"`,
|
|
1368
|
+
},
|
|
1369
|
+
],
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1113
1373
|
async updateTest(args) {
|
|
1114
1374
|
const { test_id, suite_id, labels_ids, fields, ...attributes } = args;
|
|
1115
1375
|
|
|
1376
|
+
// Extract tags from title if provided
|
|
1377
|
+
const titleTags = this.extractTagsFromTitle(attributes.title);
|
|
1378
|
+
const mergedTags = this.mergeTags(attributes.tags, titleTags);
|
|
1379
|
+
|
|
1116
1380
|
// Convert attributes to use hyphens instead of underscores for API compatibility
|
|
1117
1381
|
const apiAttributes = Object.fromEntries(
|
|
1118
1382
|
Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v])
|
|
@@ -1125,29 +1389,27 @@ class TestomatioMCPServer {
|
|
|
1125
1389
|
|
|
1126
1390
|
let data;
|
|
1127
1391
|
|
|
1128
|
-
// Handle
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
add_labels: labels_ids
|
|
1135
|
-
};
|
|
1136
|
-
|
|
1137
|
-
await this.makePutRequest('/bulk/labels', labelsRequest);
|
|
1138
|
-
|
|
1139
|
-
// After updating labels, fetch the updated test
|
|
1140
|
-
data = await this.makeRequest(`/tests/${test_id}`);
|
|
1141
|
-
} else {
|
|
1142
|
-
// Handle regular test attributes update
|
|
1143
|
-
const requestData = {
|
|
1144
|
-
test: {
|
|
1392
|
+
// Handle regular test attributes update using JSON-API format
|
|
1393
|
+
const requestData = {
|
|
1394
|
+
data: {
|
|
1395
|
+
id: test_id,
|
|
1396
|
+
type: 'tests',
|
|
1397
|
+
attributes: {
|
|
1145
1398
|
...apiAttributes,
|
|
1146
|
-
...(
|
|
1399
|
+
...(mergedTags.length > 0 && { tags: mergedTags }),
|
|
1400
|
+
...(fields && { 'custom-fields': fields })
|
|
1147
1401
|
}
|
|
1148
|
-
}
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1149
1404
|
|
|
1150
|
-
|
|
1405
|
+
data = await this.makePutRequest(`/tests/${test_id}`, requestData);
|
|
1406
|
+
|
|
1407
|
+
// Handle labels_ids using the label linking API
|
|
1408
|
+
if (labels_ids && labels_ids.length > 0) {
|
|
1409
|
+
await this.linkLabels(test_id, labels_ids);
|
|
1410
|
+
|
|
1411
|
+
// After linking labels, fetch the updated test to reflect changes
|
|
1412
|
+
data = await this.makeRequest(`/tests/${test_id}`);
|
|
1151
1413
|
}
|
|
1152
1414
|
|
|
1153
1415
|
const formattedTest = this.formatModel(data.data, 'test', [
|