@testomatio/mcp 2.2.0-beta.1 → 2.2.0-beta.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/package.json +1 -1
- package/src/api/testomatio-client.js +8 -8
- package/src/mcp/configs/entity-crud-config.js +13 -0
- package/src/mcp/definitions/branches.js +110 -0
- package/src/mcp/definitions/runs.js +11 -5
- package/src/mcp/definitions/suites.js +11 -5
- package/src/mcp/definitions/tests.js +11 -5
- package/src/mcp/list-projection.js +50 -2
- package/src/mcp/registry/handlers.js +61 -26
- package/src/mcp/registry/listings.js +57 -16
- package/src/mcp/registry/payloads.js +27 -21
- package/src/mcp/tool-definitions.js +5 -3
- package/src/mcp/tool-registry.js +5 -2
package/package.json
CHANGED
|
@@ -27,8 +27,8 @@ export class TestomatioApiClient {
|
|
|
27
27
|
return this.http.request('GET', this.buildPath(resource, id), { query });
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
async create(resource, body = {}) {
|
|
31
|
-
return this.mutate('POST', this.buildPath(resource), { body });
|
|
30
|
+
async create(resource, body = {}, query = {}) {
|
|
31
|
+
return this.mutate('POST', this.buildPath(resource), { query, body });
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
async createMultipart(resource, formData, query = {}) {
|
|
@@ -39,16 +39,16 @@ export class TestomatioApiClient {
|
|
|
39
39
|
return this.mutate('POST', this.buildPath(resource), { query, body });
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
async update(resource, id, body = {}) {
|
|
43
|
-
return this.mutate('PUT', this.buildPath(resource, id), { body });
|
|
42
|
+
async update(resource, id, body = {}, query = {}) {
|
|
43
|
+
return this.mutate('PUT', this.buildPath(resource, id), { query, body });
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
async patch(resource, id, body = {}) {
|
|
47
|
-
return this.mutate('PATCH', this.buildPath(resource, id), { body });
|
|
46
|
+
async patch(resource, id, body = {}, query = {}) {
|
|
47
|
+
return this.mutate('PATCH', this.buildPath(resource, id), { query, body });
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
async patchMultipart(resource, id, formData) {
|
|
51
|
-
return this.mutate('PATCH', this.buildPath(resource, id), { body: formData });
|
|
50
|
+
async patchMultipart(resource, id, formData, query = {}) {
|
|
51
|
+
return this.mutate('PATCH', this.buildPath(resource, id), { query, body: formData });
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
async delete(resource, id, query = {}) {
|
|
@@ -8,6 +8,7 @@ export const ENTITY_CRUD_CONFIGS = [
|
|
|
8
8
|
wrapperKey: 'test',
|
|
9
9
|
createMode: 'wrapped',
|
|
10
10
|
updateMode: 'wrapped',
|
|
11
|
+
queryArgs: ['branch'],
|
|
11
12
|
},
|
|
12
13
|
{
|
|
13
14
|
toolPrefix: 'suites',
|
|
@@ -18,6 +19,7 @@ export const ENTITY_CRUD_CONFIGS = [
|
|
|
18
19
|
wrapperKey: 'suite',
|
|
19
20
|
createMode: 'wrapped',
|
|
20
21
|
updateMode: 'wrapped',
|
|
22
|
+
queryArgs: ['branch'],
|
|
21
23
|
},
|
|
22
24
|
{
|
|
23
25
|
toolPrefix: 'runs',
|
|
@@ -26,6 +28,7 @@ export const ENTITY_CRUD_CONFIGS = [
|
|
|
26
28
|
listMethod: 'listRuns',
|
|
27
29
|
createMode: 'run',
|
|
28
30
|
updateMode: 'run',
|
|
31
|
+
queryArgs: ['branch'],
|
|
29
32
|
},
|
|
30
33
|
{
|
|
31
34
|
toolPrefix: 'testruns',
|
|
@@ -98,4 +101,14 @@ export const ENTITY_CRUD_CONFIGS = [
|
|
|
98
101
|
updateMode: 'requirement',
|
|
99
102
|
updateMethod: 'patch',
|
|
100
103
|
},
|
|
104
|
+
{
|
|
105
|
+
toolPrefix: 'branches',
|
|
106
|
+
resource: 'branches',
|
|
107
|
+
idArg: 'branch_id',
|
|
108
|
+
listMethod: 'listBranches',
|
|
109
|
+
payloadBuilder: 'buildBranchPayload',
|
|
110
|
+
wrapperKey: 'branch',
|
|
111
|
+
createMode: 'wrapped',
|
|
112
|
+
updateMode: 'wrapped',
|
|
113
|
+
},
|
|
101
114
|
];
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export const BRANCH_PARAM = {
|
|
2
|
+
"type": "string",
|
|
3
|
+
"description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature."
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export const BRANCHES_TOOLS = [
|
|
7
|
+
{
|
|
8
|
+
"name": "branches_list",
|
|
9
|
+
"description":
|
|
10
|
+
'List project branches (/api/v2/{project_id}/branches). Requires the branches feature (enterprise plan). Use filter[state] / filter[title] to narrow down.',
|
|
11
|
+
"inputSchema": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"properties": {
|
|
14
|
+
"page": {
|
|
15
|
+
"type": "integer",
|
|
16
|
+
"minimum": 1
|
|
17
|
+
},
|
|
18
|
+
"per_page": {
|
|
19
|
+
"type": "integer",
|
|
20
|
+
"minimum": 1,
|
|
21
|
+
"maximum": 100
|
|
22
|
+
},
|
|
23
|
+
"filter_state": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"enum": [
|
|
26
|
+
"active",
|
|
27
|
+
"merged"
|
|
28
|
+
],
|
|
29
|
+
"description": "Filter by branch state"
|
|
30
|
+
},
|
|
31
|
+
"filter_title": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Filter by title (partial substring match)"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"additionalProperties": false
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "branches_get",
|
|
41
|
+
"description": "Get branch by slug",
|
|
42
|
+
"inputSchema": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"properties": {
|
|
45
|
+
"branch_id": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"description": "Branch slug"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"required": [
|
|
51
|
+
"branch_id"
|
|
52
|
+
],
|
|
53
|
+
"additionalProperties": false
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "branches_create",
|
|
58
|
+
"description": "Create branch (/api/v2/{project_id}/branches)",
|
|
59
|
+
"inputSchema": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"properties": {
|
|
62
|
+
"title": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "Branch title; a slug is generated from it"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"required": [
|
|
68
|
+
"title"
|
|
69
|
+
],
|
|
70
|
+
"additionalProperties": false
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "branches_update",
|
|
75
|
+
"description": "Update branch title (/api/v2/{project_id}/branches/{slug})",
|
|
76
|
+
"inputSchema": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"properties": {
|
|
79
|
+
"branch_id": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "Branch slug"
|
|
82
|
+
},
|
|
83
|
+
"title": {
|
|
84
|
+
"type": "string"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"required": [
|
|
88
|
+
"branch_id"
|
|
89
|
+
],
|
|
90
|
+
"additionalProperties": false
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "branches_delete",
|
|
95
|
+
"description": "Delete branch by slug",
|
|
96
|
+
"inputSchema": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"properties": {
|
|
99
|
+
"branch_id": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"description": "Branch slug"
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"required": [
|
|
105
|
+
"branch_id"
|
|
106
|
+
],
|
|
107
|
+
"additionalProperties": false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RUNS_TQL_INPUT_DESCRIPTION, RUNS_TQL_REFERENCE } from './tql-reference.js';
|
|
2
|
+
import { BRANCH_PARAM } from './branches.js';
|
|
2
3
|
|
|
3
4
|
export const RUNS_TOOLS = [
|
|
4
5
|
{
|
|
@@ -19,7 +20,8 @@ export const RUNS_TOOLS = [
|
|
|
19
20
|
"tql": {
|
|
20
21
|
"type": "string",
|
|
21
22
|
"description": RUNS_TQL_INPUT_DESCRIPTION
|
|
22
|
-
}
|
|
23
|
+
},
|
|
24
|
+
"branch": BRANCH_PARAM
|
|
23
25
|
},
|
|
24
26
|
"additionalProperties": false
|
|
25
27
|
}
|
|
@@ -32,7 +34,8 @@ export const RUNS_TOOLS = [
|
|
|
32
34
|
"properties": {
|
|
33
35
|
"run_id": {
|
|
34
36
|
"type": "string"
|
|
35
|
-
}
|
|
37
|
+
},
|
|
38
|
+
"branch": BRANCH_PARAM
|
|
36
39
|
},
|
|
37
40
|
"required": [
|
|
38
41
|
"run_id"
|
|
@@ -135,7 +138,8 @@ export const RUNS_TOOLS = [
|
|
|
135
138
|
],
|
|
136
139
|
"additionalProperties": false
|
|
137
140
|
}
|
|
138
|
-
}
|
|
141
|
+
},
|
|
142
|
+
"branch": BRANCH_PARAM
|
|
139
143
|
},
|
|
140
144
|
"required": [
|
|
141
145
|
"title"
|
|
@@ -240,7 +244,8 @@ export const RUNS_TOOLS = [
|
|
|
240
244
|
],
|
|
241
245
|
"additionalProperties": false
|
|
242
246
|
}
|
|
243
|
-
}
|
|
247
|
+
},
|
|
248
|
+
"branch": BRANCH_PARAM
|
|
244
249
|
},
|
|
245
250
|
"required": [
|
|
246
251
|
"run_id"
|
|
@@ -256,7 +261,8 @@ export const RUNS_TOOLS = [
|
|
|
256
261
|
"properties": {
|
|
257
262
|
"run_id": {
|
|
258
263
|
"type": "string"
|
|
259
|
-
}
|
|
264
|
+
},
|
|
265
|
+
"branch": BRANCH_PARAM
|
|
260
266
|
},
|
|
261
267
|
"required": [
|
|
262
268
|
"run_id"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BRANCH_PARAM } from './branches.js';
|
|
1
2
|
export const SUITES_TOOLS = [
|
|
2
3
|
{
|
|
3
4
|
"name": "suites_list",
|
|
@@ -29,7 +30,8 @@ export const SUITES_TOOLS = [
|
|
|
29
30
|
},
|
|
30
31
|
"search_text": {
|
|
31
32
|
"type": "string"
|
|
32
|
-
}
|
|
33
|
+
},
|
|
34
|
+
"branch": BRANCH_PARAM
|
|
33
35
|
},
|
|
34
36
|
"additionalProperties": false
|
|
35
37
|
}
|
|
@@ -42,7 +44,8 @@ export const SUITES_TOOLS = [
|
|
|
42
44
|
"properties": {
|
|
43
45
|
"suite_id": {
|
|
44
46
|
"type": "string"
|
|
45
|
-
}
|
|
47
|
+
},
|
|
48
|
+
"branch": BRANCH_PARAM
|
|
46
49
|
},
|
|
47
50
|
"required": [
|
|
48
51
|
"suite_id"
|
|
@@ -120,7 +123,8 @@ export const SUITES_TOOLS = [
|
|
|
120
123
|
],
|
|
121
124
|
"additionalProperties": false
|
|
122
125
|
}
|
|
123
|
-
}
|
|
126
|
+
},
|
|
127
|
+
"branch": BRANCH_PARAM
|
|
124
128
|
},
|
|
125
129
|
"required": [
|
|
126
130
|
"title"
|
|
@@ -201,7 +205,8 @@ export const SUITES_TOOLS = [
|
|
|
201
205
|
],
|
|
202
206
|
"additionalProperties": false
|
|
203
207
|
}
|
|
204
|
-
}
|
|
208
|
+
},
|
|
209
|
+
"branch": BRANCH_PARAM
|
|
205
210
|
},
|
|
206
211
|
"required": [
|
|
207
212
|
"suite_id"
|
|
@@ -217,7 +222,8 @@ export const SUITES_TOOLS = [
|
|
|
217
222
|
"properties": {
|
|
218
223
|
"suite_id": {
|
|
219
224
|
"type": "string"
|
|
220
|
-
}
|
|
225
|
+
},
|
|
226
|
+
"branch": BRANCH_PARAM
|
|
221
227
|
},
|
|
222
228
|
"required": [
|
|
223
229
|
"suite_id"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TESTS_TQL_INPUT_DESCRIPTION, TESTS_TQL_REFERENCE } from './tql-reference.js';
|
|
2
|
+
import { BRANCH_PARAM } from './branches.js';
|
|
2
3
|
|
|
3
4
|
export const TESTS_TOOLS = [
|
|
4
5
|
{
|
|
@@ -19,7 +20,8 @@ export const TESTS_TOOLS = [
|
|
|
19
20
|
"tql": {
|
|
20
21
|
"type": "string",
|
|
21
22
|
"description": TESTS_TQL_INPUT_DESCRIPTION
|
|
22
|
-
}
|
|
23
|
+
},
|
|
24
|
+
"branch": BRANCH_PARAM
|
|
23
25
|
},
|
|
24
26
|
"additionalProperties": false
|
|
25
27
|
}
|
|
@@ -32,7 +34,8 @@ export const TESTS_TOOLS = [
|
|
|
32
34
|
"properties": {
|
|
33
35
|
"test_id": {
|
|
34
36
|
"type": "string"
|
|
35
|
-
}
|
|
37
|
+
},
|
|
38
|
+
"branch": BRANCH_PARAM
|
|
36
39
|
},
|
|
37
40
|
"required": [
|
|
38
41
|
"test_id"
|
|
@@ -116,7 +119,8 @@ export const TESTS_TOOLS = [
|
|
|
116
119
|
],
|
|
117
120
|
"additionalProperties": false
|
|
118
121
|
}
|
|
119
|
-
}
|
|
122
|
+
},
|
|
123
|
+
"branch": BRANCH_PARAM
|
|
120
124
|
},
|
|
121
125
|
"required": [
|
|
122
126
|
"title",
|
|
@@ -207,7 +211,8 @@ export const TESTS_TOOLS = [
|
|
|
207
211
|
],
|
|
208
212
|
"additionalProperties": false
|
|
209
213
|
}
|
|
210
|
-
}
|
|
214
|
+
},
|
|
215
|
+
"branch": BRANCH_PARAM
|
|
211
216
|
},
|
|
212
217
|
"required": [
|
|
213
218
|
"test_id"
|
|
@@ -223,7 +228,8 @@ export const TESTS_TOOLS = [
|
|
|
223
228
|
"properties": {
|
|
224
229
|
"test_id": {
|
|
225
230
|
"type": "string"
|
|
226
|
-
}
|
|
231
|
+
},
|
|
232
|
+
"branch": BRANCH_PARAM
|
|
227
233
|
},
|
|
228
234
|
"required": [
|
|
229
235
|
"test_id"
|
|
@@ -98,9 +98,13 @@ export function slimList(payload, { verbose = false, fields, entity } = {}) {
|
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
100
|
* Ask the API to omit heavy list fields only when MCP does not need them for a
|
|
101
|
-
* full or custom projection.
|
|
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`.
|
|
102
103
|
*/
|
|
103
|
-
export function backendSlimQuery({ verbose = false, fields } = {}) {
|
|
104
|
+
export function backendSlimQuery({ verbose = false, fields, count = false } = {}) {
|
|
105
|
+
if (count) {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
104
108
|
return !verbose && !(Array.isArray(fields) && fields.length) ? { slim: true } : {};
|
|
105
109
|
}
|
|
106
110
|
|
|
@@ -146,3 +150,47 @@ export function withListOptions(tools, { extraNames = [] } = {}) {
|
|
|
146
150
|
};
|
|
147
151
|
});
|
|
148
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
|
+
}
|
|
@@ -10,7 +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 }));
|
|
13
|
+
Object.assign(listArgs, backendSlimQuery({ verbose, fields, count: listArgs.count }));
|
|
14
14
|
return this.asText(
|
|
15
15
|
slimList(await this[listMethod](listArgs), {
|
|
16
16
|
verbose,
|
|
@@ -19,17 +19,27 @@ export const handlerMethods = {
|
|
|
19
19
|
})
|
|
20
20
|
);
|
|
21
21
|
};
|
|
22
|
-
handlers[`${toolPrefix}_get`] = async (args = {}) =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
handlers[`${toolPrefix}_get`] = async (args = {}) => {
|
|
23
|
+
const id = this.pickRequiredArg(args, idArg);
|
|
24
|
+
return this.asText(
|
|
25
|
+
await this.apiClient.get(resource, id, this.pickQueryArgs(spec, args))
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
handlers[`${toolPrefix}_create`] = async (args = {}) => {
|
|
29
|
+
const { query, payloadArgs } = this.splitQueryArgs(spec, args);
|
|
30
|
+
return this.asText(await this.executeCreate(spec, payloadArgs, query));
|
|
31
|
+
};
|
|
26
32
|
handlers[`${toolPrefix}_update`] = async (args = {}) => {
|
|
27
33
|
const id = this.pickRequiredArg(args, idArg);
|
|
28
|
-
const payloadArgs = this.
|
|
29
|
-
return this.asText(await this.executeUpdate(spec, id, payloadArgs));
|
|
34
|
+
const { query, payloadArgs } = this.splitQueryArgs(spec, this.omitArgs(args, [idArg]));
|
|
35
|
+
return this.asText(await this.executeUpdate(spec, id, payloadArgs, query));
|
|
36
|
+
};
|
|
37
|
+
handlers[`${toolPrefix}_delete`] = async (args = {}) => {
|
|
38
|
+
const id = this.pickRequiredArg(args, idArg);
|
|
39
|
+
return this.asText(
|
|
40
|
+
await this.apiClient.delete(resource, id, this.pickQueryArgs(spec, args))
|
|
41
|
+
);
|
|
30
42
|
};
|
|
31
|
-
handlers[`${toolPrefix}_delete`] = async (args = {}) =>
|
|
32
|
-
this.asText(await this.apiClient.delete(resource, this.pickRequiredArg(args, idArg)));
|
|
33
43
|
}
|
|
34
44
|
},
|
|
35
45
|
|
|
@@ -102,15 +112,16 @@ export const handlerMethods = {
|
|
|
102
112
|
registerGlobalHandlers(handlers) {
|
|
103
113
|
handlers.project_info = async () => this.asText(await this.apiClient.get('info'));
|
|
104
114
|
|
|
105
|
-
handlers.tags_list = async (args = {}) =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
);
|
|
115
|
+
handlers.tags_list = async (args = {}) => {
|
|
116
|
+
const { verbose, fields, ...listArgs } = args;
|
|
117
|
+
Object.assign(listArgs, backendSlimQuery({ verbose, fields, count: listArgs.count }));
|
|
118
|
+
return this.asText(slimList(await this.listTags(listArgs), { ...args, entity: 'tags' }));
|
|
119
|
+
};
|
|
109
120
|
handlers.tags_get = async ({ tag_id: tagId }) => this.asText(await this.getTagByTitle(tagId));
|
|
110
121
|
|
|
111
122
|
handlers.milestones_list = async (args = {}) => {
|
|
112
123
|
const { verbose, fields, ...listArgs } = args;
|
|
113
|
-
Object.assign(listArgs, backendSlimQuery({ verbose, fields }));
|
|
124
|
+
Object.assign(listArgs, backendSlimQuery({ verbose, fields, count: listArgs.count }));
|
|
114
125
|
return this.asText(
|
|
115
126
|
slimList(await this.listMilestones(listArgs), { verbose, fields, entity: 'milestones' })
|
|
116
127
|
);
|
|
@@ -120,7 +131,7 @@ export const handlerMethods = {
|
|
|
120
131
|
|
|
121
132
|
handlers.issues_list = async (args = {}) => {
|
|
122
133
|
const { verbose, fields, ...listArgs } = args;
|
|
123
|
-
Object.assign(listArgs, backendSlimQuery({ verbose, fields }));
|
|
134
|
+
Object.assign(listArgs, backendSlimQuery({ verbose, fields, count: listArgs.count }));
|
|
124
135
|
return this.asText(
|
|
125
136
|
slimList(await this.listIssues(listArgs), { verbose, fields, entity: 'issues' })
|
|
126
137
|
);
|
|
@@ -138,34 +149,58 @@ export const handlerMethods = {
|
|
|
138
149
|
return value;
|
|
139
150
|
},
|
|
140
151
|
|
|
141
|
-
|
|
152
|
+
omitArgs(args = {}, keys) {
|
|
142
153
|
const payload = { ...args };
|
|
143
|
-
|
|
154
|
+
for (const key of keys) {
|
|
155
|
+
delete payload[key];
|
|
156
|
+
}
|
|
144
157
|
return payload;
|
|
145
158
|
},
|
|
146
159
|
|
|
147
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Extract the entity's query args (spec.queryArgs, e.g. branch) from tool args
|
|
162
|
+
* without knowing their names in the handlers.
|
|
163
|
+
*/
|
|
164
|
+
splitQueryArgs(spec, args = {}) {
|
|
165
|
+
const queryArgs = spec.queryArgs || [];
|
|
166
|
+
const query = {};
|
|
167
|
+
const payloadArgs = { ...args };
|
|
168
|
+
for (const key of queryArgs) {
|
|
169
|
+
if (args[key] !== undefined) {
|
|
170
|
+
query[key] = args[key];
|
|
171
|
+
delete payloadArgs[key];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { query, payloadArgs };
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
pickQueryArgs(spec, args = {}) {
|
|
178
|
+
const { query } = this.splitQueryArgs(spec, args);
|
|
179
|
+
return query;
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
executeCreate(spec, args = {}, query = {}) {
|
|
148
183
|
if (spec.createMode === 'run') {
|
|
149
|
-
return this.createRunWithFallback(args);
|
|
184
|
+
return this.createRunWithFallback(args, query);
|
|
150
185
|
}
|
|
151
186
|
if (spec.createMode === 'requirement') {
|
|
152
|
-
return this.createRequirement(args);
|
|
187
|
+
return this.createRequirement(args, query);
|
|
153
188
|
}
|
|
154
189
|
const payload = this[spec.payloadBuilder](args);
|
|
155
|
-
return this.createWrapped(spec.resource, spec.wrapperKey, payload);
|
|
190
|
+
return this.createWrapped(spec.resource, spec.wrapperKey, payload, query);
|
|
156
191
|
},
|
|
157
192
|
|
|
158
|
-
executeUpdate(spec, id, args = {}) {
|
|
193
|
+
executeUpdate(spec, id, args = {}, query = {}) {
|
|
159
194
|
if (spec.updateMode === 'run') {
|
|
160
|
-
return this.updateRunWithFallback(id, args);
|
|
195
|
+
return this.updateRunWithFallback(id, args, query);
|
|
161
196
|
}
|
|
162
197
|
if (spec.updateMode === 'requirement') {
|
|
163
|
-
return this.updateRequirement(id, args);
|
|
198
|
+
return this.updateRequirement(id, args, query);
|
|
164
199
|
}
|
|
165
200
|
const payload = this[spec.payloadBuilder](args);
|
|
166
201
|
if (spec.updateMethod === 'patch') {
|
|
167
|
-
return this.patchWrapped(spec.resource, id, spec.wrapperKey, payload);
|
|
202
|
+
return this.patchWrapped(spec.resource, id, spec.wrapperKey, payload, query);
|
|
168
203
|
}
|
|
169
|
-
return this.updateWrapped(spec.resource, id, spec.wrapperKey, payload);
|
|
204
|
+
return this.updateWrapped(spec.resource, id, spec.wrapperKey, payload, query);
|
|
170
205
|
},
|
|
171
206
|
};
|
|
@@ -3,17 +3,23 @@ export const listingMethods = {
|
|
|
3
3
|
page,
|
|
4
4
|
per_page: perPage,
|
|
5
5
|
tql,
|
|
6
|
+
count,
|
|
7
|
+
group_by: groupBy,
|
|
6
8
|
slim,
|
|
9
|
+
branch,
|
|
7
10
|
} = {}) {
|
|
8
11
|
return this.apiClient.list('tests', {
|
|
9
12
|
page,
|
|
10
13
|
per_page: perPage,
|
|
11
14
|
tql,
|
|
15
|
+
count,
|
|
16
|
+
group_by: groupBy,
|
|
12
17
|
slim,
|
|
18
|
+
branch,
|
|
13
19
|
});
|
|
14
20
|
},
|
|
15
21
|
|
|
16
|
-
listSuites({ page, per_page: perPage, file_type: fileType, tag, labels, search_text: searchText, slim } = {}) {
|
|
22
|
+
listSuites({ page, per_page: perPage, file_type: fileType, tag, labels, search_text: searchText, count, group_by: groupBy, slim, branch } = {}) {
|
|
17
23
|
return this.apiClient.list('suites', {
|
|
18
24
|
page,
|
|
19
25
|
per_page: perPage,
|
|
@@ -21,7 +27,10 @@ export const listingMethods = {
|
|
|
21
27
|
tag,
|
|
22
28
|
labels,
|
|
23
29
|
search_text: searchText,
|
|
30
|
+
count,
|
|
31
|
+
group_by: groupBy,
|
|
24
32
|
slim,
|
|
33
|
+
branch,
|
|
25
34
|
});
|
|
26
35
|
},
|
|
27
36
|
|
|
@@ -29,13 +38,19 @@ export const listingMethods = {
|
|
|
29
38
|
page,
|
|
30
39
|
per_page: perPage,
|
|
31
40
|
tql,
|
|
41
|
+
count,
|
|
42
|
+
group_by: groupBy,
|
|
32
43
|
slim,
|
|
44
|
+
branch,
|
|
33
45
|
} = {}) {
|
|
34
46
|
return this.apiClient.list('runs', {
|
|
35
47
|
page,
|
|
36
48
|
per_page: perPage,
|
|
37
49
|
tql,
|
|
50
|
+
count,
|
|
51
|
+
group_by: groupBy,
|
|
38
52
|
slim,
|
|
53
|
+
branch,
|
|
39
54
|
});
|
|
40
55
|
},
|
|
41
56
|
|
|
@@ -58,6 +73,8 @@ export const listingMethods = {
|
|
|
58
73
|
envs,
|
|
59
74
|
rungroups,
|
|
60
75
|
defects,
|
|
76
|
+
count,
|
|
77
|
+
group_by: groupBy,
|
|
61
78
|
slim,
|
|
62
79
|
} = {}) {
|
|
63
80
|
return this.apiClient.list('testruns', {
|
|
@@ -79,27 +96,29 @@ export const listingMethods = {
|
|
|
79
96
|
envs: Array.isArray(envs) ? envs.join(',') : envs,
|
|
80
97
|
rungroups: Array.isArray(rungroups) ? rungroups.join(',') : rungroups,
|
|
81
98
|
defects,
|
|
99
|
+
count,
|
|
100
|
+
group_by: groupBy,
|
|
82
101
|
slim,
|
|
83
102
|
});
|
|
84
103
|
},
|
|
85
104
|
|
|
86
|
-
listRungroups({ page, per_page: perPage, slim } = {}) {
|
|
87
|
-
return this.apiClient.list('rungroups', { page, per_page: perPage, slim });
|
|
105
|
+
listRungroups({ page, per_page: perPage, count, group_by: groupBy, slim } = {}) {
|
|
106
|
+
return this.apiClient.list('rungroups', { page, per_page: perPage, count, group_by: groupBy, slim });
|
|
88
107
|
},
|
|
89
108
|
|
|
90
|
-
listSteps({ page, per_page: perPage, slim } = {}) {
|
|
91
|
-
return this.apiClient.list('steps', { page, per_page: perPage, slim });
|
|
109
|
+
listSteps({ page, per_page: perPage, count, group_by: groupBy, slim } = {}) {
|
|
110
|
+
return this.apiClient.list('steps', { page, per_page: perPage, count, group_by: groupBy, slim });
|
|
92
111
|
},
|
|
93
112
|
|
|
94
|
-
listSnippets({ page, per_page: perPage, slim } = {}) {
|
|
95
|
-
return this.apiClient.list('snippets', { page, per_page: perPage, slim });
|
|
113
|
+
listSnippets({ page, per_page: perPage, count, group_by: groupBy, slim } = {}) {
|
|
114
|
+
return this.apiClient.list('snippets', { page, per_page: perPage, count, group_by: groupBy, slim });
|
|
96
115
|
},
|
|
97
116
|
|
|
98
|
-
listLabels({ page, per_page: perPage, slim } = {}) {
|
|
99
|
-
return this.apiClient.list('labels', { page, per_page: perPage, slim });
|
|
117
|
+
listLabels({ page, per_page: perPage, count, group_by: groupBy, slim } = {}) {
|
|
118
|
+
return this.apiClient.list('labels', { page, per_page: perPage, count, group_by: groupBy, slim });
|
|
100
119
|
},
|
|
101
120
|
|
|
102
|
-
listPlans({ page, per_page: perPage, kind, hidden, labels, search_text: searchText, slim } = {}) {
|
|
121
|
+
listPlans({ page, per_page: perPage, kind, hidden, labels, search_text: searchText, count, group_by: groupBy, slim } = {}) {
|
|
103
122
|
return this.apiClient.list('plans', {
|
|
104
123
|
page,
|
|
105
124
|
per_page: perPage,
|
|
@@ -107,20 +126,42 @@ export const listingMethods = {
|
|
|
107
126
|
hidden,
|
|
108
127
|
'labels[]': labels,
|
|
109
128
|
search_text: searchText,
|
|
129
|
+
count,
|
|
130
|
+
group_by: groupBy,
|
|
110
131
|
slim,
|
|
111
132
|
});
|
|
112
133
|
},
|
|
113
134
|
|
|
114
|
-
listRequirements({ page, per_page: perPage, source, scope, slim } = {}) {
|
|
115
|
-
return this.apiClient.list('requirements', { page, per_page: perPage, source, scope, slim });
|
|
135
|
+
listRequirements({ page, per_page: perPage, source, scope, count, group_by: groupBy, slim } = {}) {
|
|
136
|
+
return this.apiClient.list('requirements', { page, per_page: perPage, source, scope, count, group_by: groupBy, slim });
|
|
116
137
|
},
|
|
117
138
|
|
|
118
|
-
listMilestones({ page, per_page: perPage, type, status, slim } = {}) {
|
|
119
|
-
return this.apiClient.list('milestones', { page, per_page: perPage, type, status, slim });
|
|
139
|
+
listMilestones({ page, per_page: perPage, type, status, count, group_by: groupBy, slim } = {}) {
|
|
140
|
+
return this.apiClient.list('milestones', { page, per_page: perPage, type, status, count, group_by: groupBy, slim });
|
|
120
141
|
},
|
|
121
142
|
|
|
122
|
-
listTags({ slim } = {}) {
|
|
123
|
-
return this.apiClient.list('tags', { slim });
|
|
143
|
+
listTags({ count, group_by: groupBy, slim } = {}) {
|
|
144
|
+
return this.apiClient.list('tags', { count, group_by: groupBy, slim });
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
listBranches({
|
|
148
|
+
page,
|
|
149
|
+
per_page: perPage,
|
|
150
|
+
filter_state: filterState,
|
|
151
|
+
filter_title: filterTitle,
|
|
152
|
+
count,
|
|
153
|
+
group_by: groupBy,
|
|
154
|
+
slim,
|
|
155
|
+
} = {}) {
|
|
156
|
+
return this.apiClient.list('branches', {
|
|
157
|
+
page,
|
|
158
|
+
per_page: perPage,
|
|
159
|
+
'filter[state]': filterState,
|
|
160
|
+
'filter[title]': filterTitle,
|
|
161
|
+
count,
|
|
162
|
+
group_by: groupBy,
|
|
163
|
+
slim,
|
|
164
|
+
});
|
|
124
165
|
},
|
|
125
166
|
|
|
126
167
|
getTagByTitle(tagId) {
|
|
@@ -2,57 +2,58 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
export const payloadMethods = {
|
|
5
|
-
async createWrapped(resource, wrapperKey, payload) {
|
|
5
|
+
async createWrapped(resource, wrapperKey, payload, query = {}) {
|
|
6
6
|
const wrappedBody = { [wrapperKey]: payload };
|
|
7
7
|
try {
|
|
8
|
-
return await this.apiClient.create(resource, payload);
|
|
8
|
+
return await this.apiClient.create(resource, payload, query);
|
|
9
9
|
} catch (error) {
|
|
10
10
|
if (!this.shouldRetryWrappedBody(error, wrapperKey)) {
|
|
11
11
|
throw error;
|
|
12
12
|
}
|
|
13
|
-
return this.apiClient.create(resource, wrappedBody);
|
|
13
|
+
return this.apiClient.create(resource, wrappedBody, query);
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
|
|
17
|
-
async updateWrapped(resource, id, wrapperKey, payload) {
|
|
17
|
+
async updateWrapped(resource, id, wrapperKey, payload, query = {}) {
|
|
18
18
|
const wrappedBody = { [wrapperKey]: payload };
|
|
19
19
|
try {
|
|
20
|
-
return await this.apiClient.update(resource, id, payload);
|
|
20
|
+
return await this.apiClient.update(resource, id, payload, query);
|
|
21
21
|
} catch (error) {
|
|
22
22
|
if (!this.shouldRetryWrappedBody(error, wrapperKey)) {
|
|
23
23
|
throw error;
|
|
24
24
|
}
|
|
25
|
-
return this.apiClient.update(resource, id, wrappedBody);
|
|
25
|
+
return this.apiClient.update(resource, id, wrappedBody, query);
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
-
async patchWrapped(resource, id, wrapperKey, payload) {
|
|
29
|
+
async patchWrapped(resource, id, wrapperKey, payload, query = {}) {
|
|
30
30
|
const wrappedBody = { [wrapperKey]: payload };
|
|
31
31
|
try {
|
|
32
|
-
return await this.apiClient.patch(resource, id, payload);
|
|
32
|
+
return await this.apiClient.patch(resource, id, payload, query);
|
|
33
33
|
} catch (error) {
|
|
34
34
|
if (!this.shouldRetryWrappedBody(error, wrapperKey)) {
|
|
35
35
|
throw error;
|
|
36
36
|
}
|
|
37
|
-
return this.apiClient.patch(resource, id, wrappedBody);
|
|
37
|
+
return this.apiClient.patch(resource, id, wrappedBody, query);
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
|
|
41
|
-
async createRequirement(args = {}) {
|
|
41
|
+
async createRequirement(args = {}, query = {}) {
|
|
42
42
|
const { files, ...payloadArgs } = args;
|
|
43
43
|
const payload = this.buildRequirementPayload(payloadArgs);
|
|
44
44
|
|
|
45
45
|
if (this.hasFiles(files)) {
|
|
46
46
|
return this.apiClient.createMultipart(
|
|
47
47
|
'requirements',
|
|
48
|
-
await this.buildRequirementFormData(payload, files)
|
|
48
|
+
await this.buildRequirementFormData(payload, files),
|
|
49
|
+
query
|
|
49
50
|
);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
return this.createWrapped('requirements', 'requirement', payload);
|
|
53
|
+
return this.createWrapped('requirements', 'requirement', payload, query);
|
|
53
54
|
},
|
|
54
55
|
|
|
55
|
-
async updateRequirement(requirementId, args = {}) {
|
|
56
|
+
async updateRequirement(requirementId, args = {}, query = {}) {
|
|
56
57
|
const { files, ...payloadArgs } = args;
|
|
57
58
|
const payload = this.buildRequirementPayload(payloadArgs);
|
|
58
59
|
|
|
@@ -60,11 +61,12 @@ export const payloadMethods = {
|
|
|
60
61
|
return this.apiClient.patchMultipart(
|
|
61
62
|
'requirements',
|
|
62
63
|
requirementId,
|
|
63
|
-
await this.buildRequirementFormData(payload, files)
|
|
64
|
+
await this.buildRequirementFormData(payload, files),
|
|
65
|
+
query
|
|
64
66
|
);
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
return this.patchWrapped('requirements', requirementId, 'requirement', payload);
|
|
69
|
+
return this.patchWrapped('requirements', requirementId, 'requirement', payload, query);
|
|
68
70
|
},
|
|
69
71
|
|
|
70
72
|
hasFiles(files) {
|
|
@@ -198,27 +200,27 @@ export const payloadMethods = {
|
|
|
198
200
|
};
|
|
199
201
|
},
|
|
200
202
|
|
|
201
|
-
async createRunWithFallback(args = {}) {
|
|
203
|
+
async createRunWithFallback(args = {}, query = {}) {
|
|
202
204
|
const payload = this.buildRunCreatePayload(args);
|
|
203
205
|
try {
|
|
204
|
-
return await this.apiClient.create('runs', payload);
|
|
206
|
+
return await this.apiClient.create('runs', payload, query);
|
|
205
207
|
} catch (error) {
|
|
206
208
|
if (!this.shouldRetryWrappedBody(error, 'run')) {
|
|
207
209
|
throw error;
|
|
208
210
|
}
|
|
209
|
-
return this.apiClient.create('runs', { run: payload });
|
|
211
|
+
return this.apiClient.create('runs', { run: payload }, query);
|
|
210
212
|
}
|
|
211
213
|
},
|
|
212
214
|
|
|
213
|
-
async updateRunWithFallback(runId, args = {}) {
|
|
215
|
+
async updateRunWithFallback(runId, args = {}, query = {}) {
|
|
214
216
|
const payload = this.buildRunUpdatePayload(args);
|
|
215
217
|
try {
|
|
216
|
-
return await this.apiClient.update('runs', runId, payload);
|
|
218
|
+
return await this.apiClient.update('runs', runId, payload, query);
|
|
217
219
|
} catch (error) {
|
|
218
220
|
if (!this.shouldRetryWrappedBody(error, 'run')) {
|
|
219
221
|
throw error;
|
|
220
222
|
}
|
|
221
|
-
return this.apiClient.update('runs', runId, { run: payload });
|
|
223
|
+
return this.apiClient.update('runs', runId, { run: payload }, query);
|
|
222
224
|
}
|
|
223
225
|
},
|
|
224
226
|
|
|
@@ -351,4 +353,8 @@ export const payloadMethods = {
|
|
|
351
353
|
confluence_url: confluenceUrl,
|
|
352
354
|
};
|
|
353
355
|
},
|
|
356
|
+
|
|
357
|
+
buildBranchPayload({ title } = {}) {
|
|
358
|
+
return { title };
|
|
359
|
+
},
|
|
354
360
|
};
|
|
@@ -14,9 +14,10 @@ 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 {
|
|
17
|
+
import { BRANCHES_TOOLS } from './definitions/branches.js';
|
|
18
|
+
import { withListOptions, withCountGroupOptions } from './list-projection.js';
|
|
18
19
|
|
|
19
|
-
export const TOOL_DEFINITIONS = withListOptions([
|
|
20
|
+
export const TOOL_DEFINITIONS = withCountGroupOptions(withListOptions([
|
|
20
21
|
...SYSTEM_TOOLS,
|
|
21
22
|
...PROJECT_TOOLS,
|
|
22
23
|
...TESTS_TOOLS,
|
|
@@ -33,4 +34,5 @@ export const TOOL_DEFINITIONS = withListOptions([
|
|
|
33
34
|
...ATTACHMENT_TOOLS,
|
|
34
35
|
...PLANS_TOOLS,
|
|
35
36
|
...REQUIREMENTS_TOOLS,
|
|
36
|
-
|
|
37
|
+
...BRANCHES_TOOLS,
|
|
38
|
+
]));
|
package/src/mcp/tool-registry.js
CHANGED
|
@@ -17,7 +17,7 @@ function withPagination(payload) {
|
|
|
17
17
|
if (!Array.isArray(payload.data) || !meta || typeof meta !== 'object') {
|
|
18
18
|
return payload;
|
|
19
19
|
}
|
|
20
|
-
const { total, page, per_page: perPage } = meta;
|
|
20
|
+
const { total, page, per_page: perPage, has_more: backendHasMore } = meta;
|
|
21
21
|
if (
|
|
22
22
|
typeof total !== 'number' ||
|
|
23
23
|
typeof page !== 'number' ||
|
|
@@ -26,7 +26,10 @@ function withPagination(payload) {
|
|
|
26
26
|
) {
|
|
27
27
|
return payload;
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
const hasMore =
|
|
30
|
+
typeof backendHasMore === 'boolean' ? backendHasMore : page * perPage < total;
|
|
31
|
+
|
|
32
|
+
if (hasMore) {
|
|
30
33
|
return {
|
|
31
34
|
...payload,
|
|
32
35
|
_note: `Showing ${payload.data.length} of ${total} (page ${page}). More available — refine the filter or request the next page.`,
|