@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.
Files changed (3) hide show
  1. package/README.md +99 -3
  2. package/index.js +318 -60
  3. 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 high priority"** → `search_tests` tool with `tql: "tag == 'critical' or label == 'ux' and priority == 'high'"`
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
- "priority == 'high' or label == 'ux'"
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
- throw new Error(`Authentication failed: HTTP ${response.status}: ${response.statusText}`);
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 { test_id, labels_ids, ...attributes } = args;
1025
+ const { suite_id, labels_ids, ...attributes } = args;
918
1026
  const requestData = {
919
- type: 'test',
920
- attributes: Object.fromEntries(Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v])),
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
- type: 'test',
944
- attributes: Object.fromEntries(Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v])),
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/${args.test_id}`, requestData);
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().catch(console.error);
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.2",
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": "echo \"Error: no test specified\" && exit 1"
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
  }