@smartbear/mcp 0.25.0 → 0.26.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.
@@ -5,12 +5,11 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
5
5
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6
6
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
7
7
  import { clientRegistry } from "./client-registry.js";
8
+ import { withRequestContext } from "./request-context.js";
8
9
  import { SmartBearMcpServer } from "./server.js";
9
10
  import { isDraining, registerShutdownHandler } from "./shutdown.js";
10
11
  import { getEnvVarName } from "./transport-stdio.js";
11
- import { getTypeDescription, isOptionalType } from "./zod-utils.js";
12
- class AuthorizationError extends Error {
13
- }
12
+ import { isOptionalType, getTypeDescription } from "./zod-utils.js";
14
13
  const PROBE_HEADERS = {
15
14
  "Content-Type": "application/json",
16
15
  "Cache-Control": "no-store"
@@ -281,7 +280,10 @@ async function handleStreamableHttpRequest(req, res, transports) {
281
280
  );
282
281
  return;
283
282
  }
284
- await transport.handleRequest(req, res, parsedBody);
283
+ await withRequestContext(
284
+ req,
285
+ async () => await transport.handleRequest(req, res, parsedBody)
286
+ );
285
287
  } catch (error) {
286
288
  console.error("Error handling StreamableHTTP request:", error);
287
289
  res.writeHead(500, { "Content-Type": "text/plain" });
@@ -326,10 +328,13 @@ async function handleLegacyMessageRequest(req, res, url, transports) {
326
328
  req.on("end", async () => {
327
329
  try {
328
330
  const parsedBody = JSON.parse(body);
329
- await session.transport.handlePostMessage(
331
+ await withRequestContext(
330
332
  req,
331
- res,
332
- parsedBody
333
+ async () => await session.transport.handlePostMessage(
334
+ req,
335
+ res,
336
+ parsedBody
337
+ )
333
338
  );
334
339
  } catch (error) {
335
340
  console.error("Error handling POST message:", error);
@@ -338,118 +343,97 @@ async function handleLegacyMessageRequest(req, res, url, transports) {
338
343
  }
339
344
  });
340
345
  }
341
- function resolveFromRequest(req, key, prefix) {
342
- const queryStringName = getQueryStringName(key, prefix);
346
+ function getConfigValue(clientPrefix, key, req) {
347
+ const queryStringName = getQueryStringName(clientPrefix, key);
343
348
  const queryParams = querystring.parse(req.url?.split("?")[1] || "");
344
349
  let value = queryParams[queryStringName] || queryParams[queryStringName.toLowerCase()];
345
350
  if (typeof value === "string") {
346
351
  return value;
347
352
  }
348
- const headerName = getHeaderName(key, prefix);
353
+ const headerName = getHeaderName(clientPrefix, key);
349
354
  value = req.headers[headerName] || req.headers[headerName.toLowerCase()];
350
355
  if (typeof value === "string") {
351
- if (value.toLowerCase().startsWith("bearer ")) {
352
- value = value.slice("bearer ".length);
353
- } else if (value.toLowerCase().startsWith("token ")) {
354
- value = value.slice("token ".length);
355
- } else if (value.toLowerCase().startsWith("basic ")) {
356
- value = value.slice("basic ".length);
357
- }
358
356
  return value;
359
357
  }
360
- const envVarName = getEnvVarName(key, prefix);
361
- return process.env[envVarName];
362
- }
363
- function makeConfigFn(req) {
364
- return (key, client) => resolveFromRequest(req, key, client?.configPrefix);
358
+ const envVarName = getEnvVarName(clientPrefix, key);
359
+ return process.env[envVarName] || null;
365
360
  }
366
361
  async function newServer(req, res) {
367
- const configFn = makeConfigFn(req);
368
- const enabledToolsets = resolveFromRequest(req, "toolsets", "smartbear") || void 0;
369
- const server = new SmartBearMcpServer(configFn, enabledToolsets);
362
+ const enabledToolsets = getConfigValue("smartbear", "toolsets", req) || void 0;
363
+ const server = new SmartBearMcpServer(enabledToolsets);
370
364
  try {
371
- const configuredCount = await clientRegistry.registerAll(
372
- server,
373
- configFn,
374
- true,
375
- false
365
+ const configuredCount = await withRequestContext(
366
+ req,
367
+ () => clientRegistry.configure(
368
+ server,
369
+ (client, key) => {
370
+ return getConfigValue(client.configPrefix, key, req);
371
+ },
372
+ true
373
+ // ignoreMissingRequiredConfigs
374
+ )
375
+ );
376
+ console.log(
377
+ `Configured ${configuredCount} clients for new server instance`
376
378
  );
377
379
  if (configuredCount === 0) {
378
380
  throw new Error(
379
- "No clients successfully configured. The request headers are missing the required configuration."
381
+ "No clients successfully configured. Missing authentication headers."
380
382
  );
381
383
  }
382
- console.log(
383
- `Configured ${configuredCount} clients for new server instance`
384
+ const hasAuth = withRequestContext(
385
+ req,
386
+ () => server.getClients().some((client) => {
387
+ if (!client.getAuthToken) return true;
388
+ return client.getAuthToken() !== null;
389
+ })
384
390
  );
385
- const hasNoAuth = !server.getClients().some((client) => client.hasAuth());
386
- if (hasNoAuth) {
387
- throw new AuthorizationError(
391
+ if (!hasAuth) {
392
+ throw new Error(
388
393
  "No clients have valid authentication credentials. Please authenticate via OAuth or provide alternative auth headers (e.g. API key or personal auth token)."
389
394
  );
390
395
  }
391
- return server;
392
396
  } catch (error) {
397
+ const headerHelp = getHttpHeadersHelp();
398
+ const errorMessage = headerHelp.length > 0 ? `Configuration error: ${error instanceof Error ? error.message : String(error)}. Please provide valid headers:
399
+ ${headerHelp.join("\n")}` : "No clients support HTTP header configuration.";
393
400
  const headers = {
394
401
  "Content-Type": "text/plain"
395
402
  };
396
- if (error instanceof AuthorizationError) {
397
- if (req.headers.host) {
398
- headers["WWW-Authenticate"] = `OAuth resource_metadata="http://${req.headers.host}/.well-known/oauth-protected-resource"`;
399
- }
400
- res.writeHead(401, headers);
401
- res.end(error.message);
402
- } else {
403
- const headerHelp = getHttpHeadersHelp();
404
- let errorMessage = `Configuration error: ${error instanceof Error ? error.message : String(error)}.`;
405
- if (headerHelp.length > 0) {
406
- errorMessage += ` Please provide valid headers:
407
- ${headerHelp.join("\n")}`;
408
- }
409
- res.writeHead(500, headers);
410
- res.end(errorMessage);
403
+ if (req.headers.host) {
404
+ headers["WWW-Authenticate"] = `OAuth resource_metadata="http://${req.headers.host}/.well-known/oauth-protected-resource"`;
411
405
  }
406
+ res.writeHead(401, headers);
407
+ res.end(errorMessage);
412
408
  return null;
413
409
  }
410
+ return server;
414
411
  }
415
- function getHeaderName(key, clientPrefix) {
416
- const prefix = `${clientPrefix ? `${clientPrefix}-${key}` : key}`;
417
- return prefix.split(/[\s\-_]/).map(
412
+ function getHeaderName(clientPrefix, key) {
413
+ return `${clientPrefix}-${key.split("_").map(
418
414
  (part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
419
- ).join("-");
415
+ ).join("-")}`;
420
416
  }
421
- function getQueryStringName(key, clientPrefix) {
422
- const prefix = `${clientPrefix ? `${clientPrefix}-${key}` : key}`;
423
- return prefix.split(/[\s\-_]/).map(
424
- (part, i) => i === 0 ? part.toLowerCase() : part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
425
- ).join("");
417
+ function getQueryStringName(clientPrefix, key) {
418
+ return `${clientPrefix.toLowerCase()}${key.split("_").map(
419
+ (part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
420
+ ).join("")}`;
426
421
  }
427
422
  function getHttpHeaders() {
428
423
  const headers = /* @__PURE__ */ new Set();
429
- for (const client of clientRegistry.getAll()) {
430
- for (const key of [
431
- ...Object.keys(client.config.shape),
432
- ...Object.keys(client.authenticationFields.shape)
433
- ]) {
434
- headers.add(getHeaderName(key, client.configPrefix));
424
+ for (const entry of clientRegistry.getAll()) {
425
+ for (const configKey of Object.keys(entry.config.shape)) {
426
+ headers.add(getHeaderName(entry.configPrefix, configKey));
435
427
  }
436
428
  }
437
429
  return Array.from(headers).sort((a, b) => a.localeCompare(b));
438
430
  }
439
431
  function getHttpHeadersHelp() {
440
432
  const messages = [];
441
- for (const client of clientRegistry.getAll()) {
442
- messages.push(` - ${client.name}:`);
443
- for (const [authKey, requirement] of Object.entries(
444
- client.authenticationFields.shape
445
- )) {
446
- const headerName = getHeaderName(authKey, client.configPrefix);
447
- messages.push(` - ${headerName}: ${getTypeDescription(requirement)}`);
448
- }
449
- for (const [configKey, requirement] of Object.entries(
450
- client.config.shape
451
- )) {
452
- const headerName = getHeaderName(configKey, client.configPrefix);
433
+ for (const entry of clientRegistry.getAll()) {
434
+ messages.push(` - ${entry.name}:`);
435
+ for (const [configKey, requirement] of Object.entries(entry.config.shape)) {
436
+ const headerName = getHeaderName(entry.configPrefix, configKey);
453
437
  const requiredTag = isOptionalType(requirement) ? " (optional)" : " (required)";
454
438
  messages.push(
455
439
  ` - ${headerName}${requiredTag}: ${getTypeDescription(requirement)}`
@@ -459,7 +443,6 @@ function getHttpHeadersHelp() {
459
443
  return messages;
460
444
  }
461
445
  export {
462
- AuthorizationError,
463
446
  drainHttpTransport,
464
447
  getBaseUrl,
465
448
  getHeaderName,
@@ -1,20 +1,19 @@
1
1
  import { enableCompileCache } from "node:module";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { clientRegistry } from "./client-registry.js";
4
- import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "./info.js";
4
+ import { USER_AGENT } from "./info.js";
5
5
  import { SmartBearMcpServer } from "./server.js";
6
6
  import { registerShutdownHandler } from "./shutdown.js";
7
- import { getTypeDescription } from "./zod-utils.js";
8
- function getConfigMessage() {
7
+ import { isOptionalType, getTypeDescription } from "./zod-utils.js";
8
+ function getNoConfigMessage() {
9
9
  const messages = [];
10
- for (const client of clientRegistry.getAll()) {
11
- messages.push(` - ${client.name}:`);
12
- for (const [configKey, requirement] of [
13
- ...Object.entries(client.authenticationFields.shape),
14
- ...Object.entries(client.config.shape)
15
- ]) {
10
+ for (const entry of clientRegistry.getAll()) {
11
+ messages.push(` - ${entry.name}:`);
12
+ for (const [configKey, requirement] of Object.entries(entry.config.shape)) {
13
+ const envVarName = getEnvVarName(entry.configPrefix, configKey);
14
+ const requiredTag = isOptionalType(requirement) ? " (optional)" : " (required)";
16
15
  messages.push(
17
- ` - ${getEnvVarName(configKey, client.configPrefix)}: ${getTypeDescription(requirement)}`
16
+ ` - ${envVarName}${requiredTag}: ${getTypeDescription(requirement)}`
18
17
  );
19
18
  }
20
19
  }
@@ -22,34 +21,33 @@ function getConfigMessage() {
22
21
  }
23
22
  async function runStdioMode() {
24
23
  if (process.argv.includes("--version")) {
25
- console.log(`${MCP_SERVER_NAME}: v${MCP_SERVER_VERSION}`);
24
+ console.log(`User-Agent: ${USER_AGENT}`);
26
25
  process.exit(0);
27
26
  } else if (process.argv.includes("--help")) {
28
27
  console.log(
29
28
  "The following environment variables can be set to configure each of the SmartBear clients:"
30
29
  );
31
- console.log(getConfigMessage().join("\n"));
30
+ console.log(getNoConfigMessage().join("\n"));
32
31
  process.exit(0);
33
32
  }
34
33
  enableCompileCache();
35
- const configFn = (key, client) => {
36
- const envVarName = getEnvVarName(key, client?.configPrefix);
37
- return process.env[envVarName];
38
- };
39
- const server = new SmartBearMcpServer(configFn, process.env.MCP_TOOLSETS);
40
- const addedCount = await clientRegistry.registerAll(
34
+ const server = new SmartBearMcpServer(process.env.MCP_TOOLSETS);
35
+ const configuredCount = await clientRegistry.configure(
41
36
  server,
42
- configFn,
43
- true,
44
- true
37
+ (client, key) => {
38
+ const envVarName = getEnvVarName(client.configPrefix, key);
39
+ return process.env[envVarName] || null;
40
+ }
45
41
  );
46
- if (addedCount === 0) {
47
- const message = getConfigMessage();
42
+ if (configuredCount === 0) {
43
+ const message = getNoConfigMessage();
48
44
  console.warn(
49
- `No clients configured. Please provide valid environment variables for at least one client:
50
- ${message.join("\n")}`
45
+ message.length > 0 ? `No clients configured. Please provide valid environment variables for at least one client:
46
+ ${message.join("\n")}` : "No clients support environment variable configuration."
51
47
  );
52
- await clientRegistry.registerAll(server, configFn, false, false);
48
+ for (const entry of clientRegistry.getAll()) {
49
+ await server.addClient(entry);
50
+ }
53
51
  }
54
52
  const transport = new StdioServerTransport();
55
53
  registerShutdownHandler("stdio-transport", async () => {
@@ -74,9 +72,8 @@ ${message.join("\n")}`
74
72
  };
75
73
  await server.connect(transport);
76
74
  }
77
- function getEnvVarName(key, clientPrefix) {
78
- const prefix = `${clientPrefix ? `${clientPrefix}-${key}` : key}`.toUpperCase();
79
- return prefix.replace(/[\s\-_]/g, "_");
75
+ function getEnvVarName(clientPrefix, key) {
76
+ return `${clientPrefix.toUpperCase().replace(/-/g, "_")}_${key.toUpperCase()}`;
80
77
  }
81
78
  export {
82
79
  getEnvVarName,
@@ -1,4 +1,4 @@
1
- const version = "0.25.0";
1
+ const version = "0.26.0";
2
2
  const config = { "mcpServerName": "SmartBear MCP Server" };
3
3
  const packageJson = {
4
4
  version,
@@ -1,26 +1,27 @@
1
1
  import zod__default from "zod";
2
- import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
2
+ import { USER_AGENT } from "../common/info.js";
3
3
  import { isSamplingPolyfillResult } from "../common/pollyfills.js";
4
+ import { getRequestHeader } from "../common/request-context.js";
4
5
  import { ToolError } from "../common/tools.js";
5
6
  import { getOADMatcherRecommendations, getUserMatcherSelection } from "./client/prompt-utils.js";
6
7
  import { PROMPTS } from "./client/prompts.js";
7
8
  import { TOOLS } from "./client/tools.js";
8
9
  const ConfigurationSchema = zod__default.object({
9
- base_url: zod__default.url().describe("Pact Broker or PactFlow base URL")
10
- });
11
- const AuthenticationSchema = zod__default.object({
12
- username: zod__default.string().describe("Username for Pact Broker").optional(),
13
- password: zod__default.string().describe("Password for Pact Broker").optional(),
14
- token: zod__default.string().describe(
10
+ base_url: zod__default.url().describe("Pact Broker or PactFlow base URL"),
11
+ token: zod__default.string().optional().describe(
15
12
  "Bearer token for PactFlow authentication (use this OR username/password)"
16
- ).optional()
13
+ ),
14
+ username: zod__default.string().optional().describe("Username for Pact Broker"),
15
+ password: zod__default.string().optional().describe("Password for Pact Broker")
17
16
  });
18
17
  class PactflowClient {
19
18
  name = "Contract Testing";
20
19
  capabilityPrefix = "contract-testing";
21
20
  configPrefix = "Pact-Broker";
22
21
  config = ConfigurationSchema;
23
- authenticationFields = AuthenticationSchema;
22
+ token;
23
+ username;
24
+ password;
24
25
  aiBaseUrl;
25
26
  baseUrl;
26
27
  _clientType;
@@ -31,7 +32,7 @@ class PactflowClient {
31
32
  return this._server;
32
33
  }
33
34
  /**
34
- * Initializes the client with auth credentials and the MCP server reference.
35
+ * Initialises the client with auth credentials and the MCP server reference.
35
36
  * Accepts either a Bearer token (PactFlow) or username/password (Pact Broker).
36
37
  * Does nothing if neither is supplied.
37
38
  *
@@ -39,21 +40,23 @@ class PactflowClient {
39
40
  * @param config - Connection config (base_url + token OR username/password).
40
41
  */
41
42
  async configure(server, config) {
42
- this._server = server;
43
- if (this._server.getEnv("username", this) && this._server.getEnv("password", this)) {
43
+ this.token = config.token;
44
+ this.username = config.username;
45
+ this.password = config.password;
46
+ if (typeof config.token === "string") {
47
+ this._clientType = "pactflow";
48
+ } else if (typeof config.username === "string" && typeof config.password === "string") {
44
49
  this._clientType = "pact_broker";
45
50
  } else {
46
51
  this._clientType = "pactflow";
47
52
  }
48
53
  this.baseUrl = config.base_url;
49
54
  this.aiBaseUrl = `${this.baseUrl}/api/ai`;
55
+ this._server = server;
50
56
  }
51
57
  /** Returns true if the client has been configured with a base URL and credentials. */
52
58
  isConfigured() {
53
- return !!this._server;
54
- }
55
- hasAuth() {
56
- return this.isConfigured() && !!this.requestHeaders;
59
+ return this.baseUrl !== void 0;
57
60
  }
58
61
  // PactFlow AI client methods
59
62
  /**
@@ -163,28 +166,38 @@ class PactflowClient {
163
166
  }
164
167
  /** Returns the current auth/content-type headers used for all requests. */
165
168
  get requestHeaders() {
166
- const token = this.server?.getEnv("token", this) || this.server?.getEnv("Authorization");
167
- if (token) {
168
- let authHeader = token;
169
- if (!token.startsWith("Basic ") && !token.startsWith("Bearer ")) {
170
- authHeader = `Bearer ${token}`;
169
+ let contextToken = getRequestHeader("Pact-Token") || getRequestHeader("Authorization");
170
+ if (Array.isArray(contextToken)) {
171
+ contextToken = contextToken[0];
172
+ }
173
+ if (contextToken) {
174
+ let authHeader = contextToken;
175
+ if (!contextToken.startsWith("Basic ") && !contextToken.startsWith("Bearer ")) {
176
+ authHeader = `Bearer ${contextToken}`;
171
177
  }
172
178
  return {
173
179
  Authorization: authHeader,
174
180
  "Content-Type": "application/json",
175
- "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
181
+ "User-Agent": USER_AGENT
176
182
  };
177
- } else {
178
- const username = this.server?.getEnv("username", this);
179
- const password = this.server?.getEnv("password", this);
180
- if (username && password) {
181
- const authString = `${username}:${password}`;
182
- return {
183
- Authorization: `Basic ${Buffer.from(authString).toString("base64")}`,
184
- "Content-Type": "application/json",
185
- "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
186
- };
183
+ }
184
+ if (this.token) {
185
+ let authHeader = this.token;
186
+ if (!authHeader.startsWith("Basic ") && !authHeader.startsWith("Bearer ")) {
187
+ authHeader = `Bearer ${authHeader}`;
187
188
  }
189
+ return {
190
+ Authorization: authHeader,
191
+ "Content-Type": "application/json",
192
+ "User-Agent": USER_AGENT
193
+ };
194
+ } else if (this.username && this.password) {
195
+ const authString = `${this.username}:${this.password}`;
196
+ return {
197
+ Authorization: `Basic ${Buffer.from(authString).toString("base64")}`,
198
+ "Content-Type": "application/json",
199
+ "User-Agent": USER_AGENT
200
+ };
188
201
  }
189
202
  return void 0;
190
203
  }
@@ -1,4 +1,4 @@
1
- import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../../../common/info.js";
1
+ import { USER_AGENT } from "../../../common/info.js";
2
2
  import { QMETRY_DEFAULTS } from "../../config/constants.js";
3
3
  import { handleQMetryFetchError, handleQMetryApiError } from "./error-handler.js";
4
4
  async function qmetryRequest({
@@ -13,7 +13,7 @@ async function qmetryRequest({
13
13
  const headers = {
14
14
  apikey: token,
15
15
  project: project || QMETRY_DEFAULTS.PROJECT_KEY,
16
- "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
16
+ "User-Agent": USER_AGENT,
17
17
  "qmetry-source": "smartbear-mcp"
18
18
  };
19
19
  if (body) {
@@ -1,4 +1,4 @@
1
- import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../../common/info.js";
1
+ import { USER_AGENT } from "../../common/info.js";
2
2
  import { QMETRY_DEFAULTS } from "../config/constants.js";
3
3
  import { QMETRY_PATHS } from "../config/rest-endpoints.js";
4
4
  import { DEFAULT_IMPORT_AUTOMATION_PAYLOAD } from "../types/automation.js";
@@ -91,7 +91,7 @@ async function importAutomationResults(token, baseUrl, project, payload) {
91
91
  const headers = {
92
92
  apikey: token,
93
93
  project: finalPayload.projectID || project || QMETRY_DEFAULTS.PROJECT_KEY,
94
- "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
94
+ "User-Agent": USER_AGENT,
95
95
  "qmetry-source": "smartbear-mcp"
96
96
  // Note: Content-Type will be set automatically by fetch for FormData
97
97
  };
@@ -1,45 +1,43 @@
1
1
  import zod__default from "zod";
2
+ import { getRequestHeader } from "../common/request-context.js";
2
3
  import { findAutoResolveConfig, autoResolveViewIdAndFolderPath } from "./client/auto-resolve.js";
3
4
  import { QMETRY_HANDLER_MAP } from "./client/handlers.js";
4
5
  import { getProjectInfo } from "./client/project.js";
5
6
  import { TOOLS } from "./client/tools/index.js";
6
7
  import { QMETRY_DEFAULTS } from "./config/constants.js";
7
8
  const ConfigurationSchema = zod__default.object({
9
+ api_key: zod__default.string().describe("QMetry API key for authentication"),
8
10
  base_url: zod__default.string().url().optional().describe(
9
11
  "Optional QMetry base URL for custom or region-specific endpoints"
10
12
  )
11
13
  });
12
- const AuthenticationSchema = zod__default.object({
13
- api_key: zod__default.string().describe("QMetry API key for authentication").optional()
14
- });
15
14
  class QmetryClient {
16
15
  name = "QMetry";
17
16
  capabilityPrefix = "qmetry";
18
17
  configPrefix = "Qmetry";
19
18
  config = ConfigurationSchema;
20
- authenticationFields = AuthenticationSchema;
19
+ token;
21
20
  projectApiKey = QMETRY_DEFAULTS.PROJECT_KEY;
22
21
  endpoint = QMETRY_DEFAULTS.BASE_URL;
23
- server;
24
- async configure(server, config) {
25
- this.server = server;
22
+ async configure(_server, config, _cache) {
23
+ this.token = config.api_key;
26
24
  if (config.base_url) {
27
25
  this.endpoint = config.base_url;
28
26
  }
29
27
  }
30
28
  isConfigured() {
31
- return !!this.server;
32
- }
33
- hasAuth() {
34
- return this.isConfigured() && !!this.getAuthToken();
35
- }
36
- getAuthToken() {
37
- return this.server?.getEnv("api_key", this);
29
+ return true;
38
30
  }
39
- getAuthTokenOrThrow() {
40
- const token = this.getAuthToken();
41
- if (!token) throw new Error("Client not configured");
42
- return token;
31
+ getToken() {
32
+ let contextToken = getRequestHeader("Qmetry-Token") || getRequestHeader("apikey");
33
+ if (Array.isArray(contextToken)) {
34
+ contextToken = contextToken[0];
35
+ }
36
+ if (contextToken) {
37
+ return contextToken;
38
+ }
39
+ if (!this.token) throw new Error("Client not configured");
40
+ return this.token;
43
41
  }
44
42
  getBaseUrl() {
45
43
  return this.endpoint;
@@ -86,7 +84,7 @@ class QmetryClient {
86
84
  let projectInfo;
87
85
  try {
88
86
  projectInfo = await getProjectInfo(
89
- this.getAuthTokenOrThrow(),
87
+ this.getToken(),
90
88
  baseUrl,
91
89
  projectKey
92
90
  );
@@ -107,7 +105,7 @@ class QmetryClient {
107
105
  }
108
106
  const { projectKey: _, baseUrl: __, ...cleanArgs } = a;
109
107
  const result = await handlerFn(
110
- this.getAuthTokenOrThrow(),
108
+ this.getToken(),
111
109
  baseUrl,
112
110
  projectKey,
113
111
  cleanArgs
@@ -1,21 +1,20 @@
1
1
  import zod__default from "zod";
2
+ import { getRequestHeader } from "../common/request-context.js";
2
3
  import { CONFIG_KEYS, API_CONFIG, SCHEMA_DESCRIPTIONS, CLIENT_CONFIG, ERROR_MESSAGES } from "./config/constants.js";
3
4
  import { ApiClient } from "./http/api-client.js";
4
5
  import { ResolverRegistry } from "./resolver/resolver-registry.js";
5
6
  const ConfigurationSchema = zod__default.object({
7
+ [CONFIG_KEYS.API_KEY]: zod__default.string().describe(SCHEMA_DESCRIPTIONS.API_KEY),
8
+ [CONFIG_KEYS.AUTOMATION_API_KEY]: zod__default.string().optional().describe(SCHEMA_DESCRIPTIONS.AUTOMATION_API_KEY),
6
9
  [CONFIG_KEYS.BASE_URL]: zod__default.string().url().optional().default(API_CONFIG.DEFAULT_BASE_URL).describe(SCHEMA_DESCRIPTIONS.BASE_URL)
7
10
  });
8
- const AuthenticationSchema = zod__default.object({
9
- api_key: zod__default.string().describe(SCHEMA_DESCRIPTIONS.API_KEY).optional(),
10
- automation_api_key: zod__default.string().describe(SCHEMA_DESCRIPTIONS.AUTOMATION_API_KEY).optional()
11
- });
12
11
  class Qtm4jClient {
13
12
  name = CLIENT_CONFIG.NAME;
14
13
  capabilityPrefix = CLIENT_CONFIG.TOOL_PREFIX;
15
14
  configPrefix = CLIENT_CONFIG.CONFIG_PREFIX;
16
15
  config = ConfigurationSchema;
17
- authenticationFields = AuthenticationSchema;
18
- server;
16
+ _apiKey;
17
+ _automationApiKey;
19
18
  baseUrl = API_CONFIG.DEFAULT_BASE_URL;
20
19
  apiClient;
21
20
  resolverRegistry;
@@ -25,7 +24,8 @@ class Qtm4jClient {
25
24
  * @param config - Configuration object containing API key and optional base URL
26
25
  */
27
26
  async configure(server, config) {
28
- this.server = server;
27
+ this._apiKey = config[CONFIG_KEYS.API_KEY];
28
+ this._automationApiKey = config[CONFIG_KEYS.AUTOMATION_API_KEY];
29
29
  if (config[CONFIG_KEYS.BASE_URL]) {
30
30
  this.baseUrl = config[CONFIG_KEYS.BASE_URL];
31
31
  }
@@ -40,20 +40,34 @@ class Qtm4jClient {
40
40
  );
41
41
  }
42
42
  /**
43
- * Check if the client is properly configured
44
- * @returns true if API key is set and client is ready
43
+ * Get authentication token with request-scoped override support
44
+ * Checks request headers first, then falls back to configured API key
45
+ * @returns API key or null if not found
45
46
  */
46
- isConfigured() {
47
- return !!this.apiClient;
48
- }
49
- hasAuth() {
50
- return this.isConfigured() && !!this.getAuthToken();
51
- }
52
47
  getAuthToken() {
53
- return this.server?.getEnv(CONFIG_KEYS.API_KEY, this) || this.server?.getEnv("Authorization") || null;
48
+ const contextHeader = getRequestHeader("Qtm4j-Api-Key") || getRequestHeader("apiKey") || getRequestHeader("Authorization");
49
+ if (contextHeader) {
50
+ let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
51
+ if (token.startsWith("Bearer ")) {
52
+ token = token.substring(7);
53
+ }
54
+ return token;
55
+ }
56
+ return this._apiKey || null;
54
57
  }
55
58
  getAutomationApiKey() {
56
- return this.server?.getEnv(CONFIG_KEYS.AUTOMATION_API_KEY, this) || null;
59
+ const headerKey = getRequestHeader("Qtm4j-Automation-Api-Key");
60
+ if (headerKey) {
61
+ return Array.isArray(headerKey) ? headerKey[0] : headerKey;
62
+ }
63
+ return this._automationApiKey || null;
64
+ }
65
+ /**
66
+ * Check if the client is properly configured
67
+ * @returns true if API key is set and client is ready
68
+ */
69
+ isConfigured() {
70
+ return this.apiClient !== void 0;
57
71
  }
58
72
  /**
59
73
  * Get the configured API client instance
@@ -1,4 +1,4 @@
1
- import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../../common/info.js";
1
+ import { USER_AGENT } from "../../common/info.js";
2
2
  import { CONTENT_TYPES, HTTP_HEADERS } from "../config/constants.js";
3
3
  class AuthService {
4
4
  apiKey;
@@ -13,7 +13,7 @@ class AuthService {
13
13
  return {
14
14
  [HTTP_HEADERS.API_KEY]: this.apiKey,
15
15
  [HTTP_HEADERS.CONTENT_TYPE]: CONTENT_TYPES.JSON,
16
- [HTTP_HEADERS.USER_AGENT]: `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
16
+ [HTTP_HEADERS.USER_AGENT]: USER_AGENT,
17
17
  [HTTP_HEADERS.ACCEPT]: CONTENT_TYPES.JSON
18
18
  };
19
19
  }