@smartbear/mcp 0.20.0 → 0.22.0

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 (28) hide show
  1. package/README.md +23 -6
  2. package/dist/common/register-clients.js +2 -0
  3. package/dist/common/server.js +1 -4
  4. package/dist/package.json.js +1 -1
  5. package/dist/qtm4j/client.js +109 -0
  6. package/dist/qtm4j/config/constants.js +169 -0
  7. package/dist/qtm4j/config/field-resolution.types.js +34 -0
  8. package/dist/qtm4j/http/api-client.js +123 -0
  9. package/dist/qtm4j/http/auth-service.js +23 -0
  10. package/dist/qtm4j/resolver/cache/cache.js +52 -0
  11. package/dist/qtm4j/resolver/resolver-registry.js +70 -0
  12. package/dist/qtm4j/resolver/resolvers/common-attribute-resolver.js +56 -0
  13. package/dist/qtm4j/resolver/resolvers/component-resolver.js +56 -0
  14. package/dist/qtm4j/resolver/resolvers/label-resolver.js +56 -0
  15. package/dist/qtm4j/resolver/resolvers/resolver.js +6 -0
  16. package/dist/qtm4j/resolver/resolvers/test-case-uid-resolver.js +28 -0
  17. package/dist/qtm4j/schema/get-test-case.schema.js +153 -0
  18. package/dist/qtm4j/schema/get-test-steps.schema.js +74 -0
  19. package/dist/qtm4j/schema/project.schema.js +43 -0
  20. package/dist/qtm4j/schema/test-case.schema.js +41 -0
  21. package/dist/qtm4j/schema/update-test-case.schema.js +45 -0
  22. package/dist/qtm4j/tool/project/get-projects.js +111 -0
  23. package/dist/qtm4j/tool/project/set-project-context.js +99 -0
  24. package/dist/qtm4j/tool/test-case/create-test-case.js +113 -0
  25. package/dist/qtm4j/tool/test-case/get-test-cases.js +295 -0
  26. package/dist/qtm4j/tool/test-case/get-test-steps.js +111 -0
  27. package/dist/qtm4j/tool/test-case/update-test-case.js +158 -0
  28. package/package.json +5 -4
package/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
  </div>
19
19
  <br />
20
20
 
21
- A Model Context Protocol (MCP) server which provides AI assistants with seamless access to SmartBear's suite of testing and monitoring tools, including [BugSnag](https://www.bugsnag.com/), [Reflect](https://reflect.run), [Swagger](https://www.smartbear.com/api-hub), [PactFlow](https://pactflow.io/), [Pact Broker](https://docs.pact.io/), [QMetry](https://www.qmetry.com/), [Zephyr](https://smartbear.com/test-management/zephyr/) and [Collaborator](https://smartbear.com/product/collaborator/).
21
+ A Model Context Protocol (MCP) server which provides AI assistants with seamless access to SmartBear's suite of testing and monitoring tools, including [BugSnag](https://www.bugsnag.com/), [Reflect](https://reflect.run), [Swagger](https://www.smartbear.com/api-hub), [PactFlow](https://pactflow.io/), [Pact Broker](https://docs.pact.io/), [QMetry](https://www.qmetry.com/), [QTM4J](https://www.qmetry.com/qmetry-test-management-for-jira), [Zephyr](https://smartbear.com/test-management/zephyr/) and [Collaborator](https://smartbear.com/product/collaborator/).
22
22
 
23
23
  ## What is MCP?
24
24
 
@@ -37,6 +37,7 @@ See individual guides for suggested prompts and supported tools and resources:
37
37
  - [QMetry](https://developer.smartbear.com/smartbear-mcp/docs/qmetry-integration) - QMetry Test Management capabilities
38
38
  - [Zephyr](https://developer.smartbear.com/smartbear-mcp/docs/zephyr-integration) - Zephyr Test Management capabilities
39
39
  - [Collaborator](https://developer.smartbear.com/smartbear-mcp/docs/collaborator-integration) - Review and Remote System Configuration management capabilities
40
+ - [QTM4J](https://developer.smartbear.com/smartbear-mcp/docs/qtm4j-integration) - QTM4J Test Management for Jira capabilities
40
41
 
41
42
  ## Remote MCP Servers
42
43
 
@@ -50,12 +51,12 @@ For BugSnag, Swagger, and Zephyr, SmartBear hosts Remote MCP Servers that you ca
50
51
 
51
52
  See the [Remote MCP Servers guide](https://developer.smartbear.com/smartbear-mcp/docs/remote-mcp-servers) for per-client setup instructions. You can connect to multiple remote servers at the same time.
52
53
 
53
- > **Need Reflect, QMetry, PactFlow, or Collaborator?** These products are only available via the local npm package below, which bundles all products into a single MCP server.
54
+ > **Need Reflect, QMetry, QTM4J, PactFlow, or Collaborator?** These products are only available via the local npm package below, which bundles all products into a single MCP server.
54
55
 
55
56
  ## Prerequisites
56
57
 
57
58
  - Node.js 20+ and npm
58
- - Access to SmartBear products (BugSnag, Reflect, Swagger, QMetry, or Zephyr)
59
+ - Access to SmartBear products (BugSnag, Reflect, Swagger, QMetry, QTM4J or Zephyr)
59
60
  - Valid API tokens for the products you want to integrate
60
61
 
61
62
  ## Local MCP Server Installation (npm)
@@ -101,7 +102,9 @@ Alternatively, you can use `npx` (or globally install) the `@smartbear/mcp` pack
101
102
  "ZEPHYR_BASE_URL": "${input:zephyr_base_url}",
102
103
  "COLLABORATOR_BASE_URL": "${input:collab_base_url}",
103
104
  "COLLABORATOR_USERNAME": "${input:collab_username}",
104
- "COLLABORATOR_LOGIN_TICKET": "${input:collab_login_ticket}"
105
+ "COLLABORATOR_LOGIN_TICKET": "${input:collab_login_ticket}",
106
+ "QTM4J_API_KEY": "${input:qtm4j_api_key}",
107
+ "QTM4J_BASE_URL": "${input:qtm4j_base_url}"
105
108
  }
106
109
  }
107
110
  },
@@ -213,7 +216,19 @@ Alternatively, you can use `npx` (or globally install) the `@smartbear/mcp` pack
213
216
  "type": "promptString",
214
217
  "description": "Collab login ticket",
215
218
  "password": true
216
- }
219
+ },
220
+ {
221
+ "id": "qtm4j_api_key",
222
+ "type": "promptString",
223
+ "description": "QTM4J API Key",
224
+ "password": true
225
+ },
226
+ {
227
+ "id": "qtm4j_base_url",
228
+ "type": "promptString",
229
+ "description": "US region (default): https://qtmcloud.qmetry.com. Australia region: https://syd-qtmcloud.qmetry.com.",
230
+ "password": false
231
+ }
217
232
  ]
218
233
  }
219
234
  ```
@@ -250,7 +265,9 @@ Add the following configuration to your `claude_desktop_config.json` to launch t
250
265
  "ZEPHYR_BASE_URL": "https://api.zephyrscale.smartbear.com/v2",
251
266
  "COLLABORATOR_BASE_URL": "your collab base url",
252
267
  "COLLABORATOR_USERNAME": "your collab user name",
253
- "COLLABORATOR_LOGIN_TICKET": "your collab login ticket"
268
+ "COLLABORATOR_LOGIN_TICKET": "your collab login ticket",
269
+ "QTM4J_API_KEY": "your_qtm4j_key",
270
+ "QTM4J_BASE_URL": "https://qtmcloud.qmetry.com"
254
271
  }
255
272
  }
256
273
  }
@@ -2,6 +2,7 @@ import { BugsnagClient } from "../bugsnag/client.js";
2
2
  import { CollaboratorClient } from "../collaborator/client.js";
3
3
  import { PactflowClient } from "../pactflow/client.js";
4
4
  import { QmetryClient } from "../qmetry/client.js";
5
+ import { Qtm4jClient } from "../qtm4j/client.js";
5
6
  import { ReflectClient } from "../reflect/client.js";
6
7
  import { SwaggerClient } from "../swagger/client.js";
7
8
  import { ZephyrClient } from "../zephyr/client.js";
@@ -12,4 +13,5 @@ clientRegistry.register(new SwaggerClient());
12
13
  clientRegistry.register(new PactflowClient());
13
14
  clientRegistry.register(new QmetryClient());
14
15
  clientRegistry.register(new ZephyrClient());
16
+ clientRegistry.register(new Qtm4jClient());
15
17
  clientRegistry.register(new CollaboratorClient());
@@ -19,12 +19,9 @@ class SmartBearMcpServer extends McpServer {
19
19
  },
20
20
  {
21
21
  capabilities: {
22
+ // resources and prompts are supported by some but not all clients
22
23
  tools: { listChanged: true },
23
24
  // Server supports dynamic tool lists
24
- resources: { listChanged: true },
25
- // Server supports dynamic resource lists
26
- prompts: { listChanged: true },
27
- // Server supports sending prompts to Host
28
25
  logging: {}
29
26
  // Server supports logging messages
30
27
  }
@@ -1,4 +1,4 @@
1
- const version = "0.20.0";
1
+ const version = "0.22.0";
2
2
  const config = { "mcpServerName": "SmartBear MCP Server" };
3
3
  const packageJson = {
4
4
  version,
@@ -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
+ };