@testomatio/mcp 1.0.14 → 2.0.0-beta.8

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/package.json CHANGED
@@ -1,26 +1,15 @@
1
1
  {
2
2
  "name": "@testomatio/mcp",
3
- "version": "1.0.14",
3
+ "version": "2.0.0-beta.8",
4
4
  "description": "Model Context Protocol server for Testomatio API",
5
- "main": "index.js",
5
+ "main": "src/index.js",
6
6
  "bin": {
7
7
  "testomatio-mcp": "index.js"
8
8
  },
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "node index.js",
12
- "dev": "node index.js",
13
- "test": "node --experimental-vm-modules node_modules/.bin/jest",
14
- "test:unit": "node --experimental-vm-modules node_modules/.bin/jest",
15
- "test:integration": "node --experimental-vm-modules node_modules/.bin/jest --config jest.integration.config.js",
16
- "test:e2e": "node --experimental-vm-modules node_modules/.bin/jest --config jest.e2e.config.js",
17
- "test:watch": "node --experimental-vm-modules node_modules/.bin/jest --watch",
18
- "test:integration:watch": "node --experimental-vm-modules node_modules/.bin/jest --config jest.integration.config.js --watch",
19
- "test:e2e:watch": "node --experimental-vm-modules node_modules/.bin/jest --config jest.e2e.config.js --watch",
20
- "test:coverage": "node --experimental-vm-modules node_modules/.bin/jest --coverage",
21
- "test:coverage:integration": "node --experimental-vm-modules node_modules/.bin/jest --config jest.integration.config.js --coverage",
22
- "test:coverage:e2e": "node --experimental-vm-modules node_modules/.bin/jest --config jest.e2e.config.js --coverage",
23
- "test:all": "npm run test:unit && npm run test:integration && npm run test:e2e"
12
+ "dev": "node index.js"
24
13
  },
25
14
  "keywords": [
26
15
  "testomatio",
@@ -32,6 +21,10 @@
32
21
  ],
33
22
  "author": "Testomatio Team",
34
23
  "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/testomatio/mcp"
27
+ },
35
28
  "dependencies": {
36
29
  "@modelcontextprotocol/sdk": "^0.4.0",
37
30
  "commander": "^12.0.0",
@@ -39,13 +32,11 @@
39
32
  },
40
33
  "files": [
41
34
  "index.js",
35
+ "src",
42
36
  "README.md"
43
37
  ],
44
38
  "engines": {
45
39
  "node": ">=18.0.0"
46
40
  },
47
- "devDependencies": {
48
- "ajv": "^8.17.1",
49
- "jest": "^30.2.0"
50
- }
41
+ "devDependencies": {}
51
42
  }
@@ -0,0 +1,68 @@
1
+ import { ApiError } from '../core/errors.js';
2
+
3
+ function buildUrl(baseUrl, path, query = {}) {
4
+ const url = new URL(path, `${baseUrl}/`);
5
+
6
+ Object.entries(query).forEach(([key, value]) => {
7
+ if (value === undefined || value === null || value === '') {
8
+ return;
9
+ }
10
+
11
+ if (Array.isArray(value)) {
12
+ value.forEach((item) => url.searchParams.append(key, String(item)));
13
+ return;
14
+ }
15
+
16
+ url.searchParams.append(key, String(value));
17
+ });
18
+
19
+ return url.toString();
20
+ }
21
+
22
+ export class HttpClient {
23
+ constructor({ baseUrl, token, logger }) {
24
+ this.baseUrl = baseUrl;
25
+ this.token = token;
26
+ this.logger = logger;
27
+ }
28
+
29
+ async request(method, path, { query, body } = {}) {
30
+ const url = buildUrl(this.baseUrl, path, query);
31
+
32
+ const headers = {
33
+ Accept: 'application/json',
34
+ Authorization: `Bearer ${this.token}`,
35
+ };
36
+
37
+ const options = {
38
+ method,
39
+ headers,
40
+ };
41
+
42
+ if (body !== undefined) {
43
+ headers['Content-Type'] = 'application/json';
44
+ options.body = JSON.stringify(body);
45
+ }
46
+
47
+ this.logger.debug('HTTP request', { method, url });
48
+ const response = await fetch(url, options);
49
+ const text = await response.text();
50
+
51
+ let payload;
52
+ try {
53
+ payload = text ? JSON.parse(text) : {};
54
+ } catch {
55
+ payload = { raw: text };
56
+ }
57
+
58
+ if (!response.ok) {
59
+ throw new ApiError(`Request failed with status ${response.status}`, {
60
+ status: response.status,
61
+ url,
62
+ payload,
63
+ });
64
+ }
65
+
66
+ return payload;
67
+ }
68
+ }
@@ -0,0 +1,46 @@
1
+ import { HttpClient } from './http-client.js';
2
+
3
+ export class TestomatioApiClient {
4
+ constructor({ baseUrl, projectId, token, logger }) {
5
+ this.projectId = projectId;
6
+ this.http = new HttpClient({
7
+ baseUrl,
8
+ token,
9
+ logger,
10
+ });
11
+ }
12
+
13
+ buildPath(resource, id = '') {
14
+ const safeResource = String(resource).replace(/^\/+|\/+$/g, '');
15
+ const safeId = id ? `/${String(id).replace(/^\/+|\/+$/g, '')}` : '';
16
+ return `/api/v2/${this.projectId}/${safeResource}${safeId}`;
17
+ }
18
+
19
+ list(resource, query = {}) {
20
+ return this.http.request('GET', this.buildPath(resource), { query });
21
+ }
22
+
23
+ get(resource, id, query = {}) {
24
+ return this.http.request('GET', this.buildPath(resource, id), { query });
25
+ }
26
+
27
+ create(resource, body = {}) {
28
+ return this.http.request('POST', this.buildPath(resource), { body });
29
+ }
30
+
31
+ createWithQuery(resource, { query = {}, body = {} } = {}) {
32
+ return this.http.request('POST', this.buildPath(resource), { query, body });
33
+ }
34
+
35
+ update(resource, id, body = {}) {
36
+ return this.http.request('PUT', this.buildPath(resource, id), { body });
37
+ }
38
+
39
+ delete(resource, id, query = {}) {
40
+ return this.http.request('DELETE', this.buildPath(resource, id), { query });
41
+ }
42
+
43
+ search(resource, query = {}) {
44
+ return this.list(resource, query);
45
+ }
46
+ }
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { createApplication } from '../index.js';
5
+ import { ConfigurationError } from '../core/errors.js';
6
+ import { getPackageVersion } from '../config/package-version.js';
7
+
8
+ export function parseArgs(argv = process.argv) {
9
+ const command = new Command();
10
+
11
+ command
12
+ .name('testomatio-mcp')
13
+ .description('Model Context Protocol server for Testomatio API v2')
14
+ .version(getPackageVersion())
15
+ .option('-t, --token <token>', 'Testomatio Project token')
16
+ .option('-p, --project <project>', 'Project ID')
17
+ .option('--base-url <url>', 'Base URL for Testomatio API')
18
+ .parse(argv);
19
+
20
+ return command.opts();
21
+ }
22
+
23
+ export async function main(argv = process.argv) {
24
+ try {
25
+ const options = parseArgs(argv);
26
+ const app = createApplication(options);
27
+ await app.mcpServer.run();
28
+ } catch (error) {
29
+ if (error instanceof ConfigurationError) {
30
+ console.error(`Configuration error: ${error.message}`);
31
+ process.exit(1);
32
+ }
33
+
34
+ console.error('Failed to start server:', error.message || error);
35
+ process.exit(1);
36
+ }
37
+ }
@@ -0,0 +1,3 @@
1
+ export const DEFAULT_BASE_URL = 'https://app.testomat.io';
2
+
3
+ export const DEFAULT_TOOL_RESPONSE = 'Tool is declared but has no handler implementation.';
@@ -0,0 +1,37 @@
1
+ import { DEFAULT_BASE_URL } from './constants.js';
2
+ import { ConfigurationError } from '../core/errors.js';
3
+
4
+ function normalizeString(value) {
5
+ return typeof value === 'string' ? value.trim() : '';
6
+ }
7
+
8
+ function normalizeBaseUrl(value) {
9
+ const normalized = normalizeString(value);
10
+ return normalized.replace(/\/+$/, '');
11
+ }
12
+
13
+ export function loadConfig(argvOptions = {}) {
14
+ const token = normalizeString(
15
+ argvOptions.token || process.env.TESTOMATIO_PROJECT_TOKEN || process.env.TESTOMATIO_API_TOKEN
16
+ );
17
+ const projectId = normalizeString(argvOptions.project || process.env.TESTOMATIO_PROJECT_ID);
18
+ const baseUrl = normalizeBaseUrl(argvOptions.baseUrl || process.env.TESTOMATIO_BASE_URL || DEFAULT_BASE_URL);
19
+
20
+ if (!token) {
21
+ throw new ConfigurationError(
22
+ 'Project token is required. Use --token <token> or set TESTOMATIO_PROJECT_TOKEN (or TESTOMATIO_API_TOKEN).'
23
+ );
24
+ }
25
+
26
+ if (!projectId) {
27
+ throw new ConfigurationError(
28
+ 'Project ID is required. Use --project <project_id> or set TESTOMATIO_PROJECT_ID'
29
+ );
30
+ }
31
+
32
+ return {
33
+ token,
34
+ projectId,
35
+ baseUrl,
36
+ };
37
+ }
@@ -0,0 +1,19 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ let cachedVersion;
6
+
7
+ export function getPackageVersion() {
8
+ if (cachedVersion) {
9
+ return cachedVersion;
10
+ }
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const packageJsonPath = path.resolve(__dirname, '../../package.json');
15
+ const raw = fs.readFileSync(packageJsonPath, 'utf8');
16
+ const pkg = JSON.parse(raw);
17
+ cachedVersion = pkg.version || '0.0.0';
18
+ return cachedVersion;
19
+ }
@@ -0,0 +1,24 @@
1
+ export class ConfigurationError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'ConfigurationError';
5
+ }
6
+ }
7
+
8
+ export class ApiError extends Error {
9
+ constructor(message, details = {}) {
10
+ super(message);
11
+ this.name = 'ApiError';
12
+ this.status = details.status;
13
+ this.url = details.url;
14
+ this.payload = details.payload;
15
+ }
16
+ }
17
+
18
+ export class NotImplementedToolError extends Error {
19
+ constructor(toolName) {
20
+ super(`Tool "${toolName}" is not registered in this MCP server`);
21
+ this.name = 'NotImplementedToolError';
22
+ this.toolName = toolName;
23
+ }
24
+ }
@@ -0,0 +1,31 @@
1
+ function shouldLog(level, currentLevel) {
2
+ const order = ['error', 'warn', 'info', 'debug'];
3
+ return order.indexOf(level) <= order.indexOf(currentLevel);
4
+ }
5
+
6
+ export function createLogger(level = process.env.LOG_LEVEL || 'info') {
7
+ const currentLevel = String(level).toLowerCase();
8
+
9
+ return {
10
+ error(message, meta) {
11
+ if (shouldLog('error', currentLevel)) {
12
+ console.error(`[error] ${message}`, meta || '');
13
+ }
14
+ },
15
+ warn(message, meta) {
16
+ if (shouldLog('warn', currentLevel)) {
17
+ console.error(`[warn] ${message}`, meta || '');
18
+ }
19
+ },
20
+ info(message, meta) {
21
+ if (shouldLog('info', currentLevel)) {
22
+ console.error(`[info] ${message}`, meta || '');
23
+ }
24
+ },
25
+ debug(message, meta) {
26
+ if (shouldLog('debug', currentLevel)) {
27
+ console.error(`[debug] ${message}`, meta || '');
28
+ }
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,11 @@
1
+ export function textResponse(text) {
2
+ return {
3
+ content: [
4
+ {
5
+ type: 'text',
6
+ text,
7
+ },
8
+ ],
9
+ };
10
+ }
11
+
package/src/index.js ADDED
@@ -0,0 +1,20 @@
1
+ import { TestomatioApiClient } from './api/testomatio-client.js';
2
+ import { loadConfig } from './config/load-config.js';
3
+ import { createLogger } from './core/logger.js';
4
+ import { TestomatioMCPServer } from './mcp/server.js';
5
+
6
+ export { TestomatioMCPServer };
7
+
8
+ export function createApplication(argvOptions = {}) {
9
+ const config = loadConfig(argvOptions);
10
+ const logger = createLogger();
11
+ const apiClient = new TestomatioApiClient({ ...config, logger });
12
+ const mcpServer = new TestomatioMCPServer({ config, apiClient, logger });
13
+
14
+ return {
15
+ config,
16
+ logger,
17
+ apiClient,
18
+ mcpServer,
19
+ };
20
+ }
@@ -0,0 +1,99 @@
1
+ export const ENTITY_CRUD_CONFIGS = [
2
+ {
3
+ toolPrefix: 'tests',
4
+ resource: 'tests',
5
+ idArg: 'test_id',
6
+ listMethod: 'listTests',
7
+ searchMethod: 'searchTests',
8
+ payloadBuilder: 'buildTestPayload',
9
+ wrapperKey: 'test',
10
+ createMode: 'wrapped',
11
+ updateMode: 'wrapped',
12
+ },
13
+ {
14
+ toolPrefix: 'suites',
15
+ resource: 'suites',
16
+ idArg: 'suite_id',
17
+ listMethod: 'listSuites',
18
+ searchMethod: 'searchSuites',
19
+ payloadBuilder: 'buildSuitePayload',
20
+ wrapperKey: 'suite',
21
+ createMode: 'wrapped',
22
+ updateMode: 'wrapped',
23
+ },
24
+ {
25
+ toolPrefix: 'runs',
26
+ resource: 'runs',
27
+ idArg: 'run_id',
28
+ listMethod: 'listRuns',
29
+ searchMethod: 'searchRuns',
30
+ createMode: 'run',
31
+ updateMode: 'run',
32
+ },
33
+ {
34
+ toolPrefix: 'testruns',
35
+ resource: 'testruns',
36
+ idArg: 'testrun_id',
37
+ listMethod: 'listTestruns',
38
+ searchMethod: 'searchTestruns',
39
+ payloadBuilder: 'buildTestrunPayload',
40
+ wrapperKey: 'testrun',
41
+ createMode: 'wrapped',
42
+ updateMode: 'wrapped',
43
+ },
44
+ {
45
+ toolPrefix: 'rungroups',
46
+ resource: 'rungroups',
47
+ idArg: 'rungroup_id',
48
+ listMethod: 'listRungroups',
49
+ searchMethod: 'searchRungroups',
50
+ payloadBuilder: 'buildRungroupPayload',
51
+ wrapperKey: 'rungroup',
52
+ createMode: 'wrapped',
53
+ updateMode: 'wrapped',
54
+ },
55
+ {
56
+ toolPrefix: 'steps',
57
+ resource: 'steps',
58
+ idArg: 'step_id',
59
+ listMethod: 'listSteps',
60
+ searchMethod: 'searchSteps',
61
+ payloadBuilder: 'buildStepPayload',
62
+ wrapperKey: 'step',
63
+ createMode: 'wrapped',
64
+ updateMode: 'wrapped',
65
+ },
66
+ {
67
+ toolPrefix: 'snippets',
68
+ resource: 'snippets',
69
+ idArg: 'snippet_id',
70
+ listMethod: 'listSnippets',
71
+ searchMethod: 'searchSnippets',
72
+ payloadBuilder: 'buildSnippetPayload',
73
+ wrapperKey: 'snippet',
74
+ createMode: 'wrapped',
75
+ updateMode: 'wrapped',
76
+ },
77
+ {
78
+ toolPrefix: 'labels',
79
+ resource: 'labels',
80
+ idArg: 'label_id',
81
+ listMethod: 'listLabels',
82
+ searchMethod: 'searchLabels',
83
+ payloadBuilder: 'buildLabelPayload',
84
+ wrapperKey: 'label',
85
+ createMode: 'wrapped',
86
+ updateMode: 'wrapped',
87
+ },
88
+ {
89
+ toolPrefix: 'plans',
90
+ resource: 'plans',
91
+ idArg: 'plan_id',
92
+ listMethod: 'listPlans',
93
+ searchMethod: 'searchPlans',
94
+ payloadBuilder: 'buildPlanPayload',
95
+ wrapperKey: 'plan',
96
+ createMode: 'wrapped',
97
+ updateMode: 'wrapped',
98
+ },
99
+ ];
@@ -0,0 +1,9 @@
1
+ export const ISSUE_RESOURCE_KEYS = ['test_id', 'suite_id', 'run_id', 'testrun_id', 'plan_id'];
2
+
3
+ export const ISSUE_SCOPED_TOOL_CONFIGS = [
4
+ { toolPrefix: 'tests', resourceKey: 'test_id' },
5
+ { toolPrefix: 'suites', resourceKey: 'suite_id' },
6
+ { toolPrefix: 'runs', resourceKey: 'run_id' },
7
+ { toolPrefix: 'testruns', resourceKey: 'testrun_id' },
8
+ { toolPrefix: 'plans', resourceKey: 'plan_id' },
9
+ ];
@@ -0,0 +1,131 @@
1
+ export const ISSUES_TOOLS = [
2
+ {
3
+ "name": "issues_list",
4
+ "description": "List linked issues (/api/v2/{project_id}/issues)",
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
+ "test_id": {
18
+ "type": "string"
19
+ },
20
+ "suite_id": {
21
+ "type": "string"
22
+ },
23
+ "run_id": {
24
+ "type": "string"
25
+ },
26
+ "testrun_id": {
27
+ "type": "integer"
28
+ },
29
+ "plan_id": {
30
+ "type": "string"
31
+ },
32
+ "source": {
33
+ "type": "string"
34
+ }
35
+ },
36
+ "additionalProperties": false
37
+ }
38
+ },
39
+ {
40
+ "name": "issues_create",
41
+ "description": "Link issue to resource (/api/v2/{project_id}/issues)",
42
+ "inputSchema": {
43
+ "type": "object",
44
+ "properties": {
45
+ "test_id": {
46
+ "type": "string"
47
+ },
48
+ "suite_id": {
49
+ "type": "string"
50
+ },
51
+ "run_id": {
52
+ "type": "string"
53
+ },
54
+ "testrun_id": {
55
+ "type": "integer"
56
+ },
57
+ "plan_id": {
58
+ "type": "string"
59
+ },
60
+ "url": {
61
+ "type": "string"
62
+ },
63
+ "jira_id": {
64
+ "type": "string"
65
+ }
66
+ },
67
+ "additionalProperties": false
68
+ }
69
+ },
70
+ {
71
+ "name": "issues_delete",
72
+ "description": "Unlink issue (/api/v2/{project_id}/issues/{id})",
73
+ "inputSchema": {
74
+ "type": "object",
75
+ "properties": {
76
+ "issue_id": {
77
+ "type": "integer"
78
+ },
79
+ "type": {
80
+ "type": "string",
81
+ "enum": [
82
+ "issue",
83
+ "jira_issue"
84
+ ]
85
+ }
86
+ },
87
+ "required": [
88
+ "issue_id",
89
+ "type"
90
+ ],
91
+ "additionalProperties": false
92
+ }
93
+ },
94
+ {
95
+ "name": "issues_search",
96
+ "description": "Search issues (delegates to issues_list filters)",
97
+ "inputSchema": {
98
+ "type": "object",
99
+ "properties": {
100
+ "page": {
101
+ "type": "integer",
102
+ "minimum": 1
103
+ },
104
+ "per_page": {
105
+ "type": "integer",
106
+ "minimum": 1,
107
+ "maximum": 100
108
+ },
109
+ "test_id": {
110
+ "type": "string"
111
+ },
112
+ "suite_id": {
113
+ "type": "string"
114
+ },
115
+ "run_id": {
116
+ "type": "string"
117
+ },
118
+ "testrun_id": {
119
+ "type": "integer"
120
+ },
121
+ "plan_id": {
122
+ "type": "string"
123
+ },
124
+ "source": {
125
+ "type": "string"
126
+ }
127
+ },
128
+ "additionalProperties": false
129
+ }
130
+ }
131
+ ];