@testomatio/mcp 2.2.0-beta → 2.2.0-beta.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.
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/index.js +2 -1
- package/src/mcp/list-projection.js +56 -0
- package/src/mcp/registry/attachments.js +2 -2
- package/src/mcp/registry/handlers.js +11 -3
- package/src/mcp/registry/issues.js +6 -3
- package/src/mcp/registry/listings.js +40 -16
- package/src/mcp/tool-definitions.js +3 -3
package/README.md
CHANGED
|
@@ -273,6 +273,7 @@ NODE_EXTRA_CA_CERTS=/path/to/company-root-ca.pem testomatio-mcp --token <TOKEN>
|
|
|
273
273
|
|
|
274
274
|
- **Run Status** - Use `runs_update` with `status_event` for transitions (finish, launch, rerun, etc.)
|
|
275
275
|
- **Search/Filter** - No dedicated `/search` endpoints; filtering is done via the `*_list` tools (`tql` for tests and runs, OpenAPI-aligned filters for other entities)
|
|
276
|
+
- **Slim List Responses** - List tools request compact API responses by default and omit heavy entity fields and null values. Pass `verbose: true` to return full objects, or `fields: ["id", "title", "description"]` to return only selected fields. Both options disable the backend `slim=true` request so heavy fields remain available when requested.
|
|
276
277
|
- **TQL** - Use `tql` as the single search/filter input for `tests_list` and `runs_list`
|
|
277
278
|
- **TQL Syntax** - For user-facing syntax details and more examples, see the official TQL docs: https://docs.testomat.io/advanced/tql/
|
|
278
279
|
- **TQL Scope** - TQL parameter descriptions keep the documented field whitelist in-band; call `tql_help` for syntax details and additional examples
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ConfigurationError } from './core/errors.js';
|
|
|
4
4
|
import { createLogger } from './core/logger.js';
|
|
5
5
|
import { TestomatioMCPServer } from './mcp/server.js';
|
|
6
6
|
import { TOOL_DEFINITIONS } from './mcp/tool-definitions.js';
|
|
7
|
-
import { slimList, withListOptions } from './mcp/list-projection.js';
|
|
7
|
+
import { backendSlimQuery, slimList, withListOptions } from './mcp/list-projection.js';
|
|
8
8
|
import { selectTools } from './mcp/tool-profiles.js';
|
|
9
9
|
import {
|
|
10
10
|
ANALYTICS_STATS_TQL_INPUT_DESCRIPTION,
|
|
@@ -22,6 +22,7 @@ export {
|
|
|
22
22
|
ConfigurationError,
|
|
23
23
|
TOOL_DEFINITIONS,
|
|
24
24
|
slimList,
|
|
25
|
+
backendSlimQuery,
|
|
25
26
|
withListOptions,
|
|
26
27
|
selectTools,
|
|
27
28
|
};
|
|
@@ -96,6 +96,18 @@ export function slimList(payload, { verbose = false, fields, entity } = {}) {
|
|
|
96
96
|
return withViewMarker(applyToEnvelope(payload, (item) => stripHeavy(item, heavyFields)));
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Ask the API to omit heavy list fields only when MCP does not need them for a
|
|
101
|
+
* full or custom projection. When `count` is requested the response is meta only,
|
|
102
|
+
* so `slim` is irrelevant and must not be sent alongside `count`.
|
|
103
|
+
*/
|
|
104
|
+
export function backendSlimQuery({ verbose = false, fields, count = false } = {}) {
|
|
105
|
+
if (count) {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
return !verbose && !(Array.isArray(fields) && fields.length) ? { slim: true } : {};
|
|
109
|
+
}
|
|
110
|
+
|
|
99
111
|
const LIST_OPTION_PROPERTIES = {
|
|
100
112
|
verbose: {
|
|
101
113
|
type: 'boolean',
|
|
@@ -138,3 +150,47 @@ export function withListOptions(tools, { extraNames = [] } = {}) {
|
|
|
138
150
|
};
|
|
139
151
|
});
|
|
140
152
|
}
|
|
153
|
+
|
|
154
|
+
// Scoped list tools (*_issues_list, *_attachments_list) are filtered to one entity,
|
|
155
|
+
// so count/group_by aggregation does not apply there.
|
|
156
|
+
const SCOPED_LIST_INFIXES = ['_issues_', '_attachments_'];
|
|
157
|
+
|
|
158
|
+
function isPrimaryListToolName(name) {
|
|
159
|
+
return (
|
|
160
|
+
typeof name === 'string' &&
|
|
161
|
+
name.endsWith('_list') &&
|
|
162
|
+
!SCOPED_LIST_INFIXES.some((infix) => name.includes(infix))
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const COUNT_PROPERTY = {
|
|
167
|
+
type: 'boolean',
|
|
168
|
+
description:
|
|
169
|
+
'Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.',
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const GROUP_BY_PROPERTY = {
|
|
173
|
+
type: 'string',
|
|
174
|
+
description:
|
|
175
|
+
'Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.',
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Inject `count` and `group_by` into every primary list tool. `group_by` is a
|
|
180
|
+
* free-form string — the backend is the source of truth for which fields each
|
|
181
|
+
* resource accepts. Scoped list tools (*_issues_list, *_attachments_list) are skipped.
|
|
182
|
+
*
|
|
183
|
+
* @param {Array} tools
|
|
184
|
+
*/
|
|
185
|
+
export function withCountGroupOptions(tools) {
|
|
186
|
+
return tools.map((tool) => {
|
|
187
|
+
if (!tool || !isPrimaryListToolName(tool.name)) return tool;
|
|
188
|
+
const inputSchema = tool.inputSchema || { type: 'object', properties: {} };
|
|
189
|
+
const properties = {
|
|
190
|
+
...(inputSchema.properties || {}),
|
|
191
|
+
count: COUNT_PROPERTY,
|
|
192
|
+
group_by: GROUP_BY_PROPERTY,
|
|
193
|
+
};
|
|
194
|
+
return { ...tool, inputSchema: { ...inputSchema, properties } };
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -4,9 +4,9 @@ import path from 'node:path';
|
|
|
4
4
|
import { ATTACHMENT_RESOURCE_KEYS } from '../configs/attachments-config.js';
|
|
5
5
|
|
|
6
6
|
export const attachmentMethods = {
|
|
7
|
-
listAttachmentsForKey({ resourceKey, resourceId }) {
|
|
7
|
+
listAttachmentsForKey({ resourceKey, resourceId, slim }) {
|
|
8
8
|
this.assertSupportedAttachmentResourceKey(resourceKey);
|
|
9
|
-
return this.apiClient.list('attachments', { [resourceKey]: resourceId });
|
|
9
|
+
return this.apiClient.list('attachments', { [resourceKey]: resourceId, slim });
|
|
10
10
|
},
|
|
11
11
|
|
|
12
12
|
async uploadAttachmentForKey({ resourceKey, resourceId, filePath }) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ENTITY_CRUD_CONFIGS } from '../configs/entity-crud-config.js';
|
|
2
2
|
import { ATTACHMENT_SCOPED_TOOL_CONFIGS } from '../configs/attachments-config.js';
|
|
3
3
|
import { ISSUE_SCOPED_TOOL_CONFIGS } from '../configs/issues-config.js';
|
|
4
|
-
import { slimList } from '../list-projection.js';
|
|
4
|
+
import { backendSlimQuery, slimList } from '../list-projection.js';
|
|
5
5
|
|
|
6
6
|
export const handlerMethods = {
|
|
7
7
|
registerEntityCrudHandlers(handlers) {
|
|
@@ -10,6 +10,7 @@ export const handlerMethods = {
|
|
|
10
10
|
|
|
11
11
|
handlers[`${toolPrefix}_list`] = async (args = {}) => {
|
|
12
12
|
const { verbose, fields, ...listArgs } = args;
|
|
13
|
+
Object.assign(listArgs, backendSlimQuery({ verbose, fields, count: listArgs.count }));
|
|
13
14
|
return this.asText(
|
|
14
15
|
slimList(await this[listMethod](listArgs), {
|
|
15
16
|
verbose,
|
|
@@ -43,6 +44,7 @@ export const handlerMethods = {
|
|
|
43
44
|
page: args.page,
|
|
44
45
|
per_page: args.per_page,
|
|
45
46
|
source: args.source,
|
|
47
|
+
...backendSlimQuery(args),
|
|
46
48
|
}),
|
|
47
49
|
{ verbose: args.verbose, fields: args.fields, entity: 'issues' }
|
|
48
50
|
)
|
|
@@ -71,6 +73,7 @@ export const handlerMethods = {
|
|
|
71
73
|
await this.listAttachmentsForKey({
|
|
72
74
|
resourceKey,
|
|
73
75
|
resourceId: this.pickRequiredArg(args, resourceKey),
|
|
76
|
+
...backendSlimQuery(args),
|
|
74
77
|
}),
|
|
75
78
|
{ verbose: args.verbose, fields: args.fields, entity: 'attachments' }
|
|
76
79
|
)
|
|
@@ -99,12 +102,16 @@ export const handlerMethods = {
|
|
|
99
102
|
registerGlobalHandlers(handlers) {
|
|
100
103
|
handlers.project_info = async () => this.asText(await this.apiClient.get('info'));
|
|
101
104
|
|
|
102
|
-
handlers.tags_list = async (args = {}) =>
|
|
103
|
-
|
|
105
|
+
handlers.tags_list = async (args = {}) => {
|
|
106
|
+
const { verbose, fields, ...listArgs } = args;
|
|
107
|
+
Object.assign(listArgs, backendSlimQuery({ verbose, fields, count: listArgs.count }));
|
|
108
|
+
return this.asText(slimList(await this.listTags(listArgs), { ...args, entity: 'tags' }));
|
|
109
|
+
};
|
|
104
110
|
handlers.tags_get = async ({ tag_id: tagId }) => this.asText(await this.getTagByTitle(tagId));
|
|
105
111
|
|
|
106
112
|
handlers.milestones_list = async (args = {}) => {
|
|
107
113
|
const { verbose, fields, ...listArgs } = args;
|
|
114
|
+
Object.assign(listArgs, backendSlimQuery({ verbose, fields, count: listArgs.count }));
|
|
108
115
|
return this.asText(
|
|
109
116
|
slimList(await this.listMilestones(listArgs), { verbose, fields, entity: 'milestones' })
|
|
110
117
|
);
|
|
@@ -114,6 +121,7 @@ export const handlerMethods = {
|
|
|
114
121
|
|
|
115
122
|
handlers.issues_list = async (args = {}) => {
|
|
116
123
|
const { verbose, fields, ...listArgs } = args;
|
|
124
|
+
Object.assign(listArgs, backendSlimQuery({ verbose, fields, count: listArgs.count }));
|
|
117
125
|
return this.asText(
|
|
118
126
|
slimList(await this.listIssues(listArgs), { verbose, fields, entity: 'issues' })
|
|
119
127
|
);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { ISSUE_RESOURCE_KEYS } from '../configs/issues-config.js';
|
|
2
2
|
|
|
3
3
|
export const issueMethods = {
|
|
4
|
-
listIssues({ page, per_page: perPage, source, ...resourceQuery } = {}) {
|
|
4
|
+
listIssues({ page, per_page: perPage, source, slim, ...resourceQuery } = {}) {
|
|
5
5
|
this.validateIssueResourceQuery(resourceQuery, { allowEmpty: true, allowMany: false });
|
|
6
6
|
return this.listIssuesForResource({
|
|
7
7
|
...resourceQuery,
|
|
8
8
|
page,
|
|
9
9
|
per_page: perPage,
|
|
10
10
|
source,
|
|
11
|
+
slim,
|
|
11
12
|
});
|
|
12
13
|
},
|
|
13
14
|
|
|
@@ -20,22 +21,24 @@ export const issueMethods = {
|
|
|
20
21
|
});
|
|
21
22
|
},
|
|
22
23
|
|
|
23
|
-
listIssuesForResource({ page, per_page: perPage, source, ...resourceQuery } = {}) {
|
|
24
|
+
listIssuesForResource({ page, per_page: perPage, source, slim, ...resourceQuery } = {}) {
|
|
24
25
|
return this.apiClient.list('issues', {
|
|
25
26
|
...resourceQuery,
|
|
26
27
|
page,
|
|
27
28
|
per_page: perPage,
|
|
28
29
|
source,
|
|
30
|
+
slim,
|
|
29
31
|
});
|
|
30
32
|
},
|
|
31
33
|
|
|
32
|
-
listIssuesForKey({ resourceKey, resourceId, page, per_page: perPage, source }) {
|
|
34
|
+
listIssuesForKey({ resourceKey, resourceId, page, per_page: perPage, source, slim }) {
|
|
33
35
|
this.assertSupportedIssueResourceKey(resourceKey);
|
|
34
36
|
return this.listIssuesForResource({
|
|
35
37
|
[resourceKey]: resourceId,
|
|
36
38
|
page,
|
|
37
39
|
per_page: perPage,
|
|
38
40
|
source,
|
|
41
|
+
slim,
|
|
39
42
|
});
|
|
40
43
|
},
|
|
41
44
|
|
|
@@ -3,15 +3,21 @@ export const listingMethods = {
|
|
|
3
3
|
page,
|
|
4
4
|
per_page: perPage,
|
|
5
5
|
tql,
|
|
6
|
+
count,
|
|
7
|
+
group_by: groupBy,
|
|
8
|
+
slim,
|
|
6
9
|
} = {}) {
|
|
7
10
|
return this.apiClient.list('tests', {
|
|
8
11
|
page,
|
|
9
12
|
per_page: perPage,
|
|
10
13
|
tql,
|
|
14
|
+
count,
|
|
15
|
+
group_by: groupBy,
|
|
16
|
+
slim,
|
|
11
17
|
});
|
|
12
18
|
},
|
|
13
19
|
|
|
14
|
-
listSuites({ page, per_page: perPage, file_type: fileType, tag, labels, search_text: searchText } = {}) {
|
|
20
|
+
listSuites({ page, per_page: perPage, file_type: fileType, tag, labels, search_text: searchText, count, group_by: groupBy, slim } = {}) {
|
|
15
21
|
return this.apiClient.list('suites', {
|
|
16
22
|
page,
|
|
17
23
|
per_page: perPage,
|
|
@@ -19,6 +25,9 @@ export const listingMethods = {
|
|
|
19
25
|
tag,
|
|
20
26
|
labels,
|
|
21
27
|
search_text: searchText,
|
|
28
|
+
count,
|
|
29
|
+
group_by: groupBy,
|
|
30
|
+
slim,
|
|
22
31
|
});
|
|
23
32
|
},
|
|
24
33
|
|
|
@@ -26,11 +35,17 @@ export const listingMethods = {
|
|
|
26
35
|
page,
|
|
27
36
|
per_page: perPage,
|
|
28
37
|
tql,
|
|
38
|
+
count,
|
|
39
|
+
group_by: groupBy,
|
|
40
|
+
slim,
|
|
29
41
|
} = {}) {
|
|
30
42
|
return this.apiClient.list('runs', {
|
|
31
43
|
page,
|
|
32
44
|
per_page: perPage,
|
|
33
45
|
tql,
|
|
46
|
+
count,
|
|
47
|
+
group_by: groupBy,
|
|
48
|
+
slim,
|
|
34
49
|
});
|
|
35
50
|
},
|
|
36
51
|
|
|
@@ -53,6 +68,9 @@ export const listingMethods = {
|
|
|
53
68
|
envs,
|
|
54
69
|
rungroups,
|
|
55
70
|
defects,
|
|
71
|
+
count,
|
|
72
|
+
group_by: groupBy,
|
|
73
|
+
slim,
|
|
56
74
|
} = {}) {
|
|
57
75
|
return this.apiClient.list('testruns', {
|
|
58
76
|
page,
|
|
@@ -73,26 +91,29 @@ export const listingMethods = {
|
|
|
73
91
|
envs: Array.isArray(envs) ? envs.join(',') : envs,
|
|
74
92
|
rungroups: Array.isArray(rungroups) ? rungroups.join(',') : rungroups,
|
|
75
93
|
defects,
|
|
94
|
+
count,
|
|
95
|
+
group_by: groupBy,
|
|
96
|
+
slim,
|
|
76
97
|
});
|
|
77
98
|
},
|
|
78
99
|
|
|
79
|
-
listRungroups({ page, per_page: perPage } = {}) {
|
|
80
|
-
return this.apiClient.list('rungroups', { page, per_page: perPage });
|
|
100
|
+
listRungroups({ page, per_page: perPage, count, group_by: groupBy, slim } = {}) {
|
|
101
|
+
return this.apiClient.list('rungroups', { page, per_page: perPage, count, group_by: groupBy, slim });
|
|
81
102
|
},
|
|
82
103
|
|
|
83
|
-
listSteps({ page, per_page: perPage } = {}) {
|
|
84
|
-
return this.apiClient.list('steps', { page, per_page: perPage });
|
|
104
|
+
listSteps({ page, per_page: perPage, count, group_by: groupBy, slim } = {}) {
|
|
105
|
+
return this.apiClient.list('steps', { page, per_page: perPage, count, group_by: groupBy, slim });
|
|
85
106
|
},
|
|
86
107
|
|
|
87
|
-
listSnippets({ page, per_page: perPage } = {}) {
|
|
88
|
-
return this.apiClient.list('snippets', { page, per_page: perPage });
|
|
108
|
+
listSnippets({ page, per_page: perPage, count, group_by: groupBy, slim } = {}) {
|
|
109
|
+
return this.apiClient.list('snippets', { page, per_page: perPage, count, group_by: groupBy, slim });
|
|
89
110
|
},
|
|
90
111
|
|
|
91
|
-
listLabels({ page, per_page: perPage } = {}) {
|
|
92
|
-
return this.apiClient.list('labels', { page, per_page: perPage });
|
|
112
|
+
listLabels({ page, per_page: perPage, count, group_by: groupBy, slim } = {}) {
|
|
113
|
+
return this.apiClient.list('labels', { page, per_page: perPage, count, group_by: groupBy, slim });
|
|
93
114
|
},
|
|
94
115
|
|
|
95
|
-
listPlans({ page, per_page: perPage, kind, hidden, labels, search_text: searchText } = {}) {
|
|
116
|
+
listPlans({ page, per_page: perPage, kind, hidden, labels, search_text: searchText, count, group_by: groupBy, slim } = {}) {
|
|
96
117
|
return this.apiClient.list('plans', {
|
|
97
118
|
page,
|
|
98
119
|
per_page: perPage,
|
|
@@ -100,19 +121,22 @@ export const listingMethods = {
|
|
|
100
121
|
hidden,
|
|
101
122
|
'labels[]': labels,
|
|
102
123
|
search_text: searchText,
|
|
124
|
+
count,
|
|
125
|
+
group_by: groupBy,
|
|
126
|
+
slim,
|
|
103
127
|
});
|
|
104
128
|
},
|
|
105
129
|
|
|
106
|
-
listRequirements({ page, per_page: perPage, source, scope } = {}) {
|
|
107
|
-
return this.apiClient.list('requirements', { page, per_page: perPage, source, scope });
|
|
130
|
+
listRequirements({ page, per_page: perPage, source, scope, count, group_by: groupBy, slim } = {}) {
|
|
131
|
+
return this.apiClient.list('requirements', { page, per_page: perPage, source, scope, count, group_by: groupBy, slim });
|
|
108
132
|
},
|
|
109
133
|
|
|
110
|
-
listMilestones({ page, per_page: perPage, type, status } = {}) {
|
|
111
|
-
return this.apiClient.list('milestones', { page, per_page: perPage, type, status });
|
|
134
|
+
listMilestones({ page, per_page: perPage, type, status, count, group_by: groupBy, slim } = {}) {
|
|
135
|
+
return this.apiClient.list('milestones', { page, per_page: perPage, type, status, count, group_by: groupBy, slim });
|
|
112
136
|
},
|
|
113
137
|
|
|
114
|
-
listTags() {
|
|
115
|
-
return this.apiClient.list('tags');
|
|
138
|
+
listTags({ count, group_by: groupBy, slim } = {}) {
|
|
139
|
+
return this.apiClient.list('tags', { count, group_by: groupBy, slim });
|
|
116
140
|
},
|
|
117
141
|
|
|
118
142
|
getTagByTitle(tagId) {
|
|
@@ -14,9 +14,9 @@ import { ISSUES_TOOLS } from './definitions/issues.js';
|
|
|
14
14
|
import { PLANS_TOOLS } from './definitions/plans.js';
|
|
15
15
|
import { REQUIREMENTS_TOOLS } from './definitions/requirements.js';
|
|
16
16
|
import { ATTACHMENT_TOOLS } from './definitions/attachments.js';
|
|
17
|
-
import { withListOptions } from './list-projection.js';
|
|
17
|
+
import { withListOptions, withCountGroupOptions } from './list-projection.js';
|
|
18
18
|
|
|
19
|
-
export const TOOL_DEFINITIONS = withListOptions([
|
|
19
|
+
export const TOOL_DEFINITIONS = withCountGroupOptions(withListOptions([
|
|
20
20
|
...SYSTEM_TOOLS,
|
|
21
21
|
...PROJECT_TOOLS,
|
|
22
22
|
...TESTS_TOOLS,
|
|
@@ -33,4 +33,4 @@ export const TOOL_DEFINITIONS = withListOptions([
|
|
|
33
33
|
...ATTACHMENT_TOOLS,
|
|
34
34
|
...PLANS_TOOLS,
|
|
35
35
|
...REQUIREMENTS_TOOLS,
|
|
36
|
-
]);
|
|
36
|
+
]));
|