@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/mcp",
3
- "version": "2.2.0-beta",
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": {
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
- this.asText(slimList(await this.listTags(), { ...args, entity: 'tags' }));
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
+ ]));