@smartbear/mcp 0.17.0 → 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.0";
1
+ const version = "0.18.0";
2
2
  const config = { "mcpServerName": "SmartBear MCP Server" };
3
3
  const packageJson = {
4
4
  version,