@testomatio/mcp 2.1.1 → 2.1.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 +13 -0
- package/package.json +1 -1
- package/src/api/testomatio-client.js +2 -2
- package/src/mcp/configs/attachments-config.js +7 -0
- package/src/mcp/definitions/attachments.js +70 -0
- package/src/mcp/registry/attachments.js +40 -0
- package/src/mcp/registry/handlers.js +31 -0
- package/src/mcp/tool-definitions.js +2 -0
- package/src/mcp/tool-registry.js +3 -0
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ Model Context Protocol (MCP) server that enables AI assistants (Claude, Cursor,
|
|
|
8
8
|
- Tests, Suites, Plans, Runs, TestRuns, RunGroups, Steps, Snippets, Labels
|
|
9
9
|
- Tags and Milestones (read-only access)
|
|
10
10
|
- Issues (global + scoped helpers for tests/suites/runs/testruns/plans)
|
|
11
|
+
- Attachments (scoped helpers for tests/suites/testruns)
|
|
11
12
|
- Requirements (including file uploads from local file paths)
|
|
12
13
|
- **Smart Search** - delegates to list endpoints with OpenAPI-aligned query/filter forwarding
|
|
13
14
|
- **Issue Linking** - link/unlink issues to any resource
|
|
@@ -178,6 +179,17 @@ Add this config to `opencode.json` in your project root, or to `~/.config/openco
|
|
|
178
179
|
}
|
|
179
180
|
```
|
|
180
181
|
|
|
182
|
+
**Upload attachment to a test:**
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"name": "tests_attachments_upload",
|
|
186
|
+
"arguments": {
|
|
187
|
+
"test_id": "123",
|
|
188
|
+
"file_path": "/path/to/screenshot.png"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
181
193
|
## Documentation
|
|
182
194
|
|
|
183
195
|
Complete tool reference: [docs/tools.md](./docs/tools.md)
|
|
@@ -247,6 +259,7 @@ NODE_EXTRA_CA_CERTS=/path/to/company-root-ca.pem testomatio-mcp --token <TOKEN>
|
|
|
247
259
|
- **TQL Syntax** - For user-facing syntax details and more examples, see the official TQL docs: https://docs.testomat.io/advanced/tql/
|
|
248
260
|
- **TQL Scope** - The full agent-oriented whitelist of documented fields lives inside MCP tool descriptions for `tests` and `runs`
|
|
249
261
|
- **Issue Linking** - Scoped helpers available: `{entity}_issues_link/unlink`
|
|
262
|
+
- **Attachments** - Scoped helpers available for tests, suites, and testruns: `{entity}_attachments_list/upload/delete`. Upload sends one local file path as multipart field `file`.
|
|
250
263
|
- **Enterprise Package** - Analytics tools are intentionally exposed only by `@testomatio/mcp-enterprise`, not by the standard `@testomatio/mcp` package
|
|
251
264
|
- **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.
|
|
252
265
|
|
package/package.json
CHANGED
|
@@ -31,8 +31,8 @@ export class TestomatioApiClient {
|
|
|
31
31
|
return this.mutate('POST', this.buildPath(resource), { body });
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
async createMultipart(resource, formData) {
|
|
35
|
-
return this.mutate('POST', this.buildPath(resource), { body: formData });
|
|
34
|
+
async createMultipart(resource, formData, query = {}) {
|
|
35
|
+
return this.mutate('POST', this.buildPath(resource), { query, body: formData });
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
async createWithQuery(resource, { query = {}, body = {} } = {}) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const ATTACHMENT_RESOURCE_KEYS = ['test_id', 'suite_id', 'testrun_id'];
|
|
2
|
+
|
|
3
|
+
export const ATTACHMENT_SCOPED_TOOL_CONFIGS = [
|
|
4
|
+
{ toolPrefix: 'tests', resourceKey: 'test_id' },
|
|
5
|
+
{ toolPrefix: 'suites', resourceKey: 'suite_id' },
|
|
6
|
+
{ toolPrefix: 'testruns', resourceKey: 'testrun_id' },
|
|
7
|
+
];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
function buildAttachmentTools({ toolPrefix, entityName, idKey, idType = 'string' }) {
|
|
2
|
+
const entityId = {
|
|
3
|
+
type: idType,
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
name: `${toolPrefix}_attachments_list`,
|
|
9
|
+
description: `List attachments for a ${entityName} (/api/v2/{project_id}/attachments?${idKey}=...)`,
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
[idKey]: entityId,
|
|
14
|
+
},
|
|
15
|
+
required: [idKey],
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: `${toolPrefix}_attachments_upload`,
|
|
21
|
+
description: `Upload one attachment to a ${entityName} (/api/v2/{project_id}/attachments?${idKey}=...)`,
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
[idKey]: entityId,
|
|
26
|
+
file_path: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'Local path to the file that will be sent as multipart/form-data field "file".',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
required: [idKey, 'file_path'],
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: `${toolPrefix}_attachments_delete`,
|
|
37
|
+
description: `Delete attachment from a ${entityName} (/api/v2/{project_id}/attachments/{id}?${idKey}=...)`,
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
[idKey]: entityId,
|
|
42
|
+
attachment_id: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
required: [idKey, 'attachment_id'],
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const ATTACHMENT_TOOLS = [
|
|
54
|
+
...buildAttachmentTools({
|
|
55
|
+
toolPrefix: 'tests',
|
|
56
|
+
entityName: 'test',
|
|
57
|
+
idKey: 'test_id',
|
|
58
|
+
}),
|
|
59
|
+
...buildAttachmentTools({
|
|
60
|
+
toolPrefix: 'suites',
|
|
61
|
+
entityName: 'suite',
|
|
62
|
+
idKey: 'suite_id',
|
|
63
|
+
}),
|
|
64
|
+
...buildAttachmentTools({
|
|
65
|
+
toolPrefix: 'testruns',
|
|
66
|
+
entityName: 'testrun',
|
|
67
|
+
idKey: 'testrun_id',
|
|
68
|
+
idType: 'integer',
|
|
69
|
+
}),
|
|
70
|
+
];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { ATTACHMENT_RESOURCE_KEYS } from '../configs/attachments-config.js';
|
|
5
|
+
|
|
6
|
+
export const attachmentMethods = {
|
|
7
|
+
listAttachmentsForKey({ resourceKey, resourceId }) {
|
|
8
|
+
this.assertSupportedAttachmentResourceKey(resourceKey);
|
|
9
|
+
return this.apiClient.list('attachments', { [resourceKey]: resourceId });
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
async uploadAttachmentForKey({ resourceKey, resourceId, filePath }) {
|
|
13
|
+
this.assertSupportedAttachmentResourceKey(resourceKey);
|
|
14
|
+
const formData = await this.buildAttachmentFormData(filePath);
|
|
15
|
+
|
|
16
|
+
return this.apiClient.createMultipart('attachments', formData, {
|
|
17
|
+
[resourceKey]: resourceId,
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
deleteAttachmentForKey({ resourceKey, resourceId, attachmentId }) {
|
|
22
|
+
this.assertSupportedAttachmentResourceKey(resourceKey);
|
|
23
|
+
return this.apiClient.delete('attachments', attachmentId, { [resourceKey]: resourceId });
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
async buildAttachmentFormData(filePath) {
|
|
27
|
+
const resolvedPath = path.resolve(String(filePath));
|
|
28
|
+
const data = await fs.readFile(resolvedPath);
|
|
29
|
+
const formData = new FormData();
|
|
30
|
+
|
|
31
|
+
formData.append('file', new Blob([data]), path.basename(resolvedPath));
|
|
32
|
+
return formData;
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
assertSupportedAttachmentResourceKey(resourceKey) {
|
|
36
|
+
if (!ATTACHMENT_RESOURCE_KEYS.includes(resourceKey)) {
|
|
37
|
+
throw new Error(`Unsupported attachment resource key: ${resourceKey}`);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ENTITY_CRUD_CONFIGS } from '../configs/entity-crud-config.js';
|
|
2
|
+
import { ATTACHMENT_SCOPED_TOOL_CONFIGS } from '../configs/attachments-config.js';
|
|
2
3
|
import { ISSUE_SCOPED_TOOL_CONFIGS } from '../configs/issues-config.js';
|
|
3
4
|
|
|
4
5
|
export const handlerMethods = {
|
|
@@ -54,6 +55,36 @@ export const handlerMethods = {
|
|
|
54
55
|
}
|
|
55
56
|
},
|
|
56
57
|
|
|
58
|
+
registerScopedAttachmentHandlers(handlers) {
|
|
59
|
+
for (const { toolPrefix, resourceKey } of ATTACHMENT_SCOPED_TOOL_CONFIGS) {
|
|
60
|
+
handlers[`${toolPrefix}_attachments_list`] = async (args = {}) =>
|
|
61
|
+
this.asText(
|
|
62
|
+
await this.listAttachmentsForKey({
|
|
63
|
+
resourceKey,
|
|
64
|
+
resourceId: this.pickRequiredArg(args, resourceKey),
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
handlers[`${toolPrefix}_attachments_upload`] = async (args = {}) =>
|
|
69
|
+
this.asText(
|
|
70
|
+
await this.uploadAttachmentForKey({
|
|
71
|
+
resourceKey,
|
|
72
|
+
resourceId: this.pickRequiredArg(args, resourceKey),
|
|
73
|
+
filePath: this.pickRequiredArg(args, 'file_path'),
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
handlers[`${toolPrefix}_attachments_delete`] = async (args = {}) =>
|
|
78
|
+
this.asText(
|
|
79
|
+
await this.deleteAttachmentForKey({
|
|
80
|
+
resourceKey,
|
|
81
|
+
resourceId: this.pickRequiredArg(args, resourceKey),
|
|
82
|
+
attachmentId: this.pickRequiredArg(args, 'attachment_id'),
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
|
|
57
88
|
registerGlobalHandlers(handlers) {
|
|
58
89
|
handlers.tags_list = async () => this.asText(await this.listTags());
|
|
59
90
|
handlers.tags_get = async ({ tag_id: tagId }) => this.asText(await this.getTagByTitle(tagId));
|
|
@@ -12,6 +12,7 @@ import { MILESTONES_TOOLS } from './definitions/milestones.js';
|
|
|
12
12
|
import { ISSUES_TOOLS } from './definitions/issues.js';
|
|
13
13
|
import { PLANS_TOOLS } from './definitions/plans.js';
|
|
14
14
|
import { REQUIREMENTS_TOOLS } from './definitions/requirements.js';
|
|
15
|
+
import { ATTACHMENT_TOOLS } from './definitions/attachments.js';
|
|
15
16
|
|
|
16
17
|
export const TOOL_DEFINITIONS = [
|
|
17
18
|
...SYSTEM_TOOLS,
|
|
@@ -26,6 +27,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
26
27
|
...TAGS_TOOLS,
|
|
27
28
|
...MILESTONES_TOOLS,
|
|
28
29
|
...ISSUES_TOOLS,
|
|
30
|
+
...ATTACHMENT_TOOLS,
|
|
29
31
|
...PLANS_TOOLS,
|
|
30
32
|
...REQUIREMENTS_TOOLS,
|
|
31
33
|
];
|
package/src/mcp/tool-registry.js
CHANGED
|
@@ -3,6 +3,7 @@ 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
5
|
import { handlerMethods } from './registry/handlers.js';
|
|
6
|
+
import { attachmentMethods } from './registry/attachments.js';
|
|
6
7
|
import { issueMethods } from './registry/issues.js';
|
|
7
8
|
import { listingMethods } from './registry/listings.js';
|
|
8
9
|
import { payloadMethods } from './registry/payloads.js';
|
|
@@ -38,6 +39,7 @@ export class ToolRegistry {
|
|
|
38
39
|
|
|
39
40
|
this.registerEntityCrudHandlers(handlers);
|
|
40
41
|
this.registerScopedIssueHandlers(handlers);
|
|
42
|
+
this.registerScopedAttachmentHandlers(handlers);
|
|
41
43
|
this.registerGlobalHandlers(handlers);
|
|
42
44
|
for (const registerHandlers of this.handlerRegistrars) {
|
|
43
45
|
registerHandlers.call(this, handlers);
|
|
@@ -80,6 +82,7 @@ export class ToolRegistry {
|
|
|
80
82
|
Object.assign(
|
|
81
83
|
ToolRegistry.prototype,
|
|
82
84
|
handlerMethods,
|
|
85
|
+
attachmentMethods,
|
|
83
86
|
listingMethods,
|
|
84
87
|
issueMethods,
|
|
85
88
|
payloadMethods
|