@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.
@@ -0,0 +1,89 @@
1
+ import { EndpointMatcherSchema, MatcherRecommendationInputSchema, } from "./ai.js";
2
+ import { OADMatcherPrompt } from "./prompts.js";
3
+ /**
4
+ * Get OpenAPI matcher recommendations using sampling.
5
+ *
6
+ * @param openAPI The OpenAPI document to analyze.
7
+ * @param server The SmartBear MCP server instance.
8
+ * @returns A promise that resolves to the matcher recommendations.
9
+ * @throws Error if unable to parse recommendations.
10
+ */
11
+ export async function getOADMatcherRecommendations(openAPI, server) {
12
+ const matcherResponse = await server.createMessage({
13
+ messages: [
14
+ {
15
+ role: "user",
16
+ content: {
17
+ type: "text",
18
+ text: OADMatcherPrompt.replace("{0}", JSON.stringify(openAPI)),
19
+ },
20
+ },
21
+ ],
22
+ maxTokens: 1000,
23
+ });
24
+ const regex = /```json[c5]?([\s\S]*?)```/i;
25
+ const match = regex.exec(matcherResponse.content.text);
26
+ if (match) {
27
+ const jsonText = match[1].trim();
28
+ const parsed = JSON.parse(jsonText);
29
+ const matcherRecommendations = MatcherRecommendationInputSchema.parse(parsed);
30
+ return matcherRecommendations;
31
+ }
32
+ else {
33
+ throw new Error("Unable to parse recommendations please provide OpenAPI matchers manually.");
34
+ }
35
+ }
36
+ /**
37
+ * Get user selection for matcher recommendations.
38
+ *
39
+ * @param recommendations The list of matcher recommendations.
40
+ * @param getInput The function to get user input.
41
+ * @returns The selected endpoint matcher.
42
+ */
43
+ export async function getUserMatcherSelection(recommendations, getInput) {
44
+ const recommendationsMap = new Map();
45
+ recommendations.forEach((rec, index) => {
46
+ recommendationsMap.set(`Recommendation ${index + 1}`, JSON.stringify(rec));
47
+ });
48
+ const result = await getInput({
49
+ message: `Select one of the generated matchers you would want to use \n\n ${recommendations
50
+ .map((rec, index) => `\n\nRecommendation ${index + 1}: \n\n\n ${JSON.stringify(rec)}`)
51
+ .join("\n\n")}`,
52
+ requestedSchema: {
53
+ type: "object",
54
+ properties: {
55
+ generatedMatchers: {
56
+ type: "string",
57
+ title: "Generated Matchers",
58
+ description: "Use the matchers generated for the OpenAPI document",
59
+ enum: recommendations.map((_, index) => `Recommendation ${index + 1}`),
60
+ },
61
+ },
62
+ required: ["generatedMatchers"],
63
+ },
64
+ });
65
+ if (result.action === "accept") {
66
+ return EndpointMatcherSchema.parse(JSON.parse(recommendationsMap.get(result.content?.generatedMatchers) ||
67
+ ""));
68
+ }
69
+ else {
70
+ const result = await getInput({
71
+ message: "Enter the matchers you would want to use for the OpenAPI document",
72
+ requestedSchema: {
73
+ type: "object",
74
+ properties: {
75
+ enteredMatchers: {
76
+ type: "string",
77
+ title: "Enter the matchers you would want to use for the OpenAPI document",
78
+ description: "Enter the matchers you would want to use for the OpenAPI document",
79
+ },
80
+ },
81
+ required: ["enteredMatchers"],
82
+ },
83
+ });
84
+ if (result.action === "accept") {
85
+ return EndpointMatcherSchema.parse(JSON.parse(result.content?.enteredMatchers));
86
+ }
87
+ return {};
88
+ }
89
+ }
@@ -0,0 +1,133 @@
1
+ import { z } from "zod";
2
+ import { zodToJsonSchema } from "zod-to-json-schema";
3
+ import { EndpointMatcherSchema } from "./ai.js";
4
+ const OADMatcherPromptOpenAPIDocExample = {
5
+ openapi: "3.1.0",
6
+ info: {
7
+ title: "My API",
8
+ version: "1.0.0",
9
+ description: "A sample API for demonstration purposes.",
10
+ },
11
+ paths: {
12
+ "/users": {
13
+ get: {
14
+ summary: "Get all users",
15
+ responses: {
16
+ "200": {
17
+ description: "A list of users",
18
+ content: {
19
+ "application/json": {
20
+ schema: {
21
+ type: "array",
22
+ items: {
23
+ $ref: "#/components/schemas/User",
24
+ },
25
+ },
26
+ },
27
+ },
28
+ },
29
+ },
30
+ },
31
+ },
32
+ },
33
+ components: {
34
+ schemas: {
35
+ User: {
36
+ type: "object",
37
+ properties: {
38
+ id: {
39
+ type: "integer",
40
+ format: "int64",
41
+ },
42
+ name: {
43
+ type: "string",
44
+ },
45
+ },
46
+ },
47
+ },
48
+ },
49
+ };
50
+ const OADMatcherPromptRecommendationExample = [
51
+ {
52
+ path: "/users",
53
+ methods: ["GET"],
54
+ statusCodes: [200, "2XX"],
55
+ operationId: "get*",
56
+ },
57
+ {
58
+ path: "/users/*",
59
+ methods: ["GET"],
60
+ statusCodes: ["2XX"],
61
+ operationId: "*User*",
62
+ },
63
+ {
64
+ path: "/users",
65
+ methods: ["GET"],
66
+ statusCodes: [200],
67
+ operationId: "getAllUsers",
68
+ },
69
+ {
70
+ path: "/users/**",
71
+ methods: ["GET"],
72
+ statusCodes: ["2XX", 404],
73
+ operationId: "get*",
74
+ },
75
+ ];
76
+ export const OADMatcherPrompt = `
77
+
78
+ Generate a list of recommendations(maximum of 5) in JSON to use with an OpenAPI matcher.
79
+ Zod Schema for the matcher to be generated is provided below in the markdown block of javascript use this to generate the recommendations for the matcher. Recommendations should contain all the fields from the schema and only output the JSON in a markdown formatted block.
80
+
81
+ \`\`\`javascript
82
+ const EndpointMatcherSchema = ${JSON.stringify(zodToJsonSchema(EndpointMatcherSchema))};
83
+ \`\`\`
84
+
85
+ Example OpenAPI document:-
86
+
87
+ if OpenAPI document provided is:-
88
+
89
+ \`\`\`json
90
+ ${JSON.stringify(OADMatcherPromptOpenAPIDocExample, null, 2)}
91
+ \`\`\`
92
+
93
+ Generated recommendations are:-
94
+
95
+ \`\`\`json
96
+ ${JSON.stringify(OADMatcherPromptRecommendationExample, null, 2)}
97
+ \`\`\`
98
+
99
+ Actual OpenAPI document:-
100
+
101
+ Now provided the below OpenAPI document:-
102
+
103
+ \`\`\`json
104
+ {0}
105
+ \`\`\`
106
+
107
+ Give JSON recommendations only provide the JSON block in markdown don't include any additional text.
108
+ `;
109
+ export const PROMPTS = [
110
+ {
111
+ name: "OpenAPI Matcher recommendations",
112
+ params: {
113
+ description: "Get OpenAPI matcher recommendations using sampling",
114
+ title: "OpenAPI Matcher recommendations",
115
+ argsSchema: {
116
+ openAPI: z.string(),
117
+ },
118
+ },
119
+ callback: function ({ openAPI }) {
120
+ return {
121
+ messages: [
122
+ {
123
+ role: "user",
124
+ content: {
125
+ type: "text",
126
+ text: OADMatcherPrompt.replace("{0}", openAPI),
127
+ },
128
+ },
129
+ ],
130
+ };
131
+ },
132
+ },
133
+ ];
@@ -10,8 +10,8 @@
10
10
  * and registered with their corresponding handler.
11
11
  */
12
12
  import { z } from "zod";
13
- import { GenerationInputSchema, RefineInputSchema } from "./ai.js";
14
- import { CanIDeploySchema } from "./base.js";
13
+ import { GenerationInputSchema, RefineInputSchema, } from "./ai.js";
14
+ import { CanIDeploySchema, MatrixSchema } from "./base.js";
15
15
  export const TOOLS = [
16
16
  {
17
17
  title: "Generate Pact Tests",
@@ -20,6 +20,7 @@ export const TOOLS = [
20
20
  zodSchema: GenerationInputSchema,
21
21
  handler: "generate",
22
22
  clients: ["pactflow"], // ONLY pactflow
23
+ enableElicitation: true,
23
24
  },
24
25
  {
25
26
  title: "Review Pact Tests",
@@ -27,7 +28,8 @@ export const TOOLS = [
27
28
  purpose: "Review Pact tests for API interactions",
28
29
  zodSchema: RefineInputSchema,
29
30
  handler: "review",
30
- clients: ["pactflow"]
31
+ clients: ["pactflow"],
32
+ enableElicitation: true,
31
33
  },
32
34
  {
33
35
  title: "Get Provider States",
@@ -51,5 +53,35 @@ export const TOOLS = [
51
53
  zodSchema: CanIDeploySchema,
52
54
  handler: "canIDeploy",
53
55
  clients: ["pactflow", "pact_broker"]
56
+ },
57
+ {
58
+ title: "Matrix",
59
+ summary: "Retrieve the comprehensive contract verification matrix that shows the relationship between consumer and provider versions, their associated pact files, and verification results stored in the Pact Broker or Pactflow. The matrix provides detailed visibility into which consumer and provider versions have been successfully verified against each other, and highlights failures with detailed information about the cause.",
60
+ purpose: "The Matrix serves as a powerful tool for teams to understand the state of their contract testing ecosystem. It enables tracking of all interactions between consumer and provider versions over time, with detailed insights into verification successes and failures. This helps teams rapidly identify compatibility issues, understand why specific verifications failed, and make informed decisions about deployments. Matrix offers a more intuitive and consolidated view of the verification status, making it easier to spot trends or problematic versions. Additionally, the Matrix supports complex queries using selectors, and can answer specific 'can-i-deploy' questions, ensuring that only compatible versions are deployed to production environments.",
61
+ useCases: [
62
+ "Quickly identify which consumer and provider version combinations have passed or failed verification.",
63
+ "Diagnose and investigate why a particular consumer-provider verification failed.",
64
+ "Visualize the overall contract compatibility across two pacticipants / services.",
65
+ "Perform advanced queries using selectors to understand compatibility within specific branches, environments, or version ranges.",
66
+ "Support informed deployment decisions by answering 'can I deploy version X of this service to production?'",
67
+ "Expose contract verification details to non-frequent API users in a more accessible format."
68
+ ],
69
+ zodSchema: MatrixSchema,
70
+ handler: "getMatrix",
71
+ clients: ["pactflow", "pact_broker"]
72
+ },
73
+ {
74
+ title: "PactFlow AI Status",
75
+ summary: "Check PactFlow AI usage status, remaining credits, and eligibility",
76
+ purpose: "Retrieve the AI feature status for the PactFlow account, including whether AI is enabled, the number of remaining and consumed AI credits, and entitlement or permission issues preventing usage.",
77
+ useCases: [
78
+ "Verify if AI functionality is enabled for the account before attempting to use AI-powered features",
79
+ "Monitor remaining and consumed AI credits to manage usage and avoid unexpected disruptions",
80
+ "Detect entitlement or permission issues when a user tries to access AI features and guide corrective actions",
81
+ "Integrate into deployment pipelines to ensure the environment is correctly configured with necessary entitlements and sufficient credits before executing AI-driven tasks",
82
+ "Fetches usage and entitlement reports for auditing, budgeting, and compliance purposes"
83
+ ],
84
+ handler: "getAIStatus",
85
+ clients: ["pactflow"]
54
86
  }
55
87
  ];
@@ -1,5 +1,7 @@
1
1
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
2
2
  import { TOOLS } from "./client/tools.js";
3
+ import { getOADMatcherRecommendations, getUserMatcherSelection } from "./client/prompt-utils.js";
4
+ import { PROMPTS } from "./client/prompts.js";
3
5
  // Tool definitions for PactFlow AI API client
4
6
  export class PactflowClient {
5
7
  name = "Contract Testing";
@@ -8,7 +10,16 @@ export class PactflowClient {
8
10
  aiBaseUrl;
9
11
  baseUrl;
10
12
  clientType;
11
- constructor(auth, baseUrl, clientType) {
13
+ server;
14
+ /**
15
+ * Creates an instance of the PactflowClient.
16
+ *
17
+ * @param auth The authentication token or credentials.
18
+ * @param baseUrl The base URL for the API.
19
+ * @param clientType The type of client (e.g., PactFlow, Pact Broker).
20
+ * @param server The SmartBear MCP server instance.
21
+ */
22
+ constructor(auth, baseUrl, clientType, server) {
12
23
  // Set headers based on the type of auth provided
13
24
  if (typeof auth === "string") {
14
25
  this.headers = {
@@ -28,16 +39,23 @@ export class PactflowClient {
28
39
  this.baseUrl = baseUrl;
29
40
  this.aiBaseUrl = `${this.baseUrl}/api/ai`;
30
41
  this.clientType = clientType;
42
+ this.server = server;
31
43
  }
32
44
  // PactFlow AI client methods
33
45
  /**
34
46
  * Generate new Pact tests based on the provided input.
35
47
  *
36
48
  * @param toolInput The input data for the generation process.
49
+ * @param getInput Function to get additional input from the user if needed.
37
50
  * @returns The result of the generation process.
38
51
  * @throws Error if the HTTP request fails or the operation times out.
39
52
  */
40
- async generate(toolInput) {
53
+ async generate(toolInput, getInput) {
54
+ if (toolInput.openapi?.document && (!toolInput.openapi?.matcher || Object.keys(toolInput.openapi.matcher).length === 0)) {
55
+ const matcherResponse = await getOADMatcherRecommendations(toolInput.openapi.document, this.server);
56
+ const userSelection = await getUserMatcherSelection(matcherResponse, getInput);
57
+ toolInput.openapi.matcher = userSelection;
58
+ }
41
59
  // Submit the generation request
42
60
  const response = await fetch(`${this.aiBaseUrl}/generate`, {
43
61
  method: "POST",
@@ -54,10 +72,16 @@ export class PactflowClient {
54
72
  * Review the provided Pact tests and suggest improvements.
55
73
  *
56
74
  * @param toolInput The input data for the review process.
75
+ * @param getInput Function to get additional input from the user if needed.
57
76
  * @returns The result of the review process.
58
77
  * @throws Error if the HTTP request fails or the operation times out.
59
78
  */
60
- async review(toolInput) {
79
+ async review(toolInput, getInput) {
80
+ if (toolInput.openapi?.document && (!toolInput.openapi?.matcher || Object.keys(toolInput.openapi.matcher).length === 0)) {
81
+ const matcherResponse = await getOADMatcherRecommendations(toolInput.openapi.document, this.server);
82
+ const userSelection = await getUserMatcherSelection(matcherResponse, getInput);
83
+ toolInput.openapi.matcher = userSelection;
84
+ }
61
85
  // Submit review request
62
86
  const response = await fetch(`${this.aiBaseUrl}/review`, {
63
87
  method: "POST",
@@ -70,6 +94,32 @@ export class PactflowClient {
70
94
  const status_response = await response.json();
71
95
  return await this.pollForCompletion(status_response, "Review Pacts");
72
96
  }
97
+ /**
98
+ * Retrieves AI status information for the current user
99
+ * and organization.
100
+ *
101
+ * @returns Entitlement containing AI status information, organization
102
+ * entitlements, and user entitlements.
103
+ * @throws Error if the request fails or returns a non-OK response.
104
+ */
105
+ async getAIStatus() {
106
+ const url = `${this.aiBaseUrl}/entitlement`;
107
+ try {
108
+ const response = await fetch(url, {
109
+ method: "GET",
110
+ headers: this.headers,
111
+ });
112
+ if (!response.ok) {
113
+ const errorText = await response.text().catch(() => "");
114
+ throw new Error(`PactFlow AI Status Request Failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
115
+ }
116
+ return (await response.json());
117
+ }
118
+ catch (error) {
119
+ process.stderr.write(`[GetAICredits] Unexpected error: ${error}\n`);
120
+ throw error;
121
+ }
122
+ }
73
123
  async getStatus(statusUrl) {
74
124
  const response = await fetch(statusUrl, {
75
125
  method: "HEAD",
@@ -153,11 +203,76 @@ export class PactflowClient {
153
203
  return (await response.json());
154
204
  }
155
205
  catch (error) {
156
- console.error("[CanIDeploy] Unexpected error:", error);
206
+ console.error(`[CanIDeploy] Unexpected error: ${error}\n`);
157
207
  throw error;
158
208
  }
159
209
  }
160
- registerTools(register, _getInput) {
210
+ /**
211
+ * Retrieves the matrix of pact verification results for the specified pacticipants.
212
+ * This allows you to see which consumer/provider combinations have been verified
213
+ * and make deployment decisions based on contract test results.
214
+ *
215
+ * @param body - Matrix query parameters including pacticipants, versions, environments, etc.
216
+ * @returns MatrixResponse containing the verification matrix, notices, and summary
217
+ * @throws Error if the request fails or returns a non-OK response
218
+ */
219
+ async getMatrix(body) {
220
+ const { q, latestby, limit } = body;
221
+ // Build query parameters manually to avoid URL encoding of square brackets
222
+ const queryParts = [];
223
+ // Add optional parameters
224
+ if (latestby) {
225
+ queryParts.push(`latestby=${encodeURIComponent(latestby)}`);
226
+ }
227
+ if (limit !== undefined) {
228
+ queryParts.push(`limit=${limit}`);
229
+ }
230
+ // Add the q parameters (pacticipant selectors)
231
+ q.forEach((selector) => {
232
+ queryParts.push(`q[]pacticipant=${encodeURIComponent(selector.pacticipant)}`);
233
+ if (selector.version) {
234
+ queryParts.push(`q[]version=${encodeURIComponent(selector.version)}`);
235
+ }
236
+ if (selector.branch) {
237
+ queryParts.push(`q[]branch=${encodeURIComponent(selector.branch)}`);
238
+ }
239
+ if (selector.environment) {
240
+ queryParts.push(`q[]environment=${encodeURIComponent(selector.environment)}`);
241
+ }
242
+ if (selector.latest !== undefined) {
243
+ queryParts.push(`q[]latest=${selector.latest}`);
244
+ }
245
+ if (selector.tag) {
246
+ queryParts.push(`q[]tag=${encodeURIComponent(selector.tag)}`);
247
+ }
248
+ if (selector.mainBranch !== undefined) {
249
+ queryParts.push(`q[]mainBranch=${selector.mainBranch}`);
250
+ }
251
+ });
252
+ const url = `${this.baseUrl}/matrix?${queryParts.join('&')}`;
253
+ try {
254
+ const response = await fetch(url, {
255
+ method: "GET",
256
+ headers: this.headers,
257
+ });
258
+ if (!response.ok) {
259
+ const errorText = await response.text().catch(() => "");
260
+ throw new Error(`Matrix Request Failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
261
+ }
262
+ return (await response.json());
263
+ }
264
+ catch (error) {
265
+ console.error("[GetMatrix] Unexpected error:", error);
266
+ throw error;
267
+ }
268
+ }
269
+ /**
270
+ * Registers tools with the provided register function.
271
+ *
272
+ * @param register - The function used to register tools.
273
+ * @param getInput - The function used to get input for tools.
274
+ */
275
+ registerTools(register, getInput) {
161
276
  for (const tool of TOOLS.filter(t => t.clients.includes(this.clientType))) {
162
277
  const { handler, clients: _, formatResponse, ...toolparams } = tool; // eslint-disable-line @typescript-eslint/no-unused-vars
163
278
  register(toolparams, async (args, _extra) => {
@@ -165,7 +280,13 @@ export class PactflowClient {
165
280
  if (typeof handler_fn !== "function") {
166
281
  throw new Error(`Handler '${handler}' not found on PactClient`);
167
282
  }
168
- const result = await handler_fn.call(this, args);
283
+ let result;
284
+ if (tool.enableElicitation) {
285
+ result = await handler_fn.call(this, args, getInput);
286
+ }
287
+ else {
288
+ result = await handler_fn.call(this, args);
289
+ }
169
290
  // Use custom response formatter if provided
170
291
  if (formatResponse) {
171
292
  return formatResponse(result);
@@ -177,4 +298,14 @@ export class PactflowClient {
177
298
  });
178
299
  }
179
300
  }
301
+ /**
302
+ * Registers prompts with the provided register function.
303
+ *
304
+ * @param register - The function used to register prompts.
305
+ */
306
+ registerPrompts(register) {
307
+ PROMPTS.forEach(prompt => {
308
+ register(prompt.name, prompt.params, prompt.callback);
309
+ });
310
+ }
180
311
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartbear/mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "MCP server for interacting SmartBear Products",
5
5
  "keywords": [
6
6
  "smartbear",
@@ -11,6 +11,7 @@
11
11
  "pactflow"
12
12
  ],
13
13
  "homepage": "https://developer.smartbear.com/smartbear-mcp",
14
+ "mcpName": "com.smartbear/smartbear-mcp",
14
15
  "repository": {
15
16
  "type": "git",
16
17
  "url": "git@github.com:SmartBear/smartbear-mcp.git"
@@ -45,7 +46,8 @@
45
46
  "@modelcontextprotocol/sdk": "^1.15.0",
46
47
  "node-cache": "^5.1.2",
47
48
  "swagger-client": "^3.35.6",
48
- "zod": "^3"
49
+ "zod": "^3",
50
+ "zod-to-json-schema": "^3.24.6"
49
51
  },
50
52
  "devDependencies": {
51
53
  "@eslint/js": "^9.29.0",