@smartbear/mcp 0.3.0 → 0.5.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.
@@ -0,0 +1,180 @@
1
+ import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
2
+ import { TOOLS } from "./client/tools.js";
3
+ // Tool definitions for PactFlow AI API client
4
+ export class PactflowClient {
5
+ name = "Contract Testing";
6
+ prefix = "contract-testing";
7
+ headers;
8
+ aiBaseUrl;
9
+ baseUrl;
10
+ clientType;
11
+ constructor(auth, baseUrl, clientType) {
12
+ // Set headers based on the type of auth provided
13
+ if (typeof auth === "string") {
14
+ this.headers = {
15
+ Authorization: `Bearer ${auth}`,
16
+ "Content-Type": "application/json",
17
+ "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
18
+ };
19
+ }
20
+ else {
21
+ const authString = `${auth.username}:${auth.password}`;
22
+ this.headers = {
23
+ Authorization: `Basic ${Buffer.from(authString).toString("base64")}`,
24
+ "Content-Type": "application/json",
25
+ "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
26
+ };
27
+ }
28
+ this.baseUrl = baseUrl;
29
+ this.aiBaseUrl = `${this.baseUrl}/api/ai`;
30
+ this.clientType = clientType;
31
+ }
32
+ // PactFlow AI client methods
33
+ /**
34
+ * Generate new Pact tests based on the provided input.
35
+ *
36
+ * @param toolInput The input data for the generation process.
37
+ * @returns The result of the generation process.
38
+ * @throws Error if the HTTP request fails or the operation times out.
39
+ */
40
+ async generate(toolInput) {
41
+ // Submit the generation request
42
+ const response = await fetch(`${this.aiBaseUrl}/generate`, {
43
+ method: "POST",
44
+ headers: this.headers,
45
+ body: JSON.stringify(toolInput),
46
+ });
47
+ if (!response.ok) {
48
+ throw new Error(`HTTP error! status: ${response.status} - ${await response.text()}`);
49
+ }
50
+ const status_response = await response.json();
51
+ return await this.pollForCompletion(status_response, "Generation");
52
+ }
53
+ /**
54
+ * Review the provided Pact tests and suggest improvements.
55
+ *
56
+ * @param toolInput The input data for the review process.
57
+ * @returns The result of the review process.
58
+ * @throws Error if the HTTP request fails or the operation times out.
59
+ */
60
+ async review(toolInput) {
61
+ // Submit review request
62
+ const response = await fetch(`${this.aiBaseUrl}/review`, {
63
+ method: "POST",
64
+ headers: this.headers,
65
+ body: JSON.stringify(toolInput),
66
+ });
67
+ if (!response.ok) {
68
+ throw new Error(`HTTP error! status: ${response.status} - ${await response.text()}`);
69
+ }
70
+ const status_response = await response.json();
71
+ return await this.pollForCompletion(status_response, "Review Pacts");
72
+ }
73
+ async getStatus(statusUrl) {
74
+ const response = await fetch(statusUrl, {
75
+ method: "HEAD",
76
+ headers: this.headers,
77
+ });
78
+ return {
79
+ status: response.status,
80
+ isComplete: response.status === 200,
81
+ };
82
+ }
83
+ async getResult(resultUrl) {
84
+ const response = await fetch(resultUrl, {
85
+ method: "GET",
86
+ headers: this.headers,
87
+ });
88
+ // Check if the response is OK (status 200)
89
+ if (!response.ok) {
90
+ throw new Error(`HTTP error! status: ${response.status}`);
91
+ }
92
+ return response.json();
93
+ }
94
+ async pollForCompletion(status_response, operationName) {
95
+ // Polling for completion
96
+ const startTime = Date.now();
97
+ const timeout = 120000; // 120 seconds
98
+ const pollInterval = 1000; // 1 second
99
+ while (Date.now() - startTime < timeout) {
100
+ const statusCheck = await this.getStatus(status_response.status_url);
101
+ if (statusCheck.isComplete) {
102
+ // Operation is complete, get the result
103
+ return await this.getResult(status_response.result_url);
104
+ }
105
+ if (statusCheck.status !== 202) {
106
+ throw new Error(`${operationName} failed with status: ${statusCheck.status}`);
107
+ }
108
+ // Wait before next poll
109
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
110
+ }
111
+ throw new Error(`${operationName} timed out after ${timeout / 1000} seconds`);
112
+ }
113
+ // PactFlow / Pact_Broker client methods
114
+ async getProviderStates({ provider }) {
115
+ const uri_encoded_provider_name = encodeURIComponent(provider);
116
+ const response = await fetch(`${this.baseUrl}/pacts/provider/${uri_encoded_provider_name}/provider-states`, {
117
+ method: "GET",
118
+ headers: this.headers,
119
+ });
120
+ if (!response.ok) {
121
+ throw new Error(`HTTP error! status: ${response.status} - ${await response.text()}`);
122
+ }
123
+ return response.json();
124
+ }
125
+ /**
126
+ * Checks if a given pacticipant version is safe to deploy
127
+ * to a specified environment.
128
+ *
129
+ * @param body - Input containing:
130
+ * - `pacticipant`: The name of the service (pacticipant).
131
+ * - `version`: The version of the pacticipant being evaluated for deployment.
132
+ * - `environment`: The target environment (e.g., staging, production).
133
+ * @returns CanIDeployResponse containing deployment decision and verification results.
134
+ * @throws Error if the request fails or returns a non-OK response.
135
+ */
136
+ async canIDeploy(body) {
137
+ const { pacticipant, version, environment } = body;
138
+ const queryParams = new URLSearchParams({
139
+ pacticipant,
140
+ version,
141
+ environment,
142
+ });
143
+ const url = `${this.baseUrl}/can-i-deploy?${queryParams.toString()}`;
144
+ try {
145
+ const response = await fetch(url, {
146
+ method: "GET",
147
+ headers: this.headers,
148
+ });
149
+ if (!response.ok) {
150
+ const errorText = await response.text().catch(() => "");
151
+ throw new Error(`Can-I-Deploy Request Failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
152
+ }
153
+ return (await response.json());
154
+ }
155
+ catch (error) {
156
+ console.error("[CanIDeploy] Unexpected error:", error);
157
+ throw error;
158
+ }
159
+ }
160
+ registerTools(register, _getInput) {
161
+ for (const tool of TOOLS.filter(t => t.clients.includes(this.clientType))) {
162
+ const { handler, clients: _, formatResponse, ...toolparams } = tool; // eslint-disable-line @typescript-eslint/no-unused-vars
163
+ register(toolparams, async (args, _extra) => {
164
+ const handler_fn = this[handler];
165
+ if (typeof handler_fn !== "function") {
166
+ throw new Error(`Handler '${handler}' not found on PactClient`);
167
+ }
168
+ const result = await handler_fn.call(this, args);
169
+ // Use custom response formatter if provided
170
+ if (formatResponse) {
171
+ return formatResponse(result);
172
+ }
173
+ // Default fallback
174
+ return {
175
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
176
+ };
177
+ });
178
+ }
179
+ }
180
+ }
@@ -3,6 +3,8 @@ import { z } from "zod";
3
3
  // ReflectClient class implementing the Client interface
4
4
  export class ReflectClient {
5
5
  headers;
6
+ name = "Reflect";
7
+ prefix = "reflect";
6
8
  constructor(token) {
7
9
  this.headers = {
8
10
  "X-API-KEY": `${token}`,
@@ -66,14 +68,29 @@ export class ReflectClient {
66
68
  });
67
69
  return response.json();
68
70
  }
69
- registerTools(server) {
70
- server.tool("list_reflect_suites", "List all reflect suites", {}, async (_args, _extra) => {
71
+ registerTools(register, _getInput) {
72
+ register({
73
+ title: "List Suites",
74
+ summary: "Retrieve a list of all reflect suites available",
75
+ parameters: [],
76
+ }, async (_args, _extra) => {
71
77
  const response = await this.listReflectSuites();
72
78
  return {
73
79
  content: [{ type: "text", text: JSON.stringify(response) }],
74
80
  };
75
81
  });
76
- server.tool("list_reflect_suite_executions", "List all executions for a given reflect suite", { suiteId: z.string().describe("ID of the reflect suite to list executions for") }, async (args, _extra) => {
82
+ register({
83
+ title: "List Suite Executions",
84
+ summary: "List all executions for a given suite",
85
+ parameters: [
86
+ {
87
+ name: "suiteId",
88
+ type: z.string(),
89
+ description: "ID of the reflect suite to list executions for",
90
+ required: true
91
+ },
92
+ ],
93
+ }, async (args, _extra) => {
77
94
  if (!args.suiteId)
78
95
  throw new Error("suiteId argument is required");
79
96
  const response = await this.listSuiteExecutions(args.suiteId);
@@ -81,9 +98,23 @@ export class ReflectClient {
81
98
  content: [{ type: "text", text: JSON.stringify(response) }],
82
99
  };
83
100
  });
84
- server.tool("reflect_suite_execution_status", "Get the status of a reflect suite execution", {
85
- suiteId: z.string().describe("ID of the reflect suite to list executions for"),
86
- executionId: z.string().describe("ID of the reflect suite execution to get status for"),
101
+ register({
102
+ title: "Get Suite Execution Status",
103
+ summary: "Get the status of a reflect suite execution",
104
+ parameters: [
105
+ {
106
+ name: "suiteId",
107
+ type: z.string(),
108
+ description: "ID of the reflect suite to get execution status for",
109
+ required: true
110
+ },
111
+ {
112
+ name: "executionId",
113
+ type: z.string(),
114
+ description: "ID of the reflect suite execution to get status for",
115
+ required: true
116
+ },
117
+ ],
87
118
  }, async (args, _extra) => {
88
119
  if (!args.suiteId || !args.executionId)
89
120
  throw new Error("Both suiteId and executionId arguments are required");
@@ -92,7 +123,18 @@ export class ReflectClient {
92
123
  content: [{ type: "text", text: JSON.stringify(response) }],
93
124
  };
94
125
  });
95
- server.tool("reflect_suite_execution", "Execute a reflect suite", { suiteId: z.string().describe("ID of the reflect suite to list executions for") }, async (args, _extra) => {
126
+ register({
127
+ title: "Execute Suite",
128
+ summary: "Execute a reflect suite",
129
+ parameters: [
130
+ {
131
+ name: "suiteId",
132
+ type: z.string(),
133
+ description: "ID of the reflect suite to list executions for",
134
+ required: true
135
+ }
136
+ ]
137
+ }, async (args, _extra) => {
96
138
  if (!args.suiteId)
97
139
  throw new Error("suiteId argument is required");
98
140
  const response = await this.executeSuite(args.suiteId);
@@ -100,9 +142,23 @@ export class ReflectClient {
100
142
  content: [{ type: "text", text: JSON.stringify(response) }],
101
143
  };
102
144
  });
103
- server.tool("cancel_reflect_suite_execution", "Cancel a reflect suite execution", {
104
- suiteId: z.string().describe("ID of the reflect suite to cancel execution for"),
105
- executionId: z.string().describe("ID of the reflect suite execution to cancel"),
145
+ register({
146
+ title: "Cancel Suite Execution",
147
+ summary: "Cancel a reflect suite execution",
148
+ parameters: [
149
+ {
150
+ name: "suiteId",
151
+ type: z.string(),
152
+ description: "ID of the reflect suite to cancel execution for",
153
+ required: true
154
+ },
155
+ {
156
+ name: "executionId",
157
+ type: z.string(),
158
+ description: "ID of the reflect suite execution to cancel",
159
+ required: true
160
+ },
161
+ ],
106
162
  }, async (args, _extra) => {
107
163
  if (!args.suiteId || !args.executionId)
108
164
  throw new Error("Both suiteId and executionId arguments are required");
@@ -111,13 +167,28 @@ export class ReflectClient {
111
167
  content: [{ type: "text", text: JSON.stringify(response) }],
112
168
  };
113
169
  });
114
- server.tool("list_reflect_tests", "List all reflect tests", {}, async (_args, _extra) => {
170
+ register({
171
+ title: "List Tests",
172
+ summary: "List all reflect tests",
173
+ parameters: [],
174
+ }, async (_args, _extra) => {
115
175
  const response = await this.listReflectTests();
116
176
  return {
117
177
  content: [{ type: "text", text: JSON.stringify(response) }],
118
178
  };
119
179
  });
120
- server.tool("run_reflect_test", "Run a reflect test", { testId: z.string().describe("ID of the reflect test to run") }, async (args, _extra) => {
180
+ register({
181
+ title: "Run Test",
182
+ summary: "Run a reflect test",
183
+ parameters: [
184
+ {
185
+ name: "testId",
186
+ type: z.string(),
187
+ description: "ID of the reflect test to run",
188
+ required: true
189
+ }
190
+ ],
191
+ }, async (args, _extra) => {
121
192
  if (!args.testId)
122
193
  throw new Error("testId argument is required");
123
194
  const response = await this.runReflectTest(args.testId);
@@ -125,9 +196,23 @@ export class ReflectClient {
125
196
  content: [{ type: "text", text: JSON.stringify(response) }],
126
197
  };
127
198
  });
128
- server.tool("reflect_test_status", "Get the status of a reflect test execution", {
129
- testId: z.string().describe("ID of the reflect test to run"),
130
- executionId: z.string().describe("ID of the reflect test execution to get status for"),
199
+ register({
200
+ title: "Get Test Status",
201
+ summary: "Get the status of a reflect test execution",
202
+ parameters: [
203
+ {
204
+ name: "testId",
205
+ type: z.string(),
206
+ description: "ID of the reflect test to run",
207
+ required: true
208
+ },
209
+ {
210
+ name: "executionId",
211
+ type: z.string(),
212
+ description: "ID of the reflect test execution to get status for",
213
+ required: true
214
+ },
215
+ ],
131
216
  }, async (args, _extra) => {
132
217
  if (!args.testId || !args.executionId)
133
218
  throw new Error("Both testId and executionId arguments are required");
@@ -137,7 +222,4 @@ export class ReflectClient {
137
222
  };
138
223
  });
139
224
  }
140
- registerResources(_server) {
141
- // Reflect does not currently support dynamic resources
142
- }
143
225
  }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@smartbear/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server for interacting SmartBear Products",
5
5
  "keywords": [
6
6
  "smartbear",
7
7
  "mcp",
8
- "insight-hub",
8
+ "bugsnag",
9
9
  "reflect",
10
- "api-hub"
10
+ "api-hub",
11
+ "pactflow"
11
12
  ],
12
13
  "homepage": "https://developer.smartbear.com/smartbear-mcp",
13
14
  "repository": {
@@ -43,10 +44,12 @@
43
44
  "@bugsnag/js": "^8.2.0",
44
45
  "@modelcontextprotocol/sdk": "^1.15.0",
45
46
  "node-cache": "^5.1.2",
47
+ "swagger-client": "^3.35.6",
46
48
  "zod": "^3"
47
49
  },
48
50
  "devDependencies": {
49
51
  "@eslint/js": "^9.29.0",
52
+ "@types/js-yaml": "^4.0.9",
50
53
  "@types/node": "^22",
51
54
  "@vitest/coverage-v8": "^3.2.4",
52
55
  "eslint": "^9.29.0",
@@ -54,6 +57,7 @@
54
57
  "shx": "^0.3.4",
55
58
  "typescript": "^5.6.2",
56
59
  "typescript-eslint": "^8.34.1",
57
- "vitest": "^3.2.4"
60
+ "vitest": "^3.2.4",
61
+ "vitest-fetch-mock": "^0.4.5"
58
62
  }
59
63
  }
@@ -1,57 +0,0 @@
1
- export function toolDescriptionTemplate(params) {
2
- const { summary, useCases, examples, parameters, hints, outputFormat } = params;
3
- let description = summary;
4
- // Parameters (essential)
5
- if (parameters.length > 0) {
6
- description += `\n\n**Parameters:** ${parameters.map(p => `${p.name} (${p.type})${p.required ? ' *required*' : ''}`).join(', ')}`;
7
- }
8
- if (outputFormat) {
9
- description += `\n\n**Output Format:** ${outputFormat}`;
10
- }
11
- // Use Cases
12
- if (useCases.length > 0) {
13
- description += `\n\n**Use Cases:** ${useCases.map((uc, i) => `${i + 1}. ${uc}`).join(' ')}`;
14
- }
15
- // Examples
16
- if (examples.length > 0) {
17
- description += `\n\n**Examples:**\n` + examples.map((ex, idx) => `${idx + 1}. ${ex.description}\n\`\`\`json\n${JSON.stringify(ex.parameters, null, 2)}\n\`\`\`${ex.expectedOutput ? `\nExpected Output: ${ex.expectedOutput}` : ''}`).join('\n\n');
18
- }
19
- // Hints
20
- if (hints.length > 0) {
21
- description += `\n\n**Tips:** ${hints.map((hint, i) => `${i + 1}. ${hint}`).join(' ')}`;
22
- }
23
- return description.trim();
24
- }
25
- // Backward-compatible version of the original function
26
- export function simpleToolDescriptionTemplate(summary, useCases, examples, hints) {
27
- return toolDescriptionTemplate({
28
- summary,
29
- purpose: summary,
30
- useCases,
31
- examples: examples.map(example => ({
32
- description: example,
33
- parameters: {}
34
- })),
35
- parameters: [],
36
- hints
37
- });
38
- }
39
- // Helper function to create parameter descriptions
40
- export function createParameter(name, type, required, description, options = {}) {
41
- return {
42
- name,
43
- type,
44
- required,
45
- description,
46
- examples: options.examples,
47
- constraints: options.constraints
48
- };
49
- }
50
- // Helper function to create examples with proper structure
51
- export function createExample(description, parameters, expectedOutput) {
52
- return {
53
- description,
54
- parameters,
55
- expectedOutput
56
- };
57
- }