@smartbear/mcp 0.25.0 → 0.25.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.
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
3
+ import { getRequestHeader } from "../common/request-context.js";
3
4
  import { ToolError } from "../common/tools.js";
4
5
  import { DEFAULT_API_BASE_URL, AUTHORIZATION_HEADER } from "./config/constants.js";
5
6
  import { ChatWithQaLead } from "./tool/tasks/chat-with-qa-lead.js";
@@ -15,33 +16,33 @@ import { RunTestsInFunctionalAreas } from "./tool/tasks/run-tests-in-functional-
15
16
  import { StopTask } from "./tool/tasks/stop-task.js";
16
17
  import { WaitForTask } from "./tool/tasks/wait-for-task.js";
17
18
  const ConfigurationSchema = z.object({
19
+ api_token: z.string().describe("BearQ workspace API token (Bearer)."),
18
20
  api_base_url: z.string().optional().describe(
19
21
  "Override the BearQ public API base URL. Defaults to https://api.bearq.smartbear.com"
20
22
  )
21
23
  });
22
- const AuthenticationSchema = z.object({
23
- api_token: z.string().describe("BearQ workspace API token (Bearer).")
24
- });
25
24
  class BearQClient {
26
- server;
25
+ _apiToken;
27
26
  _baseUrl = DEFAULT_API_BASE_URL;
28
27
  name = "BearQ";
29
28
  capabilityPrefix = "bearq";
30
29
  configPrefix = "BearQ";
31
30
  config = ConfigurationSchema;
32
- authenticationFields = AuthenticationSchema;
33
- async configure(server, config) {
34
- this.server = server;
31
+ async configure(_server, config) {
32
+ this._apiToken = config.api_token;
35
33
  if (config.api_base_url) this._baseUrl = config.api_base_url;
36
34
  }
37
35
  getAuthToken() {
38
- return this.server?.getEnv("api_token", this) || this.server?.getEnv(AUTHORIZATION_HEADER) || null;
36
+ const contextHeader = getRequestHeader(AUTHORIZATION_HEADER);
37
+ if (contextHeader) {
38
+ let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
39
+ if (token.startsWith("Bearer ")) token = token.substring(7);
40
+ return token;
41
+ }
42
+ return this._apiToken ?? null;
39
43
  }
40
44
  isConfigured() {
41
- return this.server !== void 0;
42
- }
43
- hasAuth() {
44
- return this.isConfigured() && !!this.getAuthToken();
45
+ return true;
45
46
  }
46
47
  getBaseUrl() {
47
48
  return this._baseUrl;
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
3
+ import { getRequestHeader } from "../common/request-context.js";
3
4
  import { ToolError } from "../common/tools.js";
4
5
  import { CurrentUserAPI } from "./client/api/CurrentUser.js";
5
6
  import { Configuration } from "./client/api/configuration.js";
@@ -39,14 +40,11 @@ const EXCLUDED_EVENT_FIELDS = /* @__PURE__ */ new Set([
39
40
  // This is searches multiple fields and is more a convenience for humans, we're removing to avoid over-matching
40
41
  ]);
41
42
  const ConfigurationSchema = z.object({
43
+ auth_token: z.string().describe("BugSnag personal access token"),
42
44
  project_api_key: z.string().describe("BugSnag project API key").optional(),
43
- endpoint: z.url().describe("BugSnag endpoint URL").optional()
44
- });
45
- const AuthenticationSchema = z.object({
46
- auth_token: z.string().describe("BugSnag personal access token").optional()
45
+ endpoint: z.string().url().describe("BugSnag endpoint URL").optional()
47
46
  });
48
47
  class BugsnagClient {
49
- server;
50
48
  cache;
51
49
  _projectApiKey;
52
50
  _isConfigured = false;
@@ -54,7 +52,7 @@ class BugsnagClient {
54
52
  _errorsApi;
55
53
  _projectApi;
56
54
  _appEndpoint;
57
- apiConfig;
55
+ _authToken;
58
56
  get currentUserApi() {
59
57
  if (!this._currentUserApi) throw new Error("Client not configured");
60
58
  return this._currentUserApi;
@@ -76,9 +74,7 @@ class BugsnagClient {
76
74
  configPrefix = "Bugsnag";
77
75
  config = ConfigurationSchema;
78
76
  defaultToolsets = ["Projects"];
79
- authenticationFields = AuthenticationSchema;
80
77
  async configure(server, config) {
81
- this.server = server;
82
78
  this.cache = server.getCache();
83
79
  this._appEndpoint = this.getEndpoint(
84
80
  "app",
@@ -86,17 +82,45 @@ class BugsnagClient {
86
82
  config.endpoint
87
83
  );
88
84
  this._projectApiKey = config.project_api_key;
89
- this.apiConfig = new Configuration({
90
- apiKey: () => {
91
- const authToken = this.server?.getEnv("auth_token", this);
85
+ this._authToken = config.auth_token;
86
+ await this.initializeApis(config);
87
+ }
88
+ getAuthToken() {
89
+ const contextHeader = getRequestHeader("Bugsnag-Auth-Token");
90
+ if (contextHeader) {
91
+ let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
92
+ if (token.startsWith("token ")) {
93
+ token = token.substring(6);
94
+ }
95
+ return `token ${token}`;
96
+ }
97
+ const bearerToken = this.getBearerToken();
98
+ if (bearerToken) {
99
+ return bearerToken;
100
+ }
101
+ return this._authToken ? `token ${this._authToken}` : null;
102
+ }
103
+ getBearerToken() {
104
+ const contextHeader = getRequestHeader("Authorization");
105
+ if (contextHeader) {
106
+ let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
107
+ if (token.startsWith("Bearer ")) {
108
+ token = token.substring(7);
109
+ }
110
+ return `Bearer ${token}`;
111
+ }
112
+ return null;
113
+ }
114
+ async initializeApis(config) {
115
+ const apiConfig = new Configuration({
116
+ apiKey: (_name) => {
117
+ const authToken = this.getAuthToken();
92
118
  if (authToken) {
93
- return `token ${authToken}`;
119
+ return authToken;
94
120
  }
95
- const bearerToken = this.server?.getEnv("Authorization");
96
- if (bearerToken) {
97
- return `Bearer ${bearerToken}`;
98
- }
99
- return void 0;
121
+ throw new Error(
122
+ "Authentication token not found in request headers or configuration"
123
+ );
100
124
  },
101
125
  headers: {
102
126
  "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
@@ -110,17 +134,14 @@ class BugsnagClient {
110
134
  config.endpoint
111
135
  )
112
136
  });
113
- this._currentUserApi = new CurrentUserAPI(this.apiConfig);
114
- this._errorsApi = new ErrorAPI(this.apiConfig);
115
- this._projectApi = new ProjectAPI(this.apiConfig);
137
+ this._currentUserApi = new CurrentUserAPI(apiConfig);
138
+ this._errorsApi = new ErrorAPI(apiConfig);
139
+ this._projectApi = new ProjectAPI(apiConfig);
116
140
  this._isConfigured = true;
117
141
  }
118
142
  isConfigured() {
119
143
  return this._isConfigured;
120
144
  }
121
- hasAuth() {
122
- return this.isConfigured() && !!this.apiConfig?.apiKey();
123
- }
124
145
  // If the endpoint is not provided, it will use the default API endpoint based on the project API key.
125
146
  // if the project api key is not provided, the endpoint will be the default API endpoint.
126
147
  // if the endpoint is provided, it will be used as is for custom domains, or normalized for known domains.
@@ -1,29 +1,26 @@
1
1
  import { z } from "zod";
2
+ import { getRequestHeader } from "../common/request-context.js";
2
3
  const ConfigurationSchema = z.object({
3
- base_url: z.url().describe("Collaborator server base URL")
4
- });
5
- const AuthenticationSchema = z.object({
6
- login: z.string().describe("Collaborator username for authentication").optional(),
7
- ticket: z.string().describe("Collaborator login ticket for authentication").optional()
4
+ base_url: z.url().describe("Collaborator server base URL"),
5
+ username: z.string().describe("Collaborator username for authentication").optional(),
6
+ login_ticket: z.string().describe("Collaborator login ticket for authentication").optional()
8
7
  });
9
8
  class CollaboratorClient {
10
9
  name = "Collaborator";
11
10
  capabilityPrefix = "collaborator";
12
11
  configPrefix = "Collaborator";
13
12
  config = ConfigurationSchema;
14
- authenticationFields = AuthenticationSchema;
15
13
  baseUrl;
16
- server;
17
- async configure(server, config) {
14
+ username;
15
+ loginTicket;
16
+ async configure(_server, config, _cache) {
18
17
  this.baseUrl = config.base_url;
19
- this.server = server;
18
+ this.username = config.username;
19
+ this.loginTicket = config.login_ticket;
20
20
  }
21
21
  isConfigured() {
22
22
  return this.baseUrl !== void 0;
23
23
  }
24
- hasAuth() {
25
- return this.isConfigured() && this.server?.getEnv("Login", this) !== void 0 && this.server?.getEnv("Ticket", this) !== void 0;
26
- }
27
24
  /**
28
25
  * Calls the Collaborator API with the given commands, prepending authentication automatically.
29
26
  * @param commands Array of Collaborator API commands (excluding authentication)
@@ -31,8 +28,16 @@ class CollaboratorClient {
31
28
  */
32
29
  async call(commands) {
33
30
  const url = `${this.baseUrl}/services/json/v1`;
34
- const login = this.server?.getEnv("Login", this);
35
- const ticket = this.server?.getEnv("Ticket", this);
31
+ let login = this.username;
32
+ let ticket = this.loginTicket;
33
+ const contextLogin = getRequestHeader("Collaborator-Login");
34
+ const contextTicket = getRequestHeader("Collaborator-Ticket");
35
+ if (contextLogin) {
36
+ login = Array.isArray(contextLogin) ? contextLogin[0] : contextLogin;
37
+ }
38
+ if (contextTicket) {
39
+ ticket = Array.isArray(contextTicket) ? contextTicket[0] : contextTicket;
40
+ }
36
41
  const body = [
37
42
  {
38
43
  command: "SessionService.authenticate",
@@ -1,4 +1,4 @@
1
- import { ZodURL, ZodError } from "zod";
1
+ import { ZodURL } from "zod";
2
2
  import { fullyUnwrapZodType, isOptionalType } from "./zod-utils.js";
3
3
  class ClientRegistry {
4
4
  entries = [];
@@ -84,57 +84,31 @@ class ClientRegistry {
84
84
  return this.entries.filter((entry) => this.isClientEnabled(entry.name));
85
85
  }
86
86
  /**
87
- * Registers all enabled clients on the given MCP server
87
+ * Configures all enabled clients on the given MCP server
88
88
  * @param server The MCP server on which the client is registered
89
89
  * @param getConfigValue A function that obtains a configuration value for the given client and requirement name
90
- * @returns The number of clients successfully added
90
+ * @returns The number of clients successfully configured
91
91
  */
92
- async registerAll(server, getConfigValue, configure, authorizationCheck) {
93
- if (authorizationCheck && !configure) {
94
- throw new Error(
95
- "Cannot perform authorization check without configuring clients"
96
- );
97
- }
98
- let addedCount = 0;
99
- clientLoop: for (const client of this.getAll()) {
100
- if (configure) {
101
- const config = {};
102
- for (const configKey of Object.keys(client.config.shape)) {
103
- const value = getConfigValue(configKey, client);
104
- if (value) {
105
- this.validateAllowedEndpoint(client.config.shape[configKey], value);
106
- config[configKey] = value;
107
- } else if (!isOptionalType(client.config.shape[configKey])) {
108
- continue clientLoop;
109
- }
110
- }
111
- let parsedConfig;
112
- try {
113
- parsedConfig = client.config.parse(config);
114
- } catch (error) {
115
- if (error instanceof ZodError) {
116
- console.warn(
117
- `Configuration for client ${client.name} is invalid: ${error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ")}`
118
- );
119
- } else {
120
- console.warn(
121
- `Unable to apply configuration for client ${client.name}: ${error}`
122
- );
123
- }
124
- continue;
125
- }
126
- await client.configure(server, parsedConfig);
127
- if (!client.isConfigured()) {
128
- continue;
129
- }
130
- if (authorizationCheck && !client.hasAuth()) {
131
- continue;
92
+ async configure(server, getConfigValue, ignoreMissingRequiredConfigs = false) {
93
+ let configuredCount = 0;
94
+ entryLoop: for (const entry of this.getAll()) {
95
+ const config = {};
96
+ for (const configKey of Object.keys(entry.config.shape)) {
97
+ const value = getConfigValue(entry, configKey);
98
+ if (value !== null) {
99
+ this.validateAllowedEndpoint(entry.config.shape[configKey], value);
100
+ config[configKey] = value;
101
+ } else if (!ignoreMissingRequiredConfigs && !isOptionalType(entry.config.shape[configKey])) {
102
+ continue entryLoop;
132
103
  }
133
104
  }
134
- await server.addClient(client);
135
- addedCount++;
105
+ await entry.configure(server, config);
106
+ if (entry.isConfigured()) {
107
+ await server.addClient(entry);
108
+ configuredCount++;
109
+ }
136
110
  }
137
- return addedCount;
111
+ return configuredCount;
138
112
  }
139
113
  /**
140
114
  * Clear all registrations (useful for testing)
@@ -0,0 +1,20 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const requestContextStorage = new AsyncLocalStorage();
3
+ function withRequestContext(req, fn) {
4
+ return requestContextStorage.run({ headers: req.headers }, fn);
5
+ }
6
+ function getRequestContext() {
7
+ return requestContextStorage.getStore();
8
+ }
9
+ function getRequestHeader(name) {
10
+ const context = getRequestContext();
11
+ if (!context?.headers) return void 0;
12
+ const headerValue = context.headers[name] || context.headers[name.toLowerCase()];
13
+ return headerValue;
14
+ }
15
+ export {
16
+ getRequestContext,
17
+ getRequestHeader,
18
+ requestContextStorage,
19
+ withRequestContext
20
+ };
@@ -12,8 +12,7 @@ class SmartBearMcpServer extends McpServer {
12
12
  elicitationSupported = false;
13
13
  clients = [];
14
14
  enabledToolsets;
15
- getEnvFn;
16
- constructor(getEnvFn, enabledToolsets) {
15
+ constructor(enabledToolsets) {
17
16
  super(
18
17
  {
19
18
  name: MCP_SERVER_NAME,
@@ -29,7 +28,6 @@ class SmartBearMcpServer extends McpServer {
29
28
  }
30
29
  }
31
30
  );
32
- this.getEnvFn = getEnvFn;
33
31
  this.cache = new CacheService();
34
32
  if (enabledToolsets) {
35
33
  this.enabledToolsets = enabledToolsets.split(",").map((s) => s.trim().toLowerCase());
@@ -38,17 +36,6 @@ class SmartBearMcpServer extends McpServer {
38
36
  getCache() {
39
37
  return this.cache;
40
38
  }
41
- /**
42
- * Makes the server's getEnv function available to clients, validating that it is defined in the client's authentication fields if a client is provided
43
- */
44
- getEnv = (key, client) => {
45
- if (client && !Object.keys(client.authenticationFields.shape).includes(key)) {
46
- throw new Error(
47
- `Environment variable "${key}" is not defined in the ${client.name} client's authentication schema.`
48
- );
49
- }
50
- return this.getEnvFn(key, client);
51
- };
52
39
  setSamplingSupported(supported) {
53
40
  this.samplingSupported = supported;
54
41
  }
@@ -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,
@@ -4,17 +4,16 @@ import { clientRegistry } from "./client-registry.js";
4
4
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } 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
  }
@@ -28,28 +27,27 @@ async function runStdioMode() {
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.25.1";
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
2
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } 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
181
  "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
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": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
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": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
200
+ };
188
201
  }
189
202
  return void 0;
190
203
  }
@@ -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,7 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
3
+ import { getRequestHeader } from "../common/request-context.js";
3
4
  import { ToolError } from "../common/tools.js";
4
- import { API_KEY_HEADER, AUTHORIZATION_HEADER } from "./config/constants.js";
5
+ import { API_KEY_HEADER, REFLECT_API_TOKEN_HEADER, AUTHORIZATION_HEADER } from "./config/constants.js";
5
6
  import { SapTest } from "./prompt/sap-test.js";
6
7
  import { AddPromptStep } from "./tool/recording/add-prompt-step.js";
7
8
  import { AddSegment } from "./tool/recording/add-segment.js";
@@ -17,12 +18,11 @@ import { GetTestStatus } from "./tool/tests/get-test-status.js";
17
18
  import { ListSegments } from "./tool/tests/list-segments.js";
18
19
  import { ListTests } from "./tool/tests/list-tests.js";
19
20
  import { RunTest } from "./tool/tests/run-test.js";
20
- const ConfigurationSchema = z.object({});
21
- const AuthenticationSchema = z.object({
22
- api_token: z.string().describe("Reflect API authentication token").optional()
21
+ const ConfigurationSchema = z.object({
22
+ api_token: z.string().describe("Reflect API authentication token")
23
23
  });
24
24
  class ReflectClient {
25
- _server;
25
+ _apiToken;
26
26
  activeConnections = /* @__PURE__ */ new Map();
27
27
  sessionStates = /* @__PURE__ */ new Map();
28
28
  mcpSessionConnections = /* @__PURE__ */ new Map();
@@ -30,24 +30,33 @@ class ReflectClient {
30
30
  capabilityPrefix = "reflect";
31
31
  configPrefix = "Reflect";
32
32
  config = ConfigurationSchema;
33
- authenticationFields = AuthenticationSchema;
34
- async configure(server, _config) {
35
- this._server = server;
33
+ async configure(_server, config, _cache) {
34
+ this._apiToken = config.api_token;
36
35
  }
37
36
  getAuthToken() {
38
- return this._server?.getEnv("api_token", this) || this._server?.getEnv(API_KEY_HEADER) || this._server?.getEnv(AUTHORIZATION_HEADER) || null;
37
+ const contextHeader = getRequestHeader(REFLECT_API_TOKEN_HEADER) || getRequestHeader(API_KEY_HEADER) || getRequestHeader(AUTHORIZATION_HEADER);
38
+ if (contextHeader) {
39
+ let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
40
+ if (token.startsWith("Bearer ")) {
41
+ token = token.substring(7);
42
+ }
43
+ return token;
44
+ }
45
+ return this._apiToken || null;
39
46
  }
40
47
  isConfigured() {
41
- return !!this._server;
42
- }
43
- hasAuth() {
44
- return this.isConfigured() && !!this.getAuthToken();
48
+ return true;
45
49
  }
46
50
  isOAuthRequest() {
47
- if (this._server?.getEnv("api_token", this) || this._server?.getEnv(API_KEY_HEADER)) {
51
+ if (getRequestHeader(REFLECT_API_TOKEN_HEADER) || getRequestHeader(API_KEY_HEADER)) {
52
+ return false;
53
+ }
54
+ const authHeader = getRequestHeader(AUTHORIZATION_HEADER);
55
+ if (!authHeader) {
48
56
  return false;
49
57
  }
50
- return !!this._server?.getEnv(AUTHORIZATION_HEADER);
58
+ const headerValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
59
+ return headerValue.toLowerCase().startsWith("bearer ");
51
60
  }
52
61
  getAuthHeader() {
53
62
  const token = this.getAuthToken();
@@ -1,4 +1,5 @@
1
1
  const API_KEY_HEADER = "X-API-KEY";
2
+ const REFLECT_API_TOKEN_HEADER = "Reflect-Api-Token";
2
3
  const AUTHORIZATION_HEADER = "Authorization";
3
4
  const API_HOSTNAME = "api.reflect.run";
4
5
  const WEBSOCKET_HOSTNAME = "recording.us-east-1.reflect.run";
@@ -7,6 +8,7 @@ export {
7
8
  API_HOSTNAME,
8
9
  API_KEY_HEADER,
9
10
  AUTHORIZATION_HEADER,
11
+ REFLECT_API_TOKEN_HEADER,
10
12
  WEBSOCKET_HOSTNAME,
11
13
  WEB_APP_HOSTNAME
12
14
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
3
+ import { getRequestHeader } from "../common/request-context.js";
3
4
  import "./config-utils.js";
4
5
  import { SwaggerAPI } from "./client/api.js";
5
6
  import { SwaggerConfiguration } from "./client/configuration.js";
@@ -8,45 +9,47 @@ import "./client/registry-types.js";
8
9
  import { TOOLS } from "./client/tools.js";
9
10
  import "./client/user-management-types.js";
10
11
  const ConfigurationSchema = z.object({
12
+ api_key: z.string().describe("Swagger API key for authentication"),
11
13
  portal_base_path: z.string().optional().describe("Base path for Portal API requests (optional)"),
12
14
  registry_base_path: z.string().optional().describe("Base path for Registry API requests (optional)"),
13
15
  ui_base_path: z.string().optional().describe("Base URL for the SwaggerHub UI (optional)")
14
16
  });
15
- const AuthenticationSchema = z.object({
16
- api_key: z.string().describe("Swagger API key for authentication").optional()
17
- });
18
17
  class SwaggerClient {
19
- apiConfig;
20
- server;
18
+ api;
19
+ _apiKey;
21
20
  name = "Swagger";
22
21
  capabilityPrefix = "swagger";
23
22
  configPrefix = "Swagger";
24
23
  config = ConfigurationSchema;
25
- authenticationFields = AuthenticationSchema;
26
- async configure(server, config) {
27
- this.server = server;
28
- this.apiConfig = new SwaggerConfiguration({
29
- token: () => this.getAuthToken(),
30
- portalBasePath: config.portal_base_path,
31
- registryBasePath: config.registry_base_path,
32
- uiBasePath: config.ui_base_path
33
- });
34
- }
35
- isConfigured() {
36
- return this.apiConfig !== void 0;
24
+ async configure(_server, config, _cache) {
25
+ this._apiKey = config.api_key;
26
+ this.api = new SwaggerAPI(
27
+ new SwaggerConfiguration({
28
+ token: () => this.getAuthToken(),
29
+ portalBasePath: config.portal_base_path,
30
+ registryBasePath: config.registry_base_path,
31
+ uiBasePath: config.ui_base_path
32
+ }),
33
+ `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
34
+ );
37
35
  }
38
36
  getAuthToken() {
39
- return this.server?.getEnv("api_key", this) || this.server?.getEnv("Authorization") || null;
37
+ const contextHeader = getRequestHeader("Swagger-Api-Key") || getRequestHeader("Authorization");
38
+ if (contextHeader) {
39
+ let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
40
+ if (token.startsWith("Bearer ")) {
41
+ token = token.substring(7);
42
+ }
43
+ return token;
44
+ }
45
+ return this._apiKey || null;
40
46
  }
41
- hasAuth() {
42
- return this.isConfigured() && !!this.getAuthToken();
47
+ isConfigured() {
48
+ return this.api !== void 0;
43
49
  }
44
50
  getApi() {
45
- if (!this.apiConfig) throw new Error("Client not configured");
46
- return new SwaggerAPI(
47
- this.apiConfig,
48
- `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
49
- );
51
+ if (!this.api) throw new Error("Client not configured");
52
+ return this.api;
50
53
  }
51
54
  // Delegate API methods to the SwaggerAPI instance
52
55
  async getPortals() {
@@ -1,4 +1,5 @@
1
1
  import zod__default from "zod";
2
+ import { getRequestHeader } from "../common/request-context.js";
2
3
  import { ApiClient } from "./common/api-client.js";
3
4
  import { GetEnvironments } from "./tool/environment/get-environments.js";
4
5
  import { CreateFolder } from "./tool/folder/create-folder.js";
@@ -35,32 +36,38 @@ import { GetTestExecutions } from "./tool/test-execution/get-test-executions.js"
35
36
  import { GetTestExecutionSteps } from "./tool/test-execution/get-test-steps.js";
36
37
  import { UpdateTestExecution } from "./tool/test-execution/update-test-execution.js";
37
38
  import { UpdateTestExecutionSteps } from "./tool/test-execution/update-test-steps.js";
39
+ const BASE_URL_DEFAULT = "https://api.zephyrscale.smartbear.com/v2";
38
40
  const ConfigurationSchema = zod__default.object({
39
- base_url: zod__default.url().optional().describe("Zephyr Scale API base URL").default("https://api.zephyrscale.smartbear.com/v2")
40
- });
41
- const AuthenticationSchema = zod__default.object({
42
- api_token: zod__default.string().describe("Zephyr Scale API token for authentication").optional()
41
+ api_token: zod__default.string().describe("Zephyr Scale API token for authentication"),
42
+ base_url: zod__default.string().url().optional().describe("Zephyr Scale API base URL").default(BASE_URL_DEFAULT)
43
43
  });
44
44
  class ZephyrClient {
45
45
  apiClient;
46
- server;
46
+ _apiToken;
47
47
  name = "Zephyr";
48
48
  capabilityPrefix = "zephyr";
49
49
  configPrefix = "Zephyr";
50
50
  config = ConfigurationSchema;
51
- authenticationFields = AuthenticationSchema;
52
- async configure(server, config) {
53
- this.server = server;
54
- this.apiClient = new ApiClient(() => this.getAuthToken(), config.base_url);
51
+ async configure(_server, config, _cache) {
52
+ this._apiToken = config.api_token;
53
+ this.apiClient = new ApiClient(
54
+ () => this.getAuthToken(),
55
+ config.base_url || process.env.ZEPHYR_CUSTOM_BASE_URL || BASE_URL_DEFAULT
56
+ );
55
57
  }
56
58
  getAuthToken() {
57
- return this.server?.getEnv("api_token", this) || this.server?.getEnv("Authorization") || null;
59
+ const contextHeader = getRequestHeader("Zephyr-Api-Token") || getRequestHeader("Authorization");
60
+ if (contextHeader) {
61
+ let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
62
+ if (token.startsWith("Bearer ")) {
63
+ token = token.substring(7);
64
+ }
65
+ return token;
66
+ }
67
+ return this._apiToken || null;
58
68
  }
59
69
  isConfigured() {
60
- return !!this.apiClient;
61
- }
62
- hasAuth() {
63
- return this.isConfigured() && !!this.getAuthToken();
70
+ return this.apiClient !== void 0;
64
71
  }
65
72
  getApiClient() {
66
73
  if (!this.apiClient) throw new Error("Client not configured");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartbear/mcp",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "description": "MCP server for interacting SmartBear Products",
5
5
  "keywords": [
6
6
  "smartbear",