@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.
package/README.md CHANGED
@@ -35,6 +35,7 @@ See individual guides for suggested prompts and supported tools and resources:
35
35
  - [Portal](https://developer.smartbear.com/smartbear-mcp/docs/swagger-portal-integration) - Portal and product management capabilities
36
36
  - [Studio](https://developer.smartbear.com/smartbear-mcp/docs/swagger-studio-integration) - API and Domain management capabilities, including AI-powered API generation from prompts and automatic standardization
37
37
  - [Contract Testing (PactFlow)](https://developer.smartbear.com/pactflow/default/getting-started) - Contract testing capabilities
38
+ - [Functional Testing](https://developer.smartbear.com/smartbear-mcp/docs/functional-testing-integration) - API test discovery capabilities
38
39
  - [QMetry](https://developer.smartbear.com/smartbear-mcp/docs/qmetry-integration) - QMetry Test Management capabilities
39
40
  - [Zephyr](https://developer.smartbear.com/smartbear-mcp/docs/zephyr-integration) - Zephyr Test Management capabilities
40
41
  - [Collaborator](https://developer.smartbear.com/smartbear-mcp/docs/collaborator-integration) - Review and Remote System Configuration management capabilities
@@ -52,7 +53,7 @@ For BugSnag, Swagger, and Zephyr, SmartBear hosts Remote MCP Servers that you ca
52
53
 
53
54
  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.
54
55
 
55
- > **Need BearQ, 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.
56
+ > **Need BearQ, Reflect, QMetry, QTM4J, PactFlow, Collaborator, or Functional Testing?** These products are only available via the local npm package below, which bundles all products into a single MCP server.
56
57
 
57
58
  ## Prerequisites
58
59
 
@@ -108,7 +109,8 @@ Alternatively, you can use `npx` (or globally install) the `@smartbear/mcp` pack
108
109
  "COLLABORATOR_LOGIN_TICKET": "${input:collab_login_ticket}",
109
110
  "QTM4J_API_KEY": "${input:qtm4j_api_key}",
110
111
  "QTM4J_BASE_URL": "${input:qtm4j_base_url}",
111
- "QTM4J_AUTOMATION_API_KEY": "${input:qtm4j_automation_api_key}"
112
+ "QTM4J_AUTOMATION_API_KEY": "${input:qtm4j_automation_api_key}",
113
+ "SWAGGER_FUNCTIONAL_TESTING_API_TOKEN": "${input:swagger_functional_testing_api_token}"
112
114
  }
113
115
  }
114
116
  },
@@ -250,6 +252,12 @@ Alternatively, you can use `npx` (or globally install) the `@smartbear/mcp` pack
250
252
  "type": "promptString",
251
253
  "description": "QTM4J Automation API Key - required for automation tools, leave blank to disable them",
252
254
  "password": true
255
+ },
256
+ {
257
+ "id": "swagger_functional_testing_api_token",
258
+ "type": "promptString",
259
+ "description": "Swagger Functional Testing API Token - leave blank to disable Functional Testing tools",
260
+ "password": true
253
261
  }
254
262
  ]
255
263
  }
@@ -291,7 +299,8 @@ Add the following configuration to your `claude_desktop_config.json` to launch t
291
299
  "COLLABORATOR_LOGIN_TICKET": "your collab login ticket",
292
300
  "QTM4J_API_KEY": "your_qtm4j_key",
293
301
  "QTM4J_BASE_URL": "https://qtmcloud.qmetry.com",
294
- "QTM4J_AUTOMATION_API_KEY": "your_qtm4j_automation_api_key"
302
+ "QTM4J_AUTOMATION_API_KEY": "your_qtm4j_automation_api_key",
303
+ "SWAGGER_FUNCTIONAL_TESTING_API_TOKEN": "your_swagger_functional_testing_api_token"
295
304
  }
296
305
  }
297
306
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
- import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
2
+ import { USER_AGENT } 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;
@@ -52,7 +53,7 @@ class BearQClient {
52
53
  return {
53
54
  Authorization: `Bearer ${token}`,
54
55
  "Content-Type": "application/json",
55
- "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
56
+ "User-Agent": USER_AGENT
56
57
  };
57
58
  }
58
59
  async registerTools(register, _getInput) {
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
- import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
2
+ import { USER_AGENT } 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,20 +82,48 @@ 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
- "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
126
+ "User-Agent": USER_AGENT,
103
127
  "Content-Type": "application/json",
104
128
  "X-Bugsnag-API": "true",
105
129
  "X-Version": "2"
@@ -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,27 @@
1
1
  import { z } from "zod";
2
+ import { USER_AGENT } from "../common/info.js";
3
+ import { getRequestHeader } from "../common/request-context.js";
2
4
  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()
5
+ base_url: z.url().describe("Collaborator server base URL"),
6
+ username: z.string().describe("Collaborator username for authentication").optional(),
7
+ login_ticket: z.string().describe("Collaborator login ticket for authentication").optional()
8
8
  });
9
9
  class CollaboratorClient {
10
10
  name = "Collaborator";
11
11
  capabilityPrefix = "collaborator";
12
12
  configPrefix = "Collaborator";
13
13
  config = ConfigurationSchema;
14
- authenticationFields = AuthenticationSchema;
15
14
  baseUrl;
16
- server;
17
- async configure(server, config) {
15
+ username;
16
+ loginTicket;
17
+ async configure(_server, config, _cache) {
18
18
  this.baseUrl = config.base_url;
19
- this.server = server;
19
+ this.username = config.username;
20
+ this.loginTicket = config.login_ticket;
20
21
  }
21
22
  isConfigured() {
22
23
  return this.baseUrl !== void 0;
23
24
  }
24
- hasAuth() {
25
- return this.isConfigured() && this.server?.getEnv("Login", this) !== void 0 && this.server?.getEnv("Ticket", this) !== void 0;
26
- }
27
25
  /**
28
26
  * Calls the Collaborator API with the given commands, prepending authentication automatically.
29
27
  * @param commands Array of Collaborator API commands (excluding authentication)
@@ -31,8 +29,16 @@ class CollaboratorClient {
31
29
  */
32
30
  async call(commands) {
33
31
  const url = `${this.baseUrl}/services/json/v1`;
34
- const login = this.server?.getEnv("Login", this);
35
- const ticket = this.server?.getEnv("Ticket", this);
32
+ let login = this.username;
33
+ let ticket = this.loginTicket;
34
+ const contextLogin = getRequestHeader("Collaborator-Login");
35
+ const contextTicket = getRequestHeader("Collaborator-Ticket");
36
+ if (contextLogin) {
37
+ login = Array.isArray(contextLogin) ? contextLogin[0] : contextLogin;
38
+ }
39
+ if (contextTicket) {
40
+ ticket = Array.isArray(contextTicket) ? contextTicket[0] : contextTicket;
41
+ }
36
42
  const body = [
37
43
  {
38
44
  command: "SessionService.authenticate",
@@ -42,7 +48,7 @@ class CollaboratorClient {
42
48
  ];
43
49
  const response = await fetch(url, {
44
50
  method: "POST",
45
- headers: { "Content-Type": "application/json" },
51
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
46
52
  body: JSON.stringify(body)
47
53
  });
48
54
  if (!response.ok) {
@@ -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)
@@ -1,7 +1,11 @@
1
1
  import packageJson from "../package.json.js";
2
2
  const MCP_SERVER_NAME = packageJson.config.mcpServerName;
3
3
  const MCP_SERVER_VERSION = packageJson.version;
4
+ const MCP_TRANSPORT = process.env.MCP_TRANSPORT?.toLowerCase().trim() || "stdio";
5
+ const USER_AGENT = `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION} ${MCP_TRANSPORT}`;
4
6
  export {
5
7
  MCP_SERVER_NAME,
6
- MCP_SERVER_VERSION
8
+ MCP_SERVER_VERSION,
9
+ MCP_TRANSPORT,
10
+ USER_AGENT
7
11
  };
@@ -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
  }