@testomatio/mcp 2.2.0-beta.1 → 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/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.2",
4
4
  "description": "Model Context Protocol server for Testomatio API",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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,
@@ -102,15 +102,16 @@ export const handlerMethods = {
102
102
  registerGlobalHandlers(handlers) {
103
103
  handlers.project_info = async () => this.asText(await this.apiClient.get('info'));
104
104
 
105
- handlers.tags_list = async (args = {}) =>
106
- this.asText(
107
- slimList(await this.listTags(backendSlimQuery(args)), { ...args, entity: 'tags' })
108
- );
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
+ };
109
110
  handlers.tags_get = async ({ tag_id: tagId }) => this.asText(await this.getTagByTitle(tagId));
110
111
 
111
112
  handlers.milestones_list = async (args = {}) => {
112
113
  const { verbose, fields, ...listArgs } = args;
113
- Object.assign(listArgs, backendSlimQuery({ verbose, fields }));
114
+ Object.assign(listArgs, backendSlimQuery({ verbose, fields, count: listArgs.count }));
114
115
  return this.asText(
115
116
  slimList(await this.listMilestones(listArgs), { verbose, fields, entity: 'milestones' })
116
117
  );
@@ -120,7 +121,7 @@ export const handlerMethods = {
120
121
 
121
122
  handlers.issues_list = async (args = {}) => {
122
123
  const { verbose, fields, ...listArgs } = args;
123
- Object.assign(listArgs, backendSlimQuery({ verbose, fields }));
124
+ Object.assign(listArgs, backendSlimQuery({ verbose, fields, count: listArgs.count }));
124
125
  return this.asText(
125
126
  slimList(await this.listIssues(listArgs), { verbose, fields, entity: 'issues' })
126
127
  );
@@ -3,17 +3,21 @@ export const listingMethods = {
3
3
  page,
4
4
  per_page: perPage,
5
5
  tql,
6
+ count,
7
+ group_by: groupBy,
6
8
  slim,
7
9
  } = {}) {
8
10
  return this.apiClient.list('tests', {
9
11
  page,
10
12
  per_page: perPage,
11
13
  tql,
14
+ count,
15
+ group_by: groupBy,
12
16
  slim,
13
17
  });
14
18
  },
15
19
 
16
- listSuites({ page, per_page: perPage, file_type: fileType, tag, labels, search_text: searchText, slim } = {}) {
20
+ listSuites({ page, per_page: perPage, file_type: fileType, tag, labels, search_text: searchText, count, group_by: groupBy, slim } = {}) {
17
21
  return this.apiClient.list('suites', {
18
22
  page,
19
23
  per_page: perPage,
@@ -21,6 +25,8 @@ export const listingMethods = {
21
25
  tag,
22
26
  labels,
23
27
  search_text: searchText,
28
+ count,
29
+ group_by: groupBy,
24
30
  slim,
25
31
  });
26
32
  },
@@ -29,12 +35,16 @@ export const listingMethods = {
29
35
  page,
30
36
  per_page: perPage,
31
37
  tql,
38
+ count,
39
+ group_by: groupBy,
32
40
  slim,
33
41
  } = {}) {
34
42
  return this.apiClient.list('runs', {
35
43
  page,
36
44
  per_page: perPage,
37
45
  tql,
46
+ count,
47
+ group_by: groupBy,
38
48
  slim,
39
49
  });
40
50
  },
@@ -58,6 +68,8 @@ export const listingMethods = {
58
68
  envs,
59
69
  rungroups,
60
70
  defects,
71
+ count,
72
+ group_by: groupBy,
61
73
  slim,
62
74
  } = {}) {
63
75
  return this.apiClient.list('testruns', {
@@ -79,27 +91,29 @@ export const listingMethods = {
79
91
  envs: Array.isArray(envs) ? envs.join(',') : envs,
80
92
  rungroups: Array.isArray(rungroups) ? rungroups.join(',') : rungroups,
81
93
  defects,
94
+ count,
95
+ group_by: groupBy,
82
96
  slim,
83
97
  });
84
98
  },
85
99
 
86
- listRungroups({ page, per_page: perPage, slim } = {}) {
87
- return this.apiClient.list('rungroups', { page, per_page: perPage, slim });
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 });
88
102
  },
89
103
 
90
- listSteps({ page, per_page: perPage, slim } = {}) {
91
- return this.apiClient.list('steps', { page, per_page: perPage, slim });
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 });
92
106
  },
93
107
 
94
- listSnippets({ page, per_page: perPage, slim } = {}) {
95
- return this.apiClient.list('snippets', { page, per_page: perPage, slim });
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 });
96
110
  },
97
111
 
98
- listLabels({ page, per_page: perPage, slim } = {}) {
99
- return this.apiClient.list('labels', { page, per_page: perPage, slim });
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 });
100
114
  },
101
115
 
102
- listPlans({ page, per_page: perPage, kind, hidden, labels, search_text: searchText, slim } = {}) {
116
+ listPlans({ page, per_page: perPage, kind, hidden, labels, search_text: searchText, count, group_by: groupBy, slim } = {}) {
103
117
  return this.apiClient.list('plans', {
104
118
  page,
105
119
  per_page: perPage,
@@ -107,20 +121,22 @@ export const listingMethods = {
107
121
  hidden,
108
122
  'labels[]': labels,
109
123
  search_text: searchText,
124
+ count,
125
+ group_by: groupBy,
110
126
  slim,
111
127
  });
112
128
  },
113
129
 
114
- listRequirements({ page, per_page: perPage, source, scope, slim } = {}) {
115
- return this.apiClient.list('requirements', { page, per_page: perPage, source, scope, slim });
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 });
116
132
  },
117
133
 
118
- listMilestones({ page, per_page: perPage, type, status, slim } = {}) {
119
- return this.apiClient.list('milestones', { page, per_page: perPage, type, status, slim });
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 });
120
136
  },
121
137
 
122
- listTags({ slim } = {}) {
123
- return this.apiClient.list('tags', { slim });
138
+ listTags({ count, group_by: groupBy, slim } = {}) {
139
+ return this.apiClient.list('tags', { count, group_by: groupBy, slim });
124
140
  },
125
141
 
126
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
+ ]));