@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 +19 -2
- package/dist/api-hub/client/api.js +154 -20
- package/dist/api-hub/client/registry-types.js +14 -0
- package/dist/api-hub/client/tools.js +13 -1
- package/dist/api-hub/client.js +6 -0
- package/dist/bugsnag/client/api/CurrentUser.js +5 -5
- package/dist/bugsnag/client/api/Error.js +12 -24
- package/dist/bugsnag/client/api/Project.js +16 -23
- package/dist/bugsnag/client/api/base.js +17 -5
- package/dist/bugsnag/client.js +131 -248
- package/dist/common/server.js +4 -1
- package/dist/index.js +8 -1
- package/dist/pactflow/client/tools.js +9 -9
- package/dist/pactflow/client.js +7 -6
- package/dist/zephyr/client.js +16 -0
- package/dist/zephyr/common/api-client.js +27 -0
- package/dist/zephyr/common/auth-service.js +14 -0
- package/dist/zephyr/common/types.js +35 -0
- package/dist/zephyr/tool/project/get-projects.js +54 -0
- package/dist/zephyr/tool/zephyr-tool.js +1 -0
- package/package.json +3 -2
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/),
|
|
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
|
|
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
|
|
46
|
+
// Fallback: read text and attempt heuristic JSON parse
|
|
47
47
|
const text = await response.text();
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
];
|
package/dist/api-hub/client.js
CHANGED
|
@@ -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,
|
|
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
|
-
}
|
|
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
|
-
}
|
|
60
|
+
});
|
|
61
61
|
return {
|
|
62
62
|
...data,
|
|
63
|
-
body: pickFieldsFromArray(data.body || [],
|
|
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
|
-
*
|
|
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
|
|
51
|
+
async getLatestEventOnProject(projectId, queryString = "") {
|
|
70
52
|
const url = `/projects/${projectId}/events${queryString}`;
|
|
71
|
-
|
|
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.
|
|
102
|
-
const nextUrl = new URL(options.
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
79
|
+
nextUrl = getNextUrlPathFromHeader(response.headers, this.configuration.basePath);
|
|
80
|
+
if (Array.isArray(data)) {
|
|
70
81
|
results = results.concat(data);
|
|
71
|
-
nextUrl =
|
|
82
|
+
apiResponse.nextUrl = nextUrl;
|
|
72
83
|
}
|
|
73
84
|
else {
|
|
74
85
|
apiResponse.body = data;
|
|
75
86
|
}
|
|
76
|
-
} while (
|
|
77
|
-
if (
|
|
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));
|