@smartbear/mcp 0.7.0 → 0.8.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
@@ -18,7 +18,7 @@
18
18
  </div>
19
19
  <br />
20
20
 
21
- A Model Context Protocol (MCP) server which provides AI assistants with seamless access to SmartBear's suite of testing and monitoring tools, including [BugSnag](https://www.bugsnag.com/), [Reflect](https://reflect.run), [API Hub](https://www.smartbear.com/api-hub), [PactFlow](https://pactflow.io/), [Pact Broker](https://docs.pact.io/), and [QMetry](https://www.qmetry.com/)
21
+ A Model Context Protocol (MCP) server which provides AI assistants with seamless access to SmartBear's suite of testing and monitoring tools, including [BugSnag](https://www.bugsnag.com/), [Reflect](https://reflect.run), [API Hub](https://www.smartbear.com/api-hub), [PactFlow](https://pactflow.io/), [Pact Broker](https://docs.pact.io/), [QMetry](https://www.qmetry.com/), and [Zephyr](https://smartbear.com/test-management/zephyr/).
22
22
 
23
23
  ## What is MCP?
24
24
 
@@ -33,12 +33,13 @@ See individual guides for suggested prompts and supported tools and resources:
33
33
  - [API Hub](https://developer.smartbear.com/smartbear-mcp/docs/api-hub-integration) - Portal management capabilities
34
34
  - [PactFlow](https://developer.smartbear.com/pactflow/default/getting-started) - Contract testing capabilities
35
35
  - [QMetry](https://developer.smartbear.com/smartbear-mcp/docs/qmetry-integration) - QMetry Test Management capabilities
36
+ - [Zephyr](https://developer.smartbear.com/smartbear-mcp/docs/zephyr-integration) - Zephyr Test Management capabilities
36
37
 
37
38
 
38
39
  ## Prerequisites
39
40
 
40
41
  - Node.js 20+ and npm
41
- - Access to SmartBear products (BugSnag, Reflect, API Hub, or QMetry)
42
+ - Access to SmartBear products (BugSnag, Reflect, API Hub, QMetry, or Zephyr)
42
43
  - Valid API tokens for the products you want to integrate
43
44
 
44
45
  ## Installation
@@ -77,6 +78,8 @@ Alternatively, you can use `npx` (or globally install) the `@smartbear/mcp` pack
77
78
  "PACT_BROKER_PASSWORD": "${input:pact_broker_password}",
78
79
  "QMETRY_API_KEY": "${input:qmetry_api_key}",
79
80
  "QMETRY_BASE_URL": "${input:qmetry_base_url}",
81
+ "ZEPHYR_API_TOKEN": "${input:zephyr_api_token}",
82
+ "ZEPHYR_BASE_URL": "${input:zephyr_base_url}"
80
83
  }
81
84
  }
82
85
  },
@@ -141,6 +144,18 @@ Alternatively, you can use `npx` (or globally install) the `@smartbear/mcp` pack
141
144
  "description": "By default, connects to https://testmanagement.qmetry.com. Change to a custom QMetry server URL or a region-specific endpoint if needed.",
142
145
  "password": false
143
146
  },
147
+ {
148
+ "id": "zephyr_api_token",
149
+ "type": "promptString",
150
+ "description": "Zephyr API token - leave blank to disable Zephyr tools",
151
+ "password": true
152
+ },
153
+ {
154
+ "id": "zephyr_base_url",
155
+ "type": "promptString",
156
+ "description": "Zephyr API base URL. By default, connects to https://api.zephyrscale.smartbear.com/v2. Change to region-specific endpoint if needed.",
157
+ "password": false
158
+ }
144
159
  ]
145
160
  }
146
161
  ```
@@ -170,6 +185,8 @@ Add the following configuration to your `claude_desktop_config.json` to launch t
170
185
  "PACT_BROKER_PASSWORD": "your_pact_broker_password",
171
186
  "QMETRY_API_KEY": "your_qmetry_api_key",
172
187
  "QMETRY_BASE_URL": "https://testmanagement.qmetry.com",
188
+ "ZEPHYR_API_TOKEN": "your_zephyr_api_token",
189
+ "ZEPHYR_BASE_URL": "https://api.zephyrscale.smartbear.com/v2"
173
190
  }
174
191
  }
175
192
  }
@@ -39,13 +39,26 @@ export class ApiHubAPI {
39
39
  return (await response.json());
40
40
  }
41
41
  catch (error) {
42
- console.warn("Failed to parse JSON response:", error);
42
+ console.warn("Failed to parse JSON response (declared JSON):", error);
43
43
  return defaultReturn;
44
44
  }
45
45
  }
46
- // Fallback for non-JSON responses
46
+ // Fallback: read text and attempt heuristic JSON parse
47
47
  const text = await response.text();
48
- return text ? { message: text } : defaultReturn;
48
+ if (!text)
49
+ return defaultReturn;
50
+ const trimmed = text.trim();
51
+ const firstChar = trimmed[0];
52
+ if (firstChar === "{" || firstChar === "[") {
53
+ try {
54
+ return JSON.parse(trimmed);
55
+ }
56
+ catch (error) {
57
+ console.warn("Heuristic JSON parse failed:", error);
58
+ return { message: text };
59
+ }
60
+ }
61
+ return { message: text };
49
62
  }
50
63
  /**
51
64
  * Handles HTTP responses with smart JSON parsing and fallback handling.
@@ -66,7 +79,8 @@ export class ApiHubAPI {
66
79
  method: "GET",
67
80
  headers: this.headers,
68
81
  });
69
- return response.json();
82
+ const result = await this.handleResponse(response, []);
83
+ return result;
70
84
  }
71
85
  async createPortal(body) {
72
86
  const response = await fetch(`${this.config.portalBasePath}/portals`, {
@@ -74,20 +88,32 @@ export class ApiHubAPI {
74
88
  headers: this.headers,
75
89
  body: JSON.stringify(body),
76
90
  });
77
- return response.json();
91
+ const result = await this.handleResponse(response);
92
+ if (!("id" in result)) {
93
+ throw new Error("Unexpected empty response creating portal");
94
+ }
95
+ return result;
78
96
  }
79
97
  async getPortal(portalId) {
80
98
  const response = await fetch(`${this.config.portalBasePath}/portals/${portalId}`, {
81
99
  method: "GET",
82
100
  headers: this.headers,
83
101
  });
84
- return response.json();
102
+ const result = await this.handleResponse(response);
103
+ if (!("id" in result)) {
104
+ throw new Error("Portal not found or empty response");
105
+ }
106
+ return result;
85
107
  }
86
108
  async deletePortal(portalId) {
87
- await fetch(`${this.config.portalBasePath}/portals/${portalId}`, {
109
+ const response = await fetch(`${this.config.portalBasePath}/portals/${portalId}`, {
88
110
  method: "DELETE",
89
111
  headers: this.headers,
90
112
  });
113
+ if (!response.ok) {
114
+ const errorText = await response.text().catch(() => "");
115
+ throw new Error(`API Hub deletePortal failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
116
+ }
91
117
  }
92
118
  async updatePortal(portalId, body) {
93
119
  const response = await fetch(`${this.config.portalBasePath}/portals/${portalId}`, {
@@ -102,7 +128,8 @@ export class ApiHubAPI {
102
128
  method: "GET",
103
129
  headers: this.headers,
104
130
  });
105
- return response.json();
131
+ const result = await this.handleResponse(response, []);
132
+ return result;
106
133
  }
107
134
  async createPortalProduct(portalId, body) {
108
135
  const response = await fetch(`${this.config.portalBasePath}/portals/${portalId}/products`, {
@@ -110,14 +137,22 @@ export class ApiHubAPI {
110
137
  headers: this.headers,
111
138
  body: JSON.stringify(body),
112
139
  });
113
- return response.json();
140
+ const result = await this.handleResponse(response);
141
+ if (!("id" in result)) {
142
+ throw new Error("Unexpected empty response creating product");
143
+ }
144
+ return result;
114
145
  }
115
146
  async getPortalProduct(productId) {
116
147
  const response = await fetch(`${this.config.portalBasePath}/products/${productId}`, {
117
148
  method: "GET",
118
149
  headers: this.headers,
119
150
  });
120
- return response.json();
151
+ const result = await this.handleResponse(response);
152
+ if (!("id" in result)) {
153
+ throw new Error("Product not found or empty response");
154
+ }
155
+ return result;
121
156
  }
122
157
  async deletePortalProduct(productId) {
123
158
  const response = await fetch(`${this.config.portalBasePath}/products/${productId}`, {
@@ -132,13 +167,9 @@ export class ApiHubAPI {
132
167
  headers: this.headers,
133
168
  body: JSON.stringify(body),
134
169
  });
135
- // Custom error handling for updatePortalProduct
136
- if (!response.ok) {
137
- const errorText = await response.text().catch(() => "");
138
- throw new Error(`API Hub updatePortalProduct failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
139
- }
140
- // Use handleResponse but with custom default return value
141
- return this.handleResponseWithoutErrorCheck(response, { success: true });
170
+ return this.handleResponse(response, {
171
+ success: true,
172
+ });
142
173
  }
143
174
  /**
144
175
  * Helper method for handling responses when error checking is already done.
@@ -149,9 +180,6 @@ export class ApiHubAPI {
149
180
  * @param defaultReturn - Default value to return for empty responses
150
181
  * @returns Parsed response data or fallback value
151
182
  */
152
- async handleResponseWithoutErrorCheck(response, defaultReturn = {}) {
153
- return this.parseResponse(response, defaultReturn);
154
- }
155
183
  // Registry API methods for SwaggerHub Design functionality
156
184
  /**
157
185
  * Search APIs and Domains in SwaggerHub Registry using /specs endpoint
@@ -250,4 +278,110 @@ export class ApiHubAPI {
250
278
  return response.text();
251
279
  }
252
280
  }
281
+ /**
282
+ * Create or Update API in SwaggerHub Registry
283
+ * @param params Parameters for creating or updating the API including owner, name, version, specification, and definition
284
+ * @returns Created or updated API metadata with URL. HTTP 201 indicates creation, HTTP 200 indicates update
285
+ */
286
+ async createOrUpdateApi(params) {
287
+ // Determine the format of the definition
288
+ let contentType;
289
+ let requestBody;
290
+ // Auto-detect format from the definition content
291
+ const format = this.detectDefinitionFormat(params.definition);
292
+ if (format === "yaml") {
293
+ contentType = "application/yaml";
294
+ requestBody = params.definition; // Send YAML as-is
295
+ }
296
+ else {
297
+ contentType = "application/json";
298
+ // For JSON, parse and stringify to ensure valid JSON
299
+ try {
300
+ const parsedDefinition = JSON.parse(params.definition);
301
+ requestBody = JSON.stringify(parsedDefinition);
302
+ }
303
+ catch (error) {
304
+ throw new Error(`Invalid JSON format in definition: ${error instanceof Error ? error.message : "Unknown error"}`);
305
+ }
306
+ }
307
+ // Construct the URL with query parameters
308
+ // Fixed values: visibility=private, automock=false, version=1.0.0
309
+ const searchParams = new URLSearchParams();
310
+ searchParams.append("isPrivate", "true");
311
+ const url = `${this.config.registryBasePath}/apis/${encodeURIComponent(params.owner)}/${encodeURIComponent(params.apiName)}?${searchParams.toString()}`;
312
+ // Use POST method with the appropriate content type
313
+ const response = await fetch(url, {
314
+ method: "POST",
315
+ headers: {
316
+ ...this.headers,
317
+ "Content-Type": contentType,
318
+ },
319
+ body: requestBody,
320
+ });
321
+ if (!response.ok) {
322
+ const errorText = await response.text().catch(() => "");
323
+ throw new Error(`SwaggerHub Registry API createOrUpdateApi failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}. URL: ${url}`);
324
+ }
325
+ // Determine operation type based on HTTP status code
326
+ const operation = response.status === 201 ? "create" : "update";
327
+ // Return formatted response with the required fields
328
+ // Fixed version is always 1.0.0
329
+ return {
330
+ owner: params.owner,
331
+ apiName: params.apiName,
332
+ version: "1.0.0",
333
+ url: `https://app.swaggerhub.com/apis/${params.owner}/${params.apiName}/1.0.0`,
334
+ operation,
335
+ };
336
+ }
337
+ /**
338
+ * Create API from Template in SwaggerHub Registry
339
+ * @param params Parameters for creating API from template including owner, api name, and template
340
+ * @returns Created API metadata with URL. HTTP 201 indicates creation, HTTP 200 indicates update
341
+ */
342
+ async createApiFromTemplate(params) {
343
+ // Construct the URL with query parameters
344
+ // Fixed values: visibility=private, no project, noReconcile=false
345
+ const searchParams = new URLSearchParams();
346
+ searchParams.append("isPrivate", "true");
347
+ searchParams.append("template", params.template);
348
+ const url = `${this.config.registryBasePath}/apis/${encodeURIComponent(params.owner)}/${encodeURIComponent(params.apiName)}/.template?${searchParams.toString()}`;
349
+ // Use POST method for template creation
350
+ const response = await fetch(url, {
351
+ method: "POST",
352
+ headers: this.headers,
353
+ });
354
+ if (!response.ok) {
355
+ const errorText = await response.text().catch(() => "");
356
+ throw new Error(`SwaggerHub Registry API createApiFromTemplate failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}. URL: ${url}`);
357
+ }
358
+ // Determine operation type based on HTTP status code
359
+ const operation = response.status === 201 ? "create" : "update";
360
+ // Return formatted response with the required fields
361
+ return {
362
+ owner: params.owner,
363
+ apiName: params.apiName,
364
+ template: params.template,
365
+ url: `https://app.swaggerhub.com/apis/${params.owner}/${params.apiName}`,
366
+ operation,
367
+ };
368
+ }
369
+ /**
370
+ * Auto-detect the format of an API definition string
371
+ * @param definition The API definition content
372
+ * @returns 'json' or 'yaml'
373
+ */
374
+ detectDefinitionFormat(definition) {
375
+ const trimmed = definition.trim();
376
+ if (!trimmed) {
377
+ throw new Error("Empty definition content provided");
378
+ }
379
+ try {
380
+ JSON.parse(trimmed);
381
+ return "json";
382
+ }
383
+ catch {
384
+ return "yaml";
385
+ }
386
+ }
253
387
  }
@@ -53,3 +53,17 @@ export const ApiDefinitionParamsSchema = z.object({
53
53
  .optional()
54
54
  .describe("Set to true to create models from inline schemas in OpenAPI definition (default false)"),
55
55
  });
56
+ export const CreateApiParamsSchema = z.object({
57
+ owner: z.string().describe("Organization name (owner of the API)"),
58
+ apiName: z.string().describe("API name"),
59
+ definition: z
60
+ .string()
61
+ .describe("API definition content (OpenAPI/AsyncAPI specification in JSON or YAML format). Format is automatically detected. API is created with fixed values: version 1.0.0, private visibility, automock disabled, and no project assignment."),
62
+ });
63
+ export const CreateApiFromTemplateParamsSchema = z.object({
64
+ owner: z.string().describe("Organization name (owner of the API)"),
65
+ apiName: z.string().describe("API name"),
66
+ template: z
67
+ .string()
68
+ .describe("Template name to use for creating the API. Format: owner/template-name/version (e.g., 'swagger-hub/petstore-template/1.0.0'). API is created with fixed values: private visibility, no project assignment, and reconciliation enabled."),
69
+ });
@@ -6,7 +6,7 @@
6
6
  * This follows the pattern established in the pactflow module.
7
7
  */
8
8
  import { CreatePortalArgsSchema, CreateProductArgsSchema, PortalArgsSchema, ProductArgsSchema, UpdatePortalArgsSchema, UpdateProductArgsSchema, } from "./portal-types.js";
9
- import { ApiDefinitionParamsSchema, ApiSearchParamsSchema, } from "./registry-types.js";
9
+ import { ApiDefinitionParamsSchema, ApiSearchParamsSchema, CreateApiFromTemplateParamsSchema, CreateApiParamsSchema, } from "./registry-types.js";
10
10
  export const TOOLS = [
11
11
  {
12
12
  title: "List Portals",
@@ -83,4 +83,16 @@ export const TOOLS = [
83
83
  zodSchema: ApiDefinitionParamsSchema,
84
84
  handler: "getApiDefinition",
85
85
  },
86
+ {
87
+ title: "Create or Update API",
88
+ summary: "Create a new API or update an existing API in SwaggerHub Registry for API Hub for Design. The API specification type (OpenAPI, AsyncAPI) is automatically detected from the definition content. APIs are always created with fixed values: version 1.0.0, private visibility, and automock disabled (these values cannot be changed). Returns HTTP 201 for creation, HTTP 200 for update. Response includes 'operation' field indicating whether it was a 'create' or 'update' operation along with API details and SwaggerHub URL.",
89
+ zodSchema: CreateApiParamsSchema,
90
+ handler: "createOrUpdateApi",
91
+ },
92
+ {
93
+ title: "Create API from Template",
94
+ summary: "Create a new API in SwaggerHub Registry using a predefined template. This endpoint creates APIs based on existing templates without requiring manual definition content. APIs are always created with fixed values: private visibility, no project assignment, and reconciliation enabled (these values cannot be changed). Returns HTTP 201 for creation, HTTP 200 for update. Response includes 'operation' field and API details with SwaggerHub URL.",
95
+ zodSchema: CreateApiFromTemplateParamsSchema,
96
+ handler: "createApiFromTemplate",
97
+ },
86
98
  ];
@@ -51,6 +51,12 @@ export class ApiHubClient {
51
51
  async getApiDefinition(args) {
52
52
  return this.api.getApiDefinition(args);
53
53
  }
54
+ async createOrUpdateApi(args) {
55
+ return this.api.createOrUpdateApi(args);
56
+ }
57
+ async createApiFromTemplate(args) {
58
+ return this.api.createApiFromTemplate(args);
59
+ }
54
60
  registerTools(register, _getInput) {
55
61
  TOOLS.forEach((tool) => {
56
62
  const { handler, formatResponse, ...toolParams } = tool;
@@ -1,4 +1,5 @@
1
1
  import { BaseAPI, pickFieldsFromArray } from "./base.js";
2
+ import { ProjectAPI } from "./Project.js";
2
3
  // --- API Class ---
3
4
  export class CurrentUserAPI extends BaseAPI {
4
5
  static filterFields = [
@@ -7,7 +8,6 @@ export class CurrentUserAPI extends BaseAPI {
7
8
  "upgrade_url",
8
9
  ];
9
10
  static organizationFields = ["id", "name", "slug"];
10
- static projectFields = ["id", "name", "slug", "api_key"];
11
11
  constructor(configuration) {
12
12
  super(configuration, CurrentUserAPI.filterFields);
13
13
  }
@@ -16,7 +16,7 @@ export class CurrentUserAPI extends BaseAPI {
16
16
  * GET /user/organizations
17
17
  */
18
18
  async listUserOrganizations(options = {}) {
19
- const { admin, paginate = false, ...queryOptions } = options;
19
+ const { admin, ...queryOptions } = options;
20
20
  const params = new URLSearchParams();
21
21
  if (admin !== undefined)
22
22
  params.append("admin", String(admin));
@@ -30,7 +30,7 @@ export class CurrentUserAPI extends BaseAPI {
30
30
  const data = await this.request({
31
31
  method: "GET",
32
32
  url,
33
- }, paginate);
33
+ });
34
34
  // Only return allowed fields
35
35
  return {
36
36
  ...data,
@@ -57,10 +57,10 @@ export class CurrentUserAPI extends BaseAPI {
57
57
  const data = await this.request({
58
58
  method: "GET",
59
59
  url,
60
- }, true); // Always paginate for projects
60
+ });
61
61
  return {
62
62
  ...data,
63
- body: pickFieldsFromArray(data.body || [], CurrentUserAPI.projectFields),
63
+ body: pickFieldsFromArray(data.body || [], ProjectAPI.projectFields),
64
64
  };
65
65
  }
66
66
  }
@@ -45,33 +45,21 @@ export class ErrorAPI extends BaseAPI {
45
45
  }));
46
46
  }
47
47
  /**
48
- * View the latest Event on an Error
49
- * GET /errors/{error_id}/latest_event
50
- */
51
- async viewLatestEventOnError(errorId, options = {}) {
52
- const params = new URLSearchParams();
53
- for (const [key, value] of Object.entries(options)) {
54
- if (value !== undefined)
55
- params.append(key, String(value));
56
- }
57
- const url = params.toString()
58
- ? `/errors/${errorId}/latest_event?${params}`
59
- : `/errors/${errorId}/latest_event`;
60
- return (await this.request({
61
- method: "GET",
62
- url,
63
- }));
64
- }
65
- /**
66
- * List the Events on a Project
48
+ * Get the latest Event in a Project, with optional filters
67
49
  * GET /projects/{project_id}/events
68
50
  */
69
- async listEventsOnProject(projectId, queryString = "") {
51
+ async getLatestEventOnProject(projectId, queryString = "") {
70
52
  const url = `/projects/${projectId}/events${queryString}`;
71
- return await this.request({
53
+ const response = await this.request({
72
54
  method: "GET",
73
55
  url,
74
56
  });
57
+ return {
58
+ ...response,
59
+ body: response.body && response.body.length > 0
60
+ ? response.body[0]
61
+ : undefined, // Return only the latest event
62
+ };
75
63
  }
76
64
  /**
77
65
  * View an Event by ID
@@ -98,8 +86,8 @@ export class ErrorAPI extends BaseAPI {
98
86
  async listProjectErrors(projectId, options = {}) {
99
87
  let url = `/projects/${projectId}/errors`;
100
88
  // Next links need to be used as-is to ensure results are consistent, so only the page size can be modified
101
- if (options.next !== undefined) {
102
- const nextUrl = new URL(options.next);
89
+ if (options.next_url !== undefined) {
90
+ const nextUrl = new URL(options.next_url);
103
91
  if (options.per_page !== undefined) {
104
92
  nextUrl.searchParams.set("per_page", options.per_page.toString());
105
93
  }
@@ -134,7 +122,7 @@ export class ErrorAPI extends BaseAPI {
134
122
  return (await this.request({
135
123
  method: "GET",
136
124
  url,
137
- }));
125
+ }, false));
138
126
  }
139
127
  /**
140
128
  * Update an Error on a Project
@@ -1,6 +1,15 @@
1
- import { BaseAPI, pickFields, pickFieldsFromArray, } from "./base.js";
1
+ import { BaseAPI, pickFieldsFromArray } from "./base.js";
2
2
  // --- API Class ---
3
3
  export class ProjectAPI extends BaseAPI {
4
+ static projectFields = [
5
+ "id",
6
+ "name",
7
+ "slug",
8
+ "api_key",
9
+ "stability_target_type",
10
+ "target_stability",
11
+ "critical_stability",
12
+ ];
4
13
  static filterFields = [
5
14
  "errors_url",
6
15
  "events_url",
@@ -39,11 +48,6 @@ export class ProjectAPI extends BaseAPI {
39
48
  "accumulative_daily_users_seen",
40
49
  "accumulative_daily_users_with_unhandled",
41
50
  ];
42
- static stabilityFields = [
43
- "critical_stability",
44
- "target_stability",
45
- "stability_target_type",
46
- ];
47
51
  constructor(configuration) {
48
52
  super(configuration, ProjectAPI.filterFields);
49
53
  }
@@ -80,20 +84,6 @@ export class ProjectAPI extends BaseAPI {
80
84
  body: data,
81
85
  });
82
86
  }
83
- /**
84
- * Retrieves the stability targets for a specific project.
85
- * GET /projects/{project_id} (with internal header)
86
- * @param projectId The ID of the project.
87
- * @returns A promise that resolves to the project's stability targets.
88
- */
89
- async getProjectStabilityTargets(projectId) {
90
- const url = `/projects/${projectId}`;
91
- const response = await this.request({
92
- method: "GET",
93
- url,
94
- });
95
- return pickFields(response.body || {}, ProjectAPI.stabilityFields);
96
- }
97
87
  /**
98
88
  * Lists builds for a specific project.
99
89
  * GET /projects/{project_id}/releases
@@ -107,7 +97,7 @@ export class ProjectAPI extends BaseAPI {
107
97
  const response = await this.request({
108
98
  method: "GET",
109
99
  url,
110
- });
100
+ }, false);
111
101
  return {
112
102
  ...response,
113
103
  body: pickFieldsFromArray(response.body || [], ProjectAPI.buildFields),
@@ -136,11 +126,14 @@ export class ProjectAPI extends BaseAPI {
136
126
  */
137
127
  async listReleases(projectId, opts) {
138
128
  const url = opts.next_url ??
139
- `/projects/${projectId}/release_groups?release_stage_name=${opts.release_stage_name}&visible_only=${opts.visible_only}&top_only=true`;
129
+ `/projects/${projectId}/release_groups?` +
130
+ `release_stage_name=${opts.release_stage_name ?? "production"}&` +
131
+ `visible_only=${opts.visible_only ?? false}&` +
132
+ `top_only=${opts.top_only ?? false}`;
140
133
  const response = await this.request({
141
134
  method: "GET",
142
135
  url,
143
- });
136
+ }, false);
144
137
  return {
145
138
  ...response,
146
139
  body: pickFieldsFromArray(response.body || [], ProjectAPI.releaseFields),
@@ -24,6 +24,16 @@ export function getNextUrlPathFromHeader(headers, basePath) {
24
24
  return null;
25
25
  return match.replace(basePath, "");
26
26
  }
27
+ // Utility to extract total count from headers
28
+ export function getTotalCountFromHeader(headers) {
29
+ if (!headers)
30
+ return null;
31
+ const totalCount = headers.get("X-Total-Count");
32
+ if (!totalCount)
33
+ return null;
34
+ const parsed = parseInt(totalCount, 10);
35
+ return Number.isNaN(parsed) ? null : parsed;
36
+ }
27
37
  // Ensure URL is absolute
28
38
  // The MCP tools exposed use only the path for pagination
29
39
  // For making requests, we need to ensure the URL is absolute
@@ -37,7 +47,7 @@ export class BaseAPI {
37
47
  this.configuration = configuration;
38
48
  this.filterFields = filterFields || [];
39
49
  }
40
- async request(options, paginate = false) {
50
+ async request(options, fetchAll = true) {
41
51
  const headers = {
42
52
  ...this.configuration.headers,
43
53
  ...options.headers,
@@ -66,16 +76,18 @@ export class BaseAPI {
66
76
  headers: response.headers,
67
77
  };
68
78
  const data = await response.json();
69
- if (paginate) {
79
+ nextUrl = getNextUrlPathFromHeader(response.headers, this.configuration.basePath);
80
+ if (Array.isArray(data)) {
70
81
  results = results.concat(data);
71
- nextUrl = getNextUrlPathFromHeader(response.headers, this.configuration.basePath);
82
+ apiResponse.nextUrl = nextUrl;
72
83
  }
73
84
  else {
74
85
  apiResponse.body = data;
75
86
  }
76
- } while (paginate && nextUrl);
77
- if (paginate) {
87
+ } while (fetchAll && nextUrl);
88
+ if (results.length > 0) {
78
89
  apiResponse.body = results;
90
+ apiResponse.totalCount = getTotalCountFromHeader(apiResponse.headers);
79
91
  }
80
92
  if (Array.isArray(apiResponse.body)) {
81
93
  apiResponse.body.forEach(this.sanitizeResponse.bind(this));