@testomatio/mcp 2.1.2 → 2.2.0-beta

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
@@ -10,13 +10,14 @@ Model Context Protocol (MCP) server that enables AI assistants (Claude, Cursor,
10
10
  - Issues (global + scoped helpers for tests/suites/runs/testruns/plans)
11
11
  - Attachments (scoped helpers for tests/suites/testruns)
12
12
  - Requirements (including file uploads from local file paths)
13
- - **Smart Search** - delegates to list endpoints with OpenAPI-aligned query/filter forwarding
13
+ - **Project Information** - fetch project configuration, metadata, features, and CI profiles
14
14
  - **Issue Linking** - link/unlink issues to any resource
15
15
  - **API Compatibility** - automatic handling of payload format differences (flat vs wrapped)
16
16
  - **Automatic API Sessions** - groups MCP changes in Testomat.io history using API sessions
17
17
  - **Run Management** - status transitions via `status_event` parameter
18
- - **TQL-Only Search** - `tests_list/tests_search` and `runs_list/runs_search` use `tql` as the single search/filter input
19
- - **Built-In TQL Reference** - MCP tool descriptions include exact TQL fields, syntax, and examples for agents
18
+ - **TQL-Only Search** - `tests_list` and `runs_list` use `tql` as the single search/filter input
19
+ - **Built-In TQL Reference** - TQL parameters include the exact field whitelist and examples; `tql_help` provides syntax details on demand
20
+ - **Tool Surface Profiles** - expose only the tools a session needs via `--tools full|core|read` (default `full`); cuts the per-call schema cost for long agentic sessions
20
21
 
21
22
  ## Quick Start
22
23
 
@@ -51,6 +52,22 @@ testomatio-mcp
51
52
  export TESTOMATIO_BASE_URL=https://beta.testomat.io
52
53
  ```
53
54
 
55
+ **Optional: tool surface profile**
56
+
57
+ By default the server exposes all tools. For long, token-sensitive sessions you can expose a smaller set with `--tools`:
58
+
59
+ ```bash
60
+ testomatio-mcp --token <PROJECT_TOKEN> --project <PROJECT_ID> --tools core
61
+ ```
62
+
63
+ | Profile | What's exposed |
64
+ |---------|----------------|
65
+ | `full` (default) | Everything |
66
+ | `core` | Core entities + CRUD (excludes steps, snippets, labels, rungroups, attachments) |
67
+ | `read` | Core entities, read-only (list/get) |
68
+
69
+ Values are case-insensitive; an unknown value prevents the server from starting. Set the profile at launch with the flag or the `TESTOMATIO_TOOLS` environment variable — it can't be changed mid-session. The CLI flag takes precedence when both are set.
70
+
54
71
  ## Usage with AI Assistants
55
72
 
56
73
  ### Cursor IDE
@@ -216,6 +233,7 @@ src/
216
233
  | `TESTOMATIO_API_TOKEN` | Yes* | - | Alternative token |
217
234
  | `TESTOMATIO_PROJECT_ID` | Yes | - | Project ID |
218
235
  | `TESTOMATIO_BASE_URL` | No | `https://app.testomat.io` | API base URL |
236
+ | `TESTOMATIO_TOOLS` | No | `full` | Tool profile: `full`, `core`, or `read` |
219
237
 
220
238
  *Either `TESTOMATIO_PROJECT_TOKEN` or `TESTOMATIO_API_TOKEN`
221
239
 
@@ -254,10 +272,10 @@ NODE_EXTRA_CA_CERTS=/path/to/company-root-ca.pem testomatio-mcp --token <TOKEN>
254
272
  ## Important Notes
255
273
 
256
274
  - **Run Status** - Use `runs_update` with `status_event` for transitions (finish, launch, rerun, etc.)
257
- - **Search** - No dedicated `/search` endpoints. MCP search tools delegate to list tools; for `tests` and `runs` the MCP interface is intentionally simplified to `tql`, while other entities stay closer to Public API v2 filters
258
- - **TQL** - Use `tql` as the single search/filter input for `tests_list/tests_search` and `runs_list/runs_search`
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
+ - **TQL** - Use `tql` as the single search/filter input for `tests_list` and `runs_list`
259
277
  - **TQL Syntax** - For user-facing syntax details and more examples, see the official TQL docs: https://docs.testomat.io/advanced/tql/
260
- - **TQL Scope** - The full agent-oriented whitelist of documented fields lives inside MCP tool descriptions for `tests` and `runs`
278
+ - **TQL Scope** - TQL parameter descriptions keep the documented field whitelist in-band; call `tql_help` for syntax details and additional examples
261
279
  - **Issue Linking** - Scoped helpers available: `{entity}_issues_link/unlink`
262
280
  - **Attachments** - Scoped helpers available for tests, suites, and testruns: `{entity}_attachments_list/upload/delete`. Upload sends one local file path as multipart field `file`.
263
281
  - **Enterprise Package** - Analytics tools are intentionally exposed only by `@testomatio/mcp-enterprise`, not by the standard `@testomatio/mcp` package
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/mcp",
3
- "version": "2.1.2",
3
+ "version": "2.2.0-beta",
4
4
  "description": "Model Context Protocol server for Testomatio API",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -55,10 +55,6 @@ export class TestomatioApiClient {
55
55
  return this.mutate('DELETE', this.buildPath(resource, id), { query });
56
56
  }
57
57
 
58
- search(resource, query = {}) {
59
- return this.list(resource, query);
60
- }
61
-
62
58
  async mutate(method, path, options = {}) {
63
59
  const runRequest = async () => {
64
60
  const sessionHash = await this.ensureSession();
package/src/cli/main.js CHANGED
@@ -15,6 +15,10 @@ export function parseArgs(argv = process.argv) {
15
15
  .option('-t, --token <token>', 'Testomatio Project token')
16
16
  .option('-p, --project <project>', 'Project ID')
17
17
  .option('--base-url <url>', 'Base URL for Testomatio API')
18
+ .option(
19
+ '--tools <profile>',
20
+ 'Tool surface: full (default, all tools), core (common entities only), read (read-only)'
21
+ )
18
22
  .parse(argv);
19
23
 
20
24
  return command.opts();
@@ -1,3 +1,7 @@
1
1
  export const DEFAULT_BASE_URL = 'https://app.testomat.io';
2
2
 
3
3
  export const DEFAULT_TOOL_RESPONSE = 'Tool is declared but has no handler implementation.';
4
+
5
+ export const TOOL_PROFILES = ['full', 'core', 'read'];
6
+
7
+ export const DEFAULT_PROFILE = 'full';
@@ -1,4 +1,4 @@
1
- import { DEFAULT_BASE_URL } from './constants.js';
1
+ import { DEFAULT_BASE_URL, DEFAULT_PROFILE, TOOL_PROFILES } from './constants.js';
2
2
  import { ConfigurationError } from '../core/errors.js';
3
3
 
4
4
  function normalizeString(value) {
@@ -16,6 +16,8 @@ export function loadConfig(argvOptions = {}) {
16
16
  );
17
17
  const projectId = normalizeString(argvOptions.project || process.env.TESTOMATIO_PROJECT_ID);
18
18
  const baseUrl = normalizeBaseUrl(argvOptions.baseUrl || process.env.TESTOMATIO_BASE_URL || DEFAULT_BASE_URL);
19
+ const rawToolsProfile = normalizeString(argvOptions.tools || process.env.TESTOMATIO_TOOLS).toLowerCase();
20
+ const toolsProfile = rawToolsProfile || DEFAULT_PROFILE;
19
21
 
20
22
  if (!token) {
21
23
  throw new ConfigurationError(
@@ -29,9 +31,16 @@ export function loadConfig(argvOptions = {}) {
29
31
  );
30
32
  }
31
33
 
34
+ if (!TOOL_PROFILES.includes(toolsProfile)) {
35
+ throw new ConfigurationError(
36
+ `Unknown tools profile "${toolsProfile}". Use one of: ${TOOL_PROFILES.join(', ')}.`
37
+ );
38
+ }
39
+
32
40
  return {
33
41
  token,
34
42
  projectId,
35
43
  baseUrl,
44
+ toolsProfile,
36
45
  };
37
46
  }
package/src/index.js CHANGED
@@ -4,6 +4,8 @@ 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';
8
+ import { selectTools } from './mcp/tool-profiles.js';
7
9
  import {
8
10
  ANALYTICS_STATS_TQL_INPUT_DESCRIPTION,
9
11
  ANALYTICS_STATS_TQL_REFERENCE,
@@ -19,17 +21,22 @@ export {
19
21
  ANALYTICS_TESTS_TQL_REFERENCE,
20
22
  ConfigurationError,
21
23
  TOOL_DEFINITIONS,
24
+ slimList,
25
+ withListOptions,
26
+ selectTools,
22
27
  };
23
28
 
24
29
  export function createApplication(argvOptions = {}, serverOptions = {}) {
25
30
  const config = loadConfig(argvOptions);
26
31
  const logger = createLogger();
27
32
  const apiClient = new TestomatioApiClient({ ...config, logger });
33
+ const { tools: overrideTools, ...restServerOptions } = serverOptions;
28
34
  const mcpServer = new TestomatioMCPServer({
29
35
  config,
30
36
  apiClient,
31
37
  logger,
32
- ...serverOptions,
38
+ tools: selectTools(overrideTools ?? TOOL_DEFINITIONS, config.toolsProfile),
39
+ ...restServerOptions,
33
40
  });
34
41
 
35
42
  return {
@@ -4,7 +4,6 @@ export const ENTITY_CRUD_CONFIGS = [
4
4
  resource: 'tests',
5
5
  idArg: 'test_id',
6
6
  listMethod: 'listTests',
7
- searchMethod: 'searchTests',
8
7
  payloadBuilder: 'buildTestPayload',
9
8
  wrapperKey: 'test',
10
9
  createMode: 'wrapped',
@@ -15,7 +14,6 @@ export const ENTITY_CRUD_CONFIGS = [
15
14
  resource: 'suites',
16
15
  idArg: 'suite_id',
17
16
  listMethod: 'listSuites',
18
- searchMethod: 'searchSuites',
19
17
  payloadBuilder: 'buildSuitePayload',
20
18
  wrapperKey: 'suite',
21
19
  createMode: 'wrapped',
@@ -26,7 +24,6 @@ export const ENTITY_CRUD_CONFIGS = [
26
24
  resource: 'runs',
27
25
  idArg: 'run_id',
28
26
  listMethod: 'listRuns',
29
- searchMethod: 'searchRuns',
30
27
  createMode: 'run',
31
28
  updateMode: 'run',
32
29
  },
@@ -35,7 +32,6 @@ export const ENTITY_CRUD_CONFIGS = [
35
32
  resource: 'testruns',
36
33
  idArg: 'testrun_id',
37
34
  listMethod: 'listTestruns',
38
- searchMethod: 'searchTestruns',
39
35
  payloadBuilder: 'buildTestrunPayload',
40
36
  wrapperKey: 'testrun',
41
37
  createMode: 'wrapped',
@@ -86,7 +82,6 @@ export const ENTITY_CRUD_CONFIGS = [
86
82
  resource: 'plans',
87
83
  idArg: 'plan_id',
88
84
  listMethod: 'listPlans',
89
- searchMethod: 'searchPlans',
90
85
  payloadBuilder: 'buildPlanPayload',
91
86
  wrapperKey: 'plan',
92
87
  createMode: 'wrapped',
@@ -97,7 +92,6 @@ export const ENTITY_CRUD_CONFIGS = [
97
92
  resource: 'requirements',
98
93
  idArg: 'requirement_id',
99
94
  listMethod: 'listRequirements',
100
- searchMethod: 'searchRequirements',
101
95
  payloadBuilder: 'buildRequirementPayload',
102
96
  wrapperKey: 'requirement',
103
97
  createMode: 'requirement',
@@ -91,41 +91,4 @@ export const ISSUES_TOOLS = [
91
91
  "additionalProperties": false
92
92
  }
93
93
  },
94
- {
95
- "name": "issues_search",
96
- "description": "Search issues (delegates to issues_list filters)",
97
- "inputSchema": {
98
- "type": "object",
99
- "properties": {
100
- "page": {
101
- "type": "integer",
102
- "minimum": 1
103
- },
104
- "per_page": {
105
- "type": "integer",
106
- "minimum": 1,
107
- "maximum": 100
108
- },
109
- "test_id": {
110
- "type": "string"
111
- },
112
- "suite_id": {
113
- "type": "string"
114
- },
115
- "run_id": {
116
- "type": "string"
117
- },
118
- "testrun_id": {
119
- "type": "integer"
120
- },
121
- "plan_id": {
122
- "type": "string"
123
- },
124
- "source": {
125
- "type": "string"
126
- }
127
- },
128
- "additionalProperties": false
129
- }
130
- }
131
94
  ];
@@ -247,45 +247,6 @@ export const PLANS_TOOLS = [
247
247
  "additionalProperties": false
248
248
  }
249
249
  },
250
- {
251
- "name": "plans_search",
252
- "description": "Search plans (delegates to plans list; docs has no dedicated search parameter)",
253
- "inputSchema": {
254
- "type": "object",
255
- "properties": {
256
- "search_text": {
257
- "type": "string"
258
- },
259
- "page": {
260
- "type": "integer",
261
- "minimum": 1
262
- },
263
- "per_page": {
264
- "type": "integer",
265
- "minimum": 1,
266
- "maximum": 100
267
- },
268
- "kind": {
269
- "type": "string",
270
- "enum": [
271
- "manual",
272
- "automated",
273
- "mixed"
274
- ]
275
- },
276
- "hidden": {
277
- "type": "boolean"
278
- },
279
- "labels": {
280
- "type": "array",
281
- "items": {
282
- "type": "string"
283
- }
284
- }
285
- },
286
- "additionalProperties": false
287
- }
288
- },
289
250
  {
290
251
  "name": "plans_issues_list",
291
252
  "description": "List linked issues for a plan (/api/v2/{project_id}/issues?plan_id=...)",
@@ -0,0 +1,12 @@
1
+ export const PROJECT_TOOLS = [
2
+ {
3
+ name: 'project_info',
4
+ description:
5
+ 'Get configuration and metadata for the current project (/api/v2/{project_id}/info), including framework, language, environments, labels, tags, subscription features, artifact storage status, and CI profiles.',
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {},
9
+ additionalProperties: false,
10
+ },
11
+ },
12
+ ];
@@ -133,31 +133,4 @@ export const REQUIREMENTS_TOOLS = [
133
133
  additionalProperties: false,
134
134
  },
135
135
  },
136
- {
137
- name: 'requirements_search',
138
- description: 'Search requirements (delegates to requirements list with filters)',
139
- inputSchema: {
140
- type: 'object',
141
- properties: {
142
- page: {
143
- type: 'integer',
144
- minimum: 1,
145
- },
146
- per_page: {
147
- type: 'integer',
148
- minimum: 1,
149
- maximum: 100,
150
- },
151
- source: {
152
- type: 'string',
153
- enum: ['jira', 'confluence', 'file', 'text'],
154
- },
155
- scope: {
156
- type: 'string',
157
- enum: ['global', 'attached', 'detached', 'without_suites'],
158
- },
159
- },
160
- additionalProperties: false,
161
- },
162
- },
163
136
  ];
@@ -264,29 +264,6 @@ export const RUNS_TOOLS = [
264
264
  "additionalProperties": false
265
265
  }
266
266
  },
267
- {
268
- "name": "runs_search",
269
- "description": `Search runs using TQL (delegates to runs_list). ${RUNS_TQL_REFERENCE}`,
270
- "inputSchema": {
271
- "type": "object",
272
- "properties": {
273
- "page": {
274
- "type": "integer",
275
- "minimum": 1
276
- },
277
- "per_page": {
278
- "type": "integer",
279
- "minimum": 1,
280
- "maximum": 100
281
- },
282
- "tql": {
283
- "type": "string",
284
- "description": RUNS_TQL_INPUT_DESCRIPTION
285
- }
286
- },
287
- "additionalProperties": false
288
- }
289
- },
290
267
  {
291
268
  "name": "runs_issues_list",
292
269
  "description": "List linked issues for a run (/api/v2/{project_id}/issues?run_id=...)",
@@ -69,7 +69,11 @@ export const SUITES_TOOLS = [
69
69
  "type": "string"
70
70
  },
71
71
  "file_type": {
72
- "type": "string"
72
+ "type": "string",
73
+ "enum": [
74
+ "file",
75
+ "folder"
76
+ ]
73
77
  },
74
78
  "assigned_to": {
75
79
  "type": "string"
@@ -146,7 +150,11 @@ export const SUITES_TOOLS = [
146
150
  "type": "string"
147
151
  },
148
152
  "file_type": {
149
- "type": "string"
153
+ "type": "string",
154
+ "enum": [
155
+ "file",
156
+ "folder"
157
+ ]
150
158
  },
151
159
  "assigned_to": {
152
160
  "type": "string"
@@ -217,41 +225,6 @@ export const SUITES_TOOLS = [
217
225
  "additionalProperties": false
218
226
  }
219
227
  },
220
- {
221
- "name": "suites_search",
222
- "description": "Search suites by title (delegates to suites list with search_text)",
223
- "inputSchema": {
224
- "type": "object",
225
- "properties": {
226
- "search_text": {
227
- "type": "string"
228
- },
229
- "page": {
230
- "type": "integer",
231
- "minimum": 1
232
- },
233
- "per_page": {
234
- "type": "integer",
235
- "minimum": 1,
236
- "maximum": 100
237
- },
238
- "file_type": {
239
- "type": "string",
240
- "enum": [
241
- "file",
242
- "folder"
243
- ]
244
- },
245
- "tag": {
246
- "type": "string"
247
- },
248
- "labels": {
249
- "type": "string"
250
- }
251
- },
252
- "additionalProperties": false
253
- }
254
- },
255
228
  {
256
229
  "name": "suites_issues_list",
257
230
  "description": "List linked issues for a suite (/api/v2/{project_id}/issues?suite_id=...)",
@@ -6,5 +6,13 @@ export const SYSTEM_TOOLS = [
6
6
  "type": "object",
7
7
  "properties": {}
8
8
  }
9
+ },
10
+ {
11
+ "name": "tql_help",
12
+ "description": "Full TQL (Testomat.io Query Language) reference — syntax, filter variables (tests + runs), and examples. Call this before writing a `tql` or `q` filter.",
13
+ "inputSchema": {
14
+ "type": "object",
15
+ "properties": {}
16
+ }
9
17
  }
10
18
  ];
@@ -149,7 +149,13 @@ export const TESTRUNS_TOOLS = [
149
149
  "type": "string"
150
150
  },
151
151
  "status": {
152
- "type": "string"
152
+ "type": "string",
153
+ "enum": [
154
+ "passed",
155
+ "failed",
156
+ "skipped",
157
+ "pending"
158
+ ]
153
159
  },
154
160
  "message": {
155
161
  "type": "string"
@@ -189,7 +195,13 @@ export const TESTRUNS_TOOLS = [
189
195
  "type": "string"
190
196
  },
191
197
  "status": {
192
- "type": "string"
198
+ "type": "string",
199
+ "enum": [
200
+ "passed",
201
+ "failed",
202
+ "skipped",
203
+ "pending"
204
+ ]
193
205
  },
194
206
  "message": {
195
207
  "type": "string"
@@ -229,127 +241,6 @@ export const TESTRUNS_TOOLS = [
229
241
  "additionalProperties": false
230
242
  }
231
243
  },
232
- {
233
- "name": "testruns_search",
234
- "description": "Search testruns (delegates to testruns list; docs has no dedicated search parameter)",
235
- "inputSchema": {
236
- "type": "object",
237
- "properties": {
238
- "run_id": {
239
- "type": "string"
240
- },
241
- "test_ids": {
242
- "type": [
243
- "array",
244
- "string"
245
- ],
246
- "items": {
247
- "type": "string"
248
- }
249
- },
250
- "filter_status": {
251
- "type": "string",
252
- "enum": [
253
- "passed",
254
- "failed",
255
- "skipped",
256
- "pending"
257
- ]
258
- },
259
- "filter_kind": {
260
- "type": "string",
261
- "enum": [
262
- "manual",
263
- "automated"
264
- ]
265
- },
266
- "filter_user": {
267
- "type": [
268
- "integer",
269
- "string"
270
- ]
271
- },
272
- "filter_priority": {
273
- "type": "string",
274
- "enum": [
275
- "low",
276
- "normal",
277
- "important",
278
- "high",
279
- "critical"
280
- ]
281
- },
282
- "filter_substatus": {
283
- "type": "string"
284
- },
285
- "filter_search": {
286
- "type": "string"
287
- },
288
- "page": {
289
- "type": "integer",
290
- "minimum": 1
291
- },
292
- "per_page": {
293
- "type": "integer",
294
- "minimum": 1,
295
- "maximum": 100
296
- },
297
- "filter_message": {
298
- "type": "boolean"
299
- },
300
- "filter_link": {
301
- "type": "boolean"
302
- },
303
- "filter_finished_at_date_range": {
304
- "type": "string"
305
- },
306
- "tags": {
307
- "type": [
308
- "array",
309
- "string"
310
- ],
311
- "items": {
312
- "type": "string"
313
- }
314
- },
315
- "labels": {
316
- "type": [
317
- "array",
318
- "string"
319
- ],
320
- "items": {
321
- "type": "string"
322
- }
323
- },
324
- "envs": {
325
- "type": [
326
- "array",
327
- "string"
328
- ],
329
- "items": {
330
- "type": "string"
331
- }
332
- },
333
- "rungroups": {
334
- "type": [
335
- "array",
336
- "string"
337
- ],
338
- "items": {
339
- "type": "string"
340
- }
341
- },
342
- "defects": {
343
- "type": "string",
344
- "enum": [
345
- "has_defects",
346
- "without_defects"
347
- ]
348
- }
349
- },
350
- "additionalProperties": false
351
- }
352
- },
353
244
  {
354
245
  "name": "testruns_issues_list",
355
246
  "description": "List linked issues for a testrun (/api/v2/{project_id}/issues?testrun_id=...)",
@@ -59,7 +59,14 @@ export const TESTS_TOOLS = [
59
59
  "type": "string"
60
60
  },
61
61
  "priority": {
62
- "type": "string"
62
+ "type": "string",
63
+ "enum": [
64
+ "low",
65
+ "normal",
66
+ "important",
67
+ "high",
68
+ "critical"
69
+ ]
63
70
  },
64
71
  "assigned_to": {
65
72
  "type": "string"
@@ -68,7 +75,12 @@ export const TESTS_TOOLS = [
68
75
  "type": "string"
69
76
  },
70
77
  "state": {
71
- "type": "string"
78
+ "type": "string",
79
+ "enum": [
80
+ "manual",
81
+ "detached",
82
+ "automated"
83
+ ]
72
84
  },
73
85
  "link": {
74
86
  "type": "array",
@@ -135,7 +147,14 @@ export const TESTS_TOOLS = [
135
147
  "type": "string"
136
148
  },
137
149
  "priority": {
138
- "type": "string"
150
+ "type": "string",
151
+ "enum": [
152
+ "low",
153
+ "normal",
154
+ "important",
155
+ "high",
156
+ "critical"
157
+ ]
139
158
  },
140
159
  "assigned_to": {
141
160
  "type": "string"
@@ -144,7 +163,12 @@ export const TESTS_TOOLS = [
144
163
  "type": "string"
145
164
  },
146
165
  "state": {
147
- "type": "string"
166
+ "type": "string",
167
+ "enum": [
168
+ "manual",
169
+ "detached",
170
+ "automated"
171
+ ]
148
172
  },
149
173
  "sync": {
150
174
  "type": "boolean"
@@ -207,29 +231,6 @@ export const TESTS_TOOLS = [
207
231
  "additionalProperties": false
208
232
  }
209
233
  },
210
- {
211
- "name": "tests_search",
212
- "description": `Search tests using TQL (delegates to tests_list). ${TESTS_TQL_REFERENCE}`,
213
- "inputSchema": {
214
- "type": "object",
215
- "properties": {
216
- "page": {
217
- "type": "integer",
218
- "minimum": 1
219
- },
220
- "per_page": {
221
- "type": "integer",
222
- "minimum": 1,
223
- "maximum": 100
224
- },
225
- "tql": {
226
- "type": "string",
227
- "description": TESTS_TQL_INPUT_DESCRIPTION
228
- }
229
- },
230
- "additionalProperties": false
231
- }
232
- },
233
234
  {
234
235
  "name": "tests_issues_list",
235
236
  "description": "List linked issues for a test (/api/v2/{project_id}/issues?test_id=...)",
@@ -89,62 +89,42 @@ const RUNS_TQL_EXAMPLES = [
89
89
  const COMMON_TQL_SYNTAX =
90
90
  "Supported syntax includes logical operators `and`, `or`, `not`, equality operators `==` and `!=`, list membership `in [...]`, `%` for partial text match on supported text fields, and parentheses for grouping. Ordered comparisons `>`, `<`, `>=`, `<=` are for ordered fields such as `priority`, dates, and numeric counters/durations. Use quotes for string values, for example `state == 'automated'`.";
91
91
 
92
- export const TESTS_TQL_REFERENCE =
93
- `TQL (Testomat.io Query Language) is a string expression passed in \`tql\` to filter tests. ${COMMON_TQL_SYNTAX} ` +
94
- `Documented test variables: ${TESTS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
95
- `Documented examples: ${TESTS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}. ` +
96
- 'Do not invent undocumented fields or syntax. If a query fails, simplify it to one documented predicate.';
92
+ const TESTS_TQL_FIELDS = TESTS_TQL_VARIABLES.join(', ');
93
+ const RUNS_TQL_FIELDS = RUNS_TQL_VARIABLES.join(', ');
97
94
 
98
- export const TESTS_TQL_INPUT_DESCRIPTION =
99
- `TQL filter for tests. Documented variables: ${TESTS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
100
- `Examples: ${TESTS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}.`;
95
+ export const TQL_FULL_REFERENCE = [
96
+ 'TQL (Testomat.io Query Language) is a string expression used to filter tests and runs.',
97
+ COMMON_TQL_SYNTAX,
98
+ '',
99
+ `Tests filter variables: ${TESTS_TQL_VARIABLES.map((v) => `\`${v}\``).join(', ')}.`,
100
+ `Tests examples: ${TESTS_TQL_EXAMPLES.map((e) => `\`${e}\``).join(', ')}.`,
101
+ '',
102
+ `Runs filter variables: ${RUNS_TQL_VARIABLES.map((v) => `\`${v}\``).join(', ')}.`,
103
+ 'Runs also support boolean flags used without comparison, e.g. `failed`, `finished`, `automated`, `with_defect`.',
104
+ `Runs examples: ${RUNS_TQL_EXAMPLES.map((e) => `\`${e}\``).join(', ')}.`,
105
+ '',
106
+ 'Parameter name: tests/runs/plans use `tql`; analytics tools use `q`.',
107
+ 'Do not invent undocumented fields or syntax. If a query fails, simplify it to one documented predicate.',
108
+ ].join('\n');
101
109
 
110
+ export const TESTS_TQL_REFERENCE =
111
+ 'Filter tests with `tql` (TQL); call `tql_help` for the syntax and full field list.';
102
112
  export const RUNS_TQL_REFERENCE =
103
- `TQL (Testomat.io Query Language) is a string expression passed in \`tql\` to filter runs. ${COMMON_TQL_SYNTAX} ` +
104
- 'Runs also support boolean flags without comparison such as `failed`, `finished`, `automated`, or `with_defect`. ' +
105
- `Documented run variables: ${RUNS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
106
- `Documented examples: ${RUNS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}. ` +
107
- 'Do not invent undocumented fields or syntax. If a query fails, simplify it to one documented predicate.';
108
-
109
- export const RUNS_TQL_INPUT_DESCRIPTION =
110
- `TQL filter for runs. Documented variables: ${RUNS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
111
- `Examples: ${RUNS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}.`;
112
-
113
+ 'Filter runs with `tql` (TQL); runs also accept boolean flags. Call `tql_help` for the syntax and full field list.';
113
114
  export const PLANS_TQL_REFERENCE =
114
- `TQL (Testomat.io Query Language) is a string expression passed in \`tql\` to select tests included in a plan. ${COMMON_TQL_SYNTAX} ` +
115
- `Plan TQL uses documented test variables: ${TESTS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
116
- `Documented examples: ${TESTS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}. ` +
117
- 'Use `tql` when you want the API to resolve matching tests automatically instead of sending explicit `test_ids`. Do not invent undocumented fields or syntax. If a query fails, simplify it to one documented predicate.';
118
-
119
- export const PLANS_TQL_INPUT_DESCRIPTION =
120
- 'TQL filter for selecting tests included in the plan. ' +
121
- `Documented test variables: ${TESTS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
122
- `Examples: ${TESTS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}.`;
123
-
115
+ 'Select tests for the plan with `tql` (TQL); the API resolves matching tests. Call `tql_help` for the syntax and full field list.';
124
116
  export const ANALYTICS_TESTS_TQL_REFERENCE =
125
- `TQL (Testomat.io Query Language) is a string expression passed in \`q\` to filter enterprise analytics test reports. ${COMMON_TQL_SYNTAX} ` +
126
- 'For analytics tools, the API parameter name is `q`, not `tql`. ' +
127
- `Documented analytics test variables: ${TESTS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
128
- `Documented examples: ${TESTS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}. ` +
129
- 'Do not invent undocumented fields or syntax. If a query fails, simplify it to one documented predicate.';
130
-
131
- export const ANALYTICS_TESTS_TQL_INPUT_DESCRIPTION =
132
- 'TQL filter for analytics test reports. The API parameter name is `q`, not `tql`. ' +
133
- `Documented variables: ${TESTS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
134
- `Examples: ${TESTS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}.`;
135
-
117
+ 'Filter analytics test reports with `q` (TQL). Call `tql_help` for the syntax and full field list.';
136
118
  export const ANALYTICS_STATS_TQL_REFERENCE =
137
- `TQL (Testomat.io Query Language) is a string expression passed in \`q\` to filter enterprise analytics aggregated reports. ${COMMON_TQL_SYNTAX} ` +
138
- 'For analytics tools, the API parameter name is `q`, not `tql`. ' +
139
- 'According to the official Analytics docs, analytics queries are configured using supported query variables for two data sources: `Tests Variables` and `Runs Variables`. ' +
140
- `Documented Tests Variables: ${TESTS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
141
- `Documented Runs Variables: ${RUNS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
142
- `Documented Tests examples: ${TESTS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}. ` +
143
- `Documented Runs examples: ${RUNS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}. ` +
144
- 'Use Tests Variables for test-centric filters and Runs Variables for run-centric filters such as `plan`, `rungroup`, `env`, `finished_at`, or `has_test_tag`. Do not invent undocumented fields or syntax. If a query fails, simplify it to one documented predicate.';
119
+ 'Filter analytics aggregated reports with `q` (TQL; tests or runs variables). Call `tql_help` for the syntax and full field list.';
145
120
 
121
+ export const TESTS_TQL_INPUT_DESCRIPTION =
122
+ `TQL filter for tests. Fields: ${TESTS_TQL_FIELDS}. Call \`tql_help\` for syntax. Examples: \`priority == 'high'\`, \`state == 'automated'\`, \`suite % 'Checkout'\`.`;
123
+ export const RUNS_TQL_INPUT_DESCRIPTION =
124
+ `TQL filter for runs. Fields: ${RUNS_TQL_FIELDS}. Call \`tql_help\` for syntax. Examples: \`finished and with_defect\`, \`env in ['Windows', 'Linux']\`, \`has_retries > 2\`.`;
125
+ export const PLANS_TQL_INPUT_DESCRIPTION =
126
+ `TQL to select tests for the plan. Fields: ${TESTS_TQL_FIELDS}. Call \`tql_help\` for syntax. Examples: \`priority == 'high'\`, \`tag in ['smoke', 'stage1']\`.`;
127
+ export const ANALYTICS_TESTS_TQL_INPUT_DESCRIPTION =
128
+ `TQL filter (param \`q\`) for analytics test reports. Fields: ${TESTS_TQL_FIELDS}. Call \`tql_help\` for syntax. Examples: \`priority == 'high'\`, \`state == 'automated'\`.`;
146
129
  export const ANALYTICS_STATS_TQL_INPUT_DESCRIPTION =
147
- 'TQL filter for analytics aggregated reports. The API parameter name is `q`, not `tql`. ' +
148
- `Documented Tests Variables: ${TESTS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
149
- `Documented Runs Variables: ${RUNS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
150
- `Examples: ${TESTS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}, ${RUNS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}.`;
130
+ `TQL filter (param \`q\`) for analytics reports. Test fields: ${TESTS_TQL_FIELDS}. Run fields: ${RUNS_TQL_FIELDS}. Call \`tql_help\` for syntax. Examples: \`priority == 'high'\`, \`finished_at >= '2025-07-01' and failed\`.`;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Slim projection for list-tool responses.
3
+ *
4
+ * List tools return only what an index view needs: entity-specific heavy fields
5
+ * and null values are dropped.
6
+ * `verbose:true` returns the full body; `fields:[...]` selects a custom field set.
7
+ * Every shaped response carries a `_view` marker so the caller knows it is partial.
8
+ */
9
+
10
+ const HEAVY_FIELDS_BY_ENTITY = {
11
+ tests: new Set(['description', 'code', 'cleanTitle', 'publicTitle']),
12
+ suites: new Set(['description', 'code', 'cleanTitle', 'publicTitle']),
13
+ runs: new Set(['description']),
14
+ testruns: new Set(['description', 'code']),
15
+ rungroups: new Set(['description']),
16
+ steps: new Set(['description']),
17
+ snippets: new Set(['description']),
18
+ plans: new Set(['description']),
19
+ requirements: new Set(['description']),
20
+ analytics_tests: new Set(['description', 'code', 'cleanTitle', 'publicTitle']),
21
+ };
22
+
23
+ function isNullish(value) {
24
+ return value === null || value === undefined;
25
+ }
26
+
27
+ function pickFields(item, fields) {
28
+ const result = {};
29
+ for (const key of fields) {
30
+ if (!(key in item)) continue;
31
+ const value = item[key];
32
+ if (isNullish(value)) continue;
33
+ result[key] = value;
34
+ }
35
+ return result;
36
+ }
37
+
38
+ function stripHeavy(item, heavyFields) {
39
+ const result = {};
40
+ for (const [key, value] of Object.entries(item)) {
41
+ if (heavyFields.has(key)) continue;
42
+ if (isNullish(value)) continue;
43
+ result[key] = value;
44
+ }
45
+ return result;
46
+ }
47
+
48
+ function mapItems(items, fn) {
49
+ return items.map((item) =>
50
+ item && typeof item === 'object' && !Array.isArray(item) ? fn(item) : item
51
+ );
52
+ }
53
+
54
+ function applyToEnvelope(payload, fn) {
55
+ if (!payload || typeof payload !== 'object') return payload;
56
+ if (Array.isArray(payload)) return mapItems(payload, fn);
57
+ if (Array.isArray(payload.data)) {
58
+ return { ...payload, data: mapItems(payload.data, fn) };
59
+ }
60
+ return payload;
61
+ }
62
+
63
+ const VIEW_NOTE =
64
+ 'Entity-specific heavy fields and null values removed. Pass verbose:true for full bodies or fields:[...] to select fields.';
65
+
66
+ function withViewMarker(result, note = VIEW_NOTE) {
67
+ if (
68
+ result &&
69
+ typeof result === 'object' &&
70
+ !Array.isArray(result) &&
71
+ Array.isArray(result.data)
72
+ ) {
73
+ return { ...result, _view: note };
74
+ }
75
+ return result;
76
+ }
77
+
78
+ /**
79
+ * Shape a list payload: drop entity-specific heavy and null fields by default, full body on `verbose`,
80
+ * custom field set on `fields`. A `_view` marker is added to shaped envelopes.
81
+ *
82
+ * @param {*} payload raw API response ({ data, meta } or a bare array)
83
+ * @param {{ verbose?: boolean, fields?: string[], entity?: string }} [opts]
84
+ */
85
+ export function slimList(payload, { verbose = false, fields, entity } = {}) {
86
+ if (verbose) {
87
+ return payload;
88
+ }
89
+ if (Array.isArray(fields) && fields.length) {
90
+ return withViewMarker(
91
+ applyToEnvelope(payload, (item) => pickFields(item, fields)),
92
+ `Custom fields [${fields.join(', ')}]. Pass verbose:true for the full object.`
93
+ );
94
+ }
95
+ const heavyFields = HEAVY_FIELDS_BY_ENTITY[entity] ?? new Set();
96
+ return withViewMarker(applyToEnvelope(payload, (item) => stripHeavy(item, heavyFields)));
97
+ }
98
+
99
+ const LIST_OPTION_PROPERTIES = {
100
+ verbose: {
101
+ type: 'boolean',
102
+ default: false,
103
+ description:
104
+ 'Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.',
105
+ },
106
+ fields: {
107
+ type: 'array',
108
+ items: { type: 'string' },
109
+ description:
110
+ 'Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.',
111
+ },
112
+ };
113
+
114
+ /**
115
+ * Inject `verbose`/`fields` input params into every list-style tool definition so the
116
+ * model can opt into full bodies or a custom projection. Returns shallow copies; the
117
+ * source definitions are not mutated.
118
+ *
119
+ * @param {Array} tools
120
+ * @param {{ extraNames?: string[] }} [options] additional tool names to augment
121
+ */
122
+ export function withListOptions(tools, { extraNames = [] } = {}) {
123
+ return tools.map((tool) => {
124
+ if (!tool || !tool.name) return tool;
125
+ const isListStyle = tool.name.endsWith('_list') || extraNames.includes(tool.name);
126
+ if (!isListStyle) return tool;
127
+
128
+ const inputSchema = tool.inputSchema || { type: 'object', properties: {} };
129
+ const properties = { ...(inputSchema.properties || {}), ...LIST_OPTION_PROPERTIES };
130
+
131
+ return {
132
+ ...tool,
133
+ description: `${tool.description ?? ''} Entity-specific heavy fields are stripped by default; verbose:true for full bodies.`.trim(),
134
+ inputSchema: {
135
+ ...inputSchema,
136
+ properties,
137
+ },
138
+ };
139
+ });
140
+ }
@@ -1,18 +1,23 @@
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
5
 
5
6
  export const handlerMethods = {
6
7
  registerEntityCrudHandlers(handlers) {
7
8
  for (const spec of ENTITY_CRUD_CONFIGS) {
8
- const { toolPrefix, resource, idArg, listMethod, searchMethod } = spec;
9
-
10
- handlers[`${toolPrefix}_list`] = async (args = {}) =>
11
- this.asText(await this[listMethod](args));
12
- if (searchMethod) {
13
- handlers[`${toolPrefix}_search`] = async (args = {}) =>
14
- this.asText(await this[searchMethod](args));
15
- }
9
+ const { toolPrefix, resource, idArg, listMethod } = spec;
10
+
11
+ handlers[`${toolPrefix}_list`] = async (args = {}) => {
12
+ const { verbose, fields, ...listArgs } = args;
13
+ return this.asText(
14
+ slimList(await this[listMethod](listArgs), {
15
+ verbose,
16
+ fields,
17
+ entity: toolPrefix,
18
+ })
19
+ );
20
+ };
16
21
  handlers[`${toolPrefix}_get`] = async (args = {}) =>
17
22
  this.asText(await this.apiClient.get(resource, this.pickRequiredArg(args, idArg)));
18
23
  handlers[`${toolPrefix}_create`] = async (args = {}) =>
@@ -31,13 +36,16 @@ export const handlerMethods = {
31
36
  for (const { toolPrefix, resourceKey } of ISSUE_SCOPED_TOOL_CONFIGS) {
32
37
  handlers[`${toolPrefix}_issues_list`] = async (args = {}) =>
33
38
  this.asText(
34
- await this.listIssuesForKey({
35
- resourceKey,
36
- resourceId: this.pickRequiredArg(args, resourceKey),
37
- page: args.page,
38
- per_page: args.per_page,
39
- source: args.source,
40
- })
39
+ slimList(
40
+ await this.listIssuesForKey({
41
+ resourceKey,
42
+ resourceId: this.pickRequiredArg(args, resourceKey),
43
+ page: args.page,
44
+ per_page: args.per_page,
45
+ source: args.source,
46
+ }),
47
+ { verbose: args.verbose, fields: args.fields, entity: 'issues' }
48
+ )
41
49
  );
42
50
 
43
51
  handlers[`${toolPrefix}_issues_link`] = async (args = {}) =>
@@ -59,10 +67,13 @@ export const handlerMethods = {
59
67
  for (const { toolPrefix, resourceKey } of ATTACHMENT_SCOPED_TOOL_CONFIGS) {
60
68
  handlers[`${toolPrefix}_attachments_list`] = async (args = {}) =>
61
69
  this.asText(
62
- await this.listAttachmentsForKey({
63
- resourceKey,
64
- resourceId: this.pickRequiredArg(args, resourceKey),
65
- })
70
+ slimList(
71
+ await this.listAttachmentsForKey({
72
+ resourceKey,
73
+ resourceId: this.pickRequiredArg(args, resourceKey),
74
+ }),
75
+ { verbose: args.verbose, fields: args.fields, entity: 'attachments' }
76
+ )
66
77
  );
67
78
 
68
79
  handlers[`${toolPrefix}_attachments_upload`] = async (args = {}) =>
@@ -86,16 +97,27 @@ export const handlerMethods = {
86
97
  },
87
98
 
88
99
  registerGlobalHandlers(handlers) {
89
- handlers.tags_list = async () => this.asText(await this.listTags());
100
+ handlers.project_info = async () => this.asText(await this.apiClient.get('info'));
101
+
102
+ handlers.tags_list = async (args = {}) =>
103
+ this.asText(slimList(await this.listTags(), { ...args, entity: 'tags' }));
90
104
  handlers.tags_get = async ({ tag_id: tagId }) => this.asText(await this.getTagByTitle(tagId));
91
- handlers.tags_search = async (args = {}) => this.asText(await this.searchTags(args));
92
105
 
93
- handlers.milestones_list = async (args = {}) => this.asText(await this.listMilestones(args));
106
+ handlers.milestones_list = async (args = {}) => {
107
+ const { verbose, fields, ...listArgs } = args;
108
+ return this.asText(
109
+ slimList(await this.listMilestones(listArgs), { verbose, fields, entity: 'milestones' })
110
+ );
111
+ };
94
112
  handlers.milestones_get = async ({ milestone_id: milestoneId }) =>
95
113
  this.asText(await this.apiClient.get('milestones', milestoneId));
96
114
 
97
- handlers.issues_list = async (args = {}) => this.asText(await this.listIssues(args));
98
- handlers.issues_search = async (args = {}) => this.asText(await this.searchIssues(args));
115
+ handlers.issues_list = async (args = {}) => {
116
+ const { verbose, fields, ...listArgs } = args;
117
+ return this.asText(
118
+ slimList(await this.listIssues(listArgs), { verbose, fields, entity: 'issues' })
119
+ );
120
+ };
99
121
  handlers.issues_create = async (args = {}) => this.asText(await this.createIssue(args));
100
122
  handlers.issues_delete = async ({ issue_id: issueId, type }) =>
101
123
  this.asText(await this.apiClient.delete('issues', issueId, { type }));
@@ -11,10 +11,6 @@ export const issueMethods = {
11
11
  });
12
12
  },
13
13
 
14
- searchIssues(args = {}) {
15
- return this.listIssues(args);
16
- },
17
-
18
14
  createIssue({ url, jira_id: jiraId, ...resourceQuery } = {}) {
19
15
  this.validateIssueResourceQuery(resourceQuery, { allowEmpty: false, allowMany: false });
20
16
  return this.linkIssueToResource({
@@ -11,14 +11,6 @@ export const listingMethods = {
11
11
  });
12
12
  },
13
13
 
14
- searchTests({ page, per_page: perPage, tql } = {}) {
15
- return this.listTests({
16
- page,
17
- per_page: perPage,
18
- tql,
19
- });
20
- },
21
-
22
14
  listSuites({ page, per_page: perPage, file_type: fileType, tag, labels, search_text: searchText } = {}) {
23
15
  return this.apiClient.list('suites', {
24
16
  page,
@@ -30,13 +22,6 @@ export const listingMethods = {
30
22
  });
31
23
  },
32
24
 
33
- searchSuites({ search_text: searchText, ...rest } = {}) {
34
- return this.listSuites({
35
- ...rest,
36
- search_text: searchText,
37
- });
38
- },
39
-
40
25
  listRuns({
41
26
  page,
42
27
  per_page: perPage,
@@ -49,10 +34,6 @@ export const listingMethods = {
49
34
  });
50
35
  },
51
36
 
52
- searchRuns({ page, per_page: perPage, tql } = {}) {
53
- return this.listRuns({ page, per_page: perPage, tql });
54
- },
55
-
56
37
  listTestruns({
57
38
  page,
58
39
  per_page: perPage,
@@ -95,16 +76,6 @@ export const listingMethods = {
95
76
  });
96
77
  },
97
78
 
98
- searchTestruns({ page, per_page: perPage, run_id: runId, filter_search: filterSearch, ...rest } = {}) {
99
- return this.listTestruns({
100
- page,
101
- per_page: perPage,
102
- run_id: runId,
103
- filter_search: filterSearch,
104
- ...rest,
105
- });
106
- },
107
-
108
79
  listRungroups({ page, per_page: perPage } = {}) {
109
80
  return this.apiClient.list('rungroups', { page, per_page: perPage });
110
81
  },
@@ -132,18 +103,10 @@ export const listingMethods = {
132
103
  });
133
104
  },
134
105
 
135
- searchPlans({ page, per_page: perPage, search_text: searchText, ...rest } = {}) {
136
- return this.listPlans({ page, per_page: perPage, search_text: searchText, ...rest });
137
- },
138
-
139
106
  listRequirements({ page, per_page: perPage, source, scope } = {}) {
140
107
  return this.apiClient.list('requirements', { page, per_page: perPage, source, scope });
141
108
  },
142
109
 
143
- searchRequirements({ page, per_page: perPage, source, scope } = {}) {
144
- return this.listRequirements({ page, per_page: perPage, source, scope });
145
- },
146
-
147
110
  listMilestones({ page, per_page: perPage, type, status } = {}) {
148
111
  return this.apiClient.list('milestones', { page, per_page: perPage, type, status });
149
112
  },
@@ -1,4 +1,5 @@
1
1
  import { SYSTEM_TOOLS } from './definitions/system.js';
2
+ import { PROJECT_TOOLS } from './definitions/projects.js';
2
3
  import { TESTS_TOOLS } from './definitions/tests.js';
3
4
  import { SUITES_TOOLS } from './definitions/suites.js';
4
5
  import { RUNS_TOOLS } from './definitions/runs.js';
@@ -13,9 +14,11 @@ import { ISSUES_TOOLS } from './definitions/issues.js';
13
14
  import { PLANS_TOOLS } from './definitions/plans.js';
14
15
  import { REQUIREMENTS_TOOLS } from './definitions/requirements.js';
15
16
  import { ATTACHMENT_TOOLS } from './definitions/attachments.js';
17
+ import { withListOptions } from './list-projection.js';
16
18
 
17
- export const TOOL_DEFINITIONS = [
19
+ export const TOOL_DEFINITIONS = withListOptions([
18
20
  ...SYSTEM_TOOLS,
21
+ ...PROJECT_TOOLS,
19
22
  ...TESTS_TOOLS,
20
23
  ...SUITES_TOOLS,
21
24
  ...RUNS_TOOLS,
@@ -30,4 +33,4 @@ export const TOOL_DEFINITIONS = [
30
33
  ...ATTACHMENT_TOOLS,
31
34
  ...PLANS_TOOLS,
32
35
  ...REQUIREMENTS_TOOLS,
33
- ];
36
+ ]);
@@ -0,0 +1,37 @@
1
+ const RARE_ENTITIES = new Set(['steps', 'snippets', 'labels', 'rungroups']);
2
+
3
+ function entityOf(name) {
4
+ if (name === 'system_ping') return 'system';
5
+ return name
6
+ .split('_attachments_')[0]
7
+ .split('_issues_')[0]
8
+ .replace(/_(list|get|create|update|delete|search)$/, '');
9
+ }
10
+
11
+ function isAttachment(name) {
12
+ return name.includes('_attachments_');
13
+ }
14
+
15
+ function isReadOp(name) {
16
+ return name === 'system_ping' || /_(list|get)$/.test(name) || name.endsWith('_issues_list');
17
+ }
18
+
19
+ /**
20
+ * Whether a tool is visible under the given profile. Unknown profiles fall back to full
21
+ *
22
+ * @param {string} name tool name
23
+ * @param {string} profile 'full' | 'core' | 'read'
24
+ */
25
+ export function isToolInProfile(name, profile) {
26
+ if (!profile || profile === 'full') return true;
27
+ if (isAttachment(name)) return false;
28
+ const coreEntity = !RARE_ENTITIES.has(entityOf(name));
29
+ if (profile === 'core') return coreEntity;
30
+ if (profile === 'read') return coreEntity && isReadOp(name);
31
+ return true;
32
+ }
33
+
34
+ export function selectTools(allTools, profile) {
35
+ if (!profile || profile === 'full') return allTools;
36
+ return allTools.filter((tool) => tool && isToolInProfile(tool.name, profile));
37
+ }
@@ -2,14 +2,41 @@ import { DEFAULT_TOOL_RESPONSE } from '../config/constants.js';
2
2
  import { ApiError, NotImplementedToolError } from '../core/errors.js';
3
3
  import { textResponse } from '../helpers/mcp-response.js';
4
4
  import { TOOL_DEFINITIONS } from './tool-definitions.js';
5
+ import { TQL_FULL_REFERENCE } from './definitions/tql-reference.js';
5
6
  import { handlerMethods } from './registry/handlers.js';
6
7
  import { attachmentMethods } from './registry/attachments.js';
7
8
  import { issueMethods } from './registry/issues.js';
8
9
  import { listingMethods } from './registry/listings.js';
9
10
  import { payloadMethods } from './registry/payloads.js';
10
11
 
12
+ function withPagination(payload) {
13
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
14
+ return payload;
15
+ }
16
+ const meta = payload.meta;
17
+ if (!Array.isArray(payload.data) || !meta || typeof meta !== 'object') {
18
+ return payload;
19
+ }
20
+ const { total, page, per_page: perPage } = meta;
21
+ if (
22
+ typeof total !== 'number' ||
23
+ typeof page !== 'number' ||
24
+ typeof perPage !== 'number' ||
25
+ perPage <= 0
26
+ ) {
27
+ return payload;
28
+ }
29
+ if (page * perPage < total) {
30
+ return {
31
+ ...payload,
32
+ _note: `Showing ${payload.data.length} of ${total} (page ${page}). More available — refine the filter or request the next page.`,
33
+ };
34
+ }
35
+ return payload;
36
+ }
37
+
11
38
  function formatJson(payload) {
12
- return JSON.stringify(payload, null, 2);
39
+ return JSON.stringify(payload);
13
40
  }
14
41
 
15
42
  export class ToolRegistry {
@@ -23,7 +50,7 @@ export class ToolRegistry {
23
50
  }
24
51
 
25
52
  asText(payload) {
26
- return textResponse(formatJson(payload));
53
+ return textResponse(formatJson(withPagination(payload)));
27
54
  }
28
55
 
29
56
  buildHandlers() {
@@ -35,6 +62,7 @@ export class ToolRegistry {
35
62
  baseUrl: this.config.baseUrl,
36
63
  apiVersion: 'v2',
37
64
  }),
65
+ tql_help: async () => textResponse(TQL_FULL_REFERENCE),
38
66
  };
39
67
 
40
68
  this.registerEntityCrudHandlers(handlers);