@testomatio/mcp 1.0.2 → 1.0.3
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 +84 -56
- 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
|
|
|
@@ -433,9 +433,9 @@ class TestomatioMCPServer {
|
|
|
433
433
|
async makeRequest(path, params = {}) {
|
|
434
434
|
// Ensure we have a valid JWT token
|
|
435
435
|
const jwt = await this.authenticate();
|
|
436
|
-
|
|
436
|
+
|
|
437
437
|
const url = new URL(`${this.config.baseUrl}/api/${this.config.projectId}${path}`);
|
|
438
|
-
|
|
438
|
+
|
|
439
439
|
// Add query parameters with proper array handling
|
|
440
440
|
Object.entries(params).forEach(([key, value]) => {
|
|
441
441
|
if (value !== undefined && value !== null) {
|
|
@@ -486,7 +486,7 @@ class TestomatioMCPServer {
|
|
|
486
486
|
this.jwtToken = null;
|
|
487
487
|
return this.makePostRequest(path, data);
|
|
488
488
|
}
|
|
489
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
489
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}; ${await response.text()}`);
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
return await response.json();
|
|
@@ -580,10 +580,10 @@ class TestomatioMCPServer {
|
|
|
580
580
|
formatModel(model, tagName, fields) {
|
|
581
581
|
const attributes = model.attributes || {};
|
|
582
582
|
const lines = [`<${tagName}>`];
|
|
583
|
-
|
|
583
|
+
|
|
584
584
|
// Always include ID from root level
|
|
585
585
|
lines.push(` <id>${model.id || ''}</id>`);
|
|
586
|
-
|
|
586
|
+
|
|
587
587
|
// Process specified fields
|
|
588
588
|
fields.forEach(field => {
|
|
589
589
|
let value;
|
|
@@ -599,7 +599,7 @@ class TestomatioMCPServer {
|
|
|
599
599
|
}
|
|
600
600
|
|
|
601
601
|
const formattedValue = this.formatValue(value, field);
|
|
602
|
-
|
|
602
|
+
|
|
603
603
|
if (field === 'test' && typeof value === 'object') {
|
|
604
604
|
// Special case for nested test objects
|
|
605
605
|
lines.push(` <test>${formattedValue}\n </test>`);
|
|
@@ -607,7 +607,7 @@ class TestomatioMCPServer {
|
|
|
607
607
|
lines.push(` <${xmlFieldName}>${formattedValue}</${xmlFieldName}>`);
|
|
608
608
|
}
|
|
609
609
|
});
|
|
610
|
-
|
|
610
|
+
|
|
611
611
|
lines.push(`</${tagName}>`);
|
|
612
612
|
return lines.join('\n');
|
|
613
613
|
}
|
|
@@ -615,13 +615,13 @@ class TestomatioMCPServer {
|
|
|
615
615
|
async getTests(filters = {}) {
|
|
616
616
|
const params = this.buildSearchParams(filters);
|
|
617
617
|
const data = await this.makeRequest('/tests', params);
|
|
618
|
-
const formattedTests = data.data.map(test =>
|
|
618
|
+
const formattedTests = data.data.map(test =>
|
|
619
619
|
this.formatModel(test, 'test', [
|
|
620
|
-
'title', 'description', 'code', 'priority',
|
|
620
|
+
'title', 'description', 'code', 'priority',
|
|
621
621
|
'state', 'suite-id', 'tags', 'file'
|
|
622
622
|
])
|
|
623
623
|
).join('\n\n');
|
|
624
|
-
|
|
624
|
+
|
|
625
625
|
return {
|
|
626
626
|
content: [
|
|
627
627
|
{
|
|
@@ -634,7 +634,7 @@ class TestomatioMCPServer {
|
|
|
634
634
|
|
|
635
635
|
buildSearchParams(filters = {}) {
|
|
636
636
|
const params = {};
|
|
637
|
-
|
|
637
|
+
|
|
638
638
|
// Handle basic filters
|
|
639
639
|
Object.entries(filters).forEach(([key, value]) => {
|
|
640
640
|
if (value !== undefined && value !== null) {
|
|
@@ -680,16 +680,16 @@ class TestomatioMCPServer {
|
|
|
680
680
|
async searchTests(filters = {}) {
|
|
681
681
|
const params = this.buildSearchParams(filters);
|
|
682
682
|
const data = await this.makeRequest('/tests', params);
|
|
683
|
-
|
|
684
|
-
const formattedTests = data.data.map(test =>
|
|
683
|
+
|
|
684
|
+
const formattedTests = data.data.map(test =>
|
|
685
685
|
this.formatModel(test, 'test', [
|
|
686
|
-
'title', 'description', 'code', 'priority',
|
|
686
|
+
'title', 'description', 'code', 'priority',
|
|
687
687
|
'state', 'suite-id', 'tags', 'file'
|
|
688
688
|
])
|
|
689
689
|
).join('\n\n');
|
|
690
|
-
|
|
690
|
+
|
|
691
691
|
const searchDescription = this.buildSearchDescription(filters);
|
|
692
|
-
|
|
692
|
+
|
|
693
693
|
return {
|
|
694
694
|
content: [
|
|
695
695
|
{
|
|
@@ -704,15 +704,15 @@ class TestomatioMCPServer {
|
|
|
704
704
|
// Add filter=true for suites search to include tests
|
|
705
705
|
const params = this.buildSearchParams({ ...filters, filter: true });
|
|
706
706
|
const data = await this.makeRequest('/suites', params);
|
|
707
|
-
|
|
708
|
-
const formattedSuites = data.data.map(suite =>
|
|
707
|
+
|
|
708
|
+
const formattedSuites = data.data.map(suite =>
|
|
709
709
|
this.formatModel(suite, 'suite', [
|
|
710
710
|
'title', 'description', 'test-count', 'is-root', 'file-type'
|
|
711
711
|
])
|
|
712
712
|
).join('\n\n');
|
|
713
|
-
|
|
713
|
+
|
|
714
714
|
const searchDescription = this.buildSearchDescription(filters);
|
|
715
|
-
|
|
715
|
+
|
|
716
716
|
return {
|
|
717
717
|
content: [
|
|
718
718
|
{
|
|
@@ -725,7 +725,7 @@ class TestomatioMCPServer {
|
|
|
725
725
|
|
|
726
726
|
buildSearchDescription(filters) {
|
|
727
727
|
const descriptions = [];
|
|
728
|
-
|
|
728
|
+
|
|
729
729
|
if (filters.query) {
|
|
730
730
|
if (filters.query.startsWith('@')) {
|
|
731
731
|
descriptions.push(`tagged with "${filters.query}"`);
|
|
@@ -735,41 +735,41 @@ class TestomatioMCPServer {
|
|
|
735
735
|
descriptions.push(`containing "${filters.query}"`);
|
|
736
736
|
}
|
|
737
737
|
}
|
|
738
|
-
|
|
738
|
+
|
|
739
739
|
if (filters.tql) {
|
|
740
740
|
descriptions.push(`matching TQL: "${filters.tql}"`);
|
|
741
741
|
}
|
|
742
|
-
|
|
742
|
+
|
|
743
743
|
if (filters.labels && filters.labels.length > 0) {
|
|
744
744
|
descriptions.push(`with labels: ${filters.labels.join(', ')}`);
|
|
745
745
|
}
|
|
746
|
-
|
|
746
|
+
|
|
747
747
|
if (filters.state) {
|
|
748
748
|
descriptions.push(`state: ${filters.state}`);
|
|
749
749
|
}
|
|
750
|
-
|
|
750
|
+
|
|
751
751
|
if (filters.priority) {
|
|
752
752
|
descriptions.push(`priority: ${filters.priority}`);
|
|
753
753
|
}
|
|
754
|
-
|
|
754
|
+
|
|
755
755
|
if (filters.filter && typeof filters.filter === 'object') {
|
|
756
756
|
const filterDesc = Object.entries(filters.filter)
|
|
757
757
|
.map(([key, value]) => `${key}: ${value}`)
|
|
758
758
|
.join(', ');
|
|
759
759
|
descriptions.push(`filtered by: ${filterDesc}`);
|
|
760
760
|
}
|
|
761
|
-
|
|
761
|
+
|
|
762
762
|
return descriptions.length > 0 ? ` (${descriptions.join(', ')})` : '';
|
|
763
763
|
}
|
|
764
764
|
|
|
765
765
|
async getRootSuites() {
|
|
766
766
|
const data = await this.makeRequest('/suites');
|
|
767
|
-
const formattedSuites = data.data.map(suite =>
|
|
767
|
+
const formattedSuites = data.data.map(suite =>
|
|
768
768
|
this.formatModel(suite, 'suite', [
|
|
769
769
|
'title', 'description', 'test-count', 'is-root', 'file-type'
|
|
770
770
|
])
|
|
771
771
|
).join('\n\n');
|
|
772
|
-
|
|
772
|
+
|
|
773
773
|
return {
|
|
774
774
|
content: [
|
|
775
775
|
{
|
|
@@ -785,7 +785,7 @@ class TestomatioMCPServer {
|
|
|
785
785
|
const formattedSuite = this.formatModel(data.data, 'suite', [
|
|
786
786
|
'title', 'description', 'test-count', 'is-root', 'file-type'
|
|
787
787
|
]);
|
|
788
|
-
|
|
788
|
+
|
|
789
789
|
// Format child suites and tests if they exist
|
|
790
790
|
let childContent = '';
|
|
791
791
|
if (data.data.relationships?.children?.data) {
|
|
@@ -797,18 +797,18 @@ class TestomatioMCPServer {
|
|
|
797
797
|
childContent += `\n\nChild Suites:\n${childSuites}`;
|
|
798
798
|
}
|
|
799
799
|
}
|
|
800
|
-
|
|
800
|
+
|
|
801
801
|
if (data.data.relationships?.tests?.data) {
|
|
802
802
|
const tests = data.data.relationships.tests.data
|
|
803
803
|
.map(test => this.formatModel(test, 'test', [
|
|
804
|
-
'title', 'description', 'code', 'priority',
|
|
804
|
+
'title', 'description', 'code', 'priority',
|
|
805
805
|
'state', 'suite-id', 'tags', 'file'
|
|
806
806
|
])).join('\n\n');
|
|
807
807
|
if (tests) {
|
|
808
808
|
childContent += `\n\nTests:\n${tests}`;
|
|
809
809
|
}
|
|
810
810
|
}
|
|
811
|
-
|
|
811
|
+
|
|
812
812
|
return {
|
|
813
813
|
content: [
|
|
814
814
|
{
|
|
@@ -821,13 +821,13 @@ class TestomatioMCPServer {
|
|
|
821
821
|
|
|
822
822
|
async getRuns() {
|
|
823
823
|
const data = await this.makeRequest('/runs');
|
|
824
|
-
const formattedRuns = data.data.map(run =>
|
|
824
|
+
const formattedRuns = data.data.map(run =>
|
|
825
825
|
this.formatModel(run, 'run', [
|
|
826
826
|
'status', 'title', 'tests-count', 'automated', 'duration',
|
|
827
827
|
'passed', 'failed', 'skipped', 'created-at', 'finished-at'
|
|
828
828
|
])
|
|
829
829
|
).join('\n\n');
|
|
830
|
-
|
|
830
|
+
|
|
831
831
|
return {
|
|
832
832
|
content: [
|
|
833
833
|
{
|
|
@@ -845,7 +845,7 @@ class TestomatioMCPServer {
|
|
|
845
845
|
'status', 'title', 'tests-count', 'automated', 'duration',
|
|
846
846
|
'passed', 'failed', 'skipped', 'created-at', 'finished-at'
|
|
847
847
|
]);
|
|
848
|
-
|
|
848
|
+
|
|
849
849
|
return {
|
|
850
850
|
content: [
|
|
851
851
|
{
|
|
@@ -861,14 +861,14 @@ class TestomatioMCPServer {
|
|
|
861
861
|
if (dateRange) {
|
|
862
862
|
params.finished_at_date_range = dateRange;
|
|
863
863
|
}
|
|
864
|
-
|
|
864
|
+
|
|
865
865
|
const data = await this.makeRequest('/testruns', params);
|
|
866
|
-
const formattedTestruns = data.data.map(testrun =>
|
|
866
|
+
const formattedTestruns = data.data.map(testrun =>
|
|
867
867
|
this.formatModel(testrun, 'testrun', [
|
|
868
868
|
'status', 'run-time', 'message', 'run-id', 'test'
|
|
869
869
|
])
|
|
870
870
|
).join('\n\n');
|
|
871
|
-
|
|
871
|
+
|
|
872
872
|
return {
|
|
873
873
|
content: [
|
|
874
874
|
{
|
|
@@ -881,12 +881,12 @@ class TestomatioMCPServer {
|
|
|
881
881
|
|
|
882
882
|
async getPlans(filters = {}) {
|
|
883
883
|
const data = await this.makeRequest('/plans', filters);
|
|
884
|
-
const formattedPlans = data.data.map(plan =>
|
|
884
|
+
const formattedPlans = data.data.map(plan =>
|
|
885
885
|
this.formatModel(plan, 'plan', [
|
|
886
886
|
'title', 'test-count', 'kind', 'created-at', 'tests-ids', 'labels'
|
|
887
887
|
])
|
|
888
888
|
).join('\n\n');
|
|
889
|
-
|
|
889
|
+
|
|
890
890
|
return {
|
|
891
891
|
content: [
|
|
892
892
|
{
|
|
@@ -902,7 +902,7 @@ class TestomatioMCPServer {
|
|
|
902
902
|
const formattedPlan = this.formatModel(data.data, 'plan', [
|
|
903
903
|
'title', 'test-count', 'kind', 'created-at', 'tests-ids', 'labels'
|
|
904
904
|
]);
|
|
905
|
-
|
|
905
|
+
|
|
906
906
|
return {
|
|
907
907
|
content: [
|
|
908
908
|
{
|
|
@@ -914,19 +914,33 @@ class TestomatioMCPServer {
|
|
|
914
914
|
}
|
|
915
915
|
|
|
916
916
|
async createTest(args) {
|
|
917
|
-
const {
|
|
917
|
+
const { suite_id, labels_ids, ...attributes } = args;
|
|
918
918
|
const requestData = {
|
|
919
|
-
|
|
920
|
-
|
|
919
|
+
data: {
|
|
920
|
+
type: 'tests',
|
|
921
|
+
attributes: Object.fromEntries(Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v]))
|
|
922
|
+
}
|
|
921
923
|
};
|
|
924
|
+
|
|
925
|
+
if (suite_id) {
|
|
926
|
+
requestData.data.relationships = {
|
|
927
|
+
suite: {
|
|
928
|
+
data: {
|
|
929
|
+
type: 'suites',
|
|
930
|
+
id: suite_id
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
922
936
|
if (labels_ids) requestData.labels_ids = labels_ids;
|
|
923
937
|
|
|
924
938
|
const data = await this.makePostRequest('/tests', requestData);
|
|
925
939
|
const formattedTest = this.formatModel(data.data, 'test', [
|
|
926
|
-
'title', 'description', 'code', 'priority',
|
|
940
|
+
'title', 'description', 'code', 'priority',
|
|
927
941
|
'state', 'suite-id', 'tags', 'file'
|
|
928
942
|
]);
|
|
929
|
-
|
|
943
|
+
|
|
930
944
|
return {
|
|
931
945
|
content: [
|
|
932
946
|
{
|
|
@@ -938,19 +952,33 @@ class TestomatioMCPServer {
|
|
|
938
952
|
}
|
|
939
953
|
|
|
940
954
|
async updateTest(args) {
|
|
941
|
-
const { test_id, labels_ids, ...attributes } = args;
|
|
955
|
+
const { test_id, suite_id, labels_ids, ...attributes } = args;
|
|
942
956
|
const requestData = {
|
|
943
|
-
|
|
944
|
-
|
|
957
|
+
data: {
|
|
958
|
+
type: 'tests',
|
|
959
|
+
attributes: Object.fromEntries(Object.entries(attributes).map(([k, v]) => [k.replace(/_/g, '-'), v]))
|
|
960
|
+
}
|
|
945
961
|
};
|
|
962
|
+
|
|
963
|
+
if (suite_id) {
|
|
964
|
+
requestData.data.relationships = {
|
|
965
|
+
suite: {
|
|
966
|
+
data: {
|
|
967
|
+
type: 'suites',
|
|
968
|
+
id: suite_id
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
946
974
|
if (labels_ids) requestData.labels_ids = labels_ids;
|
|
947
975
|
|
|
948
|
-
const data = await this.makePutRequest(`/tests/${
|
|
976
|
+
const data = await this.makePutRequest(`/tests/${test_id}`, requestData);
|
|
949
977
|
const formattedTest = this.formatModel(data.data, 'test', [
|
|
950
|
-
'title', 'description', 'code', 'priority',
|
|
978
|
+
'title', 'description', 'code', 'priority',
|
|
951
979
|
'state', 'suite-id', 'tags', 'file'
|
|
952
980
|
]);
|
|
953
|
-
|
|
981
|
+
|
|
954
982
|
return {
|
|
955
983
|
content: [
|
|
956
984
|
{
|
|
@@ -989,7 +1017,7 @@ function parseArgs() {
|
|
|
989
1017
|
.parse();
|
|
990
1018
|
|
|
991
1019
|
const options = program.opts();
|
|
992
|
-
|
|
1020
|
+
|
|
993
1021
|
const token = options.token || process.env.TESTOMATIO_API_TOKEN;
|
|
994
1022
|
const projectId = options.project;
|
|
995
1023
|
const baseUrl = options.baseUrl || process.env.TESTOMATIO_BASE_URL || 'https://app.testomat.io';
|