@smartbear/mcp 0.6.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.
Files changed (41) hide show
  1. package/README.md +37 -3
  2. package/dist/api-hub/client/api.js +387 -0
  3. package/dist/api-hub/client/configuration.js +27 -0
  4. package/dist/api-hub/client/index.js +5 -0
  5. package/dist/api-hub/client/portal-types.js +131 -0
  6. package/dist/api-hub/client/registry-types.js +69 -0
  7. package/dist/api-hub/client/tools.js +98 -0
  8. package/dist/api-hub/client.js +70 -404
  9. package/dist/bugsnag/client/api/CurrentUser.js +19 -13
  10. package/dist/bugsnag/client/api/Error.js +45 -57
  11. package/dist/bugsnag/client/api/Project.js +35 -30
  12. package/dist/bugsnag/client/api/base.js +24 -9
  13. package/dist/bugsnag/client/api/filters.js +9 -9
  14. package/dist/bugsnag/client.js +281 -373
  15. package/dist/common/info.js +1 -1
  16. package/dist/common/server.js +39 -28
  17. package/dist/index.js +18 -4
  18. package/dist/pactflow/client/ai.js +20 -20
  19. package/dist/pactflow/client/base.js +48 -13
  20. package/dist/pactflow/client/prompts.js +10 -12
  21. package/dist/pactflow/client/tools.js +18 -18
  22. package/dist/pactflow/client/utils.js +1 -1
  23. package/dist/pactflow/client.js +23 -15
  24. package/dist/qmetry/client/api/client-api.js +39 -0
  25. package/dist/qmetry/client/handlers.js +11 -0
  26. package/dist/qmetry/client/project.js +27 -0
  27. package/dist/qmetry/client/testcase.js +104 -0
  28. package/dist/qmetry/client/tools.js +222 -0
  29. package/dist/qmetry/client.js +95 -0
  30. package/dist/qmetry/config/constants.js +12 -0
  31. package/dist/qmetry/config/rest-endpoints.js +11 -0
  32. package/dist/qmetry/types/common.js +174 -0
  33. package/dist/qmetry/types/testcase.js +19 -0
  34. package/dist/reflect/client.js +14 -14
  35. package/dist/zephyr/client.js +16 -0
  36. package/dist/zephyr/common/api-client.js +27 -0
  37. package/dist/zephyr/common/auth-service.js +14 -0
  38. package/dist/zephyr/common/types.js +35 -0
  39. package/dist/zephyr/tool/project/get-projects.js +54 -0
  40. package/dist/zephyr/tool/zephyr-tool.js +1 -0
  41. package/package.json +8 -6
@@ -1,9 +1,13 @@
1
- import { BaseAPI, pickFieldsFromArray } from './base.js';
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
- static filterFields = ["collaborators_url", "projects_url", "upgrade_url"];
5
- static organizationFields = ['id', 'name', 'slug'];
6
- static projectFields = ['id', 'name', 'slug', 'api_key'];
5
+ static filterFields = [
6
+ "collaborators_url",
7
+ "projects_url",
8
+ "upgrade_url",
9
+ ];
10
+ static organizationFields = ["id", "name", "slug"];
7
11
  constructor(configuration) {
8
12
  super(configuration, CurrentUserAPI.filterFields);
9
13
  }
@@ -12,23 +16,25 @@ export class CurrentUserAPI extends BaseAPI {
12
16
  * GET /user/organizations
13
17
  */
14
18
  async listUserOrganizations(options = {}) {
15
- const { admin, paginate = false, ...queryOptions } = options;
19
+ const { admin, ...queryOptions } = options;
16
20
  const params = new URLSearchParams();
17
21
  if (admin !== undefined)
18
- params.append('admin', String(admin));
22
+ params.append("admin", String(admin));
19
23
  for (const [key, value] of Object.entries(queryOptions)) {
20
24
  if (value !== undefined)
21
25
  params.append(key, String(value));
22
26
  }
23
- const url = params.toString() ? `/user/organizations?${params}` : '/user/organizations';
27
+ const url = params.toString()
28
+ ? `/user/organizations?${params}`
29
+ : "/user/organizations";
24
30
  const data = await this.request({
25
- method: 'GET',
31
+ method: "GET",
26
32
  url,
27
- }, paginate);
33
+ });
28
34
  // Only return allowed fields
29
35
  return {
30
36
  ...data,
31
- body: pickFieldsFromArray(data.body || [], CurrentUserAPI.organizationFields)
37
+ body: pickFieldsFromArray(data.body || [], CurrentUserAPI.organizationFields),
32
38
  };
33
39
  }
34
40
  /**
@@ -49,12 +55,12 @@ export class CurrentUserAPI extends BaseAPI {
49
55
  ? `/organizations/${organizationId}/projects?${params}`
50
56
  : `/organizations/${organizationId}/projects`;
51
57
  const data = await this.request({
52
- method: 'GET',
58
+ method: "GET",
53
59
  url,
54
- }, true); // Always paginate for projects
60
+ });
55
61
  return {
56
62
  ...data,
57
- body: pickFieldsFromArray(data.body || [], CurrentUserAPI.projectFields)
63
+ body: pickFieldsFromArray(data.body || [], ProjectAPI.projectFields),
58
64
  };
59
65
  }
60
66
  }
@@ -1,24 +1,24 @@
1
- import { BaseAPI } from './base.js';
2
- import { toQueryString } from './filters.js';
1
+ import { BaseAPI } from "./base.js";
2
+ import { toQueryString } from "./filters.js";
3
3
  export const ErrorOperations = [
4
- 'override_severity',
5
- 'assign',
6
- 'create_issue',
7
- 'link_issue',
8
- 'unlink_issue',
9
- 'open',
10
- 'snooze',
11
- 'fix',
12
- 'ignore',
13
- 'delete',
14
- 'discard',
15
- 'undiscard'
4
+ "override_severity",
5
+ "assign",
6
+ "create_issue",
7
+ "link_issue",
8
+ "unlink_issue",
9
+ "open",
10
+ "snooze",
11
+ "fix",
12
+ "ignore",
13
+ "delete",
14
+ "discard",
15
+ "undiscard",
16
16
  ];
17
17
  export const ReopenConditions = [
18
- 'occurs_after',
19
- 'n_occurrences_in_m_hours',
20
- 'n_additional_occurrences',
21
- 'n_additional_users'
18
+ "occurs_after",
19
+ "n_occurrences_in_m_hours",
20
+ "n_additional_occurrences",
21
+ "n_additional_users",
22
22
  ];
23
23
  // --- API Class ---
24
24
  export class ErrorAPI extends BaseAPI {
@@ -40,38 +40,26 @@ export class ErrorAPI extends BaseAPI {
40
40
  ? `/projects/${projectId}/errors/${errorId}?${params}`
41
41
  : `/projects/${projectId}/errors/${errorId}`;
42
42
  return (await this.request({
43
- method: 'GET',
43
+ method: "GET",
44
44
  url,
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({
72
- method: 'GET',
53
+ const response = await this.request({
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
@@ -87,7 +75,7 @@ export class ErrorAPI extends BaseAPI {
87
75
  ? `/projects/${projectId}/events/${eventId}?${params}`
88
76
  : `/projects/${projectId}/events/${eventId}`;
89
77
  return (await this.request({
90
- method: 'GET',
78
+ method: "GET",
91
79
  url,
92
80
  }));
93
81
  }
@@ -98,10 +86,10 @@ 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
- nextUrl.searchParams.set('per_page', options.per_page.toString());
92
+ nextUrl.searchParams.set("per_page", options.per_page.toString());
105
93
  }
106
94
  url = nextUrl.toString();
107
95
  }
@@ -116,25 +104,25 @@ export class ErrorAPI extends BaseAPI {
116
104
  }
117
105
  // Add pagination and sorting parameters
118
106
  if (options.base !== undefined) {
119
- params.append('base', options.base);
107
+ params.append("base", options.base);
120
108
  }
121
109
  if (options.sort !== undefined) {
122
- params.append('sort', options.sort);
110
+ params.append("sort", options.sort);
123
111
  }
124
112
  if (options.direction !== undefined) {
125
- params.append('direction', options.direction);
113
+ params.append("direction", options.direction);
126
114
  }
127
115
  if (options.per_page !== undefined) {
128
- params.append('per_page', options.per_page.toString());
116
+ params.append("per_page", options.per_page.toString());
129
117
  }
130
118
  if (params.size > 0) {
131
119
  url = `/projects/${projectId}/errors?${params}`;
132
120
  }
133
121
  }
134
122
  return (await this.request({
135
- method: 'GET',
123
+ method: "GET",
136
124
  url,
137
- }));
125
+ }, false));
138
126
  }
139
127
  /**
140
128
  * Update an Error on a Project
@@ -150,7 +138,7 @@ export class ErrorAPI extends BaseAPI {
150
138
  ? `/projects/${projectId}/errors/${errorId}?${params}`
151
139
  : `/projects/${projectId}/errors/${errorId}`;
152
140
  return (await this.request({
153
- method: 'PATCH',
141
+ method: "PATCH",
154
142
  url,
155
143
  body: data,
156
144
  }));
@@ -168,21 +156,21 @@ export class ErrorAPI extends BaseAPI {
168
156
  });
169
157
  }
170
158
  if (options.summary_size !== undefined) {
171
- params.append('summary_size', options.summary_size.toString());
159
+ params.append("summary_size", options.summary_size.toString());
172
160
  }
173
161
  if (options.pivots && options.pivots.length > 0) {
174
- options.pivots.forEach(pivot => {
175
- params.append('pivots[]', pivot);
162
+ options.pivots.forEach((pivot) => {
163
+ params.append("pivots[]", pivot);
176
164
  });
177
165
  }
178
166
  if (options.per_page !== undefined) {
179
- params.append('per_page', options.per_page.toString());
167
+ params.append("per_page", options.per_page.toString());
180
168
  }
181
169
  const url = params.toString()
182
170
  ? `/projects/${projectId}/errors/${errorId}/pivots?${params}`
183
171
  : `/projects/${projectId}/errors/${errorId}/pivots`;
184
172
  return await this.request({
185
- method: 'GET',
173
+ method: "GET",
186
174
  url,
187
175
  });
188
176
  }
@@ -1,8 +1,27 @@
1
- import { BaseAPI, pickFieldsFromArray, pickFields } from "./base.js";
1
+ import { BaseAPI, pickFieldsFromArray } from "./base.js";
2
2
  // --- API Class ---
3
3
  export class ProjectAPI extends BaseAPI {
4
- static filterFields = ["errors_url", "events_url", "url", "html_url"];
5
- static eventFieldFields = ["custom", "display_id", "filter_options", "pivot_options"];
4
+ static projectFields = [
5
+ "id",
6
+ "name",
7
+ "slug",
8
+ "api_key",
9
+ "stability_target_type",
10
+ "target_stability",
11
+ "critical_stability",
12
+ ];
13
+ static filterFields = [
14
+ "errors_url",
15
+ "events_url",
16
+ "url",
17
+ "html_url",
18
+ ];
19
+ static eventFieldFields = [
20
+ "custom",
21
+ "display_id",
22
+ "filter_options",
23
+ "pivot_options",
24
+ ];
6
25
  static buildFields = [
7
26
  "id",
8
27
  "release_time",
@@ -29,11 +48,6 @@ export class ProjectAPI extends BaseAPI {
29
48
  "accumulative_daily_users_seen",
30
49
  "accumulative_daily_users_with_unhandled",
31
50
  ];
32
- static stabilityFields = [
33
- "critical_stability",
34
- "target_stability",
35
- "stability_target_type",
36
- ];
37
51
  constructor(configuration) {
38
52
  super(configuration, ProjectAPI.filterFields);
39
53
  }
@@ -46,13 +60,13 @@ export class ProjectAPI extends BaseAPI {
46
60
  async listProjectEventFields(projectId) {
47
61
  const url = `/projects/${projectId}/event_fields`;
48
62
  const data = await this.request({
49
- method: 'GET',
63
+ method: "GET",
50
64
  url,
51
65
  });
52
66
  // Only return allowed fields
53
67
  return {
54
68
  ...data,
55
- body: pickFieldsFromArray(data.body || [], ProjectAPI.eventFieldFields)
69
+ body: pickFieldsFromArray(data.body || [], ProjectAPI.eventFieldFields),
56
70
  };
57
71
  }
58
72
  /**
@@ -65,25 +79,11 @@ export class ProjectAPI extends BaseAPI {
65
79
  async createProject(organizationId, data) {
66
80
  const url = `/organizations/${organizationId}/projects`;
67
81
  return await this.request({
68
- method: 'POST',
82
+ method: "POST",
69
83
  url,
70
84
  body: data,
71
85
  });
72
86
  }
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
87
  /**
88
88
  * Lists builds for a specific project.
89
89
  * GET /projects/{project_id}/releases
@@ -92,11 +92,12 @@ export class ProjectAPI extends BaseAPI {
92
92
  * @returns A promise that resolves to an array of `ListReleasesResponse` objects.
93
93
  */
94
94
  async listBuilds(projectId, opts) {
95
- const url = opts.next_url ?? `/projects/${projectId}/releases${opts.release_stage ? `?release_stage=${opts.release_stage}` : ""}`;
95
+ const url = opts.next_url ??
96
+ `/projects/${projectId}/releases${opts.release_stage ? `?release_stage=${opts.release_stage}` : ""}`;
96
97
  const response = await this.request({
97
98
  method: "GET",
98
99
  url,
99
- });
100
+ }, false);
100
101
  return {
101
102
  ...response,
102
103
  body: pickFieldsFromArray(response.body || [], ProjectAPI.buildFields),
@@ -124,11 +125,15 @@ export class ProjectAPI extends BaseAPI {
124
125
  * @returns A promise that resolves to an array of `ReleaseSummaryResponse` objects.
125
126
  */
126
127
  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 url = opts.next_url ??
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}`;
128
133
  const response = await this.request({
129
134
  method: "GET",
130
- url
131
- });
135
+ url,
136
+ }, false);
132
137
  return {
133
138
  ...response,
134
139
  body: pickFieldsFromArray(response.body || [], ProjectAPI.releaseFields),
@@ -10,7 +10,7 @@ export function pickFields(obj, keys) {
10
10
  }
11
11
  // Utility to pick only allowed fields from an array of objects
12
12
  export function pickFieldsFromArray(arr, keys) {
13
- return arr.map(obj => pickFields(obj, keys));
13
+ return arr.map((obj) => pickFields(obj, keys));
14
14
  }
15
15
  // Utility to extract next URL path from Link header
16
16
  export function getNextUrlPathFromHeader(headers, basePath) {
@@ -24,11 +24,21 @@ 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
30
40
  export function ensureFullUrl(url, basePath) {
31
- return url.startsWith('http') ? url : `${basePath}${url}`;
41
+ return url.startsWith("http") ? url : `${basePath}${url}`;
32
42
  }
33
43
  export class BaseAPI {
34
44
  configuration;
@@ -37,12 +47,12 @@ 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,
44
54
  };
45
- headers['Authorization'] = `token ${this.configuration.authToken}`;
55
+ headers.Authorization = `token ${this.configuration.authToken}`;
46
56
  const fetchOptions = {
47
57
  method: options.method,
48
58
  headers,
@@ -52,6 +62,9 @@ export class BaseAPI {
52
62
  let nextUrl = options.url;
53
63
  let apiResponse;
54
64
  do {
65
+ if (!this.configuration.basePath) {
66
+ throw new Error("Base path is not configured for API requests");
67
+ }
55
68
  nextUrl = ensureFullUrl(nextUrl, this.configuration.basePath);
56
69
  const response = await fetch(nextUrl, fetchOptions);
57
70
  if (!response.ok) {
@@ -60,19 +73,21 @@ export class BaseAPI {
60
73
  }
61
74
  apiResponse = {
62
75
  status: response.status,
63
- headers: response.headers
76
+ headers: response.headers,
64
77
  };
65
78
  const data = await response.json();
66
- if (paginate) {
79
+ nextUrl = getNextUrlPathFromHeader(response.headers, this.configuration.basePath);
80
+ if (Array.isArray(data)) {
67
81
  results = results.concat(data);
68
- nextUrl = getNextUrlPathFromHeader(response.headers, this.configuration.basePath);
82
+ apiResponse.nextUrl = nextUrl;
69
83
  }
70
84
  else {
71
85
  apiResponse.body = data;
72
86
  }
73
- } while (paginate && nextUrl);
74
- if (paginate) {
87
+ } while (fetchAll && nextUrl);
88
+ if (results.length > 0) {
75
89
  apiResponse.body = results;
90
+ apiResponse.totalCount = getTotalCountFromHeader(apiResponse.headers);
76
91
  }
77
92
  if (Array.isArray(apiResponse.body)) {
78
93
  apiResponse.body.forEach(this.sanitizeResponse.bind(this));
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { z } from "zod";
8
8
  export const FilterValueSchema = z.object({
9
- type: z.enum(['eq', 'ne', 'empty']),
9
+ type: z.enum(["eq", "ne", "empty"]),
10
10
  value: z.union([z.string(), z.boolean(), z.number()]),
11
11
  });
12
12
  export const FilterObjectSchema = z.record(z.array(FilterValueSchema));
@@ -18,7 +18,7 @@ export const FilterObjectSchema = z.record(z.array(FilterValueSchema));
18
18
  */
19
19
  export function equals(value) {
20
20
  return {
21
- type: 'eq',
21
+ type: "eq",
22
22
  value,
23
23
  };
24
24
  }
@@ -30,7 +30,7 @@ export function equals(value) {
30
30
  */
31
31
  export function notEquals(value) {
32
32
  return {
33
- type: 'ne',
33
+ type: "ne",
34
34
  value,
35
35
  };
36
36
  }
@@ -42,7 +42,7 @@ export function notEquals(value) {
42
42
  */
43
43
  export function empty(isEmpty) {
44
44
  return {
45
- type: 'empty',
45
+ type: "empty",
46
46
  value: isEmpty.toString(),
47
47
  };
48
48
  }
@@ -55,7 +55,7 @@ export function empty(isEmpty) {
55
55
  */
56
56
  export function relativeTime(value, unit) {
57
57
  return {
58
- type: 'eq',
58
+ type: "eq",
59
59
  value: `${value}${unit}`,
60
60
  };
61
61
  }
@@ -67,7 +67,7 @@ export function relativeTime(value, unit) {
67
67
  */
68
68
  export function isoTime(date) {
69
69
  return {
70
- type: 'eq',
70
+ type: "eq",
71
71
  value: date.toISOString(),
72
72
  };
73
73
  }
@@ -128,8 +128,8 @@ export function addFilter(filters, field, filterValue) {
128
128
  * @returns The updated FilterObject for chaining
129
129
  */
130
130
  export function addTimeRange(filters, since, before) {
131
- addFilter(filters, 'event.since', isoTime(since));
132
- addFilter(filters, 'event.before', isoTime(before));
131
+ addFilter(filters, "event.since", isoTime(since));
132
+ addFilter(filters, "event.before", isoTime(before));
133
133
  return filters;
134
134
  }
135
135
  /**
@@ -141,7 +141,7 @@ export function addTimeRange(filters, since, before) {
141
141
  * @returns The updated FilterObject for chaining
142
142
  */
143
143
  export function addRelativeTimeRange(filters, amount, unit) {
144
- addFilter(filters, 'event.since', relativeTime(amount, unit));
144
+ addFilter(filters, "event.since", relativeTime(amount, unit));
145
145
  return filters;
146
146
  }
147
147
  /**