@testomatio/mcp 1.0.0 → 1.0.1

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 (2) hide show
  1. package/index.js +89 -48
  2. package/package.json +1 -1
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
 
@@ -317,9 +317,9 @@ class TestomatioMCPServer {
317
317
  async makeRequest(path, params = {}) {
318
318
  // Ensure we have a valid JWT token
319
319
  const jwt = await this.authenticate();
320
-
320
+
321
321
  const url = new URL(`${this.config.baseUrl}/api/${this.config.projectId}${path}`);
322
-
322
+
323
323
  // Add query parameters with proper array handling
324
324
  Object.entries(params).forEach(([key, value]) => {
325
325
  if (value !== undefined && value !== null) {
@@ -415,10 +415,10 @@ class TestomatioMCPServer {
415
415
  formatModel(model, tagName, fields) {
416
416
  const attributes = model.attributes || {};
417
417
  const lines = [`<${tagName}>`];
418
-
418
+
419
419
  // Always include ID from root level
420
420
  lines.push(` <id>${model.id || ''}</id>`);
421
-
421
+
422
422
  // Process specified fields
423
423
  fields.forEach(field => {
424
424
  let value;
@@ -434,7 +434,7 @@ class TestomatioMCPServer {
434
434
  }
435
435
 
436
436
  const formattedValue = this.formatValue(value, field);
437
-
437
+
438
438
  if (field === 'test' && typeof value === 'object') {
439
439
  // Special case for nested test objects
440
440
  lines.push(` <test>${formattedValue}\n </test>`);
@@ -442,7 +442,7 @@ class TestomatioMCPServer {
442
442
  lines.push(` <${xmlFieldName}>${formattedValue}</${xmlFieldName}>`);
443
443
  }
444
444
  });
445
-
445
+
446
446
  lines.push(`</${tagName}>`);
447
447
  return lines.join('\n');
448
448
  }
@@ -450,13 +450,13 @@ class TestomatioMCPServer {
450
450
  async getTests(filters = {}) {
451
451
  const params = this.buildSearchParams(filters);
452
452
  const data = await this.makeRequest('/tests', params);
453
- const formattedTests = data.data.map(test =>
453
+ const formattedTests = data.data.map(test =>
454
454
  this.formatModel(test, 'test', [
455
- 'title', 'description', 'code', 'priority',
455
+ 'title', 'description', 'code', 'priority',
456
456
  'state', 'suite-id', 'tags', 'file'
457
457
  ])
458
458
  ).join('\n\n');
459
-
459
+
460
460
  return {
461
461
  content: [
462
462
  {
@@ -467,19 +467,64 @@ class TestomatioMCPServer {
467
467
  };
468
468
  }
469
469
 
470
+ buildSearchParams(filters = {}) {
471
+ const params = {};
472
+
473
+ // Handle basic filters
474
+ Object.entries(filters).forEach(([key, value]) => {
475
+ if (value !== undefined && value !== null) {
476
+ if (key === 'labels' && Array.isArray(value)) {
477
+ // Labels need special array handling
478
+ value.forEach(label => {
479
+ if (!params['labels[]']) {
480
+ params['labels[]'] = [];
481
+ }
482
+ if (Array.isArray(params['labels[]'])) {
483
+ params['labels[]'].push(label);
484
+ } else {
485
+ params['labels[]'] = [params['labels[]'], label];
486
+ }
487
+ });
488
+ } else if (key === 'filter' && typeof value === 'object') {
489
+ // Handle filter hash (e.g., filter[state]=manual)
490
+ Object.entries(value).forEach(([filterKey, filterValue]) => {
491
+ params[`filter[${filterKey}]`] = filterValue;
492
+ });
493
+ } else if (Array.isArray(value)) {
494
+ // Handle other arrays
495
+ value.forEach(v => {
496
+ const paramKey = `${key}[]`;
497
+ if (!params[paramKey]) {
498
+ params[paramKey] = [];
499
+ }
500
+ if (Array.isArray(params[paramKey])) {
501
+ params[paramKey].push(v);
502
+ } else {
503
+ params[paramKey] = [params[paramKey], v];
504
+ }
505
+ });
506
+ } else {
507
+ params[key] = String(value);
508
+ }
509
+ }
510
+ });
511
+
512
+ return params;
513
+ }
514
+
470
515
  async searchTests(filters = {}) {
471
516
  const params = this.buildSearchParams(filters);
472
517
  const data = await this.makeRequest('/tests', params);
473
-
474
- const formattedTests = data.data.map(test =>
518
+
519
+ const formattedTests = data.data.map(test =>
475
520
  this.formatModel(test, 'test', [
476
- 'title', 'description', 'code', 'priority',
521
+ 'title', 'description', 'code', 'priority',
477
522
  'state', 'suite-id', 'tags', 'file'
478
523
  ])
479
524
  ).join('\n\n');
480
-
525
+
481
526
  const searchDescription = this.buildSearchDescription(filters);
482
-
527
+
483
528
  return {
484
529
  content: [
485
530
  {
@@ -494,15 +539,15 @@ class TestomatioMCPServer {
494
539
  // Add filter=true for suites search to include tests
495
540
  const params = this.buildSearchParams({ ...filters, filter: true });
496
541
  const data = await this.makeRequest('/suites', params);
497
-
498
- const formattedSuites = data.data.map(suite =>
542
+
543
+ const formattedSuites = data.data.map(suite =>
499
544
  this.formatModel(suite, 'suite', [
500
545
  'title', 'description', 'test-count', 'is-root', 'file-type'
501
546
  ])
502
547
  ).join('\n\n');
503
-
548
+
504
549
  const searchDescription = this.buildSearchDescription(filters);
505
-
550
+
506
551
  return {
507
552
  content: [
508
553
  {
@@ -515,7 +560,7 @@ class TestomatioMCPServer {
515
560
 
516
561
  buildSearchDescription(filters) {
517
562
  const descriptions = [];
518
-
563
+
519
564
  if (filters.query) {
520
565
  if (filters.query.startsWith('@')) {
521
566
  descriptions.push(`tagged with "${filters.query}"`);
@@ -525,41 +570,41 @@ class TestomatioMCPServer {
525
570
  descriptions.push(`containing "${filters.query}"`);
526
571
  }
527
572
  }
528
-
573
+
529
574
  if (filters.tql) {
530
575
  descriptions.push(`matching TQL: "${filters.tql}"`);
531
576
  }
532
-
577
+
533
578
  if (filters.labels && filters.labels.length > 0) {
534
579
  descriptions.push(`with labels: ${filters.labels.join(', ')}`);
535
580
  }
536
-
581
+
537
582
  if (filters.state) {
538
583
  descriptions.push(`state: ${filters.state}`);
539
584
  }
540
-
585
+
541
586
  if (filters.priority) {
542
587
  descriptions.push(`priority: ${filters.priority}`);
543
588
  }
544
-
589
+
545
590
  if (filters.filter && typeof filters.filter === 'object') {
546
591
  const filterDesc = Object.entries(filters.filter)
547
592
  .map(([key, value]) => `${key}: ${value}`)
548
593
  .join(', ');
549
594
  descriptions.push(`filtered by: ${filterDesc}`);
550
595
  }
551
-
596
+
552
597
  return descriptions.length > 0 ? ` (${descriptions.join(', ')})` : '';
553
598
  }
554
599
 
555
600
  async getRootSuites() {
556
601
  const data = await this.makeRequest('/suites');
557
- const formattedSuites = data.data.map(suite =>
602
+ const formattedSuites = data.data.map(suite =>
558
603
  this.formatModel(suite, 'suite', [
559
604
  'title', 'description', 'test-count', 'is-root', 'file-type'
560
605
  ])
561
606
  ).join('\n\n');
562
-
607
+
563
608
  return {
564
609
  content: [
565
610
  {
@@ -575,7 +620,7 @@ class TestomatioMCPServer {
575
620
  const formattedSuite = this.formatModel(data.data, 'suite', [
576
621
  'title', 'description', 'test-count', 'is-root', 'file-type'
577
622
  ]);
578
-
623
+
579
624
  // Format child suites and tests if they exist
580
625
  let childContent = '';
581
626
  if (data.data.relationships?.children?.data) {
@@ -587,18 +632,18 @@ class TestomatioMCPServer {
587
632
  childContent += `\n\nChild Suites:\n${childSuites}`;
588
633
  }
589
634
  }
590
-
635
+
591
636
  if (data.data.relationships?.tests?.data) {
592
637
  const tests = data.data.relationships.tests.data
593
638
  .map(test => this.formatModel(test, 'test', [
594
- 'title', 'description', 'code', 'priority',
639
+ 'title', 'description', 'code', 'priority',
595
640
  'state', 'suite-id', 'tags', 'file'
596
641
  ])).join('\n\n');
597
642
  if (tests) {
598
643
  childContent += `\n\nTests:\n${tests}`;
599
644
  }
600
645
  }
601
-
646
+
602
647
  return {
603
648
  content: [
604
649
  {
@@ -611,13 +656,13 @@ class TestomatioMCPServer {
611
656
 
612
657
  async getRuns() {
613
658
  const data = await this.makeRequest('/runs');
614
- const formattedRuns = data.data.map(run =>
659
+ const formattedRuns = data.data.map(run =>
615
660
  this.formatModel(run, 'run', [
616
661
  'status', 'title', 'tests-count', 'automated', 'duration',
617
662
  'passed', 'failed', 'skipped', 'created-at', 'finished-at'
618
663
  ])
619
664
  ).join('\n\n');
620
-
665
+
621
666
  return {
622
667
  content: [
623
668
  {
@@ -635,7 +680,7 @@ class TestomatioMCPServer {
635
680
  'status', 'title', 'tests-count', 'automated', 'duration',
636
681
  'passed', 'failed', 'skipped', 'created-at', 'finished-at'
637
682
  ]);
638
-
683
+
639
684
  return {
640
685
  content: [
641
686
  {
@@ -651,14 +696,14 @@ class TestomatioMCPServer {
651
696
  if (dateRange) {
652
697
  params.finished_at_date_range = dateRange;
653
698
  }
654
-
699
+
655
700
  const data = await this.makeRequest('/testruns', params);
656
- const formattedTestruns = data.data.map(testrun =>
701
+ const formattedTestruns = data.data.map(testrun =>
657
702
  this.formatModel(testrun, 'testrun', [
658
703
  'status', 'run-time', 'message', 'run-id', 'test'
659
704
  ])
660
705
  ).join('\n\n');
661
-
706
+
662
707
  return {
663
708
  content: [
664
709
  {
@@ -671,12 +716,12 @@ class TestomatioMCPServer {
671
716
 
672
717
  async getPlans(filters = {}) {
673
718
  const data = await this.makeRequest('/plans', filters);
674
- const formattedPlans = data.data.map(plan =>
719
+ const formattedPlans = data.data.map(plan =>
675
720
  this.formatModel(plan, 'plan', [
676
721
  'title', 'test-count', 'kind', 'created-at', 'tests-ids', 'labels'
677
722
  ])
678
723
  ).join('\n\n');
679
-
724
+
680
725
  return {
681
726
  content: [
682
727
  {
@@ -692,7 +737,7 @@ class TestomatioMCPServer {
692
737
  const formattedPlan = this.formatModel(data.data, 'plan', [
693
738
  'title', 'test-count', 'kind', 'created-at', 'tests-ids', 'labels'
694
739
  ]);
695
-
740
+
696
741
  return {
697
742
  content: [
698
743
  {
@@ -731,7 +776,7 @@ function parseArgs() {
731
776
  .parse();
732
777
 
733
778
  const options = program.opts();
734
-
779
+
735
780
  const token = options.token || process.env.TESTOMATIO_API_TOKEN;
736
781
  const projectId = options.project;
737
782
  const baseUrl = options.baseUrl || process.env.TESTOMATIO_BASE_URL || 'https://app.testomat.io';
@@ -761,8 +806,4 @@ async function main() {
761
806
  }
762
807
  }
763
808
 
764
- if (import.meta.url === `file://${process.argv[1]}`) {
765
- main().catch(console.error);
766
- }
767
-
768
- export { TestomatioMCPServer };
809
+ 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.1",
4
4
  "description": "Model Context Protocol server for Testomatio API",
5
5
  "main": "index.js",
6
6
  "bin": {