@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/mcp",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "description": "Model Context Protocol server for Testomatio API",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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
  ];
@@ -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