@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/mcp",
3
- "version": "2.2.0-beta.1",
3
+ "version": "2.2.0-beta.3",
4
4
  "description": "Model Context Protocol server for Testomatio API",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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
- this.asText(await this.apiClient.get(resource, this.pickRequiredArg(args, idArg)));
24
- handlers[`${toolPrefix}_create`] = async (args = {}) =>
25
- this.asText(await this.executeCreate(spec, args));
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.omitArg(args, idArg);
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
- this.asText(
107
- slimList(await this.listTags(backendSlimQuery(args)), { ...args, entity: 'tags' })
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
- omitArg(args = {}, key) {
152
+ omitArgs(args = {}, keys) {
142
153
  const payload = { ...args };
143
- delete payload[key];
154
+ for (const key of keys) {
155
+ delete payload[key];
156
+ }
144
157
  return payload;
145
158
  },
146
159
 
147
- executeCreate(spec, args = {}) {
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 { withListOptions } from './list-projection.js';
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
+ ]));
@@ -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
- if (page * perPage < total) {
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.`,