@testomatio/mcp 2.0.1-beta → 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 +63 -5
- package/package.json +3 -2
- package/src/api/http-client.js +12 -3
- package/src/api/testomatio-client.js +89 -8
- package/src/index.js +23 -2
- package/src/mcp/configs/entity-crud-config.js +12 -0
- package/src/mcp/definitions/milestones.js +43 -0
- package/src/mcp/definitions/requirements.js +163 -0
- package/src/mcp/definitions/runs.js +6 -4
- package/src/mcp/definitions/tests.js +6 -4
- package/src/mcp/definitions/tql-reference.js +133 -0
- package/src/mcp/registry/handlers.js +13 -0
- package/src/mcp/registry/listings.js +12 -0
- package/src/mcp/registry/payloads.js +87 -0
- package/src/mcp/server.js +47 -4
- package/src/mcp/tool-definitions.js +4 -0
- package/src/mcp/tool-registry.js +7 -2
package/README.md
CHANGED
|
@@ -6,14 +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
|
+
- Requirements (including file uploads from local file paths)
|
|
11
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
|
|
15
17
|
- **TQL-Only Search** - `tests_list/tests_search` and `runs_list/runs_search` use `tql` as the single search/filter input
|
|
16
|
-
- **
|
|
18
|
+
- **Built-In TQL Reference** - MCP tool descriptions include exact TQL fields, syntax, and examples for agents
|
|
17
19
|
|
|
18
20
|
## Quick Start
|
|
19
21
|
|
|
@@ -23,6 +25,8 @@ Model Context Protocol (MCP) server that enables AI assistants (Claude, Cursor,
|
|
|
23
25
|
npm install -g @testomatio/mcp@latest
|
|
24
26
|
```
|
|
25
27
|
|
|
28
|
+
Need enterprise analytics tools? Install `@testomatio/mcp-enterprise@latest` instead. Details are in the `Enterprise Analytics` section below.
|
|
29
|
+
|
|
26
30
|
### Configuration
|
|
27
31
|
|
|
28
32
|
**Required credentials:**
|
|
@@ -135,7 +139,7 @@ Add this config to `opencode.json` in your project root, or to `~/.config/openco
|
|
|
135
139
|
```json
|
|
136
140
|
{
|
|
137
141
|
"name": "tests_list",
|
|
138
|
-
"arguments": { "page": 1, "per_page": 50, "tql": "priority == high" }
|
|
142
|
+
"arguments": { "page": 1, "per_page": 50, "tql": "priority == 'high'" }
|
|
139
143
|
}
|
|
140
144
|
```
|
|
141
145
|
|
|
@@ -240,9 +244,11 @@ NODE_EXTRA_CA_CERTS=/path/to/company-root-ca.pem testomatio-mcp --token <TOKEN>
|
|
|
240
244
|
- **Run Status** - Use `runs_update` with `status_event` for transitions (finish, launch, rerun, etc.)
|
|
241
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
|
|
242
246
|
- **TQL** - Use `tql` as the single search/filter input for `tests_list/tests_search` and `runs_list/runs_search`
|
|
243
|
-
- **TQL
|
|
244
|
-
- **
|
|
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`
|
|
245
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.
|
|
246
252
|
|
|
247
253
|
## Development
|
|
248
254
|
|
|
@@ -263,3 +269,55 @@ For local MCP development, point Claude Desktop to the checked-out entrypoint:
|
|
|
263
269
|
}
|
|
264
270
|
}
|
|
265
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.1-beta",
|
|
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",
|
package/src/api/http-client.js
CHANGED
|
@@ -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
|
-
|
|
44
|
-
|
|
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.
|
|
30
|
+
async create(resource, body = {}) {
|
|
31
|
+
return this.mutate('POST', this.buildPath(resource), { body });
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
return this.
|
|
34
|
+
async createMultipart(resource, formData) {
|
|
35
|
+
return this.mutate('POST', this.buildPath(resource), { body: formData });
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
return this.
|
|
38
|
+
async createWithQuery(resource, { query = {}, body = {} } = {}) {
|
|
39
|
+
return this.mutate('POST', this.buildPath(resource), { query, body });
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
return this.
|
|
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({
|
|
28
|
+
const mcpServer = new TestomatioMCPServer({
|
|
29
|
+
config,
|
|
30
|
+
apiClient,
|
|
31
|
+
logger,
|
|
32
|
+
...serverOptions,
|
|
33
|
+
});
|
|
13
34
|
|
|
14
35
|
return {
|
|
15
36
|
config,
|
|
@@ -92,4 +92,16 @@ export const ENTITY_CRUD_CONFIGS = [
|
|
|
92
92
|
createMode: 'wrapped',
|
|
93
93
|
updateMode: 'wrapped',
|
|
94
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
|
+
},
|
|
95
107
|
];
|
|
@@ -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
|
+
];
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
export const REQUIREMENTS_TOOLS = [
|
|
2
|
+
{
|
|
3
|
+
name: 'requirements_list',
|
|
4
|
+
description: 'List requirements (/api/v2/{project_id}/requirements)',
|
|
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
|
+
source: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
enum: ['jira', 'confluence', 'file', 'text'],
|
|
20
|
+
},
|
|
21
|
+
scope: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
enum: ['global', 'attached', 'detached', 'without_suites'],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'requirements_get',
|
|
31
|
+
description: 'Get requirement by ID',
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
requirement_id: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
required: ['requirement_id'],
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'requirements_create',
|
|
45
|
+
description: 'Create requirement (/api/v2/{project_id}/requirements)',
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
title: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
},
|
|
52
|
+
source_type: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
enum: ['jira', 'confluence', 'file', 'text'],
|
|
55
|
+
},
|
|
56
|
+
description: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: 'Required for text requirements. Must be at least 500 characters.',
|
|
59
|
+
},
|
|
60
|
+
details: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
},
|
|
63
|
+
active: {
|
|
64
|
+
type: 'boolean',
|
|
65
|
+
},
|
|
66
|
+
global: {
|
|
67
|
+
type: 'boolean',
|
|
68
|
+
},
|
|
69
|
+
confluence_url: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: 'Required for confluence requirements.',
|
|
72
|
+
},
|
|
73
|
+
files: {
|
|
74
|
+
type: 'array',
|
|
75
|
+
items: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
},
|
|
78
|
+
description: 'Local file paths to upload for file requirements.',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
required: ['title', 'source_type'],
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'requirements_update',
|
|
87
|
+
description: 'Update requirement (/api/v2/{project_id}/requirements/{id})',
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
requirement_id: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
},
|
|
94
|
+
title: {
|
|
95
|
+
type: 'string',
|
|
96
|
+
},
|
|
97
|
+
description: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
description: 'Only applied for text requirements.',
|
|
100
|
+
},
|
|
101
|
+
details: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
},
|
|
104
|
+
active: {
|
|
105
|
+
type: 'boolean',
|
|
106
|
+
},
|
|
107
|
+
global: {
|
|
108
|
+
type: 'boolean',
|
|
109
|
+
},
|
|
110
|
+
files: {
|
|
111
|
+
type: 'array',
|
|
112
|
+
items: {
|
|
113
|
+
type: 'string',
|
|
114
|
+
},
|
|
115
|
+
description: 'Local file paths to upload for file requirements.',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
required: ['requirement_id'],
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'requirements_delete',
|
|
124
|
+
description: 'Delete requirement (/api/v2/{project_id}/requirements/{id})',
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
requirement_id: {
|
|
129
|
+
type: 'string',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
required: ['requirement_id'],
|
|
133
|
+
additionalProperties: false,
|
|
134
|
+
},
|
|
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
|
+
];
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { RUNS_TQL_INPUT_DESCRIPTION, RUNS_TQL_REFERENCE } from './tql-reference.js';
|
|
2
|
+
|
|
1
3
|
export const RUNS_TOOLS = [
|
|
2
4
|
{
|
|
3
5
|
"name": "runs_list",
|
|
4
|
-
"description":
|
|
6
|
+
"description": `List runs (/api/v2/{project_id}/runs). ${RUNS_TQL_REFERENCE}`,
|
|
5
7
|
"inputSchema": {
|
|
6
8
|
"type": "object",
|
|
7
9
|
"properties": {
|
|
@@ -16,7 +18,7 @@ export const RUNS_TOOLS = [
|
|
|
16
18
|
},
|
|
17
19
|
"tql": {
|
|
18
20
|
"type": "string",
|
|
19
|
-
"description":
|
|
21
|
+
"description": RUNS_TQL_INPUT_DESCRIPTION
|
|
20
22
|
}
|
|
21
23
|
},
|
|
22
24
|
"additionalProperties": false
|
|
@@ -264,7 +266,7 @@ export const RUNS_TOOLS = [
|
|
|
264
266
|
},
|
|
265
267
|
{
|
|
266
268
|
"name": "runs_search",
|
|
267
|
-
"description":
|
|
269
|
+
"description": `Search runs using TQL (delegates to runs_list). ${RUNS_TQL_REFERENCE}`,
|
|
268
270
|
"inputSchema": {
|
|
269
271
|
"type": "object",
|
|
270
272
|
"properties": {
|
|
@@ -279,7 +281,7 @@ export const RUNS_TOOLS = [
|
|
|
279
281
|
},
|
|
280
282
|
"tql": {
|
|
281
283
|
"type": "string",
|
|
282
|
-
"description":
|
|
284
|
+
"description": RUNS_TQL_INPUT_DESCRIPTION
|
|
283
285
|
}
|
|
284
286
|
},
|
|
285
287
|
"additionalProperties": false
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { TESTS_TQL_INPUT_DESCRIPTION, TESTS_TQL_REFERENCE } from './tql-reference.js';
|
|
2
|
+
|
|
1
3
|
export const TESTS_TOOLS = [
|
|
2
4
|
{
|
|
3
5
|
"name": "tests_list",
|
|
4
|
-
"description":
|
|
6
|
+
"description": `List tests (/api/v2/{project_id}/tests). ${TESTS_TQL_REFERENCE}`,
|
|
5
7
|
"inputSchema": {
|
|
6
8
|
"type": "object",
|
|
7
9
|
"properties": {
|
|
@@ -16,7 +18,7 @@ export const TESTS_TOOLS = [
|
|
|
16
18
|
},
|
|
17
19
|
"tql": {
|
|
18
20
|
"type": "string",
|
|
19
|
-
"description":
|
|
21
|
+
"description": TESTS_TQL_INPUT_DESCRIPTION
|
|
20
22
|
}
|
|
21
23
|
},
|
|
22
24
|
"additionalProperties": false
|
|
@@ -207,7 +209,7 @@ export const TESTS_TOOLS = [
|
|
|
207
209
|
},
|
|
208
210
|
{
|
|
209
211
|
"name": "tests_search",
|
|
210
|
-
"description":
|
|
212
|
+
"description": `Search tests using TQL (delegates to tests_list). ${TESTS_TQL_REFERENCE}`,
|
|
211
213
|
"inputSchema": {
|
|
212
214
|
"type": "object",
|
|
213
215
|
"properties": {
|
|
@@ -222,7 +224,7 @@ export const TESTS_TOOLS = [
|
|
|
222
224
|
},
|
|
223
225
|
"tql": {
|
|
224
226
|
"type": "string",
|
|
225
|
-
"description":
|
|
227
|
+
"description": TESTS_TQL_INPUT_DESCRIPTION
|
|
226
228
|
}
|
|
227
229
|
},
|
|
228
230
|
"additionalProperties": false
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const TESTS_TQL_VARIABLES = [
|
|
2
|
+
'tag',
|
|
3
|
+
'label',
|
|
4
|
+
'priority',
|
|
5
|
+
'issue',
|
|
6
|
+
'jira',
|
|
7
|
+
'state',
|
|
8
|
+
'status',
|
|
9
|
+
'custom_status',
|
|
10
|
+
'created_at',
|
|
11
|
+
'updated_at',
|
|
12
|
+
'last_run_at',
|
|
13
|
+
'executed_at',
|
|
14
|
+
'created_by',
|
|
15
|
+
'assigned_to',
|
|
16
|
+
'suite',
|
|
17
|
+
'test',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const RUNS_TQL_VARIABLES = [
|
|
21
|
+
'title',
|
|
22
|
+
'plan',
|
|
23
|
+
'rungroup',
|
|
24
|
+
'env',
|
|
25
|
+
'tag',
|
|
26
|
+
'label',
|
|
27
|
+
'jira',
|
|
28
|
+
'duration',
|
|
29
|
+
'passed_count',
|
|
30
|
+
'failed_count',
|
|
31
|
+
'skipped_count',
|
|
32
|
+
'automated',
|
|
33
|
+
'manual',
|
|
34
|
+
'mixed',
|
|
35
|
+
'finished',
|
|
36
|
+
'unfinished',
|
|
37
|
+
'passed',
|
|
38
|
+
'failed',
|
|
39
|
+
'terminated',
|
|
40
|
+
'published',
|
|
41
|
+
'private',
|
|
42
|
+
'archived',
|
|
43
|
+
'unarchived',
|
|
44
|
+
'with_defect',
|
|
45
|
+
'has_defect',
|
|
46
|
+
'has_test',
|
|
47
|
+
'has_test_tag',
|
|
48
|
+
'has_test_label',
|
|
49
|
+
'has_suite',
|
|
50
|
+
'has_message',
|
|
51
|
+
'has_custom_status',
|
|
52
|
+
'has_assigned_to',
|
|
53
|
+
'has_retries',
|
|
54
|
+
'has_test_duration',
|
|
55
|
+
'has_priority',
|
|
56
|
+
'created_at',
|
|
57
|
+
'updated_at',
|
|
58
|
+
'launched_at',
|
|
59
|
+
'finished_at',
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const TESTS_TQL_EXAMPLES = [
|
|
63
|
+
"priority == 'high'",
|
|
64
|
+
"state == 'automated'",
|
|
65
|
+
"tag in ['smoke', 'stage1'] and status == 'failed'",
|
|
66
|
+
"suite % 'Checkout'",
|
|
67
|
+
"test % 'User login'",
|
|
68
|
+
"created_at < 1.month_ago",
|
|
69
|
+
"jira in ['JST-1', 'JST-2']",
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const RUNS_TQL_EXAMPLES = [
|
|
73
|
+
"title % 'Manual tests'",
|
|
74
|
+
"plan == '{PLAN_ID}'",
|
|
75
|
+
"env in ['Windows', 'Linux']",
|
|
76
|
+
"failed and has_test_tag == 'regression'",
|
|
77
|
+
'finished and with_defect',
|
|
78
|
+
'has_retries > 2',
|
|
79
|
+
"automated and env == 'Production' and has_message % 'Server Error'",
|
|
80
|
+
"finished_at >= '2025-07-01' and finished_at <= '2025-07-31' and failed",
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const COMMON_TQL_SYNTAX =
|
|
84
|
+
"Supported syntax includes `==`, `!=`, `>`, `<`, `>=`, `<=`, `in [...]`, `%` for partial text match, `and`, `or`, `not`, and parentheses for grouping. Use quotes for string values, for example `state == 'automated'`.";
|
|
85
|
+
|
|
86
|
+
export const TESTS_TQL_REFERENCE =
|
|
87
|
+
`TQL (Testomat.io Query Language) is a string expression passed in \`tql\` to filter tests. ${COMMON_TQL_SYNTAX} ` +
|
|
88
|
+
`Documented test variables: ${TESTS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
|
|
89
|
+
`Documented examples: ${TESTS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}. ` +
|
|
90
|
+
'Do not invent undocumented fields or syntax. If a query fails, simplify it to one documented predicate.';
|
|
91
|
+
|
|
92
|
+
export const TESTS_TQL_INPUT_DESCRIPTION =
|
|
93
|
+
`TQL filter for tests. Documented variables: ${TESTS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
|
|
94
|
+
`Examples: ${TESTS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}.`;
|
|
95
|
+
|
|
96
|
+
export const RUNS_TQL_REFERENCE =
|
|
97
|
+
`TQL (Testomat.io Query Language) is a string expression passed in \`tql\` to filter runs. ${COMMON_TQL_SYNTAX} ` +
|
|
98
|
+
'Runs also support boolean flags without comparison such as `failed`, `finished`, `automated`, or `with_defect`. ' +
|
|
99
|
+
`Documented run variables: ${RUNS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
|
|
100
|
+
`Documented examples: ${RUNS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}. ` +
|
|
101
|
+
'Do not invent undocumented fields or syntax. If a query fails, simplify it to one documented predicate.';
|
|
102
|
+
|
|
103
|
+
export const RUNS_TQL_INPUT_DESCRIPTION =
|
|
104
|
+
`TQL filter for runs. Documented variables: ${RUNS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
|
|
105
|
+
`Examples: ${RUNS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}.`;
|
|
106
|
+
|
|
107
|
+
export const ANALYTICS_TESTS_TQL_REFERENCE =
|
|
108
|
+
`TQL (Testomat.io Query Language) is a string expression passed in \`q\` to filter enterprise analytics test reports. ${COMMON_TQL_SYNTAX} ` +
|
|
109
|
+
'For analytics tools, the API parameter name is `q`, not `tql`. ' +
|
|
110
|
+
`Documented analytics test variables: ${TESTS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
|
|
111
|
+
`Documented examples: ${TESTS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}. ` +
|
|
112
|
+
'Do not invent undocumented fields or syntax. If a query fails, simplify it to one documented predicate.';
|
|
113
|
+
|
|
114
|
+
export const ANALYTICS_TESTS_TQL_INPUT_DESCRIPTION =
|
|
115
|
+
'TQL filter for analytics test reports. The API parameter name is `q`, not `tql`. ' +
|
|
116
|
+
`Documented variables: ${TESTS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
|
|
117
|
+
`Examples: ${TESTS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}.`;
|
|
118
|
+
|
|
119
|
+
export const ANALYTICS_STATS_TQL_REFERENCE =
|
|
120
|
+
`TQL (Testomat.io Query Language) is a string expression passed in \`q\` to filter enterprise analytics aggregated reports. ${COMMON_TQL_SYNTAX} ` +
|
|
121
|
+
'For analytics tools, the API parameter name is `q`, not `tql`. ' +
|
|
122
|
+
'According to the official Analytics docs, analytics queries are configured using supported query variables for two data sources: `Tests Variables` and `Runs Variables`. ' +
|
|
123
|
+
`Documented Tests Variables: ${TESTS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
|
|
124
|
+
`Documented Runs Variables: ${RUNS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
|
|
125
|
+
`Documented Tests examples: ${TESTS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}. ` +
|
|
126
|
+
`Documented Runs examples: ${RUNS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}. ` +
|
|
127
|
+
'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.';
|
|
128
|
+
|
|
129
|
+
export const ANALYTICS_STATS_TQL_INPUT_DESCRIPTION =
|
|
130
|
+
'TQL filter for analytics aggregated reports. The API parameter name is `q`, not `tql`. ' +
|
|
131
|
+
`Documented Tests Variables: ${TESTS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
|
|
132
|
+
`Documented Runs Variables: ${RUNS_TQL_VARIABLES.map((item) => `\`${item}\``).join(', ')}. ` +
|
|
133
|
+
`Examples: ${TESTS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}, ${RUNS_TQL_EXAMPLES.map((item) => `\`${item}\``).join(', ')}.`;
|
|
@@ -59,6 +59,10 @@ export const handlerMethods = {
|
|
|
59
59
|
handlers.tags_get = async ({ tag_id: tagId }) => this.asText(await this.getTagByTitle(tagId));
|
|
60
60
|
handlers.tags_search = async (args = {}) => this.asText(await this.searchTags(args));
|
|
61
61
|
|
|
62
|
+
handlers.milestones_list = async (args = {}) => this.asText(await this.listMilestones(args));
|
|
63
|
+
handlers.milestones_get = async ({ milestone_id: milestoneId }) =>
|
|
64
|
+
this.asText(await this.apiClient.get('milestones', milestoneId));
|
|
65
|
+
|
|
62
66
|
handlers.issues_list = async (args = {}) => this.asText(await this.listIssues(args));
|
|
63
67
|
handlers.issues_search = async (args = {}) => this.asText(await this.searchIssues(args));
|
|
64
68
|
handlers.issues_create = async (args = {}) => this.asText(await this.createIssue(args));
|
|
@@ -84,6 +88,9 @@ export const handlerMethods = {
|
|
|
84
88
|
if (spec.createMode === 'run') {
|
|
85
89
|
return this.createRunWithFallback(args);
|
|
86
90
|
}
|
|
91
|
+
if (spec.createMode === 'requirement') {
|
|
92
|
+
return this.createRequirement(args);
|
|
93
|
+
}
|
|
87
94
|
const payload = this[spec.payloadBuilder](args);
|
|
88
95
|
return this.createWrapped(spec.resource, spec.wrapperKey, payload);
|
|
89
96
|
},
|
|
@@ -92,7 +99,13 @@ export const handlerMethods = {
|
|
|
92
99
|
if (spec.updateMode === 'run') {
|
|
93
100
|
return this.updateRunWithFallback(id, args);
|
|
94
101
|
}
|
|
102
|
+
if (spec.updateMode === 'requirement') {
|
|
103
|
+
return this.updateRequirement(id, args);
|
|
104
|
+
}
|
|
95
105
|
const payload = this[spec.payloadBuilder](args);
|
|
106
|
+
if (spec.updateMethod === 'patch') {
|
|
107
|
+
return this.patchWrapped(spec.resource, id, spec.wrapperKey, payload);
|
|
108
|
+
}
|
|
96
109
|
return this.updateWrapped(spec.resource, id, spec.wrapperKey, payload);
|
|
97
110
|
},
|
|
98
111
|
};
|
|
@@ -136,6 +136,18 @@ export const listingMethods = {
|
|
|
136
136
|
return this.listPlans({ page, per_page: perPage, search_text: searchText, ...rest });
|
|
137
137
|
},
|
|
138
138
|
|
|
139
|
+
listRequirements({ page, per_page: perPage, source, scope } = {}) {
|
|
140
|
+
return this.apiClient.list('requirements', { page, per_page: perPage, source, scope });
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
searchRequirements({ page, per_page: perPage, source, scope } = {}) {
|
|
144
|
+
return this.listRequirements({ page, per_page: perPage, source, scope });
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
listMilestones({ page, per_page: perPage, type, status } = {}) {
|
|
148
|
+
return this.apiClient.list('milestones', { page, per_page: perPage, type, status });
|
|
149
|
+
},
|
|
150
|
+
|
|
139
151
|
listTags() {
|
|
140
152
|
return this.apiClient.list('tags');
|
|
141
153
|
},
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
1
4
|
export const payloadMethods = {
|
|
2
5
|
async createWrapped(resource, wrapperKey, payload) {
|
|
3
6
|
const wrappedBody = { [wrapperKey]: payload };
|
|
@@ -23,6 +26,70 @@ export const payloadMethods = {
|
|
|
23
26
|
}
|
|
24
27
|
},
|
|
25
28
|
|
|
29
|
+
async patchWrapped(resource, id, wrapperKey, payload) {
|
|
30
|
+
const wrappedBody = { [wrapperKey]: payload };
|
|
31
|
+
try {
|
|
32
|
+
return await this.apiClient.patch(resource, id, payload);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (!this.shouldRetryWrappedBody(error, wrapperKey)) {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
return this.apiClient.patch(resource, id, wrappedBody);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async createRequirement(args = {}) {
|
|
42
|
+
const { files, ...payloadArgs } = args;
|
|
43
|
+
const payload = this.buildRequirementPayload(payloadArgs);
|
|
44
|
+
|
|
45
|
+
if (this.hasFiles(files)) {
|
|
46
|
+
return this.apiClient.createMultipart(
|
|
47
|
+
'requirements',
|
|
48
|
+
await this.buildRequirementFormData(payload, files)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return this.createWrapped('requirements', 'requirement', payload);
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async updateRequirement(requirementId, args = {}) {
|
|
56
|
+
const { files, ...payloadArgs } = args;
|
|
57
|
+
const payload = this.buildRequirementPayload(payloadArgs);
|
|
58
|
+
|
|
59
|
+
if (this.hasFiles(files)) {
|
|
60
|
+
return this.apiClient.patchMultipart(
|
|
61
|
+
'requirements',
|
|
62
|
+
requirementId,
|
|
63
|
+
await this.buildRequirementFormData(payload, files)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return this.patchWrapped('requirements', requirementId, 'requirement', payload);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
hasFiles(files) {
|
|
71
|
+
return Array.isArray(files) && files.length > 0;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async buildRequirementFormData(payload, files = []) {
|
|
75
|
+
const formData = new FormData();
|
|
76
|
+
|
|
77
|
+
Object.entries(payload).forEach(([key, value]) => {
|
|
78
|
+
if (value === undefined || value === null) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
formData.append(key, typeof value === 'boolean' ? String(value) : value);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
for (const filePath of files) {
|
|
85
|
+
const resolvedPath = path.resolve(String(filePath));
|
|
86
|
+
const data = await fs.readFile(resolvedPath);
|
|
87
|
+
formData.append('files', new Blob([data]), path.basename(resolvedPath));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return formData;
|
|
91
|
+
},
|
|
92
|
+
|
|
26
93
|
buildTestPayload({
|
|
27
94
|
title,
|
|
28
95
|
suite_id: suiteId,
|
|
@@ -262,4 +329,24 @@ export const payloadMethods = {
|
|
|
262
329
|
link,
|
|
263
330
|
};
|
|
264
331
|
},
|
|
332
|
+
|
|
333
|
+
buildRequirementPayload({
|
|
334
|
+
title,
|
|
335
|
+
source_type: sourceType,
|
|
336
|
+
description,
|
|
337
|
+
details,
|
|
338
|
+
active,
|
|
339
|
+
global,
|
|
340
|
+
confluence_url: confluenceUrl,
|
|
341
|
+
} = {}) {
|
|
342
|
+
return {
|
|
343
|
+
title,
|
|
344
|
+
source_type: sourceType,
|
|
345
|
+
description,
|
|
346
|
+
details,
|
|
347
|
+
active,
|
|
348
|
+
global,
|
|
349
|
+
confluence_url: confluenceUrl,
|
|
350
|
+
};
|
|
351
|
+
},
|
|
265
352
|
};
|
package/src/mcp/server.js
CHANGED
|
@@ -7,14 +7,30 @@ import { createLogger } from '../core/logger.js';
|
|
|
7
7
|
import { getPackageVersion } from '../config/package-version.js';
|
|
8
8
|
|
|
9
9
|
export class TestomatioMCPServer {
|
|
10
|
-
constructor({
|
|
10
|
+
constructor({
|
|
11
|
+
config,
|
|
12
|
+
apiClient,
|
|
13
|
+
logger,
|
|
14
|
+
tools = TOOL_DEFINITIONS,
|
|
15
|
+
name = 'testomatio-mcp-server',
|
|
16
|
+
registryOptions = {},
|
|
17
|
+
}) {
|
|
11
18
|
this.config = config;
|
|
19
|
+
this.apiClient = apiClient;
|
|
12
20
|
this.logger = logger || createLogger();
|
|
13
|
-
this.
|
|
21
|
+
this.tools = tools;
|
|
22
|
+
this.toolRegistry = new ToolRegistry({
|
|
23
|
+
config,
|
|
24
|
+
apiClient,
|
|
25
|
+
logger: this.logger,
|
|
26
|
+
tools,
|
|
27
|
+
...registryOptions,
|
|
28
|
+
});
|
|
29
|
+
this.cleanupStarted = false;
|
|
14
30
|
|
|
15
31
|
this.server = new Server(
|
|
16
32
|
{
|
|
17
|
-
name
|
|
33
|
+
name,
|
|
18
34
|
version: getPackageVersion(),
|
|
19
35
|
},
|
|
20
36
|
{
|
|
@@ -29,7 +45,7 @@ export class TestomatioMCPServer {
|
|
|
29
45
|
|
|
30
46
|
setupHandlers() {
|
|
31
47
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
32
|
-
tools:
|
|
48
|
+
tools: this.tools,
|
|
33
49
|
}));
|
|
34
50
|
|
|
35
51
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -44,6 +60,33 @@ export class TestomatioMCPServer {
|
|
|
44
60
|
async run() {
|
|
45
61
|
const transport = new StdioServerTransport();
|
|
46
62
|
await this.server.connect(transport);
|
|
63
|
+
this.installSessionCleanup();
|
|
47
64
|
this.logger.info('Testomatio MCP server started');
|
|
48
65
|
}
|
|
66
|
+
|
|
67
|
+
installSessionCleanup() {
|
|
68
|
+
const cleanup = async () => {
|
|
69
|
+
if (this.cleanupStarted) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.cleanupStarted = true;
|
|
74
|
+
await this.apiClient?.stopSession?.();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
this.server.onclose = () => {
|
|
78
|
+
void cleanup();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
process.once('beforeExit', () => {
|
|
82
|
+
void cleanup();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
86
|
+
process.once(signal, async () => {
|
|
87
|
+
await cleanup();
|
|
88
|
+
process.exit(0);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
49
92
|
}
|
|
@@ -8,8 +8,10 @@ import { STEPS_TOOLS } from './definitions/steps.js';
|
|
|
8
8
|
import { SNIPPETS_TOOLS } from './definitions/snippets.js';
|
|
9
9
|
import { LABELS_TOOLS } from './definitions/labels.js';
|
|
10
10
|
import { TAGS_TOOLS } from './definitions/tags.js';
|
|
11
|
+
import { MILESTONES_TOOLS } from './definitions/milestones.js';
|
|
11
12
|
import { ISSUES_TOOLS } from './definitions/issues.js';
|
|
12
13
|
import { PLANS_TOOLS } from './definitions/plans.js';
|
|
14
|
+
import { REQUIREMENTS_TOOLS } from './definitions/requirements.js';
|
|
13
15
|
|
|
14
16
|
export const TOOL_DEFINITIONS = [
|
|
15
17
|
...SYSTEM_TOOLS,
|
|
@@ -22,6 +24,8 @@ export const TOOL_DEFINITIONS = [
|
|
|
22
24
|
...SNIPPETS_TOOLS,
|
|
23
25
|
...LABELS_TOOLS,
|
|
24
26
|
...TAGS_TOOLS,
|
|
27
|
+
...MILESTONES_TOOLS,
|
|
25
28
|
...ISSUES_TOOLS,
|
|
26
29
|
...PLANS_TOOLS,
|
|
30
|
+
...REQUIREMENTS_TOOLS,
|
|
27
31
|
];
|
package/src/mcp/tool-registry.js
CHANGED
|
@@ -12,10 +12,12 @@ function formatJson(payload) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export class ToolRegistry {
|
|
15
|
-
constructor({ config, apiClient, logger }) {
|
|
15
|
+
constructor({ config, apiClient, logger, tools = TOOL_DEFINITIONS, handlerRegistrars = [] }) {
|
|
16
16
|
this.config = config;
|
|
17
17
|
this.apiClient = apiClient;
|
|
18
18
|
this.logger = logger;
|
|
19
|
+
this.tools = tools;
|
|
20
|
+
this.handlerRegistrars = handlerRegistrars;
|
|
19
21
|
this.handlers = this.buildHandlers();
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -37,8 +39,11 @@ export class ToolRegistry {
|
|
|
37
39
|
this.registerEntityCrudHandlers(handlers);
|
|
38
40
|
this.registerScopedIssueHandlers(handlers);
|
|
39
41
|
this.registerGlobalHandlers(handlers);
|
|
42
|
+
for (const registerHandlers of this.handlerRegistrars) {
|
|
43
|
+
registerHandlers.call(this, handlers);
|
|
44
|
+
}
|
|
40
45
|
|
|
41
|
-
for (const tool of
|
|
46
|
+
for (const tool of this.tools) {
|
|
42
47
|
if (tool.name === 'system_ping') continue;
|
|
43
48
|
if (!handlers[tool.name]) {
|
|
44
49
|
handlers[tool.name] = async () => textResponse(`${DEFAULT_TOOL_RESPONSE} (${tool.name})`);
|