@smartbear/mcp 0.5.0 → 0.6.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
@@ -9,7 +9,7 @@
9
9
 
10
10
  <!-- Badges -->
11
11
  <div>
12
- <a href="https://github.com/SmartBear/smartbear-mcp/actions/workflows/test.yml"><img src="https://github.com/SmartBear/smartbear-mcp/workflows/Test%20Suite/badge.svg" alt="Test Status"></a>
12
+ <a href="https://github.com/SmartBear/smartbear-mcp/actions/workflows/node-ci.yml"><img src="https://github.com/SmartBear/smartbear-mcp/actions/workflows/node-ci.yml/badge.svg?branch=next" alt="Test Status"></a>
13
13
  <a href="https://smartbear.github.io/smartbear-mcp/"><img src="https://img.shields.io/badge/coverage-dynamic-brightgreen" alt="Coverage"></a>
14
14
  <a href="https://www.npmjs.com/package/@smartbear/mcp"><img src="https://img.shields.io/npm/v/@smartbear/mcp" alt="npm version"></a>
15
15
  <a href="https://modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP-Compatible-blue" alt="MCP Compatible"></a>
@@ -165,113 +165,7 @@ For detailed introduction, examples, and advanced configuration visit our 📖 [
165
165
 
166
166
  ## Local Development
167
167
 
168
- For developers who want to contribute to the SmartBear MCP server, customize its functionality, or work with the latest development features, you can build and run the server directly from source code. This approach gives you full control over the implementation and allows you to make modifications as needed.
169
-
170
- 1. **Clone the repository:**
171
- ```bash
172
- git clone https://github.com/SmartBear/smartbear-mcp.git
173
- cd smartbear-mcp
174
- ```
175
-
176
- 2. **Install dependencies:**
177
- ```bash
178
- npm install
179
- ```
180
-
181
- 3. **Build the server:**
182
- ```bash
183
- npm run build
184
- ```
185
-
186
- 4. **Add the server to your IDE** configuration updating `.vscode/mcp.json` (or equivalent) to point to your local build
187
-
188
- <details>
189
- <summary><strong>📋 VSCode (mcp.json)</strong></summary>
190
-
191
- ```json
192
- {
193
- "servers": {
194
- "smartbear": {
195
- "type": "stdio",
196
- "command": "node",
197
- "dev": { // <-- To allow debugging in VS Code
198
- "watch": "dist/**/*.js",
199
- "debug": {
200
- "type": "node"
201
- },
202
- },
203
- "args": ["<PATH_TO_SMARTBEAR_MCP_REPO>/dist/index.js"],
204
- "env": {
205
- "BUGSNAG_AUTH_TOKEN": "${input:bugsnag_auth_token}",
206
- "BUGSNAG_PROJECT_API_KEY": "${input:bugsnag_project_api_key}",
207
- "REFLECT_API_TOKEN": "${input:reflect_api_token}",
208
- "API_HUB_API_KEY": "${input:api_hub_api_key}",
209
- "PACT_BROKER_BASE_URL": "${input:pact_broker_base_url}",
210
- "PACT_BROKER_TOKEN": "${input:pact_broker_token}",
211
- "PACT_BROKER_USERNAME": "${input:pact_broker_username}",
212
- "PACT_BROKER_PASSWORD": "${input:pact_broker_password}"
213
- }
214
- }
215
- },
216
- "inputs": [
217
- {
218
- "id": "bugsnag_auth_token",
219
- "type": "promptString",
220
- "description": "BugSnag Auth Token - leave blank to disable BugSnag tools",
221
- "password": true
222
- },
223
- {
224
- "id": "bugsnag_project_api_key",
225
- "type": "promptString",
226
- "description": "BugSnag Project API Key - for single project interactions",
227
- "password": false
228
- },
229
- {
230
- "id": "reflect_api_token",
231
- "type": "promptString",
232
- "description": "Reflect API Token - leave blank to disable Reflect tools",
233
- "password": true
234
- },
235
- {
236
- "id": "api_hub_api_key",
237
- "type": "promptString",
238
- "description": "API Hub API Key - leave blank to disable API Hub tools",
239
- "password": true
240
- },
241
- {
242
- "id": "pact_broker_base_url",
243
- "type": "promptString",
244
- "description": "PactFlow or Pact Broker base url - leave blank to disable PactFlow tools",
245
- "password": true
246
- },
247
- {
248
- "id": "pact_broker_token",
249
- "type": "promptString",
250
- "description": "PactFlow Authentication Token",
251
- "password": true
252
- },
253
- {
254
- "id": "pact_broker_username",
255
- "type": "promptString",
256
- "description": "Pact Broker Username",
257
- "password": true
258
- },
259
- {
260
- "id": "pact_broker_password",
261
- "type": "promptString",
262
- "description": "Pact Broker Password",
263
- "password": true
264
- },
265
- ]
266
- }
267
- ```
268
- </details>
269
-
270
- 5. **Local testing** using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) web interface:
271
-
272
- ```bash
273
- BUGSNAG_AUTH_TOKEN="your_token" REFLECT_API_TOKEN="your_reflect_token" API_HUB_API_KEY="your_api_hub_key" PACT_BROKER_BASE_URL="your-pactflow-url" PACT_BROKER_TOKEN="your-pactflow-token" PACT_BROKER_USERNAME="your-pact-broker-username" PACT_BROKER_PASSWORD="your-pact-broker-password" npx @modelcontextprotocol/inspector npx @smartbear/mcp@latest
274
- ```
168
+ For developers who want to contribute to the SmartBear MCP server, please see the [CONTRIBUTING.md](CONTRIBUTING.md) guide.
275
169
 
276
170
  ## License
277
171
 
@@ -39,7 +39,7 @@ export class CurrentUserAPI extends BaseAPI {
39
39
  * @returns A promise that resolves to the list of projects in the organization
40
40
  */
41
41
  async getOrganizationProjects(organizationId, options = {}) {
42
- const { paginate = false, ...queryOptions } = options;
42
+ const { ...queryOptions } = options;
43
43
  const params = new URLSearchParams();
44
44
  for (const [key, value] of Object.entries(queryOptions)) {
45
45
  if (value !== undefined)
@@ -51,8 +51,7 @@ export class CurrentUserAPI extends BaseAPI {
51
51
  const data = await this.request({
52
52
  method: 'GET',
53
53
  url,
54
- }, paginate);
55
- // Only return allowed fields
54
+ }, true); // Always paginate for projects
56
55
  return {
57
56
  ...data,
58
57
  body: pickFieldsFromArray(data.body || [], CurrentUserAPI.projectFields)
@@ -1,12 +1,38 @@
1
- import { BaseAPI, pickFieldsFromArray } from "./base.js";
1
+ import { BaseAPI, pickFieldsFromArray, pickFields } from "./base.js";
2
2
  // --- API Class ---
3
3
  export class ProjectAPI extends BaseAPI {
4
4
  static filterFields = ["errors_url", "events_url", "url", "html_url"];
5
- static eventFieldFields = [
6
- 'custom',
7
- 'display_id',
8
- 'filter_options',
9
- 'pivot_options'
5
+ static eventFieldFields = ["custom", "display_id", "filter_options", "pivot_options"];
6
+ static buildFields = [
7
+ "id",
8
+ "release_time",
9
+ "app_version",
10
+ "release_stage",
11
+ "errors_introduced_count",
12
+ "errors_seen_count",
13
+ "total_sessions_count",
14
+ "unhandled_sessions_count",
15
+ "accumulative_daily_users_seen",
16
+ "accumulative_daily_users_with_unhandled",
17
+ ];
18
+ static releaseFields = [
19
+ "id",
20
+ "release_stage_name",
21
+ "app_version",
22
+ "first_released_at",
23
+ "first_release_id",
24
+ "releases_count",
25
+ "visible",
26
+ "total_sessions_count",
27
+ "unhandled_sessions_count",
28
+ "sessions_count_in_last_24h",
29
+ "accumulative_daily_users_seen",
30
+ "accumulative_daily_users_with_unhandled",
31
+ ];
32
+ static stabilityFields = [
33
+ "critical_stability",
34
+ "target_stability",
35
+ "stability_target_type",
10
36
  ];
11
37
  constructor(configuration) {
12
38
  super(configuration, ProjectAPI.filterFields);
@@ -44,4 +70,94 @@ export class ProjectAPI extends BaseAPI {
44
70
  body: data,
45
71
  });
46
72
  }
73
+ /**
74
+ * Retrieves the stability targets for a specific project.
75
+ * GET /projects/{project_id} (with internal header)
76
+ * @param projectId The ID of the project.
77
+ * @returns A promise that resolves to the project's stability targets.
78
+ */
79
+ async getProjectStabilityTargets(projectId) {
80
+ const url = `/projects/${projectId}`;
81
+ const response = await this.request({
82
+ method: "GET",
83
+ url,
84
+ });
85
+ return pickFields(response.body || {}, ProjectAPI.stabilityFields);
86
+ }
87
+ /**
88
+ * Lists builds for a specific project.
89
+ * GET /projects/{project_id}/releases
90
+ * @param projectId The ID of the project.
91
+ * @param opts Options for listing releases, including filtering by release stage.
92
+ * @returns A promise that resolves to an array of `ListReleasesResponse` objects.
93
+ */
94
+ async listBuilds(projectId, opts) {
95
+ const url = opts.next_url ?? `/projects/${projectId}/releases${opts.release_stage ? `?release_stage=${opts.release_stage}` : ""}`;
96
+ const response = await this.request({
97
+ method: "GET",
98
+ url,
99
+ });
100
+ return {
101
+ ...response,
102
+ body: pickFieldsFromArray(response.body || [], ProjectAPI.buildFields),
103
+ };
104
+ }
105
+ /**
106
+ * Retrieves a specific build from a project.
107
+ * GET /projects/{project_id}/releases/{release_id}
108
+ * @param projectId The ID of the project.
109
+ * @param buildId The ID of the release to retrieve.
110
+ * @returns A promise that resolves to the release data.
111
+ */
112
+ async getBuild(projectId, buildId) {
113
+ const url = `/projects/${projectId}/releases/${buildId}`;
114
+ return await this.request({
115
+ method: "GET",
116
+ url,
117
+ });
118
+ }
119
+ /**
120
+ * Lists releases for a specific project.
121
+ * GET /projects/{project_id}/release_groups
122
+ * @param projectId The ID of the project.
123
+ * @param opts Options for listing releases, including filtering by release stage and visibility.
124
+ * @returns A promise that resolves to an array of `ReleaseSummaryResponse` objects.
125
+ */
126
+ async listReleases(projectId, opts) {
127
+ const url = opts.next_url ?? `/projects/${projectId}/release_groups?release_stage_name=${opts.release_stage_name}&visible_only=${opts.visible_only}&top_only=true`;
128
+ const response = await this.request({
129
+ method: "GET",
130
+ url
131
+ });
132
+ return {
133
+ ...response,
134
+ body: pickFieldsFromArray(response.body || [], ProjectAPI.releaseFields),
135
+ };
136
+ }
137
+ /**
138
+ * Retrieves a specific release by its ID.
139
+ * GET /release_groups/{release_id}
140
+ * @param releaseId The ID of the release to retrieve.
141
+ * @returns A promise that resolves to the release data.
142
+ */
143
+ async getRelease(releaseId) {
144
+ const url = `/release_groups/${releaseId}`;
145
+ return await this.request({
146
+ method: "GET",
147
+ url,
148
+ });
149
+ }
150
+ /**
151
+ * Lists builds associated with a specific release group.
152
+ * GET /release_groups/{release_id}/releases
153
+ * @param releaseId The ID of the release group.
154
+ * @return A promise that resolves to an array of `BuildResponse` objects.
155
+ */
156
+ async listBuildsInRelease(releaseId) {
157
+ const url = `/release_groups/${releaseId}/releases`;
158
+ return await this.request({
159
+ method: "GET",
160
+ url,
161
+ }, true);
162
+ }
47
163
  }
@@ -12,6 +12,24 @@ export function pickFields(obj, keys) {
12
12
  export function pickFieldsFromArray(arr, keys) {
13
13
  return arr.map(obj => pickFields(obj, keys));
14
14
  }
15
+ // Utility to extract next URL path from Link header
16
+ export function getNextUrlPathFromHeader(headers, basePath) {
17
+ if (!headers)
18
+ return null;
19
+ const link = headers.get("link") || headers.get("Link");
20
+ if (!link)
21
+ return null;
22
+ const match = link.match(/<([^>]+)>;\s*rel="next"/)?.[1];
23
+ if (!match)
24
+ return null;
25
+ return match.replace(basePath, "");
26
+ }
27
+ // Ensure URL is absolute
28
+ // The MCP tools exposed use only the path for pagination
29
+ // For making requests, we need to ensure the URL is absolute
30
+ export function ensureFullUrl(url, basePath) {
31
+ return url.startsWith('http') ? url : `${basePath}${url}`;
32
+ }
15
33
  export class BaseAPI {
16
34
  configuration;
17
35
  filterFields;
@@ -30,11 +48,11 @@ export class BaseAPI {
30
48
  headers,
31
49
  body: options.body ? JSON.stringify(options.body) : undefined,
32
50
  };
33
- const url = options.url.startsWith('http') ? options.url : `${this.configuration.basePath || ''}${options.url}`;
34
51
  let results = [];
35
- let nextUrl = url;
52
+ let nextUrl = options.url;
36
53
  let apiResponse;
37
54
  do {
55
+ nextUrl = ensureFullUrl(nextUrl, this.configuration.basePath);
38
56
  const response = await fetch(nextUrl, fetchOptions);
39
57
  if (!response.ok) {
40
58
  const errorText = await response.text();
@@ -47,14 +65,7 @@ export class BaseAPI {
47
65
  const data = await response.json();
48
66
  if (paginate) {
49
67
  results = results.concat(data);
50
- const link = response.headers.get('Link');
51
- if (link) {
52
- const match = link.match(/<([^>]+)>;\s*rel="next"/);
53
- nextUrl = match ? match[1] : undefined;
54
- }
55
- else {
56
- nextUrl = undefined;
57
- }
68
+ nextUrl = getNextUrlPathFromHeader(response.headers, this.configuration.basePath);
58
69
  }
59
70
  else {
60
71
  apiResponse.body = data;