@smartbear/mcp 0.17.1 → 0.18.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.
@@ -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,7 +40,7 @@ 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({
42
- auth_token: z.string().describe("BugSnag personal authentication token"),
43
+ auth_token: z.string().describe("BugSnag personal access token").optional(),
43
44
  project_api_key: z.string().describe("BugSnag project API key").optional(),
44
45
  endpoint: z.string().url().describe("BugSnag endpoint URL").optional()
45
46
  });
@@ -51,6 +52,7 @@ class BugsnagClient {
51
52
  _errorsApi;
52
53
  _projectApi;
53
54
  _appEndpoint;
55
+ _authToken;
54
56
  get currentUserApi() {
55
57
  if (!this._currentUserApi) throw new Error("Client not configured");
56
58
  return this._currentUserApi;
@@ -78,8 +80,47 @@ class BugsnagClient {
78
80
  config.project_api_key,
79
81
  config.endpoint
80
82
  );
83
+ this._projectApiKey = config.project_api_key;
84
+ this._authToken = config.auth_token;
85
+ await this.initializeApis(config);
86
+ }
87
+ getAuthToken() {
88
+ const contextHeader = getRequestHeader("Bugsnag-Auth-Token");
89
+ if (contextHeader) {
90
+ let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
91
+ if (token.startsWith("token ")) {
92
+ token = token.substring(6);
93
+ }
94
+ return `token ${token}`;
95
+ }
96
+ const bearerToken = this.getBearerToken();
97
+ if (bearerToken) {
98
+ return bearerToken;
99
+ }
100
+ return this._authToken ? `token ${this._authToken}` : null;
101
+ }
102
+ getBearerToken() {
103
+ const contextHeader = getRequestHeader("Authorization");
104
+ if (contextHeader) {
105
+ let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
106
+ if (token.startsWith("Bearer ")) {
107
+ token = token.substring(7);
108
+ }
109
+ return `Bearer ${token}`;
110
+ }
111
+ return null;
112
+ }
113
+ async initializeApis(config) {
81
114
  const apiConfig = new Configuration({
82
- apiKey: `token ${config.auth_token}`,
115
+ apiKey: (_name) => {
116
+ const authToken = this.getAuthToken();
117
+ if (authToken) {
118
+ return authToken;
119
+ }
120
+ throw new Error(
121
+ "Authentication token not found in request headers or configuration"
122
+ );
123
+ },
83
124
  headers: {
84
125
  "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
85
126
  "Content-Type": "application/json",
@@ -95,9 +136,7 @@ class BugsnagClient {
95
136
  this._currentUserApi = new CurrentUserAPI(apiConfig);
96
137
  this._errorsApi = new ErrorAPI(apiConfig);
97
138
  this._projectApi = new ProjectAPI(apiConfig);
98
- this._projectApiKey = config.project_api_key;
99
139
  this._isConfigured = true;
100
- return;
101
140
  }
102
141
  isConfigured() {
103
142
  return this._isConfigured;
@@ -1,8 +1,9 @@
1
1
  import { z } from "zod";
2
+ import { getRequestHeader } from "../common/request-context.js";
2
3
  const ConfigurationSchema = z.object({
3
4
  base_url: z.url().describe("Collaborator server base URL"),
4
- username: z.string().describe("Collaborator username for authentication"),
5
- login_ticket: z.string().describe("Collaborator login ticket for authentication")
5
+ username: z.string().describe("Collaborator username for authentication").optional(),
6
+ login_ticket: z.string().describe("Collaborator login ticket for authentication").optional()
6
7
  });
7
8
  class CollaboratorClient {
8
9
  name = "Collaborator";
@@ -18,7 +19,7 @@ class CollaboratorClient {
18
19
  this.loginTicket = config.login_ticket;
19
20
  }
20
21
  isConfigured() {
21
- return this.baseUrl !== void 0 && this.username !== void 0 && this.loginTicket !== void 0;
22
+ return this.baseUrl !== void 0;
22
23
  }
23
24
  /**
24
25
  * Calls the Collaborator API with the given commands, prepending authentication automatically.
@@ -27,10 +28,20 @@ class CollaboratorClient {
27
28
  */
28
29
  async call(commands) {
29
30
  const url = `${this.baseUrl}/services/json/v1`;
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
+ }
30
41
  const body = [
31
42
  {
32
43
  command: "SessionService.authenticate",
33
- args: { login: this.username, ticket: this.loginTicket }
44
+ args: { login, ticket }
34
45
  },
35
46
  ...commands
36
47
  ];
@@ -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
+ };
@@ -56,6 +56,9 @@ class SmartBearMcpServer extends McpServer {
56
56
  isElicitationSupported() {
57
57
  return this.elicitationSupported;
58
58
  }
59
+ getClients() {
60
+ return this.clients;
61
+ }
59
62
  async cleanupSession(mcpSessionId) {
60
63
  for (const client of this.clients) {
61
64
  await client.cleanupSession?.(mcpSessionId);
@@ -4,8 +4,19 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4
4
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
5
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
6
6
  import { clientRegistry } from "./client-registry.js";
7
+ import { withRequestContext } from "./request-context.js";
7
8
  import { SmartBearMcpServer } from "./server.js";
9
+ import { getEnvVarName } from "./transport-stdio.js";
8
10
  import { isOptionalType } from "./zod-utils.js";
11
+ function getBaseUrl(req) {
12
+ const baseUrlOverride = process.env.BASE_URL;
13
+ if (baseUrlOverride) {
14
+ return baseUrlOverride;
15
+ }
16
+ const protocol = req.headers["x-forwarded-proto"] || "http";
17
+ const host = req.headers["x-forwarded-host"] || req.headers.host;
18
+ return `${protocol}://${host}`;
19
+ }
9
20
  async function runHttpMode() {
10
21
  const PORT = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3e3;
11
22
  const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",") || [
@@ -20,6 +31,7 @@ async function runHttpMode() {
20
31
  // Required for StreamableHTTP
21
32
  "x-custom-auth-headers",
22
33
  // used by mcp-inspector
34
+ "mcp-protocol-version",
23
35
  ...allowedAuthHeaders
24
36
  ].join(", ");
25
37
  const httpServer = createServer(
@@ -39,7 +51,8 @@ async function runHttpMode() {
39
51
  res.end();
40
52
  return;
41
53
  }
42
- const url = new URL(req.url || "/", `http://${req.headers.host}`);
54
+ const baseUrl = getBaseUrl(req);
55
+ const url = new URL(req.url || "/", baseUrl);
43
56
  if (req.method === "GET" && url.pathname === "/health") {
44
57
  res.writeHead(200, { "Content-Type": "application/json" });
45
58
  res.end(
@@ -47,6 +60,17 @@ async function runHttpMode() {
47
60
  );
48
61
  return;
49
62
  }
63
+ if (req.method === "GET" && (url.pathname === "/.well-known/oauth-protected-resource" || url.pathname === "/.well-known/oauth-protected-resource/mcp")) {
64
+ const authServerUrl = process.env.OAUTH_AUTHORIZATION_SERVER_URL || "http://localhost:7070";
65
+ res.writeHead(200, { "Content-Type": "application/json" });
66
+ res.end(
67
+ JSON.stringify({
68
+ resource: `${baseUrl}/mcp`,
69
+ authorization_servers: [authServerUrl]
70
+ })
71
+ );
72
+ return;
73
+ }
50
74
  if (url.pathname === "/mcp") {
51
75
  await handleStreamableHttpRequest(req, res, transports);
52
76
  return;
@@ -190,7 +214,10 @@ async function handleStreamableHttpRequest(req, res, transports) {
190
214
  );
191
215
  return;
192
216
  }
193
- await transport.handleRequest(req, res, parsedBody);
217
+ await withRequestContext(
218
+ req,
219
+ async () => await transport.handleRequest(req, res, parsedBody)
220
+ );
194
221
  } catch (error) {
195
222
  console.error("Error handling StreamableHTTP request:", error);
196
223
  res.writeHead(500, { "Content-Type": "text/plain" });
@@ -235,10 +262,13 @@ async function handleLegacyMessageRequest(req, res, url, transports) {
235
262
  req.on("end", async () => {
236
263
  try {
237
264
  const parsedBody = JSON.parse(body);
238
- await session.transport.handlePostMessage(
265
+ await withRequestContext(
239
266
  req,
240
- res,
241
- parsedBody
267
+ async () => await session.transport.handlePostMessage(
268
+ req,
269
+ res,
270
+ parsedBody
271
+ )
242
272
  );
243
273
  } catch (error) {
244
274
  console.error("Error handling POST message:", error);
@@ -250,19 +280,49 @@ async function handleLegacyMessageRequest(req, res, url, transports) {
250
280
  async function newServer(req, res) {
251
281
  const server = new SmartBearMcpServer();
252
282
  try {
253
- await clientRegistry.configure(server, (client, key) => {
254
- const headerName = getHeaderName(client, key);
255
- const value = req.headers[headerName] || req.headers[headerName.toLowerCase()];
256
- if (typeof value === "string") {
257
- return value;
258
- }
259
- return null;
260
- });
283
+ const configuredCount = await withRequestContext(
284
+ req,
285
+ () => clientRegistry.configure(server, (client, key) => {
286
+ const headerName = getHeaderName(client, key);
287
+ const value = req.headers[headerName] || req.headers[headerName.toLowerCase()];
288
+ if (typeof value === "string") {
289
+ return value;
290
+ }
291
+ const envVarName = getEnvVarName(client, key);
292
+ return process.env[envVarName] || null;
293
+ })
294
+ );
295
+ console.log(
296
+ `Configured ${configuredCount} clients for new server instance`
297
+ );
298
+ if (configuredCount === 0) {
299
+ throw new Error(
300
+ "No clients successfully configured. Missing authentication headers."
301
+ );
302
+ }
303
+ const hasAuth = withRequestContext(
304
+ req,
305
+ () => server.getClients().some((client) => {
306
+ if (!client.getAuthToken) return true;
307
+ return client.getAuthToken() !== null;
308
+ })
309
+ );
310
+ if (!hasAuth) {
311
+ throw new Error(
312
+ "No clients have valid authentication credentials. Please authenticate via OAuth or provide alternative auth headers (e.g. API key or personal auth token)."
313
+ );
314
+ }
261
315
  } catch (error) {
262
316
  const headerHelp = getHttpHeadersHelp();
263
317
  const errorMessage = headerHelp.length > 0 ? `Configuration error: ${error instanceof Error ? error.message : String(error)}. Please provide valid headers:
264
318
  ${headerHelp.join("\n")}` : "No clients support HTTP header configuration.";
265
- res.writeHead(401, { "Content-Type": "text/plain" });
319
+ const headers = {
320
+ "Content-Type": "text/plain"
321
+ };
322
+ if (req.headers.host) {
323
+ headers["WWW-Authenticate"] = `OAuth resource_metadata="http://${req.headers.host}/.well-known/oauth-protected-resource"`;
324
+ }
325
+ res.writeHead(401, headers);
266
326
  res.end(errorMessage);
267
327
  return null;
268
328
  }
@@ -297,5 +357,8 @@ function getHttpHeadersHelp() {
297
357
  return messages;
298
358
  }
299
359
  export {
360
+ getBaseUrl,
361
+ getHeaderName,
362
+ newServer,
300
363
  runHttpMode
301
364
  };
@@ -60,5 +60,6 @@ function getEnvVarName(client, key) {
60
60
  return `${client.configPrefix.toUpperCase().replace(/-/g, "_")}_${key.toUpperCase()}`;
61
61
  }
62
62
  export {
63
+ getEnvVarName,
63
64
  runStdioMode
64
65
  };
@@ -1,4 +1,4 @@
1
- const version = "0.17.1";
1
+ const version = "0.18.0";
2
2
  const config = { "mcpServerName": "SmartBear MCP Server" };
3
3
  const packageJson = {
4
4
  version,
@@ -1,6 +1,7 @@
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";
@@ -18,7 +19,9 @@ class PactflowClient {
18
19
  toolPrefix = "contract-testing";
19
20
  configPrefix = "Pact-Broker";
20
21
  config = ConfigurationSchema;
21
- headers;
22
+ token;
23
+ username;
24
+ password;
22
25
  aiBaseUrl;
23
26
  baseUrl;
24
27
  _clientType;
@@ -37,23 +40,15 @@ class PactflowClient {
37
40
  * @param config - Connection config (base_url + token OR username/password).
38
41
  */
39
42
  async configure(server, config) {
43
+ this.token = config.token;
44
+ this.username = config.username;
45
+ this.password = config.password;
40
46
  if (typeof config.token === "string") {
41
- this.headers = {
42
- Authorization: `Bearer ${config.token}`,
43
- "Content-Type": "application/json",
44
- "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
45
- };
46
47
  this._clientType = "pactflow";
47
48
  } else if (typeof config.username === "string" && typeof config.password === "string") {
48
- const authString = `${config.username}:${config.password}`;
49
- this.headers = {
50
- Authorization: `Basic ${Buffer.from(authString).toString("base64")}`,
51
- "Content-Type": "application/json",
52
- "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
53
- };
54
49
  this._clientType = "pact_broker";
55
50
  } else {
56
- return;
51
+ this._clientType = "pactflow";
57
52
  }
58
53
  this.baseUrl = config.base_url;
59
54
  this.aiBaseUrl = `${this.baseUrl}/api/ai`;
@@ -155,7 +150,7 @@ class PactflowClient {
155
150
  async getStatus(statusUrl) {
156
151
  const response = await fetch(statusUrl, {
157
152
  method: "HEAD",
158
- headers: this.headers
153
+ headers: this.requestHeaders
159
154
  });
160
155
  return {
161
156
  status: response.status,
@@ -164,7 +159,40 @@ class PactflowClient {
164
159
  }
165
160
  /** Returns the current auth/content-type headers used for all requests. */
166
161
  get requestHeaders() {
167
- return this.headers;
162
+ let contextToken = getRequestHeader("Pact-Token") || getRequestHeader("Authorization");
163
+ if (Array.isArray(contextToken)) {
164
+ contextToken = contextToken[0];
165
+ }
166
+ if (contextToken) {
167
+ let authHeader = contextToken;
168
+ if (!contextToken.startsWith("Basic ") && !contextToken.startsWith("Bearer ")) {
169
+ authHeader = `Bearer ${contextToken}`;
170
+ }
171
+ return {
172
+ Authorization: authHeader,
173
+ "Content-Type": "application/json",
174
+ "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
175
+ };
176
+ }
177
+ if (this.token) {
178
+ let authHeader = this.token;
179
+ if (!authHeader.startsWith("Basic ") && !authHeader.startsWith("Bearer ")) {
180
+ authHeader = `Bearer ${authHeader}`;
181
+ }
182
+ return {
183
+ Authorization: authHeader,
184
+ "Content-Type": "application/json",
185
+ "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
186
+ };
187
+ } else if (this.username && this.password) {
188
+ const authString = `${this.username}:${this.password}`;
189
+ return {
190
+ Authorization: `Basic ${Buffer.from(authString).toString("base64")}`,
191
+ "Content-Type": "application/json",
192
+ "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
193
+ };
194
+ }
195
+ return void 0;
168
196
  }
169
197
  /**
170
198
  * Fetches the final result of a completed async operation.
@@ -176,7 +204,7 @@ class PactflowClient {
176
204
  async getResult(resultUrl) {
177
205
  const response = await fetch(resultUrl, {
178
206
  method: "GET",
179
- headers: this.headers
207
+ headers: this.requestHeaders
180
208
  });
181
209
  if (!response.ok) {
182
210
  throw new ToolError(`HTTP error! status: ${response.status}`);
@@ -224,7 +252,7 @@ class PactflowClient {
224
252
  try {
225
253
  const response = await fetch(url, {
226
254
  method,
227
- headers: this.headers,
255
+ headers: this.requestHeaders,
228
256
  ...body && { body: JSON.stringify(body) }
229
257
  });
230
258
  if (!response.ok) {
@@ -374,7 +402,7 @@ class PactflowClient {
374
402
  try {
375
403
  const response = await fetch(url, {
376
404
  method: "GET",
377
- headers: this.headers
405
+ headers: this.requestHeaders
378
406
  });
379
407
  if (!response.ok) {
380
408
  const errorText = await response.text().catch(() => "");
@@ -399,7 +427,7 @@ class PactflowClient {
399
427
  try {
400
428
  const response = await fetch(url, {
401
429
  method: "GET",
402
- headers: this.headers
430
+ headers: this.requestHeaders
403
431
  });
404
432
  if (!response.ok) {
405
433
  const errorText = await response.text().catch(() => "");
@@ -1,11 +1,12 @@
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({
8
- api_key: zod__default.string().describe("QMetry API key for authentication"),
9
+ api_key: zod__default.string().describe("QMetry API key for authentication").optional(),
9
10
  base_url: zod__default.string().url().optional().describe(
10
11
  "Optional QMetry base URL for custom or region-specific endpoints"
11
12
  )
@@ -25,9 +26,16 @@ class QmetryClient {
25
26
  }
26
27
  }
27
28
  isConfigured() {
28
- return this.token !== void 0;
29
+ return true;
29
30
  }
30
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
+ }
31
39
  if (!this.token) throw new Error("Client not configured");
32
40
  return this.token;
33
41
  }
@@ -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 { API_KEY_HEADER } from "./config/constants.js";
5
6
  import { SapTest } from "./prompt/sap-test.js";
@@ -18,11 +19,10 @@ 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
21
  const ConfigurationSchema = z.object({
21
- api_token: z.string().describe("Reflect API authentication token")
22
+ api_token: z.string().describe("Reflect API authentication token").optional()
22
23
  });
23
24
  class ReflectClient {
24
- headers = {};
25
- apiToken = "";
25
+ _apiToken;
26
26
  activeConnections = /* @__PURE__ */ new Map();
27
27
  sessionStates = /* @__PURE__ */ new Map();
28
28
  mcpSessionConnections = /* @__PURE__ */ new Map();
@@ -31,21 +31,35 @@ class ReflectClient {
31
31
  configPrefix = "Reflect";
32
32
  config = ConfigurationSchema;
33
33
  async configure(_server, config, _cache) {
34
- this.apiToken = config.api_token;
35
- this.headers = {
36
- [API_KEY_HEADER]: `${config.api_token}`,
37
- "Content-Type": "application/json",
38
- "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
39
- };
34
+ this._apiToken = config.api_token;
35
+ }
36
+ getAuthToken() {
37
+ const contextHeader = getRequestHeader("Reflect-Api-Token") || getRequestHeader("X-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._apiToken || null;
40
46
  }
41
47
  isConfigured() {
42
- return Object.keys(this.headers).length !== 0;
48
+ return true;
43
49
  }
44
50
  getApiToken() {
45
- return this.apiToken;
51
+ return this.getAuthToken() || "";
46
52
  }
47
53
  getHeaders() {
48
- return this.headers;
54
+ const token = this.getAuthToken();
55
+ if (!token) {
56
+ throw new Error("Reflect API token not found");
57
+ }
58
+ return {
59
+ [API_KEY_HEADER]: token,
60
+ "Content-Type": "application/json",
61
+ "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
62
+ };
49
63
  }
50
64
  getSessionState(sessionId) {
51
65
  return this.sessionStates.get(sessionId);
@@ -11,10 +11,13 @@ function hasErrorsFound(value) {
11
11
  }
12
12
  class SwaggerAPI {
13
13
  config;
14
- headers;
14
+ userAgent;
15
15
  constructor(config, userAgent) {
16
16
  this.config = config;
17
- this.headers = config.getHeaders(userAgent);
17
+ this.userAgent = userAgent;
18
+ }
19
+ get headers() {
20
+ return this.config.getHeaders(this.userAgent);
18
21
  }
19
22
  /**
20
23
  * Core response parsing logic shared between different response handlers.
@@ -1,28 +1,36 @@
1
1
  class SwaggerConfiguration {
2
- token;
2
+ tokenProvider;
3
3
  portalBasePath;
4
4
  registryBasePath;
5
5
  uiBasePath;
6
6
  userManagementBasePath;
7
- headers;
7
+ defaultHeaders;
8
8
  constructor(param) {
9
- this.token = param.token;
9
+ if (typeof param.token === "string") {
10
+ this.tokenProvider = () => param.token;
11
+ } else {
12
+ this.tokenProvider = param.token;
13
+ }
10
14
  this.portalBasePath = param.portalBasePath || "https://api.portal.swaggerhub.com/v1";
11
15
  this.registryBasePath = param.registryBasePath || "https://api.swaggerhub.com";
12
16
  this.uiBasePath = param.uiBasePath || "https://app.swaggerhub.com";
13
17
  this.userManagementBasePath = param.userManagementBasePath || `${this.registryBasePath}/user-management/v1`;
14
- this.headers = {
15
- Authorization: `Bearer ${this.token}`,
18
+ this.defaultHeaders = {
16
19
  "Content-Type": "application/json",
17
20
  ...param.headers
18
21
  };
19
22
  }
20
23
  /**
21
- * Get headers with User-Agent included
24
+ * Get headers with User-Agent included and dynamic Auth token
22
25
  */
23
26
  getHeaders(userAgent) {
27
+ const token = this.tokenProvider();
28
+ if (!token) {
29
+ throw new Error("Swagger API token not found");
30
+ }
24
31
  return {
25
- ...this.headers,
32
+ Authorization: `Bearer ${token}`,
33
+ ...this.defaultHeaders,
26
34
  "User-Agent": userAgent
27
35
  };
28
36
  }
@@ -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,21 +9,23 @@ 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({
11
- api_key: z.string().describe("Swagger API key for authentication"),
12
+ api_key: z.string().describe("Swagger API key for authentication").optional(),
12
13
  portal_base_path: z.string().optional().describe("Base path for Portal API requests (optional)"),
13
14
  registry_base_path: z.string().optional().describe("Base path for Registry API requests (optional)"),
14
15
  ui_base_path: z.string().optional().describe("Base URL for the SwaggerHub UI (optional)")
15
16
  });
16
17
  class SwaggerClient {
17
18
  api;
19
+ _apiKey;
18
20
  name = "Swagger";
19
21
  toolPrefix = "swagger";
20
22
  configPrefix = "Swagger";
21
23
  config = ConfigurationSchema;
22
24
  async configure(_server, config, _cache) {
25
+ this._apiKey = config.api_key;
23
26
  this.api = new SwaggerAPI(
24
27
  new SwaggerConfiguration({
25
- token: config.api_key,
28
+ token: () => this.getAuthToken(),
26
29
  portalBasePath: config.portal_base_path,
27
30
  registryBasePath: config.registry_base_path,
28
31
  uiBasePath: config.ui_base_path
@@ -30,6 +33,17 @@ class SwaggerClient {
30
33
  `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
31
34
  );
32
35
  }
36
+ getAuthToken() {
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;
46
+ }
33
47
  isConfigured() {
34
48
  return this.api !== void 0;
35
49
  }
@@ -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";
@@ -37,21 +38,34 @@ import { UpdateTestExecution } from "./tool/test-execution/update-test-execution
37
38
  import { UpdateTestExecutionSteps } from "./tool/test-execution/update-test-steps.js";
38
39
  const BASE_URL_DEFAULT = "https://api.zephyrscale.smartbear.com/v2";
39
40
  const ConfigurationSchema = zod__default.object({
40
- api_token: zod__default.string().describe("Zephyr Scale API token for authentication"),
41
+ api_token: zod__default.string().describe("Zephyr Scale API token for authentication").optional(),
41
42
  base_url: zod__default.string().url().optional().describe("Zephyr Scale API base URL").default(BASE_URL_DEFAULT)
42
43
  });
43
44
  class ZephyrClient {
44
45
  apiClient;
46
+ _apiToken;
45
47
  name = "Zephyr";
46
48
  toolPrefix = "zephyr";
47
49
  configPrefix = "Zephyr";
48
50
  config = ConfigurationSchema;
49
51
  async configure(_server, config, _cache) {
52
+ this._apiToken = config.api_token;
50
53
  this.apiClient = new ApiClient(
51
- config.api_token,
54
+ () => this.getAuthToken(),
52
55
  config.base_url || BASE_URL_DEFAULT
53
56
  );
54
57
  }
58
+ getAuthToken() {
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;
68
+ }
55
69
  isConfigured() {
56
70
  return this.apiClient !== void 0;
57
71
  }
@@ -2,10 +2,24 @@ import { ToolError } from "../../common/tools.js";
2
2
  import { AuthService } from "./auth-service.js";
3
3
  class ApiClient {
4
4
  baseUrl;
5
- defaultHeaders;
6
- constructor(bearerToken, baseUrl) {
5
+ tokenProvider;
6
+ constructor(tokenOrProvider, baseUrl) {
7
7
  this.baseUrl = baseUrl.trim().replace(/\/$/, "");
8
- this.defaultHeaders = new AuthService(bearerToken).getAuthHeaders();
8
+ if (typeof tokenOrProvider === "string") {
9
+ this.tokenProvider = () => tokenOrProvider;
10
+ } else {
11
+ this.tokenProvider = tokenOrProvider;
12
+ }
13
+ }
14
+ get defaultHeaders() {
15
+ return this.getHeaders();
16
+ }
17
+ getHeaders() {
18
+ const token = this.tokenProvider();
19
+ if (!token) {
20
+ throw new ToolError("Zephyr API token not found");
21
+ }
22
+ return new AuthService(token).getAuthHeaders();
9
23
  }
10
24
  getUrl(endpoint, params) {
11
25
  const url = new URL(this.baseUrl + endpoint);
@@ -21,7 +35,7 @@ class ApiClient {
21
35
  async get(endpoint, params) {
22
36
  const response = await fetch(this.getUrl(endpoint, params), {
23
37
  method: "GET",
24
- headers: this.defaultHeaders
38
+ headers: this.getHeaders()
25
39
  });
26
40
  return await this.validateAndGetResponseBody(response);
27
41
  }
@@ -29,7 +43,7 @@ class ApiClient {
29
43
  const response = await fetch(this.getUrl(endpoint), {
30
44
  method: "POST",
31
45
  headers: {
32
- ...this.defaultHeaders,
46
+ ...this.getHeaders(),
33
47
  "Content-Type": "application/json"
34
48
  },
35
49
  body: JSON.stringify(body)
@@ -40,7 +54,7 @@ class ApiClient {
40
54
  const response = await fetch(this.getUrl(endpoint), {
41
55
  method: "PUT",
42
56
  headers: {
43
- ...this.defaultHeaders,
57
+ ...this.getHeaders(),
44
58
  "Content-Type": "application/json"
45
59
  },
46
60
  body: JSON.stringify(body)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartbear/mcp",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "MCP server for interacting SmartBear Products",
5
5
  "keywords": [
6
6
  "smartbear",