@testomatio/mcp 2.2.1 → 3.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -2
- package/package.json +11 -4
- package/src/api/testomatio-client.js +6 -2
- package/src/cli/main.js +1 -0
- package/src/config/load-config.js +35 -5
- package/src/core/path-segment.js +21 -0
- package/src/mcp/create-server.js +16 -0
- package/src/mcp/definitions/shares.js +110 -0
- package/src/mcp/registry/shares.js +53 -0
- package/src/mcp/server.js +35 -15
- package/src/mcp/tool-definitions.js +2 -0
- package/src/mcp/tool-registry.js +4 -1
package/README.md
CHANGED
|
@@ -47,7 +47,15 @@ export TESTOMATIO_PROJECT_ID=<PROJECT_ID>
|
|
|
47
47
|
testomatio-mcp
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
**Optional: custom
|
|
50
|
+
**Optional: custom host**
|
|
51
|
+
```bash
|
|
52
|
+
export TESTOMATIO_HOST=beta.testomat.io
|
|
53
|
+
testomatio-mcp --host beta.testomat.io
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
A bare hostname is expanded to `https://<host>`. For full control use
|
|
57
|
+
`--base-url` / `TESTOMATIO_BASE_URL`, which takes precedence over the host option:
|
|
58
|
+
|
|
51
59
|
```bash
|
|
52
60
|
export TESTOMATIO_BASE_URL=https://beta.testomat.io
|
|
53
61
|
```
|
|
@@ -151,6 +159,48 @@ Add this config to `opencode.json` in your project root, or to `~/.config/openco
|
|
|
151
159
|
}
|
|
152
160
|
```
|
|
153
161
|
|
|
162
|
+
## HTTP Transport
|
|
163
|
+
|
|
164
|
+
Besides stdio, the server runs over Streamable HTTP on a Cloudflare Worker hosted by
|
|
165
|
+
Testomat.io. The project is part of the URL, so every tool signature stays the same:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
https://mcp.testomat.io/mcp/<project_id>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Point an MCP client at that URL with a project token:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"mcpServers": {
|
|
176
|
+
"testomatio": {
|
|
177
|
+
"url": "https://mcp.testomat.io/mcp/<PROJECT_ID>",
|
|
178
|
+
"headers": {
|
|
179
|
+
"Authorization": "Bearer <PROJECT_TOKEN>"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Web connectors such as claude.ai have nowhere to put a static token and instead run
|
|
187
|
+
OAuth 2.1 with PKCE and Dynamic Client Registration against the same URL. Tokens
|
|
188
|
+
starting with `testomat_` or `tstmt_` always bypass OAuth and are passed straight
|
|
189
|
+
through, so IDE clients and CI keep working.
|
|
190
|
+
|
|
191
|
+
The endpoint is POST only; `GET` returns `405`, because the server never initiates
|
|
192
|
+
traffic. Testing with `curl` requires both media types in `Accept`:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
curl -sS https://mcp.testomat.io/mcp/<PROJECT_ID> \
|
|
196
|
+
-H "Authorization: Bearer <PROJECT_TOKEN>" \
|
|
197
|
+
-H "Content-Type: application/json" \
|
|
198
|
+
-H "Accept: application/json, text/event-stream" \
|
|
199
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Self-hosted installations keep using stdio.
|
|
203
|
+
|
|
154
204
|
## Quick Examples
|
|
155
205
|
|
|
156
206
|
**List tests:**
|
|
@@ -232,7 +282,8 @@ src/
|
|
|
232
282
|
| `TESTOMATIO_PROJECT_TOKEN` | Yes* | - | Project token (preferred) |
|
|
233
283
|
| `TESTOMATIO_API_TOKEN` | Yes* | - | Alternative token |
|
|
234
284
|
| `TESTOMATIO_PROJECT_ID` | Yes | - | Project ID |
|
|
235
|
-
| `
|
|
285
|
+
| `TESTOMATIO_HOST` | No | - | API host, e.g. `beta.testomat.io` |
|
|
286
|
+
| `TESTOMATIO_BASE_URL` | No | `https://app.testomat.io` | API base URL, wins over `TESTOMATIO_HOST` |
|
|
236
287
|
| `TESTOMATIO_TOOLS` | No | `full` | Tool profile: `full`, `core`, or `read` |
|
|
237
288
|
|
|
238
289
|
*Either `TESTOMATIO_PROJECT_TOKEN` or `TESTOMATIO_API_TOKEN`
|
|
@@ -287,6 +338,7 @@ NODE_EXTRA_CA_CERTS=/path/to/company-root-ca.pem testomatio-mcp --token <TOKEN>
|
|
|
287
338
|
```bash
|
|
288
339
|
npm install
|
|
289
340
|
npm run start -- --token <TOKEN> --project <PROJECT_ID>
|
|
341
|
+
npm test
|
|
290
342
|
```
|
|
291
343
|
|
|
292
344
|
For local MCP development, point Claude Desktop to the checked-out entrypoint:
|
|
@@ -372,3 +424,37 @@ Example `analytics_charts_results` call:
|
|
|
372
424
|
}
|
|
373
425
|
}
|
|
374
426
|
```
|
|
427
|
+
|
|
428
|
+
### Worker deployment
|
|
429
|
+
|
|
430
|
+
The `worker/` directory holds the Cloudflare Worker and is excluded from the npm
|
|
431
|
+
package. Deploy it from that directory:
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
cd worker
|
|
435
|
+
npx wrangler kv namespace create OAUTH_KV
|
|
436
|
+
npx wrangler secret put TESTOMATIO_MCP_WORKER_SECRET
|
|
437
|
+
npx wrangler deploy
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Put the namespace id returned by the first command into `kv_namespaces` in
|
|
441
|
+
`worker/wrangler.jsonc`. `TESTOMATIO_MCP_WORKER_SECRET` is the shared secret used to
|
|
442
|
+
redeem authorization codes against Testomat.io server-to-server and is never
|
|
443
|
+
committed.
|
|
444
|
+
|
|
445
|
+
Before exposing the Worker publicly, configure Cloudflare rate-limiting rules for
|
|
446
|
+
`/register`, `/authorize`, `/token`, and `/mcp/*`. These endpoints intentionally
|
|
447
|
+
support unauthenticated OAuth discovery and client registration, so rate limiting
|
|
448
|
+
belongs at the edge rather than in per-isolate memory. Keep separate rules and KV
|
|
449
|
+
namespaces for beta and production.
|
|
450
|
+
|
|
451
|
+
A beta worker is the same code deployed to the `beta` environment, which targets
|
|
452
|
+
`https://beta.testomat.io` and keeps its own KV namespace so beta grants never
|
|
453
|
+
reach the production one:
|
|
454
|
+
|
|
455
|
+
```bash
|
|
456
|
+
npx wrangler kv namespace create OAUTH_KV --env beta
|
|
457
|
+
npx wrangler secret put TESTOMATIO_MCP_WORKER_SECRET --env beta
|
|
458
|
+
npx wrangler deploy --env beta
|
|
459
|
+
```
|
|
460
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0-beta.2",
|
|
4
4
|
"description": "Model Context Protocol server for Testomatio API",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node index.js",
|
|
12
12
|
"dev": "node index.js",
|
|
13
|
-
"sync:enterprise-package": "node scripts/sync-enterprise-package.js"
|
|
13
|
+
"sync:enterprise-package": "node scripts/sync-enterprise-package.js",
|
|
14
|
+
"test": "vitest run"
|
|
14
15
|
},
|
|
15
16
|
"keywords": [
|
|
16
17
|
"testomatio",
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
"url": "https://github.com/testomatio/mcp"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
30
|
-
"@modelcontextprotocol/sdk": "^
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
31
32
|
"commander": "^12.0.0",
|
|
32
33
|
"dotenv": "^17.2.3"
|
|
33
34
|
},
|
|
@@ -39,5 +40,11 @@
|
|
|
39
40
|
"engines": {
|
|
40
41
|
"node": ">=18.0.0"
|
|
41
42
|
},
|
|
42
|
-
"devDependencies": {
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@cfworker/json-schema": "^4.1.1",
|
|
45
|
+
"@cloudflare/vitest-pool-workers": "^0.22.0",
|
|
46
|
+
"@cloudflare/workers-oauth-provider": "^0.10.3",
|
|
47
|
+
"vitest": "^4.1.11",
|
|
48
|
+
"wrangler": "^4.131.2"
|
|
49
|
+
}
|
|
43
50
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HttpClient } from './http-client.js';
|
|
2
|
+
import { encodePathParameter } from '../core/path-segment.js';
|
|
2
3
|
|
|
3
4
|
export class TestomatioApiClient {
|
|
4
5
|
constructor({ baseUrl, projectId, token, logger }) {
|
|
@@ -14,9 +15,12 @@ export class TestomatioApiClient {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
buildPath(resource, id = '') {
|
|
18
|
+
const safeProjectId = encodePathParameter(this.projectId, 'Project ID');
|
|
17
19
|
const safeResource = String(resource).replace(/^\/+|\/+$/g, '');
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
+
const resourceId = String(id ?? '');
|
|
21
|
+
const safeId = resourceId ? `/${encodePathParameter(resourceId, 'Resource ID')}` : '';
|
|
22
|
+
|
|
23
|
+
return `/api/v2/${safeProjectId}/${safeResource}${safeId}`;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
list(resource, query = {}) {
|
package/src/cli/main.js
CHANGED
|
@@ -15,6 +15,7 @@ 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('--host <host>', 'Testomatio host, e.g. beta.testomat.io')
|
|
18
19
|
.option(
|
|
19
20
|
'--tools <profile>',
|
|
20
21
|
'Tool surface: full (default, all tools), core (common entities only), read (read-only)'
|
|
@@ -10,13 +10,43 @@ function normalizeBaseUrl(value) {
|
|
|
10
10
|
return normalized.replace(/\/+$/, '');
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
function hostToBaseUrl(value) {
|
|
14
|
+
const host = normalizeBaseUrl(value);
|
|
15
|
+
|
|
16
|
+
if (!host) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return /^https?:\/\//i.test(host) ? host : `https://${host}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveBaseUrl(argvOptions = {}, env = process.env) {
|
|
24
|
+
const explicitBaseUrl = normalizeBaseUrl(argvOptions.baseUrl || env.TESTOMATIO_BASE_URL);
|
|
25
|
+
if (explicitBaseUrl) {
|
|
26
|
+
return explicitBaseUrl;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const hostBaseUrl = hostToBaseUrl(argvOptions.host || env.TESTOMATIO_HOST);
|
|
30
|
+
if (hostBaseUrl) {
|
|
31
|
+
return hostBaseUrl;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return DEFAULT_BASE_URL;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function loadServerConfig(argvOptions = {}, env = process.env) {
|
|
38
|
+
return {
|
|
39
|
+
baseUrl: resolveBaseUrl(argvOptions, env),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function loadConfig(argvOptions = {}, env = process.env) {
|
|
14
44
|
const token = normalizeString(
|
|
15
|
-
argvOptions.token ||
|
|
45
|
+
argvOptions.token || env.TESTOMATIO_PROJECT_TOKEN || env.TESTOMATIO_API_TOKEN
|
|
16
46
|
);
|
|
17
|
-
const projectId = normalizeString(argvOptions.project ||
|
|
18
|
-
const baseUrl =
|
|
19
|
-
const rawToolsProfile = normalizeString(argvOptions.tools ||
|
|
47
|
+
const projectId = normalizeString(argvOptions.project || env.TESTOMATIO_PROJECT_ID);
|
|
48
|
+
const baseUrl = resolveBaseUrl(argvOptions, env);
|
|
49
|
+
const rawToolsProfile = normalizeString(argvOptions.tools || env.TESTOMATIO_TOOLS).toLowerCase();
|
|
20
50
|
const toolsProfile = rawToolsProfile || DEFAULT_PROFILE;
|
|
21
51
|
|
|
22
52
|
if (!token) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function pathParameter(value, label = 'Path parameter') {
|
|
2
|
+
const segment = String(value ?? '');
|
|
3
|
+
|
|
4
|
+
if (!segment || segment === '.' || segment === '..' || segment.includes('/') || segment.includes('\\')) {
|
|
5
|
+
throw new TypeError(`${label} must be a single URL path segment`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return segment;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function encodePathParameter(value, label) {
|
|
12
|
+
return encodeURIComponent(pathParameter(value, label));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function decodePathParameter(value, label) {
|
|
16
|
+
try {
|
|
17
|
+
return pathParameter(decodeURIComponent(value), label);
|
|
18
|
+
} catch {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TestomatioApiClient } from '../api/testomatio-client.js';
|
|
2
|
+
import { createLogger } from '../core/logger.js';
|
|
3
|
+
import { TestomatioMCPServer } from './server.js';
|
|
4
|
+
|
|
5
|
+
export function createMcpServer({ token, projectId, baseUrl, logger, version, jsonSchemaValidator }) {
|
|
6
|
+
const config = { token, projectId, baseUrl };
|
|
7
|
+
const serverLogger = logger || createLogger();
|
|
8
|
+
|
|
9
|
+
return new TestomatioMCPServer({
|
|
10
|
+
config,
|
|
11
|
+
apiClient: new TestomatioApiClient({ ...config, logger: serverLogger }),
|
|
12
|
+
logger: serverLogger,
|
|
13
|
+
version,
|
|
14
|
+
jsonSchemaValidator,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export const SHARES_TOOLS = [
|
|
2
|
+
{
|
|
3
|
+
"name": "tests_share",
|
|
4
|
+
"description": "Share tests into a suite of another project (/api/v2/{project_id}/shares/tests). Select tests by test_ids, labels, or both (combined). The source project stays the single source of truth; shared copies in the target project are read-only until unlinked. Re-sharing into the same target project does not duplicate. Requests are processed asynchronously: status 'queued' means accepted, not completed. Matched tests that are themselves shared copies are skipped and listed in skipped_test_ids. Source and target projects must be of the same type (Classic/BDD).",
|
|
5
|
+
"inputSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"test_ids": {
|
|
9
|
+
"type": "array",
|
|
10
|
+
"items": {
|
|
11
|
+
"type": "string"
|
|
12
|
+
},
|
|
13
|
+
"description": "Test IDs to share. At least one of test_ids/labels is required. Max 1000 tests per request."
|
|
14
|
+
},
|
|
15
|
+
"labels": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": {
|
|
18
|
+
"type": "string"
|
|
19
|
+
},
|
|
20
|
+
"description": "Label slugs or titles; every test carrying any of these labels is shared, in addition to test_ids."
|
|
21
|
+
},
|
|
22
|
+
"target_project_id": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "Project ID (slug) of the destination project. Must be accessible to the current user."
|
|
25
|
+
},
|
|
26
|
+
"target_suite_id": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"description": "Suite ID in the target project to place the shared tests into. Must be a file-type suite, not a folder."
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"required": [
|
|
32
|
+
"target_project_id",
|
|
33
|
+
"target_suite_id"
|
|
34
|
+
],
|
|
35
|
+
"additionalProperties": false
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "suites_share",
|
|
40
|
+
"description": "Share suites (with their tests) into one or more other projects (/api/v2/{project_id}/shares/suites). Select suites by suite_ids, labels, or both (combined). File-type suites are linked (read-only copies that stay in sync); folder suites are deep-copied as regular editable copies. Re-sharing a linked suite into a project that already has it does not duplicate. Suites that are themselves shared copies link to their original source. Omit destination_folder_id to share into the root of the target project(s). Requests are processed asynchronously: status 'queued' means accepted, not completed. Source and target projects must be of the same type (Classic/BDD).",
|
|
41
|
+
"inputSchema": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": {
|
|
44
|
+
"suite_ids": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": {
|
|
47
|
+
"type": "string"
|
|
48
|
+
},
|
|
49
|
+
"description": "Suite IDs to share. At least one of suite_ids/labels is required. Max 200 suites per request."
|
|
50
|
+
},
|
|
51
|
+
"labels": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": {
|
|
54
|
+
"type": "string"
|
|
55
|
+
},
|
|
56
|
+
"description": "Label slugs or titles; every suite carrying any of these labels is shared, in addition to suite_ids."
|
|
57
|
+
},
|
|
58
|
+
"target_project_ids": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"items": {
|
|
61
|
+
"type": "string"
|
|
62
|
+
},
|
|
63
|
+
"description": "Project IDs (slugs) of the destination projects. Must be accessible to the current user."
|
|
64
|
+
},
|
|
65
|
+
"destination_folder_id": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"description": "Folder suite ID in the target project to place the shared suites into. Only allowed when sharing to a single target project."
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"required": [
|
|
71
|
+
"target_project_ids"
|
|
72
|
+
],
|
|
73
|
+
"additionalProperties": false
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "tests_unshare",
|
|
78
|
+
"description": "Remove a test's share, converting the shared copy back into a regular, editable test (/api/v2/{project_id}/shares/tests/{id}). Only the shared copy can be targeted — the original source test is untouched. Must be called against the project that holds the shared copy.",
|
|
79
|
+
"inputSchema": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"properties": {
|
|
82
|
+
"test_id": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"description": "ID of the shared test copy to unlink."
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"required": [
|
|
88
|
+
"test_id"
|
|
89
|
+
],
|
|
90
|
+
"additionalProperties": false
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "suites_unshare",
|
|
95
|
+
"description": "Remove a suite's share, converting the shared copy back into a regular, editable suite (/api/v2/{project_id}/shares/suites/{id}). Only the shared (linked) copy can be targeted — the original source suite is untouched. Must be called against the project that holds the shared copy.",
|
|
96
|
+
"inputSchema": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"properties": {
|
|
99
|
+
"suite_id": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"description": "ID of the shared suite copy to unlink."
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"required": [
|
|
105
|
+
"suite_id"
|
|
106
|
+
],
|
|
107
|
+
"additionalProperties": false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
];
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const shareMethods = {
|
|
2
|
+
registerShareHandlers(handlers) {
|
|
3
|
+
handlers.tests_share = async (args = {}) => {
|
|
4
|
+
const payload = {
|
|
5
|
+
target_project_id: this.pickRequiredArg(args, 'target_project_id'),
|
|
6
|
+
target_suite_id: this.pickRequiredArg(args, 'target_suite_id'),
|
|
7
|
+
};
|
|
8
|
+
this.pickSelectionArgs(args, ['test_ids', 'labels'], payload);
|
|
9
|
+
return this.asText(await this.apiClient.create('shares/tests', payload));
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
handlers.suites_share = async (args = {}) => {
|
|
13
|
+
const payload = {
|
|
14
|
+
target_project_ids: this.pickRequiredArg(args, 'target_project_ids'),
|
|
15
|
+
};
|
|
16
|
+
this.pickSelectionArgs(args, ['suite_ids', 'labels'], payload);
|
|
17
|
+
if (args.destination_folder_id !== undefined) {
|
|
18
|
+
payload.destination_folder_id = args.destination_folder_id;
|
|
19
|
+
}
|
|
20
|
+
return this.asText(await this.apiClient.create('shares/suites', payload));
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
handlers.tests_unshare = async (args = {}) =>
|
|
24
|
+
this.asText(await this.apiClient.delete('shares/tests', this.pickRequiredArg(args, 'test_id')));
|
|
25
|
+
|
|
26
|
+
handlers.suites_unshare = async (args = {}) =>
|
|
27
|
+
this.asText(
|
|
28
|
+
await this.apiClient.delete('shares/suites', this.pickRequiredArg(args, 'suite_id'))
|
|
29
|
+
);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Copy the selection args (e.g. test_ids/labels) into the payload, requiring at least
|
|
34
|
+
* one of them — the API rejects a share request without a selection.
|
|
35
|
+
*/
|
|
36
|
+
pickSelectionArgs(args = {}, keys, payload) {
|
|
37
|
+
const provided = keys.filter((key) => {
|
|
38
|
+
const value = args[key];
|
|
39
|
+
if (value === undefined || value === null || value === '') return false;
|
|
40
|
+
if (Array.isArray(value) && value.length === 0) return false;
|
|
41
|
+
return true;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (provided.length === 0) {
|
|
45
|
+
throw new Error(`Provide at least one selection argument: ${keys.join(', ')}.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const key of provided) {
|
|
49
|
+
payload[key] = args[key];
|
|
50
|
+
}
|
|
51
|
+
return provided;
|
|
52
|
+
},
|
|
53
|
+
};
|
package/src/mcp/server.js
CHANGED
|
@@ -14,6 +14,8 @@ export class TestomatioMCPServer {
|
|
|
14
14
|
tools = TOOL_DEFINITIONS,
|
|
15
15
|
name = 'testomatio-mcp-server',
|
|
16
16
|
registryOptions = {},
|
|
17
|
+
version,
|
|
18
|
+
jsonSchemaValidator,
|
|
17
19
|
}) {
|
|
18
20
|
this.config = config;
|
|
19
21
|
this.apiClient = apiClient;
|
|
@@ -26,17 +28,19 @@ export class TestomatioMCPServer {
|
|
|
26
28
|
tools,
|
|
27
29
|
...registryOptions,
|
|
28
30
|
});
|
|
29
|
-
this.
|
|
31
|
+
this.closePromise = null;
|
|
32
|
+
this.sessionCleanupPromise = null;
|
|
30
33
|
|
|
31
34
|
this.server = new Server(
|
|
32
35
|
{
|
|
33
36
|
name,
|
|
34
|
-
version: getPackageVersion(),
|
|
37
|
+
version: version || getPackageVersion(),
|
|
35
38
|
},
|
|
36
39
|
{
|
|
37
40
|
capabilities: {
|
|
38
41
|
tools: {},
|
|
39
42
|
},
|
|
43
|
+
jsonSchemaValidator,
|
|
40
44
|
}
|
|
41
45
|
);
|
|
42
46
|
|
|
@@ -57,36 +61,52 @@ export class TestomatioMCPServer {
|
|
|
57
61
|
});
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
async
|
|
61
|
-
const transport = new StdioServerTransport();
|
|
64
|
+
async connect(transport) {
|
|
62
65
|
await this.server.connect(transport);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async run() {
|
|
69
|
+
await this.connect(new StdioServerTransport());
|
|
63
70
|
this.installSessionCleanup();
|
|
64
71
|
this.logger.info('Testomatio MCP server started');
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
close() {
|
|
75
|
+
if (!this.closePromise) {
|
|
76
|
+
this.closePromise = (async () => {
|
|
77
|
+
try {
|
|
78
|
+
await this.server.close();
|
|
79
|
+
} finally {
|
|
80
|
+
await this.#stopSession();
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
}
|
|
72
84
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
};
|
|
85
|
+
return this.closePromise;
|
|
86
|
+
}
|
|
76
87
|
|
|
88
|
+
installSessionCleanup() {
|
|
77
89
|
this.server.onclose = () => {
|
|
78
|
-
void
|
|
90
|
+
void this.#stopSession();
|
|
79
91
|
};
|
|
80
92
|
|
|
81
93
|
process.once('beforeExit', () => {
|
|
82
|
-
void
|
|
94
|
+
void this.#stopSession();
|
|
83
95
|
});
|
|
84
96
|
|
|
85
97
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
86
98
|
process.once(signal, async () => {
|
|
87
|
-
await
|
|
99
|
+
await this.#stopSession();
|
|
88
100
|
process.exit(0);
|
|
89
101
|
});
|
|
90
102
|
}
|
|
91
103
|
}
|
|
104
|
+
|
|
105
|
+
#stopSession() {
|
|
106
|
+
if (!this.sessionCleanupPromise) {
|
|
107
|
+
this.sessionCleanupPromise = Promise.resolve().then(() => this.apiClient?.stopSession?.());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return this.sessionCleanupPromise;
|
|
111
|
+
}
|
|
92
112
|
}
|
|
@@ -2,6 +2,7 @@ import { SYSTEM_TOOLS } from './definitions/system.js';
|
|
|
2
2
|
import { PROJECT_TOOLS } from './definitions/projects.js';
|
|
3
3
|
import { TESTS_TOOLS } from './definitions/tests.js';
|
|
4
4
|
import { SUITES_TOOLS } from './definitions/suites.js';
|
|
5
|
+
import { SHARES_TOOLS } from './definitions/shares.js';
|
|
5
6
|
import { RUNS_TOOLS } from './definitions/runs.js';
|
|
6
7
|
import { TESTRUNS_TOOLS } from './definitions/testruns.js';
|
|
7
8
|
import { RUNGROUPS_TOOLS } from './definitions/rungroups.js';
|
|
@@ -22,6 +23,7 @@ export const TOOL_DEFINITIONS = withCountGroupOptions(withListOptions([
|
|
|
22
23
|
...PROJECT_TOOLS,
|
|
23
24
|
...TESTS_TOOLS,
|
|
24
25
|
...SUITES_TOOLS,
|
|
26
|
+
...SHARES_TOOLS,
|
|
25
27
|
...RUNS_TOOLS,
|
|
26
28
|
...TESTRUNS_TOOLS,
|
|
27
29
|
...RUNGROUPS_TOOLS,
|
package/src/mcp/tool-registry.js
CHANGED
|
@@ -8,6 +8,7 @@ import { attachmentMethods } from './registry/attachments.js';
|
|
|
8
8
|
import { issueMethods } from './registry/issues.js';
|
|
9
9
|
import { listingMethods } from './registry/listings.js';
|
|
10
10
|
import { payloadMethods } from './registry/payloads.js';
|
|
11
|
+
import { shareMethods } from './registry/shares.js';
|
|
11
12
|
|
|
12
13
|
function withPagination(payload) {
|
|
13
14
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
@@ -72,6 +73,7 @@ export class ToolRegistry {
|
|
|
72
73
|
this.registerScopedIssueHandlers(handlers);
|
|
73
74
|
this.registerScopedAttachmentHandlers(handlers);
|
|
74
75
|
this.registerGlobalHandlers(handlers);
|
|
76
|
+
this.registerShareHandlers(handlers);
|
|
75
77
|
for (const registerHandlers of this.handlerRegistrars) {
|
|
76
78
|
registerHandlers.call(this, handlers);
|
|
77
79
|
}
|
|
@@ -116,5 +118,6 @@ Object.assign(
|
|
|
116
118
|
attachmentMethods,
|
|
117
119
|
listingMethods,
|
|
118
120
|
issueMethods,
|
|
119
|
-
payloadMethods
|
|
121
|
+
payloadMethods,
|
|
122
|
+
shareMethods
|
|
120
123
|
);
|