@smartbear/mcp 0.19.2 → 0.21.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.
Files changed (63) hide show
  1. package/README.md +37 -7
  2. package/assets/icon.png +0 -0
  3. package/dist/bugsnag/client.js +19 -12
  4. package/dist/collaborator/client.js +10 -10
  5. package/dist/common/client-registry.js +2 -2
  6. package/dist/common/register-clients.js +2 -0
  7. package/dist/common/server.js +74 -111
  8. package/dist/common/shutdown.js +165 -0
  9. package/dist/common/transport-http.js +94 -12
  10. package/dist/common/transport-stdio.js +16 -2
  11. package/dist/common/zod-utils.js +62 -7
  12. package/dist/index.js +2 -0
  13. package/dist/package.json.js +1 -1
  14. package/dist/pactflow/client/prompts.js +19 -18
  15. package/dist/pactflow/client/tools.js +8 -13
  16. package/dist/pactflow/client.js +26 -12
  17. package/dist/qmetry/client/tools/testsuite-tools.js +2 -2
  18. package/dist/qmetry/client.js +1 -1
  19. package/dist/qtm4j/client.js +109 -0
  20. package/dist/qtm4j/config/constants.js +169 -0
  21. package/dist/qtm4j/config/field-resolution.types.js +34 -0
  22. package/dist/qtm4j/http/api-client.js +123 -0
  23. package/dist/qtm4j/http/auth-service.js +23 -0
  24. package/dist/qtm4j/resolver/cache/cache.js +52 -0
  25. package/dist/qtm4j/resolver/resolver-registry.js +70 -0
  26. package/dist/qtm4j/resolver/resolvers/common-attribute-resolver.js +56 -0
  27. package/dist/qtm4j/resolver/resolvers/component-resolver.js +56 -0
  28. package/dist/qtm4j/resolver/resolvers/label-resolver.js +56 -0
  29. package/dist/qtm4j/resolver/resolvers/resolver.js +6 -0
  30. package/dist/qtm4j/resolver/resolvers/test-case-uid-resolver.js +28 -0
  31. package/dist/qtm4j/schema/get-test-case.schema.js +153 -0
  32. package/dist/qtm4j/schema/get-test-steps.schema.js +74 -0
  33. package/dist/qtm4j/schema/project.schema.js +43 -0
  34. package/dist/qtm4j/schema/test-case.schema.js +41 -0
  35. package/dist/qtm4j/schema/update-test-case.schema.js +45 -0
  36. package/dist/qtm4j/tool/project/get-projects.js +111 -0
  37. package/dist/qtm4j/tool/project/set-project-context.js +99 -0
  38. package/dist/qtm4j/tool/test-case/create-test-case.js +113 -0
  39. package/dist/qtm4j/tool/test-case/get-test-cases.js +295 -0
  40. package/dist/qtm4j/tool/test-case/get-test-steps.js +111 -0
  41. package/dist/qtm4j/tool/test-case/update-test-case.js +158 -0
  42. package/dist/reflect/client.js +3 -3
  43. package/dist/reflect/prompt/sap-test.js +5 -5
  44. package/dist/reflect/tool/recording/add-prompt-step.js +6 -14
  45. package/dist/reflect/tool/recording/add-segment.js +4 -14
  46. package/dist/reflect/tool/recording/connect-to-session.js +3 -8
  47. package/dist/reflect/tool/recording/delete-previous-step.js +3 -8
  48. package/dist/reflect/tool/recording/get-screenshot.js +4 -14
  49. package/dist/reflect/tool/suites/cancel-suite-execution.js +4 -14
  50. package/dist/reflect/tool/suites/execute-suite.js +3 -8
  51. package/dist/reflect/tool/suites/get-suite-execution-status.js +4 -14
  52. package/dist/reflect/tool/suites/list-suite-executions.js +3 -8
  53. package/dist/reflect/tool/suites/list-suites.js +2 -1
  54. package/dist/reflect/tool/tests/get-test-status.js +3 -8
  55. package/dist/reflect/tool/tests/list-segments.js +5 -20
  56. package/dist/reflect/tool/tests/list-tests.js +2 -1
  57. package/dist/reflect/tool/tests/run-test.js +3 -8
  58. package/dist/swagger/client/api.js +11 -2
  59. package/dist/swagger/client/portal-types.js +0 -3
  60. package/dist/swagger/client/tools.js +0 -1
  61. package/dist/swagger/client.js +1 -1
  62. package/dist/zephyr/client.js +1 -1
  63. package/package.json +6 -4
@@ -926,8 +926,8 @@ const TESTSUITE_TOOLS = [
926
926
  entityType: "TCR",
927
927
  qmTsRunId: "2720260",
928
928
  runStatusID: 123266,
929
- username: "dhaval.mistry",
930
- password: "Ispl123#",
929
+ username: "test.user",
930
+ password: "password",
931
931
  isBulkOperation: false
932
932
  },
933
933
  expectedOutput: "Test case run status updated with Part 11 Compliance authentication"
@@ -13,7 +13,7 @@ const ConfigurationSchema = zod__default.object({
13
13
  });
14
14
  class QmetryClient {
15
15
  name = "QMetry";
16
- toolPrefix = "qmetry";
16
+ capabilityPrefix = "qmetry";
17
17
  configPrefix = "Qmetry";
18
18
  config = ConfigurationSchema;
19
19
  token;
@@ -0,0 +1,109 @@
1
+ import zod__default from "zod";
2
+ import { getRequestHeader } from "../common/request-context.js";
3
+ import { CONFIG_KEYS, API_CONFIG, SCHEMA_DESCRIPTIONS, CLIENT_CONFIG, ERROR_MESSAGES } from "./config/constants.js";
4
+ import { ApiClient } from "./http/api-client.js";
5
+ import { ResolverRegistry } from "./resolver/resolver-registry.js";
6
+ const ConfigurationSchema = zod__default.object({
7
+ [CONFIG_KEYS.API_KEY]: zod__default.string().describe(SCHEMA_DESCRIPTIONS.API_KEY),
8
+ [CONFIG_KEYS.BASE_URL]: zod__default.string().url().optional().default(API_CONFIG.DEFAULT_BASE_URL).describe(SCHEMA_DESCRIPTIONS.BASE_URL)
9
+ });
10
+ class Qtm4jClient {
11
+ name = CLIENT_CONFIG.NAME;
12
+ capabilityPrefix = CLIENT_CONFIG.TOOL_PREFIX;
13
+ configPrefix = CLIENT_CONFIG.CONFIG_PREFIX;
14
+ config = ConfigurationSchema;
15
+ _apiKey;
16
+ baseUrl = API_CONFIG.DEFAULT_BASE_URL;
17
+ apiClient;
18
+ resolverRegistry;
19
+ /**
20
+ * Configure the QTM4J client with API credentials
21
+ * @param server - MCP Server instance
22
+ * @param config - Configuration object containing API key and optional base URL
23
+ */
24
+ async configure(server, config) {
25
+ this._apiKey = config[CONFIG_KEYS.API_KEY];
26
+ if (config[CONFIG_KEYS.BASE_URL]) {
27
+ this.baseUrl = config[CONFIG_KEYS.BASE_URL];
28
+ }
29
+ this.apiClient = new ApiClient(() => this.getAuthToken(), this.baseUrl);
30
+ this.resolverRegistry = new ResolverRegistry(
31
+ this.apiClient,
32
+ server.getCache()
33
+ );
34
+ }
35
+ /**
36
+ * Get authentication token with request-scoped override support
37
+ * Checks request headers first, then falls back to configured API key
38
+ * @returns API key or null if not found
39
+ */
40
+ getAuthToken() {
41
+ const contextHeader = getRequestHeader("Qtm4j-Api-Key") || getRequestHeader("apiKey") || getRequestHeader("Authorization");
42
+ if (contextHeader) {
43
+ let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
44
+ if (token.startsWith("Bearer ")) {
45
+ token = token.substring(7);
46
+ }
47
+ return token;
48
+ }
49
+ return this._apiKey || null;
50
+ }
51
+ /**
52
+ * Check if the client is properly configured
53
+ * @returns true if API key is set and client is ready
54
+ */
55
+ isConfigured() {
56
+ return this.apiClient !== void 0;
57
+ }
58
+ /**
59
+ * Get the configured API client instance
60
+ * @returns ApiClient instance
61
+ * @throws Error if client is not configured
62
+ */
63
+ getApiClient() {
64
+ if (!this.apiClient) {
65
+ throw new Error(ERROR_MESSAGES.CLIENT_NOT_CONFIGURED);
66
+ }
67
+ return this.apiClient;
68
+ }
69
+ getResolverRegistry() {
70
+ if (!this.resolverRegistry) {
71
+ throw new Error(ERROR_MESSAGES.CLIENT_NOT_CONFIGURED);
72
+ }
73
+ return this.resolverRegistry;
74
+ }
75
+ requireProjectContext() {
76
+ return this.getResolverRegistry().requireProjectContext();
77
+ }
78
+ /**
79
+ * Register all QTM4J tools with the MCP server
80
+ *
81
+ * This method creates tool instances and registers them with the MCP server.
82
+ * Each tool is prefixed with 'qtm4j_' (e.g., qtm4j_get_projects)
83
+ *
84
+ * @param register - Function to register tools with MCP server
85
+ * @param _getInput - Function to get user input (not used currently)
86
+ */
87
+ async registerTools(register, _getInput) {
88
+ const { GetProjects } = await import("./tool/project/get-projects.js");
89
+ const { SetProjectContext } = await import("./tool/project/set-project-context.js");
90
+ const { CreateTestCase } = await import("./tool/test-case/create-test-case.js");
91
+ const { GetTestCases } = await import("./tool/test-case/get-test-cases.js");
92
+ const { GetTestSteps } = await import("./tool/test-case/get-test-steps.js");
93
+ const { UpdateTestCase } = await import("./tool/test-case/update-test-case.js");
94
+ const tools = [
95
+ new GetProjects(this),
96
+ new SetProjectContext(this),
97
+ new CreateTestCase(this),
98
+ new GetTestCases(this),
99
+ new GetTestSteps(this),
100
+ new UpdateTestCase(this)
101
+ ];
102
+ for (const tool of tools) {
103
+ register(tool.specification, tool.handle);
104
+ }
105
+ }
106
+ }
107
+ export {
108
+ Qtm4jClient
109
+ };
@@ -0,0 +1,169 @@
1
+ const API_CONFIG = {
2
+ /** Default base URL for QTM4J Cloud */
3
+ DEFAULT_BASE_URL: "https://qtmcloud.qmetry.com",
4
+ /** API version prefix */
5
+ API_VERSION: "/rest/api/latest"
6
+ };
7
+ const ENDPOINTS = {
8
+ /** Projects endpoint */
9
+ PROJECTS: `${API_CONFIG.API_VERSION}/projects`,
10
+ /** Create test case endpoint */
11
+ CREATE_TEST_CASE: `${API_CONFIG.API_VERSION}/testcases`,
12
+ /** Search test cases endpoint */
13
+ SEARCH_TEST_CASES: `${API_CONFIG.API_VERSION}/testcases/search`,
14
+ /** Resolve test case keys → internal UIDs for a given project */
15
+ RESOLVE_TEST_CASE_IDS: (projectId) => `${API_CONFIG.API_VERSION}/projects/${projectId}/mcp/testcases/resolve-ids`,
16
+ /** Update test case endpoint */
17
+ UPDATE_TEST_CASE: (id, versionNo) => `${API_CONFIG.API_VERSION}/testcases/${id}/versions/${versionNo}`,
18
+ /** Test steps search endpoint */
19
+ TEST_STEPS: (id, versionNo) => `${API_CONFIG.API_VERSION}/testcases/${id}/versions/${versionNo}/teststeps/search`,
20
+ /** Common attributes endpoint (priority, statuses) */
21
+ COMMON_ATTRIBUTES: (projectId) => `${API_CONFIG.API_VERSION}/projects/${projectId}/mcp/common-attributes`,
22
+ /** Labels search endpoint */
23
+ LABELS: (projectId) => `${API_CONFIG.API_VERSION}/projects/${projectId}/mcp/labels`,
24
+ /** Components search endpoint */
25
+ COMPONENTS: (projectId) => `${API_CONFIG.API_VERSION}/projects/${projectId}/mcp/components`
26
+ };
27
+ const HTTP_HEADERS = {
28
+ /** API key header name */
29
+ API_KEY: "apiKey",
30
+ /** Content type header */
31
+ CONTENT_TYPE: "Content-Type",
32
+ /** User agent header */
33
+ USER_AGENT: "User-Agent",
34
+ /** Accept header */
35
+ ACCEPT: "Accept",
36
+ /** Content length header */
37
+ CONTENT_LENGTH: "content-length"
38
+ };
39
+ const CONTENT_TYPES = {
40
+ /** JSON content type */
41
+ JSON: "application/json"
42
+ };
43
+ const HTTP_METHODS = {
44
+ GET: "GET",
45
+ POST: "POST",
46
+ PUT: "PUT"
47
+ };
48
+ const HTTP_STATUS = {
49
+ /** No content status code */
50
+ NO_CONTENT: 204
51
+ };
52
+ const CLIENT_CONFIG = {
53
+ /** Client name */
54
+ NAME: "QTM4J",
55
+ /** Tool prefix for all QTM4J tools */
56
+ TOOL_PREFIX: "qtm4j",
57
+ /** Configuration prefix */
58
+ CONFIG_PREFIX: "Qtm4j"
59
+ };
60
+ const PAGINATION = {
61
+ /** Default starting position for pagination */
62
+ DEFAULT_START_AT: 0,
63
+ /** Default maximum results for projects */
64
+ DEFAULT_MAX_RESULTS_PROJECTS: 100,
65
+ /** Default maximum results for test cases */
66
+ DEFAULT_MAX_RESULTS_TEST_CASES: 50,
67
+ /** Maximum allowed results per request */
68
+ MAX_ALLOWED_RESULTS: 100,
69
+ /** Maximum allowed results per request for test cases (backend enforced) */
70
+ MAX_ALLOWED_RESULTS_TEST_CASES: 50,
71
+ /** Default maximum results for test steps */
72
+ DEFAULT_MAX_RESULTS_TEST_STEPS: 50,
73
+ /** Maximum allowed results per request for test steps */
74
+ MAX_ALLOWED_RESULTS_TEST_STEPS: 100,
75
+ /** Minimum allowed results per request */
76
+ MIN_ALLOWED_RESULTS: 1
77
+ };
78
+ const ERROR_MESSAGES = {
79
+ /** Client not configured error */
80
+ CLIENT_NOT_CONFIGURED: "QTM4J client not configured. Please set API key.",
81
+ /** Request failed template */
82
+ REQUEST_FAILED: (status, errorText) => `Request failed with status ${status}: ${errorText}`
83
+ };
84
+ const TOOL_NAMES = {
85
+ /** Get Projects tool */
86
+ GET_PROJECTS: {
87
+ TITLE: "Get Projects",
88
+ SUMMARY: "Get all projects from QTM4J with optional filtering"
89
+ },
90
+ /** Set Project Context tool */
91
+ SET_PROJECT_CONTEXT: {
92
+ TITLE: "Set Project Context",
93
+ SUMMARY: "Set the active QTM4J project for the current session. Must be called before any project-specific operation. Pre-loads priority and status values so you can map user-provided names to valid options via NLP."
94
+ },
95
+ /** Create Test Case tool */
96
+ CREATE_TEST_CASE: {
97
+ TITLE: "Create Test Case",
98
+ SUMMARY: "Create a new test case in a QTM4J project. Supports auto-resolving human-readable names for priority, status, labels, and components."
99
+ },
100
+ /** Search Test Cases tool */
101
+ SEARCH_TEST_CASES: {
102
+ TITLE: "Search Test Cases",
103
+ SUMMARY: "Search and filter test cases in a QTM4J project with support for pagination, field selection, and sorting."
104
+ },
105
+ /** Get Test Steps tool */
106
+ GET_TEST_STEPS: {
107
+ TITLE: "Get Test Steps",
108
+ SUMMARY: "Get test steps for a test case by its key and version. Accepts the human-readable key (e.g. 'SCRUM-TC-145') and resolves it to the internal ID automatically."
109
+ },
110
+ /** Update Test Case tool */
111
+ UPDATE_TEST_CASE: {
112
+ TITLE: "Update Test Case",
113
+ SUMMARY: "Update an existing test case in QTM4J. Supports auto-resolving human-readable names for priority, status, labels, and components. Labels and components support add/delete operations."
114
+ }
115
+ };
116
+ const CONFIG_KEYS = {
117
+ /** API key configuration key */
118
+ API_KEY: "api_key",
119
+ /** Base URL configuration key */
120
+ BASE_URL: "base_url"
121
+ };
122
+ const SCHEMA_DESCRIPTIONS = {
123
+ /** API key description */
124
+ API_KEY: "QTM4J API key for authentication",
125
+ /** Base URL description */
126
+ BASE_URL: "QTM4J base URL (default: https://qtmcloud.qmetry.com). Can be customized for on-premise installations.",
127
+ /** Start at description */
128
+ START_AT: "Zero-indexed starting position for pagination",
129
+ /** Max results projects description */
130
+ MAX_RESULTS_PROJECTS: "Maximum number of results per page (1-100)",
131
+ /** Search text description */
132
+ SEARCH_TEXT: "Search text for project key or project name",
133
+ /** QMetry enabled description */
134
+ QMETRY_ENABLED: "Filter by QMetry enabled status",
135
+ /** Project object description */
136
+ PROJECT_OBJECT: "Project object"
137
+ };
138
+ const RESPONSE_FIELDS = {
139
+ /** Start at field */
140
+ START_AT: "startAt",
141
+ /** Max results field */
142
+ MAX_RESULTS: "maxResults",
143
+ FIELDS: "fields",
144
+ SORT: "sort"
145
+ };
146
+ const EMPTY_VALUES = {
147
+ /** Empty object */
148
+ OBJECT: {},
149
+ /** Empty string */
150
+ STRING: "",
151
+ /** Zero value */
152
+ ZERO: 0
153
+ };
154
+ export {
155
+ API_CONFIG,
156
+ CLIENT_CONFIG,
157
+ CONFIG_KEYS,
158
+ CONTENT_TYPES,
159
+ EMPTY_VALUES,
160
+ ENDPOINTS,
161
+ ERROR_MESSAGES,
162
+ HTTP_HEADERS,
163
+ HTTP_METHODS,
164
+ HTTP_STATUS,
165
+ PAGINATION,
166
+ RESPONSE_FIELDS,
167
+ SCHEMA_DESCRIPTIONS,
168
+ TOOL_NAMES
169
+ };
@@ -0,0 +1,34 @@
1
+ const ResolverKeys = {
2
+ /**
3
+ * Fields fetched from the common-attributes API (batch-loaded on project context set).
4
+ * Values are cached per project and eagerly available for tools.
5
+ */
6
+ CommonAttribute: {
7
+ TESTCASE_STATUS: "testcase_status",
8
+ TEST_PLAN_STATUS: "testplan_status",
9
+ TEST_CYCLE_STATUS: "testcycle_status",
10
+ PRIORITY: "priority",
11
+ TESTCASE_FOLDER: "testcase_folder"
12
+ },
13
+ /**
14
+ * Fields with dedicated search APIs (fetched on-demand, not batch-loaded).
15
+ * Resolved lazily when tools reference them — no eager preloading.
16
+ */
17
+ SearchableField: {
18
+ LABEL: "label",
19
+ COMPONENTS: "components",
20
+ TEST_CASE_KEY_TO_UID: "testCaseKeyToUid"
21
+ }
22
+ };
23
+ var InputField = /* @__PURE__ */ ((InputField2) => {
24
+ InputField2["PRIORITY"] = "priority";
25
+ InputField2["STATUS"] = "status";
26
+ InputField2["COMPONENTS"] = "components";
27
+ InputField2["LABELS"] = "labels";
28
+ InputField2["FOLDER"] = "folderId";
29
+ return InputField2;
30
+ })(InputField || {});
31
+ export {
32
+ InputField,
33
+ ResolverKeys
34
+ };
@@ -0,0 +1,123 @@
1
+ import { ToolError } from "../../common/tools.js";
2
+ import { EMPTY_VALUES, ERROR_MESSAGES, HTTP_METHODS, CONTENT_TYPES, HTTP_HEADERS, HTTP_STATUS } from "../config/constants.js";
3
+ import { AuthService } from "./auth-service.js";
4
+ class ApiClient {
5
+ baseUrl;
6
+ tokenProvider;
7
+ constructor(tokenOrProvider, baseUrl) {
8
+ this.baseUrl = baseUrl.trim().replace(/\/$/, EMPTY_VALUES.STRING);
9
+ if (typeof tokenOrProvider === "string") {
10
+ this.tokenProvider = () => tokenOrProvider;
11
+ } else {
12
+ this.tokenProvider = tokenOrProvider;
13
+ }
14
+ }
15
+ /**
16
+ * Get authentication headers for current request
17
+ * Calls token provider to support request-scoped credentials
18
+ * @returns Record of HTTP headers including API key
19
+ * @throws ToolError if token is not available
20
+ */
21
+ getHeaders() {
22
+ const token = this.tokenProvider();
23
+ if (!token) {
24
+ throw new ToolError(ERROR_MESSAGES.CLIENT_NOT_CONFIGURED);
25
+ }
26
+ return new AuthService(token).getAuthHeaders();
27
+ }
28
+ /**
29
+ * Construct full URL with query parameters
30
+ * @param endpoint - API endpoint path
31
+ * @param params - Optional query parameters
32
+ * @returns Complete URL string
33
+ */
34
+ getUrl(endpoint, params) {
35
+ const url = new URL(this.baseUrl + endpoint);
36
+ if (params) {
37
+ Object.entries(params).forEach(([key, value]) => {
38
+ if (value !== void 0) {
39
+ url.searchParams.append(key, String(value));
40
+ }
41
+ });
42
+ }
43
+ return url.toString();
44
+ }
45
+ /**
46
+ * Perform GET request
47
+ * @param endpoint - API endpoint path
48
+ * @param params - Optional query parameters
49
+ * @returns Parsed response data
50
+ */
51
+ async get(endpoint, params) {
52
+ const response = await fetch(this.getUrl(endpoint, params), {
53
+ method: HTTP_METHODS.GET,
54
+ headers: this.getHeaders()
55
+ });
56
+ return await this.validateAndGetResponseBody(response);
57
+ }
58
+ /**
59
+ * Perform POST request
60
+ * @param endpoint - API endpoint path
61
+ * @param body - Request body object
62
+ * @returns Parsed response data
63
+ */
64
+ async post(endpoint, body) {
65
+ const response = await fetch(this.getUrl(endpoint), {
66
+ method: HTTP_METHODS.POST,
67
+ headers: {
68
+ ...this.getHeaders(),
69
+ [HTTP_HEADERS.CONTENT_TYPE]: CONTENT_TYPES.JSON
70
+ },
71
+ body: JSON.stringify(body)
72
+ });
73
+ return await this.validateAndGetResponseBody(response);
74
+ }
75
+ /**
76
+ * Perform PUT request
77
+ * @param endpoint - API endpoint path
78
+ * @param body - Request body object
79
+ * @returns Parsed response data
80
+ */
81
+ async put(endpoint, body) {
82
+ const response = await fetch(this.getUrl(endpoint), {
83
+ method: HTTP_METHODS.PUT,
84
+ headers: {
85
+ ...this.getHeaders(),
86
+ [HTTP_HEADERS.CONTENT_TYPE]: CONTENT_TYPES.JSON
87
+ },
88
+ body: JSON.stringify(body)
89
+ });
90
+ return await this.validateAndGetResponseBody(response);
91
+ }
92
+ /**
93
+ * Validate HTTP response and extract body
94
+ * Handles various response types: JSON, text, empty responses
95
+ * @param response - HTTP response object
96
+ * @returns Parsed response data or error
97
+ * @throws ToolError if request fails
98
+ */
99
+ async validateAndGetResponseBody(response) {
100
+ if (!response.ok) {
101
+ const errorText = await response.text();
102
+ throw new ToolError(
103
+ ERROR_MESSAGES.REQUEST_FAILED(response.status, errorText)
104
+ );
105
+ }
106
+ const contentLength = response.headers.get(HTTP_HEADERS.CONTENT_LENGTH);
107
+ if (response.status === HTTP_STATUS.NO_CONTENT || contentLength === String(EMPTY_VALUES.ZERO)) {
108
+ return EMPTY_VALUES.OBJECT;
109
+ }
110
+ const text = await response.text();
111
+ if (!text?.trim()) {
112
+ return EMPTY_VALUES.OBJECT;
113
+ }
114
+ try {
115
+ return JSON.parse(text);
116
+ } catch {
117
+ return { data: text };
118
+ }
119
+ }
120
+ }
121
+ export {
122
+ ApiClient
123
+ };
@@ -0,0 +1,23 @@
1
+ import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../../common/info.js";
2
+ import { CONTENT_TYPES, HTTP_HEADERS } from "../config/constants.js";
3
+ class AuthService {
4
+ apiKey;
5
+ constructor(apiKey) {
6
+ this.apiKey = apiKey.trim();
7
+ }
8
+ /**
9
+ * Get authentication headers for QTM4J API requests
10
+ * @returns Record of HTTP headers including API key authorization
11
+ */
12
+ getAuthHeaders() {
13
+ return {
14
+ [HTTP_HEADERS.API_KEY]: this.apiKey,
15
+ [HTTP_HEADERS.CONTENT_TYPE]: CONTENT_TYPES.JSON,
16
+ [HTTP_HEADERS.USER_AGENT]: `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
17
+ [HTTP_HEADERS.ACCEPT]: CONTENT_TYPES.JSON
18
+ };
19
+ }
20
+ }
21
+ export {
22
+ AuthService
23
+ };
@@ -0,0 +1,52 @@
1
+ class Cache {
2
+ trackedKeys = /* @__PURE__ */ new Map();
3
+ cacheService;
4
+ constructor(cacheService) {
5
+ this.cacheService = cacheService;
6
+ }
7
+ compositeKey(projectKey, fieldKey) {
8
+ return `qtm4j:${projectKey}:${fieldKey}`;
9
+ }
10
+ get(projectKey, fieldKey) {
11
+ return this.cacheService.get(
12
+ this.compositeKey(projectKey, fieldKey)
13
+ );
14
+ }
15
+ set(projectKey, fieldKey, values) {
16
+ const key = this.compositeKey(projectKey, fieldKey);
17
+ const existing = this.cacheService.get(key) ?? {};
18
+ this.cacheService.set(key, { ...existing, ...values });
19
+ if (!this.trackedKeys.has(projectKey)) {
20
+ this.trackedKeys.set(projectKey, /* @__PURE__ */ new Set());
21
+ }
22
+ this.trackedKeys.get(projectKey).add(key);
23
+ }
24
+ has(projectKey, fieldKey) {
25
+ return this.cacheService.get(this.compositeKey(projectKey, fieldKey)) !== void 0;
26
+ }
27
+ clear(projectKey) {
28
+ if (projectKey) {
29
+ const keys = this.trackedKeys.get(projectKey);
30
+ if (keys) {
31
+ for (const key of keys) {
32
+ this.cacheService.del(key);
33
+ }
34
+ this.trackedKeys.delete(projectKey);
35
+ }
36
+ } else {
37
+ for (const keys of this.trackedKeys.values()) {
38
+ for (const key of keys) {
39
+ this.cacheService.del(key);
40
+ }
41
+ }
42
+ this.trackedKeys.clear();
43
+ }
44
+ }
45
+ /** Case-insensitive lookup of a name within a cached field. */
46
+ matchValue(projectKey, fieldKey, name) {
47
+ return this.get(projectKey, fieldKey)?.[name.toLowerCase()];
48
+ }
49
+ }
50
+ export {
51
+ Cache
52
+ };
@@ -0,0 +1,70 @@
1
+ import { CommonAttributeResolver } from "./resolvers/common-attribute-resolver.js";
2
+ import { ComponentResolver } from "./resolvers/component-resolver.js";
3
+ import { LabelResolver } from "./resolvers/label-resolver.js";
4
+ import { TestCaseUidResolver } from "./resolvers/test-case-uid-resolver.js";
5
+ const ERROR_NO_PROJECT_CONTEXT = "No active project set. Please call set_project_context before performing this operation.";
6
+ class ResolverRegistry {
7
+ resolverByKey;
8
+ commonAttributes;
9
+ testCaseUidResolver;
10
+ projectContext;
11
+ constructor(apiClient, cacheService) {
12
+ this.commonAttributes = new CommonAttributeResolver(
13
+ apiClient,
14
+ cacheService
15
+ );
16
+ this.testCaseUidResolver = new TestCaseUidResolver(apiClient);
17
+ const labelResolver = new LabelResolver(apiClient, cacheService);
18
+ const componentResolver = new ComponentResolver(apiClient, cacheService);
19
+ this.resolverByKey = /* @__PURE__ */ new Map();
20
+ for (const resolver of [
21
+ this.commonAttributes,
22
+ labelResolver,
23
+ componentResolver,
24
+ this.testCaseUidResolver
25
+ ]) {
26
+ for (const key of resolver.fieldKeys) {
27
+ this.resolverByKey.set(key, resolver);
28
+ }
29
+ }
30
+ }
31
+ /**
32
+ * Returns the resolver responsible for the given resolver key.
33
+ * Tools use this with their FIELD_CONFIG map to dispatch field resolution.
34
+ */
35
+ getResolver(resolverKey) {
36
+ const resolver = this.resolverByKey.get(resolverKey);
37
+ if (!resolver)
38
+ throw new Error(`No resolver registered for key '${resolverKey}'`);
39
+ return resolver;
40
+ }
41
+ /**
42
+ * Direct typed access to the CommonAttributeResolver, used by set_project_context to
43
+ * eagerly load all common attribute fields (priority, statuses) for LLM NLP mapping.
44
+ */
45
+ getCommonAttributeResolver() {
46
+ return this.commonAttributes;
47
+ }
48
+ setProjectContext(context) {
49
+ this.projectContext = context;
50
+ }
51
+ requireProjectContext() {
52
+ if (!this.projectContext) {
53
+ throw new Error(ERROR_NO_PROJECT_CONTEXT);
54
+ }
55
+ return this.projectContext;
56
+ }
57
+ clearCache() {
58
+ for (const resolver of new Set(this.resolverByKey.values())) {
59
+ resolver.clearCache();
60
+ }
61
+ }
62
+ clearProjectCache(projectKey) {
63
+ for (const resolver of new Set(this.resolverByKey.values())) {
64
+ resolver.clearCache(projectKey);
65
+ }
66
+ }
67
+ }
68
+ export {
69
+ ResolverRegistry
70
+ };
@@ -0,0 +1,56 @@
1
+ import { ENDPOINTS } from "../../config/constants.js";
2
+ import { ResolverKeys } from "../../config/field-resolution.types.js";
3
+ import { Cache } from "../cache/cache.js";
4
+ import { Resolver } from "./resolver.js";
5
+ class CommonAttributeResolver extends Resolver {
6
+ fieldKeys = Object.values(
7
+ ResolverKeys.CommonAttribute
8
+ );
9
+ cache;
10
+ apiClient;
11
+ constructor(apiClient, cacheService) {
12
+ super();
13
+ this.apiClient = apiClient;
14
+ this.cache = new Cache(cacheService);
15
+ }
16
+ // Priority and status are always single values — no array handling needed.
17
+ async resolve(inputField, resolverKey, body, context, warnings) {
18
+ const name = body[inputField];
19
+ if (name == null) return;
20
+ const id = await this.resolveAndReturn(
21
+ context.projectKey,
22
+ context.projectId,
23
+ resolverKey,
24
+ name
25
+ );
26
+ if (id !== void 0) {
27
+ body[inputField] = Number(id);
28
+ } else {
29
+ warnings.push(
30
+ `Skipped ${inputField} '${name}' — not available in the current project.`
31
+ );
32
+ }
33
+ }
34
+ async resolveAndReturn(projectKey, projectId, resolverKey, name) {
35
+ const cached = this.cache.matchValue(projectKey, resolverKey, name);
36
+ if (cached !== void 0) return cached;
37
+ await this.preload(projectKey, projectId);
38
+ return this.cache.matchValue(projectKey, resolverKey, name);
39
+ }
40
+ async preload(projectKey, projectId) {
41
+ const response = await this.apiClient.get(
42
+ ENDPOINTS.COMMON_ATTRIBUTES(projectId)
43
+ );
44
+ const attributes = response;
45
+ for (const [key, values] of Object.entries(attributes)) {
46
+ this.cache.set(projectKey, key, values);
47
+ }
48
+ return attributes;
49
+ }
50
+ clearCache(projectKey) {
51
+ this.cache.clear(projectKey);
52
+ }
53
+ }
54
+ export {
55
+ CommonAttributeResolver
56
+ };