@smartbear/mcp 0.2.2 → 0.4.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/dist/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@smartbear/mcp",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "MCP server for interacting SmartBear Products",
5
5
  "keywords": [
6
6
  "smartbear",
7
7
  "mcp",
8
8
  "insight-hub",
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": {
@@ -31,20 +32,29 @@
31
32
  "build": "tsc && shx chmod +x dist/*.js",
32
33
  "lint": "eslint . --ext .ts",
33
34
  "prepare": "npm run build",
34
- "watch": "tsc --watch"
35
+ "watch": "tsc --watch",
36
+ "test": "vitest",
37
+ "test:watch": "vitest --watch",
38
+ "test:coverage": "vitest --coverage",
39
+ "test:coverage:ci": "vitest --coverage --reporter=verbose",
40
+ "test:run": "vitest run",
41
+ "coverage:check": "vitest --coverage --reporter=verbose --config vitest.config.coverage.ts"
35
42
  },
36
43
  "dependencies": {
37
44
  "@bugsnag/js": "^8.2.0",
38
45
  "@modelcontextprotocol/sdk": "^1.15.0",
39
- "node-cache": "^5.1.2"
46
+ "node-cache": "^5.1.2",
47
+ "zod": "^3"
40
48
  },
41
49
  "devDependencies": {
42
50
  "@eslint/js": "^9.29.0",
43
51
  "@types/node": "^22",
52
+ "@vitest/coverage-v8": "^3.2.4",
44
53
  "eslint": "^9.29.0",
45
54
  "globals": "^16.2.0",
46
55
  "shx": "^0.3.4",
47
56
  "typescript": "^5.6.2",
48
- "typescript-eslint": "^8.34.1"
57
+ "typescript-eslint": "^8.34.1",
58
+ "vitest": "^3.2.4"
49
59
  }
50
60
  }
@@ -0,0 +1,127 @@
1
+ import { z } from "zod";
2
+ // Type definitions for PactFlow AI API
3
+ export const GenerationLanguages = [
4
+ "javascript",
5
+ "typescript",
6
+ "java",
7
+ "golang",
8
+ "dotnet",
9
+ "kotlin",
10
+ "swift",
11
+ "php",
12
+ ];
13
+ export const HttpMethods = [
14
+ "GET",
15
+ "PUT",
16
+ "POST",
17
+ "DELETE",
18
+ "OPTIONS",
19
+ "HEAD",
20
+ "PATCH",
21
+ "TRACE",
22
+ ];
23
+ // zod schemas
24
+ export const FileInputSchema = z.object({
25
+ filename: z
26
+ .string()
27
+ .optional()
28
+ .describe("Filename (helps identify file type and context)"),
29
+ language: z
30
+ .string()
31
+ .optional()
32
+ .describe("Programming language (e.g., 'javascript', 'java', 'python') for better analysis"),
33
+ body: z
34
+ .string()
35
+ .describe("Complete file contents - client code, models, test files, etc."),
36
+ });
37
+ export const OpenAPISchema = z
38
+ .object({
39
+ openapi: z
40
+ .string()
41
+ .optional()
42
+ .describe("For OpenAPI version (e.g., '3.0.0')"),
43
+ swagger: z
44
+ .string()
45
+ .describe("For OpenAPI documents version 2.x (e.g., '2.0')")
46
+ .optional(),
47
+ paths: z
48
+ .record(z.string(), z.record(z.string(), z.any()))
49
+ .describe("OpenAPI paths object containing all API endpoints"),
50
+ components: z
51
+ .record(z.string(), z.record(z.string(), z.any()))
52
+ .optional()
53
+ .describe("OpenAPI components section (schemas, responses, etc.)"),
54
+ })
55
+ .passthrough()
56
+ .describe("The complete OpenAPI document describing the API")
57
+ .refine((data) => data.openapi || data.swagger, {
58
+ message: "Either 'openapi' (for v3+) or 'swagger' (for v2) must be provided",
59
+ path: ["openapi"],
60
+ })
61
+ .optional();
62
+ export const EndpointMatcherSchema = z
63
+ .object({
64
+ path: z
65
+ .string()
66
+ .optional()
67
+ .describe("Path pattern to match specific endpoints (e.g., '/users/{id}', '/users/*', '/users/**'). Supports glob patterns: ? (single char), * (excluding /), ** (including /)"),
68
+ methods: z
69
+ .array(z.enum(HttpMethods))
70
+ .optional()
71
+ .describe("HTTP methods to include (e.g., ['GET', 'POST']). If not specified, all methods are matched"),
72
+ statusCodes: z
73
+ .array(z.union([z.number(), z.string()]))
74
+ .optional()
75
+ .describe("Response status codes to include (e.g., [200, '2XX', 404]). Use 'X' as wildcard (e.g., '2XX' for 200-299). Defaults to successful codes (2XX)"),
76
+ operationId: z
77
+ .string()
78
+ .optional()
79
+ .describe("OpenAPI operation ID to match (e.g., 'getUserById', 'get*'). Supports glob patterns"),
80
+ })
81
+ .required()
82
+ .describe("REQUIRED: Matcher to specify which endpoints from the OpenAPI document to generate tests for. At least one matcher field must be provided");
83
+ export const OpenAPIWithMatcherSchema = z
84
+ .object({
85
+ document: OpenAPISchema,
86
+ matcher: EndpointMatcherSchema,
87
+ })
88
+ .describe("If provided, the OpenAPI document which describes the API being tested and is accompanied by a matcher which will be used to identify the interactions in the OpenAPI document which are relevant to the Pact refinement process.");
89
+ export const RefineInputSchema = z.object({
90
+ pactTests: FileInputSchema.describe("Primary pact tests that needs to be refined."),
91
+ code: z
92
+ .array(FileInputSchema)
93
+ .describe("Collection of source code files to analyze and extract API interactions from. Include client code, data models, existing tests, or any code that makes API calls")
94
+ .optional(),
95
+ userInstructions: z
96
+ .string()
97
+ .describe("Optional free-form instructions that provide additional context or specify areas of focus during the refinement process of the Pact test.")
98
+ .optional(),
99
+ errorMessages: z
100
+ .array(z.string())
101
+ .describe("Optional error output from failed contract test runs. These can be used to better understand the context or failures observed and guide the recommendations toward resolving specific issues.")
102
+ .optional(),
103
+ openapi: OpenAPIWithMatcherSchema.optional(),
104
+ });
105
+ export const RequestResponsePairSchema = z
106
+ .object({
107
+ request: FileInputSchema,
108
+ response: FileInputSchema,
109
+ })
110
+ .describe("Direct request/response pair for a specific interaction. Use this when you have concrete examples of API requests and responses");
111
+ export const GenerationInputSchema = z.object({
112
+ language: z
113
+ .enum(GenerationLanguages)
114
+ .optional()
115
+ .describe("Target language for the generated Pact tests. If not provided, will be inferred from other inputs."),
116
+ requestResponse: RequestResponsePairSchema.optional(),
117
+ code: z
118
+ .array(FileInputSchema)
119
+ .optional()
120
+ .describe("Collection of source code files to analyze and extract API interactions from. Include client code, data models, existing tests, or any code that makes API calls"),
121
+ openapi: OpenAPIWithMatcherSchema.optional(),
122
+ additionalInstructions: z
123
+ .string()
124
+ .optional()
125
+ .describe("Optional free-form instructions to guide the generation process (e.g., 'Focus on error scenarios', 'Include authentication headers', 'Use specific test framework patterns')"),
126
+ testTemplate: FileInputSchema.optional().describe("Optional test template to use as a basis for generation. Helps ensure generated tests follow your specific patterns, frameworks, and coding standards"),
127
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ /**
2
+ * TOOLS
3
+ *
4
+ * Extends the default `ToolParams` with:
5
+ * - `handler`: Name of the `PactflowClient` method to execute.
6
+ * - `clients`: List of `ClientType`s allowed to use the tool.
7
+ * - `formatResponse` (optional): Formats the tool's output before returning.
8
+ *
9
+ * In `PactflowClient.registerTools()`, tools are filtered by `clientType`
10
+ * and registered with their corresponding handler.
11
+ */
12
+ import { z } from "zod";
13
+ import { GenerationInputSchema, RefineInputSchema } from "./ai.js";
14
+ export const TOOLS = [
15
+ {
16
+ title: "Generate Pact Tests",
17
+ summary: "Generate Pact tests using PactFlow AI. You can provide one or more of the following input types: (1) request/response pairs for specific interactions, (2) code files to analyze and extract interactions from, and/or (3) OpenAPI document to generate tests for specific endpoints. When providing an OpenAPI document, a matcher is required to specify which endpoints to generate tests for.",
18
+ purpose: "Generate Pact tests for API interactions",
19
+ zodSchema: GenerationInputSchema,
20
+ handler: "generate",
21
+ clients: ["pactflow"], // ONLY pactflow
22
+ },
23
+ {
24
+ title: "Review Pact Tests",
25
+ summary: "Review Pact tests using PactFlow AI. You can provide the following inputs: (1) Pact tests to be reviewed along with metadata",
26
+ purpose: "Review Pact tests for API interactions",
27
+ zodSchema: RefineInputSchema,
28
+ handler: "review",
29
+ clients: ["pactflow"]
30
+ },
31
+ {
32
+ title: "Get Provider States",
33
+ summary: "Retrieve the states of a specific provider",
34
+ purpose: "A provider state in Pact defines the specific preconditions that must be met on the provider side before a consumer–provider interaction can be tested. It sets up the provider in the right context—such as ensuring a particular user or record exists—so that the provider can return the response the consumer expects. This makes contract tests reliable, repeatable, and isolated by injecting or configuring the necessary data and conditions directly into the provider before each test runs.",
35
+ parameters: [
36
+ {
37
+ name: "provider",
38
+ type: z.string(),
39
+ description: "name of the provider to retrieve states for",
40
+ required: true
41
+ }
42
+ ],
43
+ handler: "getProviderStates",
44
+ clients: ["pactflow", "pact_broker"]
45
+ }
46
+ ];
@@ -0,0 +1,132 @@
1
+ import { TOOLS } from "./client/tools.js";
2
+ import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.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
+ async generate(body) {
34
+ // Submit the generation request
35
+ const response = await fetch(`${this.aiBaseUrl}/generate`, {
36
+ method: "POST",
37
+ headers: this.headers,
38
+ body: JSON.stringify(body),
39
+ });
40
+ if (!response.ok) {
41
+ throw new Error(`HTTP error! status: ${response.status}`);
42
+ }
43
+ const status_response = await response.json();
44
+ return await this.pollForCompletion(status_response, "Generation");
45
+ }
46
+ async review(body) {
47
+ // submit review request
48
+ const response = await fetch(`${this.aiBaseUrl}/review`, {
49
+ method: "POST",
50
+ headers: this.headers,
51
+ body: JSON.stringify(body),
52
+ });
53
+ if (!response.ok) {
54
+ throw new Error(`HTTP error! status: ${response.status}`);
55
+ }
56
+ const status_response = await response.json();
57
+ return await this.pollForCompletion(status_response, "Review Pacts");
58
+ }
59
+ async getStatus(statusUrl) {
60
+ const response = await fetch(statusUrl, {
61
+ method: "HEAD",
62
+ headers: this.headers,
63
+ });
64
+ return {
65
+ status: response.status,
66
+ isComplete: response.status === 200,
67
+ };
68
+ }
69
+ async getResult(resultUrl) {
70
+ const response = await fetch(resultUrl, {
71
+ method: "GET",
72
+ headers: this.headers,
73
+ });
74
+ // Check if the response is OK (status 200)
75
+ if (!response.ok) {
76
+ throw new Error(`HTTP error! status: ${response.status}`);
77
+ }
78
+ return response.json();
79
+ }
80
+ async pollForCompletion(status_response, operationName) {
81
+ // Polling for completion
82
+ const startTime = Date.now();
83
+ const timeout = 120000; // 120 seconds
84
+ const pollInterval = 1000; // 1 second
85
+ while (Date.now() - startTime < timeout) {
86
+ const statusCheck = await this.getStatus(status_response.status_url);
87
+ if (statusCheck.isComplete) {
88
+ // Operation is complete, get the result
89
+ return await this.getResult(status_response.result_url);
90
+ }
91
+ if (statusCheck.status !== 202) {
92
+ throw new Error(`${operationName} failed with status: ${statusCheck.status}`);
93
+ }
94
+ // Wait before next poll
95
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
96
+ }
97
+ throw new Error(`${operationName} timed out after ${timeout / 1000} seconds`);
98
+ }
99
+ // PactFlow / Pact_Broker client methods
100
+ async getProviderStates({ provider }) {
101
+ const uri_encoded_provider_name = encodeURIComponent(provider);
102
+ const response = await fetch(`${this.baseUrl}/pacts/provider/${uri_encoded_provider_name}/provider-states`, {
103
+ method: "GET",
104
+ headers: this.headers,
105
+ });
106
+ if (!response.ok) {
107
+ throw new Error(`HTTP error! status: ${response.status} - ${await response.text()}`);
108
+ }
109
+ return response.json();
110
+ }
111
+ registerTools(register, _getInput) {
112
+ for (const tool of TOOLS.filter(t => t.clients.includes(this.clientType))) {
113
+ const { handler, clients, formatResponse, ...toolparams } = tool;
114
+ console.log(clients);
115
+ register(toolparams, async (args, _extra) => {
116
+ const handler_fn = this[handler];
117
+ if (typeof handler_fn !== "function") {
118
+ throw new Error(`Handler '${handler}' not found on PactClient`);
119
+ }
120
+ const result = await handler_fn.call(this, args);
121
+ // Use custom response formatter if provided
122
+ if (formatResponse) {
123
+ return formatResponse(result);
124
+ }
125
+ // Default fallback
126
+ return {
127
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
128
+ };
129
+ });
130
+ }
131
+ }
132
+ }
@@ -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
  }