@testomatio/mcp 2.0.0 → 2.0.1-beta.1

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
@@ -6,12 +6,16 @@ Model Context Protocol (MCP) server that enables AI assistants (Claude, Cursor,
6
6
 
7
7
  - **Full CRUD** for core entities:
8
8
  - Tests, Suites, Plans, Runs, TestRuns, RunGroups, Steps, Snippets, Labels
9
- - Tags (read-only access)
9
+ - Tags and Milestones (read-only access)
10
10
  - Issues (global + scoped helpers for tests/suites/runs/testruns/plans)
11
- - **Smart Search** - delegates to list endpoints with query/filter forwarding
11
+ - Requirements (including file uploads from local file paths)
12
+ - **Smart Search** - delegates to list endpoints with OpenAPI-aligned query/filter forwarding
12
13
  - **Issue Linking** - link/unlink issues to any resource
13
14
  - **API Compatibility** - automatic handling of payload format differences (flat vs wrapped)
15
+ - **Automatic API Sessions** - groups MCP changes in Testomat.io history using API sessions
14
16
  - **Run Management** - status transitions via `status_event` parameter
17
+ - **TQL-Only Search** - `tests_list/tests_search` and `runs_list/runs_search` use `tql` as the single search/filter input
18
+ - **Built-In TQL Reference** - MCP tool descriptions include exact TQL fields, syntax, and examples for agents
15
19
 
16
20
  ## Quick Start
17
21
 
@@ -21,6 +25,8 @@ Model Context Protocol (MCP) server that enables AI assistants (Claude, Cursor,
21
25
  npm install -g @testomatio/mcp@latest
22
26
  ```
23
27
 
28
+ Need enterprise analytics tools? Install `@testomatio/mcp-enterprise@latest` instead. Details are in the `Enterprise Analytics` section below.
29
+
24
30
  ### Configuration
25
31
 
26
32
  **Required credentials:**
@@ -133,7 +139,7 @@ Add this config to `opencode.json` in your project root, or to `~/.config/openco
133
139
  ```json
134
140
  {
135
141
  "name": "tests_list",
136
- "arguments": { "page": 1, "per_page": 50 }
142
+ "arguments": { "page": 1, "per_page": 50, "tql": "priority == 'high'" }
137
143
  }
138
144
  ```
139
145
 
@@ -178,16 +184,16 @@ Complete tool reference: [docs/tools.md](./docs/tools.md)
178
184
 
179
185
  ## Project Structure
180
186
 
181
- ```
187
+ ```text
182
188
  src/
183
- ├── config/ # Config loading, defaults
184
- ├── core/ # Errors, logger
185
- ├── api/ # HTTP client, Testomat.io API client
186
- ├── mcp/ # MCP server, tools, registry
187
- │ ├── definitions/ # Tool definitions by entity
188
- │ ├── configs/ # Registry generation configs
189
- │ └── registry/ # Tool handlers
190
- └── cli/ # CLI bootstrap
189
+ |- config/ # Config loading, defaults
190
+ |- core/ # Errors, logger
191
+ |- api/ # HTTP client, Testomat.io API client
192
+ |- mcp/ # MCP server, tools, registry
193
+ | |- definitions/ # Tool definitions by entity
194
+ | |- configs/ # Registry generation configs
195
+ | `- registry/ # Tool handlers
196
+ `- cli/ # CLI bootstrap
191
197
  ```
192
198
 
193
199
  ## Environment Variables
@@ -236,8 +242,13 @@ NODE_EXTRA_CA_CERTS=/path/to/company-root-ca.pem testomatio-mcp --token <TOKEN>
236
242
  ## Important Notes
237
243
 
238
244
  - **Run Status** - Use `runs_update` with `status_event` for transitions (finish, launch, rerun, etc.)
239
- - **Search** - No dedicated `/search` endpoints; search uses list with filters
245
+ - **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
246
+ - **TQL** - Use `tql` as the single search/filter input for `tests_list/tests_search` and `runs_list/runs_search`
247
+ - **TQL Syntax** - For user-facing syntax details and more examples, see the official TQL docs: https://docs.testomat.io/advanced/tql/
248
+ - **TQL Scope** - The full agent-oriented whitelist of documented fields lives inside MCP tool descriptions for `tests` and `runs`
240
249
  - **Issue Linking** - Scoped helpers available: `{entity}_issues_link/unlink`
250
+ - **Enterprise Package** - Analytics tools are intentionally exposed only by `@testomatio/mcp-enterprise`, not by the standard `@testomatio/mcp` package
251
+ - **API Sessions** - The server automatically starts a Testomat.io session before the first `POST`, `PUT`, or `DELETE` request, sends the returned session hash as `X-Session-Hash` on later mutating requests, and stops the session when the MCP server shuts down. `GET` requests do not start or use sessions.
241
252
 
242
253
  ## Development
243
254
 
@@ -258,3 +269,55 @@ For local MCP development, point Claude Desktop to the checked-out entrypoint:
258
269
  }
259
270
  }
260
271
  ```
272
+
273
+ ## Enterprise Analytics
274
+
275
+ Enterprise analytics is available only in the separate `@testomatio/mcp-enterprise` package.
276
+
277
+ Installation:
278
+
279
+ ```bash
280
+ npm install -g @testomatio/mcp-enterprise@latest
281
+ ```
282
+
283
+ Run:
284
+
285
+ ```bash
286
+ testomatio-mcp-enterprise --token <PROJECT_TOKEN> --project <PROJECT_ID>
287
+ ```
288
+
289
+ Included tools:
290
+
291
+ - `analytics_tests` - `GET /api/v2/{project_id}/analytics/tests/{kind}`
292
+ - `analytics_stats` - `GET /api/v2/{project_id}/analytics/stats/{kind}`
293
+
294
+ Analytics endpoints require the `api_analytics` subscription feature. Use `q` as the TQL filter parameter for analytics tools.
295
+
296
+ Example `analytics_tests` call:
297
+
298
+ ```json
299
+ {
300
+ "name": "analytics_tests",
301
+ "arguments": {
302
+ "kind": "flaky",
303
+ "q": "priority == 'high'",
304
+ "days": 30,
305
+ "page": 1,
306
+ "per_page": 20
307
+ }
308
+ }
309
+ ```
310
+
311
+ Example `analytics_stats` call:
312
+
313
+ ```json
314
+ {
315
+ "name": "analytics_stats",
316
+ "arguments": {
317
+ "kind": "success-rate-by-date",
318
+ "q": "tag IN ['@smoke']",
319
+ "from": "2026-04-01",
320
+ "to": "2026-04-30"
321
+ }
322
+ }
323
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/mcp",
3
- "version": "2.0.0",
3
+ "version": "2.0.1-beta.1",
4
4
  "description": "Model Context Protocol server for Testomatio API",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -9,7 +9,8 @@
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "node index.js",
12
- "dev": "node index.js"
12
+ "dev": "node index.js",
13
+ "sync:enterprise-package": "node scripts/sync-enterprise-package.js"
13
14
  },
14
15
  "keywords": [
15
16
  "testomatio",
@@ -1,5 +1,9 @@
1
1
  import { ApiError } from '../core/errors.js';
2
2
 
3
+ function isFormData(body) {
4
+ return typeof FormData !== 'undefined' && body instanceof FormData;
5
+ }
6
+
3
7
  function buildUrl(baseUrl, path, query = {}) {
4
8
  const url = new URL(path, `${baseUrl}/`);
5
9
 
@@ -26,12 +30,13 @@ export class HttpClient {
26
30
  this.logger = logger;
27
31
  }
28
32
 
29
- async request(method, path, { query, body } = {}) {
33
+ async request(method, path, { query, body, headers: requestHeaders = {} } = {}) {
30
34
  const url = buildUrl(this.baseUrl, path, query);
31
35
 
32
36
  const headers = {
33
37
  Accept: 'application/json',
34
38
  Authorization: `Bearer ${this.token}`,
39
+ ...requestHeaders,
35
40
  };
36
41
 
37
42
  const options = {
@@ -40,8 +45,12 @@ export class HttpClient {
40
45
  };
41
46
 
42
47
  if (body !== undefined) {
43
- headers['Content-Type'] = 'application/json';
44
- options.body = JSON.stringify(body);
48
+ if (isFormData(body)) {
49
+ options.body = body;
50
+ } else {
51
+ headers['Content-Type'] = 'application/json';
52
+ options.body = JSON.stringify(body);
53
+ }
45
54
  }
46
55
 
47
56
  this.logger.debug('HTTP request', { method, url });
@@ -3,6 +3,9 @@ import { HttpClient } from './http-client.js';
3
3
  export class TestomatioApiClient {
4
4
  constructor({ baseUrl, projectId, token, logger }) {
5
5
  this.projectId = projectId;
6
+ this.logger = logger;
7
+ this.sessionHash = null;
8
+ this.sessionPromise = null;
6
9
  this.http = new HttpClient({
7
10
  baseUrl,
8
11
  token,
@@ -24,23 +27,101 @@ export class TestomatioApiClient {
24
27
  return this.http.request('GET', this.buildPath(resource, id), { query });
25
28
  }
26
29
 
27
- create(resource, body = {}) {
28
- return this.http.request('POST', this.buildPath(resource), { body });
30
+ async create(resource, body = {}) {
31
+ return this.mutate('POST', this.buildPath(resource), { body });
29
32
  }
30
33
 
31
- createWithQuery(resource, { query = {}, body = {} } = {}) {
32
- return this.http.request('POST', this.buildPath(resource), { query, body });
34
+ async createMultipart(resource, formData) {
35
+ return this.mutate('POST', this.buildPath(resource), { body: formData });
33
36
  }
34
37
 
35
- update(resource, id, body = {}) {
36
- return this.http.request('PUT', this.buildPath(resource, id), { body });
38
+ async createWithQuery(resource, { query = {}, body = {} } = {}) {
39
+ return this.mutate('POST', this.buildPath(resource), { query, body });
37
40
  }
38
41
 
39
- delete(resource, id, query = {}) {
40
- return this.http.request('DELETE', this.buildPath(resource, id), { query });
42
+ async update(resource, id, body = {}) {
43
+ return this.mutate('PUT', this.buildPath(resource, id), { body });
44
+ }
45
+
46
+ async patch(resource, id, body = {}) {
47
+ return this.mutate('PATCH', this.buildPath(resource, id), { body });
48
+ }
49
+
50
+ async patchMultipart(resource, id, formData) {
51
+ return this.mutate('PATCH', this.buildPath(resource, id), { body: formData });
52
+ }
53
+
54
+ async delete(resource, id, query = {}) {
55
+ return this.mutate('DELETE', this.buildPath(resource, id), { query });
41
56
  }
42
57
 
43
58
  search(resource, query = {}) {
44
59
  return this.list(resource, query);
45
60
  }
61
+
62
+ async mutate(method, path, options = {}) {
63
+ const sessionHash = await this.ensureSession();
64
+ return this.http.request(method, path, {
65
+ ...options,
66
+ headers: {
67
+ ...options.headers,
68
+ 'X-Session-Hash': sessionHash,
69
+ },
70
+ });
71
+ }
72
+
73
+ async ensureSession() {
74
+ if (this.sessionHash) {
75
+ return this.sessionHash;
76
+ }
77
+
78
+ if (!this.sessionPromise) {
79
+ this.sessionPromise = this.startSession().finally(() => {
80
+ this.sessionPromise = null;
81
+ });
82
+ }
83
+
84
+ return this.sessionPromise;
85
+ }
86
+
87
+ async startSession() {
88
+ const response = await this.http.request('POST', this.buildPath('sessions'), {
89
+ body: { description: 'MCP session' },
90
+ });
91
+ const sessionHash = response?.data?.hash || response?.hash;
92
+
93
+ if (!sessionHash) {
94
+ throw new Error('Failed to start Testomat.io API session: missing session hash');
95
+ }
96
+
97
+ this.sessionHash = sessionHash;
98
+ return sessionHash;
99
+ }
100
+
101
+ async stopSession() {
102
+ if (this.sessionPromise) {
103
+ try {
104
+ await this.sessionPromise;
105
+ } catch {
106
+ return;
107
+ }
108
+ }
109
+
110
+ if (!this.sessionHash) {
111
+ return;
112
+ }
113
+
114
+ const sessionHash = this.sessionHash;
115
+ this.sessionHash = null;
116
+
117
+ try {
118
+ await this.http.request('DELETE', this.buildPath('sessions', sessionHash));
119
+ } catch (error) {
120
+ if (error?.status !== 404) {
121
+ this.logger?.error('Failed to stop Testomat.io API session', {
122
+ error: error?.message || error,
123
+ });
124
+ }
125
+ }
126
+ }
46
127
  }
package/src/index.js CHANGED
@@ -1,15 +1,36 @@
1
1
  import { TestomatioApiClient } from './api/testomatio-client.js';
2
2
  import { loadConfig } from './config/load-config.js';
3
+ import { ConfigurationError } from './core/errors.js';
3
4
  import { createLogger } from './core/logger.js';
4
5
  import { TestomatioMCPServer } from './mcp/server.js';
6
+ import { TOOL_DEFINITIONS } from './mcp/tool-definitions.js';
7
+ import {
8
+ ANALYTICS_STATS_TQL_INPUT_DESCRIPTION,
9
+ ANALYTICS_STATS_TQL_REFERENCE,
10
+ ANALYTICS_TESTS_TQL_INPUT_DESCRIPTION,
11
+ ANALYTICS_TESTS_TQL_REFERENCE,
12
+ } from './mcp/definitions/tql-reference.js';
5
13
 
6
14
  export { TestomatioMCPServer };
15
+ export {
16
+ ANALYTICS_STATS_TQL_INPUT_DESCRIPTION,
17
+ ANALYTICS_STATS_TQL_REFERENCE,
18
+ ANALYTICS_TESTS_TQL_INPUT_DESCRIPTION,
19
+ ANALYTICS_TESTS_TQL_REFERENCE,
20
+ ConfigurationError,
21
+ TOOL_DEFINITIONS,
22
+ };
7
23
 
8
- export function createApplication(argvOptions = {}) {
24
+ export function createApplication(argvOptions = {}, serverOptions = {}) {
9
25
  const config = loadConfig(argvOptions);
10
26
  const logger = createLogger();
11
27
  const apiClient = new TestomatioApiClient({ ...config, logger });
12
- const mcpServer = new TestomatioMCPServer({ config, apiClient, logger });
28
+ const mcpServer = new TestomatioMCPServer({
29
+ config,
30
+ apiClient,
31
+ logger,
32
+ ...serverOptions,
33
+ });
13
34
 
14
35
  return {
15
36
  config,
@@ -46,7 +46,6 @@ export const ENTITY_CRUD_CONFIGS = [
46
46
  resource: 'rungroups',
47
47
  idArg: 'rungroup_id',
48
48
  listMethod: 'listRungroups',
49
- searchMethod: 'searchRungroups',
50
49
  payloadBuilder: 'buildRungroupPayload',
51
50
  wrapperKey: 'rungroup',
52
51
  createMode: 'wrapped',
@@ -57,7 +56,6 @@ export const ENTITY_CRUD_CONFIGS = [
57
56
  resource: 'steps',
58
57
  idArg: 'step_id',
59
58
  listMethod: 'listSteps',
60
- searchMethod: 'searchSteps',
61
59
  payloadBuilder: 'buildStepPayload',
62
60
  wrapperKey: 'step',
63
61
  createMode: 'wrapped',
@@ -68,7 +66,6 @@ export const ENTITY_CRUD_CONFIGS = [
68
66
  resource: 'snippets',
69
67
  idArg: 'snippet_id',
70
68
  listMethod: 'listSnippets',
71
- searchMethod: 'searchSnippets',
72
69
  payloadBuilder: 'buildSnippetPayload',
73
70
  wrapperKey: 'snippet',
74
71
  createMode: 'wrapped',
@@ -79,7 +76,6 @@ export const ENTITY_CRUD_CONFIGS = [
79
76
  resource: 'labels',
80
77
  idArg: 'label_id',
81
78
  listMethod: 'listLabels',
82
- searchMethod: 'searchLabels',
83
79
  payloadBuilder: 'buildLabelPayload',
84
80
  wrapperKey: 'label',
85
81
  createMode: 'wrapped',
@@ -96,4 +92,16 @@ export const ENTITY_CRUD_CONFIGS = [
96
92
  createMode: 'wrapped',
97
93
  updateMode: 'wrapped',
98
94
  },
95
+ {
96
+ toolPrefix: 'requirements',
97
+ resource: 'requirements',
98
+ idArg: 'requirement_id',
99
+ listMethod: 'listRequirements',
100
+ searchMethod: 'searchRequirements',
101
+ payloadBuilder: 'buildRequirementPayload',
102
+ wrapperKey: 'requirement',
103
+ createMode: 'requirement',
104
+ updateMode: 'requirement',
105
+ updateMethod: 'patch',
106
+ },
99
107
  ];
@@ -13,9 +13,6 @@ export const LABELS_TOOLS = [
13
13
  "type": "integer",
14
14
  "minimum": 1,
15
15
  "maximum": 100
16
- },
17
- "query": {
18
- "type": "string"
19
16
  }
20
17
  },
21
18
  "additionalProperties": false
@@ -148,26 +145,4 @@ export const LABELS_TOOLS = [
148
145
  "additionalProperties": false
149
146
  }
150
147
  },
151
- {
152
- "name": "labels_search",
153
- "description": "Search labels (delegates to labels list; docs has no dedicated search parameter)",
154
- "inputSchema": {
155
- "type": "object",
156
- "properties": {
157
- "query": {
158
- "type": "string"
159
- },
160
- "page": {
161
- "type": "integer",
162
- "minimum": 1
163
- },
164
- "per_page": {
165
- "type": "integer",
166
- "minimum": 1,
167
- "maximum": 100
168
- }
169
- },
170
- "additionalProperties": false
171
- }
172
- }
173
148
  ];
@@ -0,0 +1,43 @@
1
+ export const MILESTONES_TOOLS = [
2
+ {
3
+ name: 'milestones_list',
4
+ description: 'List milestones (/api/v2/{project_id}/milestones)',
5
+ inputSchema: {
6
+ type: 'object',
7
+ properties: {
8
+ page: {
9
+ type: 'integer',
10
+ minimum: 1,
11
+ },
12
+ per_page: {
13
+ type: 'integer',
14
+ minimum: 1,
15
+ maximum: 100,
16
+ },
17
+ type: {
18
+ type: 'string',
19
+ description: 'Filter by milestone type (title), e.g. Sprint or Release.',
20
+ },
21
+ status: {
22
+ type: 'string',
23
+ enum: ['created', 'active', 'closed'],
24
+ },
25
+ },
26
+ additionalProperties: false,
27
+ },
28
+ },
29
+ {
30
+ name: 'milestones_get',
31
+ description: 'Get milestone by ID',
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ milestone_id: {
36
+ type: 'string',
37
+ },
38
+ },
39
+ required: ['milestone_id'],
40
+ additionalProperties: false,
41
+ },
42
+ },
43
+ ];
@@ -14,7 +14,24 @@ export const PLANS_TOOLS = [
14
14
  "minimum": 1,
15
15
  "maximum": 100
16
16
  },
17
- "query": {
17
+ "kind": {
18
+ "type": "string",
19
+ "enum": [
20
+ "manual",
21
+ "automated",
22
+ "mixed"
23
+ ]
24
+ },
25
+ "hidden": {
26
+ "type": "boolean"
27
+ },
28
+ "labels": {
29
+ "type": "array",
30
+ "items": {
31
+ "type": "string"
32
+ }
33
+ },
34
+ "search_text": {
18
35
  "type": "string"
19
36
  }
20
37
  },
@@ -95,6 +112,7 @@ export const PLANS_TOOLS = [
95
112
  "label",
96
113
  "custom_field",
97
114
  "tag",
115
+ "milestone",
98
116
  "issue",
99
117
  "jira"
100
118
  ]
@@ -179,6 +197,7 @@ export const PLANS_TOOLS = [
179
197
  "label",
180
198
  "custom_field",
181
199
  "tag",
200
+ "milestone",
182
201
  "issue",
183
202
  "jira"
184
203
  ]
@@ -224,7 +243,7 @@ export const PLANS_TOOLS = [
224
243
  "inputSchema": {
225
244
  "type": "object",
226
245
  "properties": {
227
- "query": {
246
+ "search_text": {
228
247
  "type": "string"
229
248
  },
230
249
  "page": {
@@ -235,6 +254,23 @@ export const PLANS_TOOLS = [
235
254
  "type": "integer",
236
255
  "minimum": 1,
237
256
  "maximum": 100
257
+ },
258
+ "kind": {
259
+ "type": "string",
260
+ "enum": [
261
+ "manual",
262
+ "automated",
263
+ "mixed"
264
+ ]
265
+ },
266
+ "hidden": {
267
+ "type": "boolean"
268
+ },
269
+ "labels": {
270
+ "type": "array",
271
+ "items": {
272
+ "type": "string"
273
+ }
238
274
  }
239
275
  },
240
276
  "additionalProperties": false