@smartbear/mcp 0.10.0 → 0.11.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.
Files changed (35) hide show
  1. package/README.md +10 -8
  2. package/dist/bugsnag/client/api/index.js +2 -0
  3. package/dist/bugsnag/client/filters.js +0 -6
  4. package/dist/bugsnag/client.js +234 -376
  5. package/dist/bugsnag/input-schemas.js +51 -0
  6. package/dist/collaborator/client.js +18 -5
  7. package/dist/common/cache.js +63 -0
  8. package/dist/common/client-registry.js +128 -0
  9. package/dist/common/register-clients.js +31 -0
  10. package/dist/common/server.js +30 -9
  11. package/dist/common/transport-http.js +377 -0
  12. package/dist/common/transport-stdio.js +43 -0
  13. package/dist/index.js +20 -70
  14. package/dist/pactflow/client.js +39 -19
  15. package/dist/qmetry/client.js +24 -9
  16. package/dist/reflect/client.js +10 -4
  17. package/dist/{api-hub → swagger}/client/api.js +2 -2
  18. package/dist/{api-hub → swagger}/client/configuration.js +1 -1
  19. package/dist/{api-hub → swagger}/client/index.js +2 -2
  20. package/dist/{api-hub → swagger}/client/tools.js +4 -4
  21. package/dist/{api-hub → swagger}/client.js +47 -35
  22. package/dist/swagger/config-utils.js +18 -0
  23. package/dist/zephyr/client.js +40 -8
  24. package/dist/zephyr/common/rest-api-schemas.js +79 -78
  25. package/dist/zephyr/tool/priority/get-priorities.js +43 -0
  26. package/dist/zephyr/tool/status/get-statuses.js +49 -0
  27. package/dist/zephyr/tool/test-case/get-test-case.js +39 -0
  28. package/dist/zephyr/tool/test-case/get-test-cases.js +64 -0
  29. package/dist/zephyr/tool/test-cycle/get-test-cycle.js +39 -0
  30. package/dist/zephyr/tool/test-cycle/get-test-cycles.js +2 -2
  31. package/dist/zephyr/tool/test-execution/get-test-execution.js +39 -0
  32. package/package.json +2 -2
  33. /package/dist/{api-hub → swagger}/client/portal-types.js +0 -0
  34. /package/dist/{api-hub → swagger}/client/registry-types.js +0 -0
  35. /package/dist/{api-hub → swagger}/client/user-management-types.js +0 -0
@@ -0,0 +1,51 @@
1
+ import z from "zod";
2
+ const filterValueSchema = z.object({
3
+ type: z.enum(["eq", "ne", "empty"]),
4
+ value: z.union([z.string(), z.boolean(), z.number()]),
5
+ });
6
+ /**
7
+ * A collection of input parameter schemas for reuse between tools.
8
+ * Add new entries when common parameters are identified.
9
+ */
10
+ export const toolInputParameters = {
11
+ empty: z.object({}).describe("No parameters are required for this tool"),
12
+ projectId: z
13
+ .string()
14
+ .optional()
15
+ .describe("ID of the project. This is optional if a current project is set and is used to set the current project for BugSnag tools."),
16
+ errorId: z.string().describe("Unique identifier of the error"),
17
+ releaseId: z.string().describe("ID of the release"),
18
+ buildId: z.string().describe("ID of the build"),
19
+ direction: z
20
+ .enum(["asc", "desc"])
21
+ .describe("Sort direction for ordering results")
22
+ .default("desc"),
23
+ filters: z
24
+ .record(z.array(filterValueSchema))
25
+ .describe("Apply filters to narrow down the error list. Use the List Project Event Filters tool to discover available filter fields. " +
26
+ "Time filters support extended ISO 8601 format (e.g. 2018-05-20T00:00:00Z) or relative format (e.g. 7d, 24h).")
27
+ .default({
28
+ "event.since": [{ type: "eq", value: "30d" }],
29
+ "error.status": [{ type: "eq", value: "open" }],
30
+ }),
31
+ nextUrl: z
32
+ .string()
33
+ .describe("URL for retrieving the next page of results. Use the value in the previous response to get the next page when more results are available. " +
34
+ "Only values provided in the output from this tool can be used. Do not attempt to construct it manually.")
35
+ .optional(),
36
+ page: z.number().describe("Page number to return (starts from 1)").default(1),
37
+ perPage: z
38
+ .number()
39
+ .describe("How many results to return per page.")
40
+ .min(1)
41
+ .max(100)
42
+ .default(30),
43
+ releaseStage: z
44
+ .string()
45
+ .describe("Filter releases by this stage (e.g. production, staging), defaults to 'production'")
46
+ .default("production"),
47
+ sort: z
48
+ .enum(["first_seen", "last_seen", "events", "users", "unsorted"])
49
+ .describe("Field to sort the errors by")
50
+ .default("last_seen"),
51
+ };
@@ -1,14 +1,24 @@
1
1
  import { z } from "zod";
2
+ const ConfigurationSchema = z.object({
3
+ base_url: z.string().url().describe("Collaborator server base URL"),
4
+ username: z.string().describe("Collaborator username for authentication"),
5
+ login_ticket: z
6
+ .string()
7
+ .describe("Collaborator login ticket for authentication"),
8
+ });
2
9
  export class CollaboratorClient {
3
10
  name = "Collaborator";
4
- prefix = "collaborator";
11
+ toolPrefix = "collaborator";
12
+ configPrefix = "Collaborator";
13
+ config = ConfigurationSchema;
5
14
  baseUrl;
6
15
  username;
7
16
  loginTicket;
8
- constructor(baseUrl, username, loginTicket) {
9
- this.baseUrl = baseUrl;
10
- this.username = username;
11
- this.loginTicket = loginTicket;
17
+ async configure(_server, config, _cache) {
18
+ this.baseUrl = config.base_url;
19
+ this.username = config.username;
20
+ this.loginTicket = config.login_ticket;
21
+ return true;
12
22
  }
13
23
  /**
14
24
  * Calls the Collaborator API with the given commands, prepending authentication automatically.
@@ -16,6 +26,9 @@ export class CollaboratorClient {
16
26
  * @returns Raw Collaborator API response
17
27
  */
18
28
  async call(commands) {
29
+ if (!this.baseUrl || !this.username || !this.loginTicket) {
30
+ throw new Error("Collaborator client not configured");
31
+ }
19
32
  const url = `${this.baseUrl}/services/json/v1`;
20
33
  // Always prepend authentication command automatically
21
34
  const body = [
@@ -0,0 +1,63 @@
1
+ import NodeCache from "node-cache";
2
+ /**
3
+ * Common cache service that can be shared across all clients.
4
+ * Wraps NodeCache and provides a way to disable caching entirely.
5
+ * Reads CACHE_ENABLED and CACHE_TTL environment variables for configuration.
6
+ */
7
+ export class CacheService {
8
+ cache;
9
+ enabled;
10
+ constructor() {
11
+ // Read configuration from environment variables
12
+ this.enabled = process.env.CACHE_ENABLED !== "false";
13
+ const ttl = process.env.CACHE_TTL
14
+ ? Number.parseInt(process.env.CACHE_TTL, 10)
15
+ : 86400; // Default 24 hours
16
+ this.cache = this.enabled
17
+ ? new NodeCache({
18
+ stdTTL: ttl,
19
+ })
20
+ : null;
21
+ }
22
+ /**
23
+ * Get a value from the cache
24
+ */
25
+ get(key) {
26
+ if (!this.enabled || !this.cache) {
27
+ return undefined;
28
+ }
29
+ return this.cache.get(key);
30
+ }
31
+ /**
32
+ * Set a value in the cache
33
+ */
34
+ set(key, value) {
35
+ if (!this.enabled || !this.cache) {
36
+ return false;
37
+ }
38
+ return this.cache.set(key, value);
39
+ }
40
+ /**
41
+ * Delete a value from the cache
42
+ */
43
+ del(key) {
44
+ if (!this.enabled || !this.cache) {
45
+ return 0;
46
+ }
47
+ return this.cache.del(key);
48
+ }
49
+ /**
50
+ * Check if caching is enabled
51
+ */
52
+ isEnabled() {
53
+ return this.enabled;
54
+ }
55
+ /**
56
+ * Clear all cache entries
57
+ */
58
+ flushAll() {
59
+ if (this.enabled && this.cache) {
60
+ this.cache.flushAll();
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,128 @@
1
+ import { ZodOptional, ZodString } from "zod";
2
+ /**
3
+ * Central registry for all MCP clients
4
+ * Add new clients here to make them automatically available
5
+ */
6
+ class ClientRegistry {
7
+ entries = [];
8
+ enabledClients = null;
9
+ /**
10
+ * Configure which clients should be enabled based on MCP_CLIENTS env var
11
+ * If not set or empty, all clients are enabled
12
+ * If set, should be comma-separated list of client names (case-insensitive)
13
+ */
14
+ constructor() {
15
+ const enabledClientsEnv = process.env.MCP_CLIENTS?.trim();
16
+ if (!enabledClientsEnv) {
17
+ // Empty or not set = all clients enabled
18
+ this.enabledClients = null;
19
+ return;
20
+ }
21
+ // Parse comma-separated list and normalize to lowercase for comparison
22
+ this.enabledClients = new Set(enabledClientsEnv
23
+ .split(",")
24
+ .map((name) => name.trim().toLowerCase())
25
+ .filter((name) => name.length > 0));
26
+ }
27
+ /**
28
+ * Check if a client is enabled based on MCP_CLIENTS configuration
29
+ */
30
+ isClientEnabled(name) {
31
+ if (this.enabledClients === null) {
32
+ return true; // All clients enabled
33
+ }
34
+ return this.enabledClients.has(name.toLowerCase());
35
+ }
36
+ /**
37
+ * Validate if a config option is an Allowed Endpoint URL
38
+ * Supports both exact matches and regex patterns
39
+ * Patterns starting with / and ending with / are treated as regex
40
+ * @param zodType The Zod type definition for the config option
41
+ * @param value The actual config value to validate
42
+ */
43
+ validateAllowedEndpoint(zodType, value) {
44
+ if (zodType instanceof ZodOptional) {
45
+ zodType = zodType._def.innerType;
46
+ }
47
+ if (zodType instanceof ZodString) {
48
+ if (zodType.isURL) {
49
+ const allowedEndpoints = process.env.MCP_ALLOWED_ENDPOINTS?.split(",");
50
+ if (allowedEndpoints) {
51
+ for (const endpoint of allowedEndpoints) {
52
+ const trimmedEndpoint = endpoint.trim();
53
+ // Check if this is a regex pattern (wrapped in /)
54
+ if (trimmedEndpoint.startsWith("/") &&
55
+ trimmedEndpoint.endsWith("/")) {
56
+ try {
57
+ const pattern = trimmedEndpoint.slice(1, -1); // Remove leading/trailing /
58
+ const regex = new RegExp(pattern);
59
+ if (regex.test(value)) {
60
+ return;
61
+ }
62
+ }
63
+ catch (error) {
64
+ console.warn(`Invalid regex pattern in MCP_ALLOWED_ENDPOINTS: ${trimmedEndpoint}, error: ${error}`);
65
+ }
66
+ }
67
+ else {
68
+ // Exact match
69
+ if (value === trimmedEndpoint) {
70
+ return;
71
+ }
72
+ }
73
+ }
74
+ throw new Error(`URL ${value} is not allowed`);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ /**
80
+ * Register a client class
81
+ * @param name Display name for the client (for logging)
82
+ */
83
+ register(client) {
84
+ this.entries.push(client);
85
+ }
86
+ /**
87
+ * Get all registered clients (filtered by MCP_CLIENTS if configured)
88
+ */
89
+ getAll() {
90
+ return this.entries.filter((entry) => this.isClientEnabled(entry.name));
91
+ }
92
+ /**
93
+ * Configures all enabled clients on the given MCP server
94
+ * @param server The MCP server on which the client is registered
95
+ * @param getConfigValue A function that obtains a configuration value for the given client and requirement name
96
+ * @returns The number of clients successfully configured
97
+ */
98
+ async configure(server, getConfigValue) {
99
+ let configuredCount = 0;
100
+ entryLoop: for (const entry of this.getAll()) {
101
+ const config = {};
102
+ for (const configKey of Object.keys(entry.config.shape)) {
103
+ const value = getConfigValue(entry, configKey);
104
+ if (value !== null) {
105
+ // validate if a config option is an Allowed Endpoint URL
106
+ this.validateAllowedEndpoint(entry.config.shape[configKey], value);
107
+ config[configKey] = value;
108
+ }
109
+ else if (!entry.config.shape[configKey].isOptional()) {
110
+ continue entryLoop; // Skip configuring this client - missing required config
111
+ }
112
+ }
113
+ if (await entry.configure(server, config)) {
114
+ server.addClient(entry);
115
+ configuredCount++;
116
+ }
117
+ }
118
+ return configuredCount;
119
+ }
120
+ /**
121
+ * Clear all registrations (useful for testing)
122
+ */
123
+ clear() {
124
+ this.entries = [];
125
+ }
126
+ }
127
+ // Create and export the singleton registry
128
+ export const clientRegistry = new ClientRegistry();
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Client registration module
3
+ *
4
+ * This file registers all available MCP clients with the client registry.
5
+ * To add a new client:
6
+ * 1. Import the client class
7
+ * 2. Call clientRegistry.register() with the client details
8
+ * 3. Specify if the client needs the MCP server instance or async initialization
9
+ */
10
+ import { BugsnagClient } from "../bugsnag/client.js";
11
+ import { CollaboratorClient } from "../collaborator/client.js";
12
+ import { PactflowClient } from "../pactflow/client.js";
13
+ import { QmetryClient } from "../qmetry/client.js";
14
+ import { ReflectClient } from "../reflect/client.js";
15
+ import { SwaggerClient } from "../swagger/client.js";
16
+ import { ZephyrClient } from "../zephyr/client.js";
17
+ import { clientRegistry } from "./client-registry.js";
18
+ // Register Reflect client
19
+ clientRegistry.register(new ReflectClient());
20
+ // Register Bugsnag client
21
+ clientRegistry.register(new BugsnagClient());
22
+ // Register Swagger client
23
+ clientRegistry.register(new SwaggerClient());
24
+ // Register PactFlow/Pact Broker client
25
+ clientRegistry.register(new PactflowClient());
26
+ // Register QMetry client
27
+ clientRegistry.register(new QmetryClient());
28
+ // Register Zephyr client
29
+ clientRegistry.register(new ZephyrClient());
30
+ // Register Collaborator client
31
+ clientRegistry.register(new CollaboratorClient());
@@ -1,9 +1,11 @@
1
1
  import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { ZodAny, ZodArray, ZodBoolean, ZodEnum, ZodIntersection, ZodLiteral, ZodNumber, ZodObject, ZodOptional, ZodString, ZodUnion, } from "zod";
2
+ import { ZodAny, ZodArray, ZodBoolean, ZodDefault, ZodEnum, ZodIntersection, ZodLiteral, ZodNumber, ZodObject, ZodOptional, ZodRecord, ZodString, ZodUnion, } from "zod";
3
3
  import Bugsnag from "../common/bugsnag.js";
4
+ import { CacheService } from "./cache.js";
4
5
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "./info.js";
5
6
  import { ToolError } from "./types.js";
6
7
  export class SmartBearMcpServer extends McpServer {
8
+ cache;
7
9
  constructor() {
8
10
  super({
9
11
  name: MCP_SERVER_NAME,
@@ -18,10 +20,14 @@ export class SmartBearMcpServer extends McpServer {
18
20
  prompts: {}, // Server supports sending prompts to Host
19
21
  },
20
22
  });
23
+ this.cache = new CacheService();
24
+ }
25
+ getCache() {
26
+ return this.cache;
21
27
  }
22
28
  addClient(client) {
23
29
  client.registerTools((params, cb) => {
24
- const toolName = `${client.prefix}_${params.title.replace(/\s+/g, "_").toLowerCase()}`;
30
+ const toolName = `${client.toolPrefix}_${params.title.replace(/\s+/g, "_").toLowerCase()}`;
25
31
  const toolTitle = `${client.name}: ${params.title}`;
26
32
  return super.registerTool(toolName, {
27
33
  title: toolTitle,
@@ -65,7 +71,7 @@ export class SmartBearMcpServer extends McpServer {
65
71
  });
66
72
  if (client.registerResources) {
67
73
  client.registerResources((name, path, cb) => {
68
- const url = `${client.prefix}://${name}/${path}`;
74
+ const url = `${client.toolPrefix}://${name}/${path}`;
69
75
  return super.registerResource(name, new ResourceTemplate(url, {
70
76
  list: undefined,
71
77
  }), {}, async (url, variables, extra) => {
@@ -184,16 +190,31 @@ export class SmartBearMcpServer extends McpServer {
184
190
  }
185
191
  return description.trim();
186
192
  }
187
- formatParameterDescription(key, field) {
193
+ formatParameterDescription(key, field, description = null, isOptional = false, defaultValue = null) {
194
+ description = description ?? (field.description || null);
195
+ if (field instanceof ZodOptional) {
196
+ field = field.unwrap();
197
+ return this.formatParameterDescription(key, field, description, true, defaultValue);
198
+ }
199
+ if (field instanceof ZodDefault) {
200
+ defaultValue = JSON.stringify(field._def.defaultValue());
201
+ field = field.removeDefault();
202
+ return this.formatParameterDescription(key, field, description, true, defaultValue);
203
+ }
188
204
  return (`- ${key} (${this.getReadableTypeName(field)})` +
189
- `${field.isOptional() ? "" : " *required*"}` +
190
- `${field.description ? `: ${field.description}` : ""}` +
191
- `${key === "examples" && field instanceof ZodEnum ? ` (e.g. ${Object.keys(field.enum).join(", ")})` : ""}` +
192
- `${key === "constraints" && field instanceof ZodEnum ? `\n - ${Object.keys(field.enum).join("\n - ")}` : ""}`);
205
+ `${isOptional ? "" : " *required*"}` +
206
+ `${description ? `: ${description}` : ""}` +
207
+ `${defaultValue ? ` (default: ${defaultValue})` : ""}`);
193
208
  }
194
209
  getReadableTypeName(zodType) {
195
210
  if (zodType instanceof ZodOptional) {
196
- zodType = zodType._def.innerType;
211
+ return this.getReadableTypeName(zodType.unwrap());
212
+ }
213
+ if (zodType instanceof ZodDefault) {
214
+ return this.getReadableTypeName(zodType.removeDefault());
215
+ }
216
+ if (zodType instanceof ZodRecord) {
217
+ return `record<${this.getReadableTypeName(zodType.keySchema)}, ${this.getReadableTypeName(zodType.valueSchema)}>`;
197
218
  }
198
219
  if (zodType instanceof ZodString)
199
220
  return "string";