@smartbear/mcp 0.27.2 → 0.29.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.
@@ -1,3 +1,4 @@
1
+ import { appendClientIdentity } from "../../common/info.js";
1
2
  import { ToolError } from "../../common/tools.js";
2
3
  const API_HOSTNAME = "api.reflect.run";
3
4
  const FUNCTIONAL_TESTING_API_KEY_HEADER = "X-API-KEY";
@@ -16,11 +17,27 @@ class FunctionalTestingAPI {
16
17
  return {
17
18
  [FUNCTIONAL_TESTING_API_KEY_HEADER]: token,
18
19
  "Content-Type": "application/json",
19
- "User-Agent": this.userAgent
20
+ "User-Agent": appendClientIdentity(this.userAgent)
20
21
  };
21
22
  }
23
+ async ftFetch(input, init) {
24
+ let response;
25
+ try {
26
+ response = await fetch(input, init);
27
+ } catch {
28
+ throw new ToolError(
29
+ "Swagger Functional Testing service is currently unreachable. Retry after a moment."
30
+ );
31
+ }
32
+ if (response.status === 401 || response.status === 403) {
33
+ throw new ToolError(
34
+ "Authentication failed. Verify your API token is valid and has not expired."
35
+ );
36
+ }
37
+ return response;
38
+ }
22
39
  async listTests() {
23
- const response = await fetch(`${this.baseUrl}/tests`, {
40
+ const response = await this.ftFetch(`${this.baseUrl}/tests`, {
24
41
  method: "GET",
25
42
  headers: this.getFtHeaders()
26
43
  });
@@ -33,8 +50,8 @@ class FunctionalTestingAPI {
33
50
  }
34
51
  async runTest(args) {
35
52
  if (!args.testId) throw new ToolError("testId argument is required");
36
- const response = await fetch(
37
- `${this.baseUrl}/tests/${args.testId}/executions`,
53
+ const response = await this.ftFetch(
54
+ `${this.baseUrl}/tests/${encodeURIComponent(args.testId)}/executions`,
38
55
  {
39
56
  method: "POST",
40
57
  headers: this.getFtHeaders()
@@ -51,8 +68,8 @@ class FunctionalTestingAPI {
51
68
  if (!args.executionId) {
52
69
  throw new ToolError("executionId argument is required");
53
70
  }
54
- const response = await fetch(
55
- `${this.baseUrl}/executions/${args.executionId}`,
71
+ const response = await this.ftFetch(
72
+ `${this.baseUrl}/executions/${encodeURIComponent(args.executionId)}`,
56
73
  {
57
74
  method: "GET",
58
75
  headers: this.getFtHeaders()
@@ -63,8 +80,173 @@ class FunctionalTestingAPI {
63
80
  `Failed to get test status: ${response.status} ${response.statusText}`
64
81
  );
65
82
  }
83
+ const data = await response.json();
84
+ if (Array.isArray(data.tests)) {
85
+ for (const test of data.tests) {
86
+ const run = test.run;
87
+ if (run) {
88
+ delete run.videoUrl;
89
+ }
90
+ }
91
+ }
92
+ return data;
93
+ }
94
+ async listSuiteExecutions(args) {
95
+ if (!args.suiteId) throw new ToolError("suiteId argument is required");
96
+ const response = await this.ftFetch(
97
+ `${this.baseUrl}/suites/${encodeURIComponent(args.suiteId)}/executions`,
98
+ {
99
+ method: "GET",
100
+ headers: this.getFtHeaders()
101
+ }
102
+ );
103
+ if (!response.ok) {
104
+ throw new ToolError(suiteExecutionsErrorMessage(response));
105
+ }
106
+ const data = await response.json();
107
+ return {
108
+ ...data,
109
+ executions: {
110
+ data: data.executions.data.map(
111
+ ({ executionId, status, isFinished }) => ({
112
+ executionId,
113
+ status,
114
+ isFinished
115
+ })
116
+ )
117
+ }
118
+ };
119
+ }
120
+ async listSuites() {
121
+ const response = await this.ftFetch(`${this.baseUrl}/suites`, {
122
+ method: "GET",
123
+ headers: this.getFtHeaders()
124
+ });
125
+ if (!response.ok) {
126
+ throw new ToolError(
127
+ `Failed to list Functional Testing suites: ${response.status} ${response.statusText}`
128
+ );
129
+ }
66
130
  return response.json();
67
131
  }
132
+ async cancelSuiteExecution(args) {
133
+ if (!args.suiteId) throw new ToolError("suiteId argument is required");
134
+ if (!args.executionId) {
135
+ throw new ToolError("executionId argument is required");
136
+ }
137
+ const response = await this.ftFetch(
138
+ `${this.baseUrl}/suites/${encodeURIComponent(args.suiteId)}/executions/${encodeURIComponent(args.executionId)}/cancel`,
139
+ {
140
+ method: "PATCH",
141
+ headers: this.getFtHeaders()
142
+ }
143
+ );
144
+ if (!response.ok) {
145
+ throw new ToolError(cancelSuiteExecutionErrorMessage(response));
146
+ }
147
+ return response.json();
148
+ }
149
+ async runSuite(args) {
150
+ if (!args.suiteId) throw new ToolError("suiteId argument is required");
151
+ const body = args.tunnelAgentName ? JSON.stringify({
152
+ overrides: { agent: { name: args.tunnelAgentName } }
153
+ }) : void 0;
154
+ let response;
155
+ try {
156
+ response = await fetch(
157
+ `${this.baseUrl}/suites/${args.suiteId}/executions`,
158
+ {
159
+ method: "POST",
160
+ headers: this.getFtHeaders(),
161
+ body
162
+ }
163
+ );
164
+ } catch {
165
+ throw new ToolError(
166
+ "Swagger Functional Testing service is currently unreachable. Retry after a moment."
167
+ );
168
+ }
169
+ if (response.status === 401 || response.status === 403) {
170
+ throw new ToolError(
171
+ "Authentication failed. Verify your API token is valid and has not expired."
172
+ );
173
+ }
174
+ if (!response.ok) {
175
+ throw new ToolError(
176
+ `Failed to run suite: ${response.status} ${response.statusText}`
177
+ );
178
+ }
179
+ return this.withoutField("url", response);
180
+ }
181
+ async getSuiteExecution(args) {
182
+ if (!args.suiteId) throw new ToolError("suiteId argument is required");
183
+ if (!args.executionId) {
184
+ throw new ToolError("executionId argument is required");
185
+ }
186
+ let response;
187
+ try {
188
+ response = await fetch(
189
+ `${this.baseUrl}/suites/${args.suiteId}/executions/${args.executionId}`,
190
+ {
191
+ method: "GET",
192
+ headers: this.getFtHeaders()
193
+ }
194
+ );
195
+ } catch {
196
+ throw new ToolError(
197
+ "Swagger Functional Testing service is currently unreachable. Retry after a moment."
198
+ );
199
+ }
200
+ if (response.status === 401 || response.status === 403) {
201
+ throw new ToolError(
202
+ "Authentication failed. Verify your API token is valid and has not expired."
203
+ );
204
+ }
205
+ if (!response.ok) {
206
+ throw new ToolError(
207
+ `Failed to get suite execution status: ${response.status} ${response.statusText}`
208
+ );
209
+ }
210
+ const data = await this.withoutField("url", response);
211
+ const testsData = data.tests?.data;
212
+ if (Array.isArray(testsData)) {
213
+ for (const test of testsData) {
214
+ if (Array.isArray(test.runs)) {
215
+ for (const run of test.runs) {
216
+ delete run.videoUrl;
217
+ }
218
+ }
219
+ }
220
+ }
221
+ return data;
222
+ }
223
+ async withoutField(field, response) {
224
+ const data = await response.json();
225
+ delete data[field];
226
+ return data;
227
+ }
228
+ }
229
+ function suiteExecutionsErrorMessage(response) {
230
+ switch (response.status) {
231
+ // Defensive: the Reflect API currently returns 200 with an empty
232
+ // `executions.data` list for an unknown suiteId rather than a 404, so this
233
+ // branch is not expected to fire today. Kept in case the API starts
234
+ // returning 404 for missing suites.
235
+ case 404:
236
+ return "Test suite not found. Verify the suiteId is correct and belongs to your workspace.";
237
+ default:
238
+ return `Failed to list suite executions: ${response.status} ${response.statusText}`;
239
+ }
240
+ }
241
+ function cancelSuiteExecutionErrorMessage(response) {
242
+ switch (response.status) {
243
+ case 404:
244
+ return "Suite execution not found. Verify the suiteId and executionId are correct and belong to your workspace.";
245
+ case 409:
246
+ return "Suite execution cannot be cancelled because it has already finished.";
247
+ default:
248
+ return `Failed to cancel suite execution: ${response.status} ${response.statusText}`;
249
+ }
68
250
  }
69
251
  export {
70
252
  FUNCTIONAL_TESTING_API_KEY_HEADER,
@@ -1,10 +1,12 @@
1
- import { RunFunctionalTestingTestParamsSchema, GetFunctionalTestingExecutionTestSchema } from "./functional-testing-types.js";
1
+ import { RunFunctionalTestingTestParamsSchema, GetFunctionalTestingExecutionTestSchema, ListFunctionalTestingSuiteExecutionsSchema, CancelFunctionalTestingSuiteExecutionSchema, RunFunctionalTestingSuiteParamsSchema, GetFunctionalTestingSuiteExecutionSchema } from "./functional-testing-types.js";
2
2
  const FUNCTIONAL_TESTING_TOOLS = [
3
3
  {
4
4
  title: "List Tests",
5
5
  toolset: "Functional Testing",
6
6
  summary: "Lists all API tests available in your Swagger Functional Testing account. Use this tool when you need to discover available tests before running them or checking their status. Do not use this tool to retrieve test execution results or history.",
7
- handler: "listFunctionalTestingTests"
7
+ handler: "listFunctionalTestingTests",
8
+ idempotent: true,
9
+ readOnly: true
8
10
  },
9
11
  {
10
12
  title: "Run Test",
@@ -21,7 +23,52 @@ const FUNCTIONAL_TESTING_TOOLS = [
21
23
  summary: "Get the status of a Swagger Functional Testing test execution. It returns information about the execution such as its status (running, passed or failed), run time, as well as the break down of the status of each test step.",
22
24
  inputSchema: GetFunctionalTestingExecutionTestSchema,
23
25
  handler: "getFunctionalTestingExecution",
26
+ idempotent: true,
27
+ readOnly: true
28
+ },
29
+ {
30
+ title: "List Suite Executions",
31
+ toolset: "Functional Testing",
32
+ summary: "Lists all executions for a given test suite in your Swagger Functional Testing workspace. Use this tool when you need to review execution history and timings for a specific suite. Do not use this tool to retrieve the status of a single execution or individual test results.",
33
+ inputSchema: ListFunctionalTestingSuiteExecutionsSchema,
34
+ handler: "listFunctionalTestingSuiteExecutions",
35
+ readOnly: true,
36
+ idempotent: true
37
+ },
38
+ {
39
+ title: "Cancel Suite Execution",
40
+ toolset: "Functional Testing",
41
+ summary: "Cancels an ongoing test suite execution in your Swagger Functional Testing workspace. Use this tool when you need to stop a long-running or accidentally triggered suite run. Do not use this tool to cancel individual test runs.",
42
+ inputSchema: CancelFunctionalTestingSuiteExecutionSchema,
43
+ handler: "cancelFunctionalTestingSuiteExecution",
44
+ readOnly: false,
24
45
  idempotent: false
46
+ },
47
+ {
48
+ title: "List Suites",
49
+ toolset: "Functional Testing",
50
+ summary: "Lists all test suites available in your Swagger Functional Testing workspace. Use this tool when you need to discover available suites before running them or checking their execution history. Do not use this tool to retrieve individual tests or test suite execution results.",
51
+ handler: "listFunctionalTestingSuites",
52
+ idempotent: true,
53
+ readOnly: true
54
+ },
55
+ {
56
+ title: "Run Suite",
57
+ toolset: "Functional Testing",
58
+ summary: "Runs a specific test suite in your Swagger Functional Testing workspace. The execution is asynchronous — it returns an executionId, not results directly. Use `swagger_get_suite_status` with your suiteId and executionId to track progress and retrieve the final per-test results. Optionally accepts a `tunnelAgentName` argument to override the suite's saved tunnel for this run. Do not use this tool to run a single test — use `swagger_run_test` instead.",
59
+ inputSchema: RunFunctionalTestingSuiteParamsSchema,
60
+ handler: "runFunctionalTestingSuite",
61
+ idempotent: false,
62
+ readOnly: false
63
+ },
64
+ {
65
+ title: "Get Suite Status",
66
+ toolset: "Functional Testing",
67
+ summary: "Get the status of a Swagger Functional Testing suite execution. Returns the overall status (pending, canceled, passed or failed), whether the run is finished, and a per-test breakdown with pass/fail. Use this to poll for the outcome of a suite run triggered by `swagger_run_suite`. Requires both `suiteId` and the `executionId` arguments returned by `swagger_run_suite`.",
68
+ inputSchema: GetFunctionalTestingSuiteExecutionSchema,
69
+ handler: "getFunctionalTestingSuiteExecution",
70
+ idempotent: true,
71
+ readOnly: true
25
72
  }
26
73
  ];
27
74
  export {
@@ -5,7 +5,28 @@ const RunFunctionalTestingTestParamsSchema = z.object({
5
5
  const GetFunctionalTestingExecutionTestSchema = z.object({
6
6
  executionId: z.string().describe("ID of the Functional Testing execution").trim().min(1)
7
7
  });
8
+ const ListFunctionalTestingSuiteExecutionsSchema = z.object({
9
+ suiteId: z.string().describe("ID of the Functional Testing suite to list executions for").trim().min(1)
10
+ });
11
+ const CancelFunctionalTestingSuiteExecutionSchema = z.object({
12
+ suiteId: z.string().describe("ID of the Functional Testing suite the execution belongs to").trim().min(1),
13
+ executionId: z.string().describe("ID of the Functional Testing suite execution to cancel").trim().min(1)
14
+ });
15
+ const RunFunctionalTestingSuiteParamsSchema = z.object({
16
+ suiteId: z.string().describe("ID of the Functional Testing suite to run").trim().min(1),
17
+ tunnelAgentName: z.string().describe(
18
+ "Optional tunnel agent name to override the suite's saved tunnel for this run. When omitted, the suite's saved tunnel overrides are used, falling back to each test's saved tunnel."
19
+ ).trim().min(1).optional()
20
+ });
21
+ const GetFunctionalTestingSuiteExecutionSchema = z.object({
22
+ suiteId: z.string().describe("ID of the Functional Testing suite").trim().min(1),
23
+ executionId: z.string().describe("ID of the Functional Testing suite execution").trim().min(1)
24
+ });
8
25
  export {
26
+ CancelFunctionalTestingSuiteExecutionSchema,
9
27
  GetFunctionalTestingExecutionTestSchema,
28
+ GetFunctionalTestingSuiteExecutionSchema,
29
+ ListFunctionalTestingSuiteExecutionsSchema,
30
+ RunFunctionalTestingSuiteParamsSchema,
10
31
  RunFunctionalTestingTestParamsSchema
11
32
  };
@@ -216,7 +216,10 @@ const CreateDocumentationPageArgsSchema = z.object({
216
216
  portalId: z.string().describe("Portal UUID or subdomain - unique identifier for the portal"),
217
217
  productId: z.string().uuid().describe("Product UUID - unique identifier for the product"),
218
218
  pageTitle: z.string().describe(
219
- "Title of the documentation page - will be displayed in navigation (3-255 characters, used to generate the page slug)"
219
+ "Title of the documentation page - will be displayed in navigation (3-255 characters)"
220
+ ),
221
+ pageSlug: z.string().optional().describe(
222
+ "URL slug for the documentation page. 3-255 characters, lowercase, alphanumeric with hyphens, underscores, or dots (e.g. 'my-page'). If not provided, the slug is generated from the page title."
220
223
  ),
221
224
  pageContent: z.string().optional().describe(
222
225
  "Content of the documentation page. Provide HTML when contentType is 'html', Markdown when contentType is 'markdown'."
@@ -181,6 +181,22 @@ class SwaggerClient {
181
181
  async getFunctionalTestingExecution(args) {
182
182
  return this.withFunctionalTesting((ftApi) => ftApi.getTestExecution(args));
183
183
  }
184
+ async listFunctionalTestingSuiteExecutions(args) {
185
+ return this.withFunctionalTesting(
186
+ (ftApi) => ftApi.listSuiteExecutions(args)
187
+ );
188
+ }
189
+ async cancelFunctionalTestingSuiteExecution(args) {
190
+ return this.withFunctionalTesting(
191
+ (ftApi) => ftApi.cancelSuiteExecution(args)
192
+ );
193
+ }
194
+ async runFunctionalTestingSuite(args) {
195
+ return this.withFunctionalTesting((ftApi) => ftApi.runSuite(args));
196
+ }
197
+ async getFunctionalTestingSuiteExecution(args) {
198
+ return this.withFunctionalTesting((ftApi) => ftApi.getSuiteExecution(args));
199
+ }
184
200
  /**
185
201
  * Perform an operation with the Functional Testing API.
186
202
  * Throws a ToolError if Functional Testing is not configured
@@ -191,6 +207,9 @@ class SwaggerClient {
191
207
  }
192
208
  return fn(this.ftApi);
193
209
  }
210
+ async listFunctionalTestingSuites() {
211
+ return this.withFunctionalTesting((ftApi) => ftApi.listSuites());
212
+ }
194
213
  async registerTools(register, _getInput) {
195
214
  TOOLS.forEach((tool) => {
196
215
  if (tool.toolset === "Functional Testing" && !this.ftApi) {
@@ -1,4 +1,4 @@
1
- import { USER_AGENT } from "../../common/info.js";
1
+ import { getUserAgent } from "../../common/info.js";
2
2
  class AuthService {
3
3
  bearerToken;
4
4
  constructor(accessToken) {
@@ -8,7 +8,7 @@ class AuthService {
8
8
  return {
9
9
  Authorization: `Bearer ${this.bearerToken}`,
10
10
  "Content-Type": "application/json",
11
- "User-Agent": USER_AGENT,
11
+ "User-Agent": getUserAgent(),
12
12
  "zscale-source": "smartbear-mcp"
13
13
  };
14
14
  }