@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.
Files changed (3) hide show
  1. package/README.md +102 -33
  2. package/index.js +302 -48
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # Testomatio MCP Server
1
+ # Testomat.io MCP Server
2
2
 
3
- A Model Context Protocol (MCP) server for Testomatio API integration with AI assistants like Cursor.
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
- - Testomatio account with API access
11
+ - Testomat.io account with API access
12
12
 
13
- ### Install via npm
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 [Testomatio](https://app.testomat.io)
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
- ### Option 3: Global Installation
108
+ ## Features
118
109
 
119
- If you've installed the package globally:
110
+ ### Tools
120
111
 
121
- ```json
122
- {
123
- "mcpServers": {
124
- "testomatio": {
125
- "command": "testomatio-mcp",
126
- "args": ["--token", "testomat_YOUR_TOKEN_HERE", "--project", "YOUR_PROJECT_ID"]
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 Testomatio project settings
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 Testomatio API, refer to the [Testomatio API Documentation](https://app.testomat.io/docs/api/).
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 [Testomatio Documentation](https://docs.testomat.io)
257
+ 1. Check the [Testomat.io Documentation](https://docs.testomat.io)
189
258
  2. Open an issue on GitHub
190
- 3. Contact Testomatio support
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 Testomatio API endpoints
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
- if (import.meta.url === `file://${process.argv[1]}`) {
765
- main().catch(console.error);
766
- }
767
-
768
- export { TestomatioMCPServer };
1022
+ main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Model Context Protocol server for Testomatio API",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -33,4 +33,4 @@
33
33
  "engines": {
34
34
  "node": ">=18.0.0"
35
35
  }
36
- }
36
+ }