@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.
- package/index.js +89 -48
- 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
|
-
|
|
765
|
-
main().catch(console.error);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
export { TestomatioMCPServer };
|
|
809
|
+
main().catch(console.error);
|