@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.
- package/README.md +86 -25
- package/dist/api-hub/client.js +298 -52
- package/dist/{insight-hub → bugsnag}/client/api/CurrentUser.js +2 -1
- package/dist/{insight-hub → bugsnag}/client/api/Error.js +37 -4
- package/dist/{insight-hub → bugsnag}/client/api/Project.js +2 -1
- package/dist/{insight-hub → bugsnag}/client/api/base.js +18 -1
- package/dist/{insight-hub → bugsnag}/client/api/filters.js +2 -2
- package/dist/bugsnag/client.js +687 -0
- package/dist/common/info.js +1 -1
- package/dist/common/server.js +148 -0
- package/dist/index.js +38 -29
- package/dist/pactflow/client/ai.js +146 -0
- package/dist/pactflow/client/base.js +6 -0
- package/dist/pactflow/client/tools.js +55 -0
- package/dist/pactflow/client/utils.js +70 -0
- package/dist/pactflow/client.js +180 -0
- package/dist/reflect/client.js +100 -18
- package/package.json +8 -4
- package/dist/common/templates.js +0 -57
- package/dist/insight-hub/client.js +0 -621
- package/dist/package.json +0 -59
- package/dist/tests/unit/common/templates.test.js +0 -149
- package/dist/tests/unit/insight-hub/api-utilities.test.js +0 -31
- package/dist/tests/unit/insight-hub/client.test.js +0 -920
- package/dist/tests/unit/insight-hub/filters.test.js +0 -93
- package/dist/vitest.config.js +0 -57
- /package/dist/{insight-hub → bugsnag}/client/api/index.js +0 -0
- /package/dist/{insight-hub → bugsnag}/client/configuration.js +0 -0
- /package/dist/{insight-hub → bugsnag}/client/index.js +0 -0
package/dist/common/info.js
CHANGED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { ZodAny, ZodArray, ZodBoolean, ZodEnum, ZodLiteral, ZodNumber, ZodObject, ZodString, ZodUnion, } from "zod";
|
|
3
|
+
import Bugsnag from "../common/bugsnag.js";
|
|
4
|
+
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "./info.js";
|
|
5
|
+
export class SmartBearMcpServer extends McpServer {
|
|
6
|
+
constructor() {
|
|
7
|
+
super({
|
|
8
|
+
name: MCP_SERVER_NAME,
|
|
9
|
+
version: MCP_SERVER_VERSION,
|
|
10
|
+
}, {
|
|
11
|
+
capabilities: {
|
|
12
|
+
resources: { listChanged: true }, // Server supports dynamic resource lists
|
|
13
|
+
tools: { listChanged: true }, // Server supports dynamic tool lists
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
addClient(client) {
|
|
18
|
+
client.registerTools((params, cb) => {
|
|
19
|
+
const toolName = `${client.prefix}_${params.title.replace(/\s+/g, "_").toLowerCase()}`;
|
|
20
|
+
const toolTitle = `${client.name}: ${params.title}`;
|
|
21
|
+
return super.registerTool(toolName, {
|
|
22
|
+
title: toolTitle,
|
|
23
|
+
description: this.getDescription(params),
|
|
24
|
+
inputSchema: this.getInputSchema(params),
|
|
25
|
+
annotations: this.getAnnotations(toolTitle, params),
|
|
26
|
+
}, async (args, extra) => {
|
|
27
|
+
try {
|
|
28
|
+
return await cb(args, extra);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
Bugsnag.notify(e);
|
|
32
|
+
throw e;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}, (params, options) => {
|
|
36
|
+
return this.server.elicitInput(params, options);
|
|
37
|
+
});
|
|
38
|
+
if (client.registerResources) {
|
|
39
|
+
client.registerResources((name, path, cb) => {
|
|
40
|
+
return super.registerResource(name, new ResourceTemplate(`${client.prefix}://${name}/${path}`, { list: undefined }), {}, async (url, variables, extra) => {
|
|
41
|
+
try {
|
|
42
|
+
return await cb(url, variables, extra);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
Bugsnag.notify(e);
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
getAnnotations(toolTitle, params) {
|
|
53
|
+
const annotations = {
|
|
54
|
+
title: toolTitle,
|
|
55
|
+
readOnlyHint: params.readOnly ?? true,
|
|
56
|
+
destructiveHint: params.destructive ?? false,
|
|
57
|
+
idempotentHint: params.idempotent ?? true,
|
|
58
|
+
openWorldHint: params.openWorld ?? false,
|
|
59
|
+
};
|
|
60
|
+
return annotations;
|
|
61
|
+
}
|
|
62
|
+
getInputSchema(params) {
|
|
63
|
+
const args = {};
|
|
64
|
+
for (const param of params.parameters ?? []) {
|
|
65
|
+
args[param.name] = param.type;
|
|
66
|
+
if (param.description) {
|
|
67
|
+
args[param.name] = args[param.name].describe(param.description);
|
|
68
|
+
}
|
|
69
|
+
if (!param.required) {
|
|
70
|
+
args[param.name] = args[param.name].optional();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (params.zodSchema && params.zodSchema instanceof ZodObject) {
|
|
74
|
+
for (const key of Object.keys(params.zodSchema.shape)) {
|
|
75
|
+
const field = params.zodSchema.shape[key];
|
|
76
|
+
args[key] = field;
|
|
77
|
+
if (field.description) {
|
|
78
|
+
args[key] = args[key].describe(field.description);
|
|
79
|
+
}
|
|
80
|
+
if (field.isOptional()) {
|
|
81
|
+
args[key] = args[key].optional();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return args;
|
|
86
|
+
}
|
|
87
|
+
getDescription(params) {
|
|
88
|
+
const { summary, useCases, examples, parameters, zodSchema, hints, outputFormat } = params;
|
|
89
|
+
let description = summary;
|
|
90
|
+
// Parameters if available otherwise use zodSchema
|
|
91
|
+
if ((parameters ?? []).length > 0) {
|
|
92
|
+
description += `\n\n**Parameters:**\n${parameters?.map(p => `- ${p.name} (${this.getReadableTypeName(p.type)})${p.required ? ' *required*' : ''}` +
|
|
93
|
+
`${p.description ? `: ${p.description}` : ''}` +
|
|
94
|
+
`${p.examples ? ` (e.g. ${p.examples.join(', ')})` : ''}` +
|
|
95
|
+
`${p.constraints ? `\n - ${p.constraints.join('\n - ')}` : ''}`).join('\n')}`;
|
|
96
|
+
}
|
|
97
|
+
if (zodSchema && zodSchema instanceof ZodObject) {
|
|
98
|
+
description += "\n\n**Parameters:**\n";
|
|
99
|
+
description += Object.keys(zodSchema.shape)
|
|
100
|
+
.map(key => this.formatParameterDescription(key, zodSchema.shape[key]))
|
|
101
|
+
.join('\n');
|
|
102
|
+
}
|
|
103
|
+
if (outputFormat) {
|
|
104
|
+
description += `\n\n**Output Format:** ${outputFormat}`;
|
|
105
|
+
}
|
|
106
|
+
// Use Cases
|
|
107
|
+
if (useCases && useCases.length > 0) {
|
|
108
|
+
description += `\n\n**Use Cases:** ${useCases.map((uc, i) => `${i + 1}. ${uc}`).join(' ')}`;
|
|
109
|
+
}
|
|
110
|
+
// Examples
|
|
111
|
+
if (examples && examples.length > 0) {
|
|
112
|
+
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');
|
|
113
|
+
}
|
|
114
|
+
// Hints
|
|
115
|
+
if (hints && hints.length > 0) {
|
|
116
|
+
description += `\n\n**Hints:** ${hints.map((hint, i) => `${i + 1}. ${hint}`).join(' ')}`;
|
|
117
|
+
}
|
|
118
|
+
return description.trim();
|
|
119
|
+
}
|
|
120
|
+
formatParameterDescription(key, field) {
|
|
121
|
+
return `- ${key} (${this.getReadableTypeName(field)})` +
|
|
122
|
+
`${field.isOptional() ? '' : ' *required*'}` +
|
|
123
|
+
`${field.description ? `: ${field.description}` : ''}` +
|
|
124
|
+
`${key === "examples" && field instanceof ZodEnum ? ` (e.g. ${Object.keys(field.enum).join(', ')})` : ''}` +
|
|
125
|
+
`${key === "constraints" && field instanceof ZodEnum ? `\n - ${Object.keys(field.enum).join('\n - ')}` : ''}`;
|
|
126
|
+
}
|
|
127
|
+
getReadableTypeName(zodType) {
|
|
128
|
+
if (zodType instanceof ZodString)
|
|
129
|
+
return 'string';
|
|
130
|
+
if (zodType instanceof ZodNumber)
|
|
131
|
+
return 'number';
|
|
132
|
+
if (zodType instanceof ZodBoolean)
|
|
133
|
+
return 'boolean';
|
|
134
|
+
if (zodType instanceof ZodArray)
|
|
135
|
+
return 'array';
|
|
136
|
+
if (zodType instanceof ZodObject)
|
|
137
|
+
return 'object';
|
|
138
|
+
if (zodType instanceof ZodEnum)
|
|
139
|
+
return 'enum';
|
|
140
|
+
if (zodType instanceof ZodLiteral)
|
|
141
|
+
return 'literal';
|
|
142
|
+
if (zodType instanceof ZodUnion)
|
|
143
|
+
return 'union';
|
|
144
|
+
if (zodType instanceof ZodAny)
|
|
145
|
+
return 'any';
|
|
146
|
+
return 'any';
|
|
147
|
+
}
|
|
148
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,48 +1,57 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
3
|
import Bugsnag from "./common/bugsnag.js";
|
|
5
|
-
import {
|
|
6
|
-
import { InsightHubClient } from "./insight-hub/client.js";
|
|
4
|
+
import { BugsnagClient } from "./bugsnag/client.js";
|
|
7
5
|
import { ReflectClient } from "./reflect/client.js";
|
|
8
6
|
import { ApiHubClient } from "./api-hub/client.js";
|
|
7
|
+
import { SmartBearMcpServer } from "./common/server.js";
|
|
8
|
+
import { PactflowClient } from "./pactflow/client.js";
|
|
9
9
|
// This is used to report errors in the MCP server itself
|
|
10
|
-
// If you want to use your own BugSnag API key, set the
|
|
11
|
-
const McpServerBugsnagAPIKey = process.env.
|
|
10
|
+
// If you want to use your own BugSnag API key, set the MCP_SERVER_BUGSNAG_API_KEY environment variable
|
|
11
|
+
const McpServerBugsnagAPIKey = process.env.MCP_SERVER_BUGSNAG_API_KEY;
|
|
12
12
|
if (McpServerBugsnagAPIKey) {
|
|
13
13
|
Bugsnag.start(McpServerBugsnagAPIKey);
|
|
14
14
|
}
|
|
15
15
|
async function main() {
|
|
16
|
-
const server = new
|
|
17
|
-
name: MCP_SERVER_NAME,
|
|
18
|
-
version: MCP_SERVER_VERSION,
|
|
19
|
-
}, {
|
|
20
|
-
capabilities: {
|
|
21
|
-
resources: { listChanged: true }, // Server supports dynamic resource lists
|
|
22
|
-
tools: { listChanged: true }, // Server supports dynamic tool lists
|
|
23
|
-
},
|
|
24
|
-
});
|
|
16
|
+
const server = new SmartBearMcpServer();
|
|
25
17
|
const reflectToken = process.env.REFLECT_API_TOKEN;
|
|
26
|
-
const
|
|
18
|
+
const bugsnagToken = process.env.BUGSNAG_AUTH_TOKEN;
|
|
27
19
|
const apiHubToken = process.env.API_HUB_API_KEY;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
const pactBrokerToken = process.env.PACT_BROKER_TOKEN;
|
|
21
|
+
const pactBrokerUrl = process.env.PACT_BROKER_BASE_URL;
|
|
22
|
+
const pactBrokerUsername = process.env.PACT_BROKER_USERNAME;
|
|
23
|
+
const pactBrokerPassword = process.env.PACT_BROKER_PASSWORD;
|
|
24
|
+
let client_defined = false;
|
|
32
25
|
if (reflectToken) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
reflectClient.registerResources(server);
|
|
26
|
+
server.addClient(new ReflectClient(reflectToken));
|
|
27
|
+
client_defined = true;
|
|
36
28
|
}
|
|
37
|
-
if (
|
|
38
|
-
const
|
|
39
|
-
await
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
if (bugsnagToken) {
|
|
30
|
+
const bugsnagClient = new BugsnagClient(bugsnagToken, process.env.BUGSNAG_PROJECT_API_KEY, process.env.BUGSNAG_ENDPOINT);
|
|
31
|
+
await bugsnagClient.initialize();
|
|
32
|
+
server.addClient(bugsnagClient);
|
|
33
|
+
client_defined = true;
|
|
42
34
|
}
|
|
43
35
|
if (apiHubToken) {
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
server.addClient(new ApiHubClient(apiHubToken));
|
|
37
|
+
client_defined = true;
|
|
38
|
+
}
|
|
39
|
+
if (pactBrokerUrl) {
|
|
40
|
+
if (pactBrokerToken) {
|
|
41
|
+
server.addClient(new PactflowClient(pactBrokerToken, pactBrokerUrl, "pactflow"));
|
|
42
|
+
client_defined = true;
|
|
43
|
+
}
|
|
44
|
+
else if (pactBrokerUsername && pactBrokerPassword) {
|
|
45
|
+
server.addClient(new PactflowClient({ username: pactBrokerUsername, password: pactBrokerPassword }, pactBrokerUrl, "pact_broker"));
|
|
46
|
+
client_defined = true;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.error("If the Pact Broker base URL is specified, you must specify either (a) a PactFlow token, or (b) a Pact Broker username and password pair.");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!client_defined) {
|
|
53
|
+
console.error("Please set one of REFLECT_API_TOKEN, BUGSNAG_AUTH_TOKEN, API_HUB_API_KEY or PACT_BROKER_BASE_URL / (and relevant Pact auth) environment variables");
|
|
54
|
+
process.exit(1);
|
|
46
55
|
}
|
|
47
56
|
const transport = new StdioServerTransport();
|
|
48
57
|
await server.connect(transport);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { addOpenAPISpecToSchema } from "./utils.js";
|
|
3
|
+
// Type definitions for PactFlow AI API
|
|
4
|
+
export const GenerationLanguages = [
|
|
5
|
+
"javascript",
|
|
6
|
+
"typescript",
|
|
7
|
+
"java",
|
|
8
|
+
"golang",
|
|
9
|
+
"dotnet",
|
|
10
|
+
"kotlin",
|
|
11
|
+
"swift",
|
|
12
|
+
"php",
|
|
13
|
+
];
|
|
14
|
+
export const HttpMethods = [
|
|
15
|
+
"GET",
|
|
16
|
+
"PUT",
|
|
17
|
+
"POST",
|
|
18
|
+
"DELETE",
|
|
19
|
+
"OPTIONS",
|
|
20
|
+
"HEAD",
|
|
21
|
+
"PATCH",
|
|
22
|
+
"TRACE",
|
|
23
|
+
];
|
|
24
|
+
// zod schemas
|
|
25
|
+
export const FileInputSchema = z.object({
|
|
26
|
+
filename: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Filename (helps identify file type and context)"),
|
|
30
|
+
language: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Programming language (e.g., 'javascript', 'java', 'python') for better analysis"),
|
|
34
|
+
body: z
|
|
35
|
+
.string()
|
|
36
|
+
.describe("Complete file contents - client code, models, test files, etc."),
|
|
37
|
+
});
|
|
38
|
+
export const OpenAPISchema = z
|
|
39
|
+
.object({
|
|
40
|
+
openapi: z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("For OpenAPI version (e.g., '3.0.0')"),
|
|
44
|
+
swagger: z
|
|
45
|
+
.string()
|
|
46
|
+
.describe("For OpenAPI documents version 2.x (e.g., '2.0')")
|
|
47
|
+
.optional(),
|
|
48
|
+
paths: z
|
|
49
|
+
.record(z.string(), z.record(z.string(), z.any()))
|
|
50
|
+
.describe("OpenAPI paths object containing all API endpoints"),
|
|
51
|
+
components: z
|
|
52
|
+
.record(z.string(), z.record(z.string(), z.any()))
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("OpenAPI components section (schemas, responses, etc.)"),
|
|
55
|
+
})
|
|
56
|
+
.passthrough()
|
|
57
|
+
.describe("The complete OpenAPI document describing the API.")
|
|
58
|
+
.refine((data) => data.openapi || data.swagger, {
|
|
59
|
+
message: "Either 'openapi' (for v3+) or 'swagger' (for v2) must be provided",
|
|
60
|
+
path: ["openapi"],
|
|
61
|
+
});
|
|
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 RemoteOpenAPIDocumentSchema = z
|
|
84
|
+
.object({
|
|
85
|
+
authToken: z
|
|
86
|
+
.string()
|
|
87
|
+
.describe("Auth Bearer Token if the OpenAPI spec requires authentication.")
|
|
88
|
+
.optional(),
|
|
89
|
+
authScheme: z
|
|
90
|
+
.string()
|
|
91
|
+
.describe("Authentication scheme (e.g., 'Bearer', 'Basic'). Default scheme passed should be Bearer if authToken is specified and this field is not set.")
|
|
92
|
+
.default("Bearer")
|
|
93
|
+
.optional(),
|
|
94
|
+
url: z
|
|
95
|
+
.string()
|
|
96
|
+
.url("Must be a valid openapi url")
|
|
97
|
+
.describe("URL of the remote OpenAPI document.")
|
|
98
|
+
.optional(),
|
|
99
|
+
})
|
|
100
|
+
.describe("Use this schema to fetch openapi documents present over a url.");
|
|
101
|
+
export const OpenAPIWithMatcherSchema = z
|
|
102
|
+
.object({
|
|
103
|
+
document: OpenAPISchema.describe("The OpenAPI document describing the API being tested. if document is not provided, don't add the field if remoteOpenAPIDocument is provided.").optional(),
|
|
104
|
+
matcher: EndpointMatcherSchema,
|
|
105
|
+
remoteDocument: RemoteOpenAPIDocumentSchema.optional().describe("The remote OpenAPI document to use for the review/generation in case openapi document is not provided. If provided do not include the document field under openapi."),
|
|
106
|
+
})
|
|
107
|
+
.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.").transform(addOpenAPISpecToSchema);
|
|
108
|
+
export const RefineInputSchema = z.object({
|
|
109
|
+
pactTests: FileInputSchema.describe("Primary pact tests that needs to be refined."),
|
|
110
|
+
code: z
|
|
111
|
+
.array(FileInputSchema)
|
|
112
|
+
.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")
|
|
113
|
+
.optional(),
|
|
114
|
+
userInstructions: z
|
|
115
|
+
.string()
|
|
116
|
+
.describe("Optional free-form instructions that provide additional context or specify areas of focus during the refinement process of the Pact test.")
|
|
117
|
+
.optional(),
|
|
118
|
+
errorMessages: z
|
|
119
|
+
.array(z.string())
|
|
120
|
+
.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.")
|
|
121
|
+
.optional(),
|
|
122
|
+
openapi: OpenAPIWithMatcherSchema.optional(),
|
|
123
|
+
});
|
|
124
|
+
export const RequestResponsePairSchema = z
|
|
125
|
+
.object({
|
|
126
|
+
request: FileInputSchema,
|
|
127
|
+
response: FileInputSchema,
|
|
128
|
+
})
|
|
129
|
+
.describe("Direct request/response pair for a specific interaction. Use this when you have concrete examples of API requests and responses");
|
|
130
|
+
export const GenerationInputSchema = z.object({
|
|
131
|
+
language: z
|
|
132
|
+
.enum(GenerationLanguages)
|
|
133
|
+
.optional()
|
|
134
|
+
.describe("Target language for the generated Pact tests. If not provided, will be inferred from other inputs."),
|
|
135
|
+
requestResponse: RequestResponsePairSchema.optional(),
|
|
136
|
+
code: z
|
|
137
|
+
.array(FileInputSchema)
|
|
138
|
+
.optional()
|
|
139
|
+
.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"),
|
|
140
|
+
openapi: OpenAPIWithMatcherSchema.optional(),
|
|
141
|
+
additionalInstructions: z
|
|
142
|
+
.string()
|
|
143
|
+
.optional()
|
|
144
|
+
.describe("Optional free-form instructions to guide the generation process (e.g., 'Focus on error scenarios', 'Include authentication headers', 'Use specific test framework patterns')"),
|
|
145
|
+
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"),
|
|
146
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
import { CanIDeploySchema } from "./base.js";
|
|
15
|
+
export const TOOLS = [
|
|
16
|
+
{
|
|
17
|
+
title: "Generate Pact Tests",
|
|
18
|
+
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.",
|
|
19
|
+
purpose: "Generate Pact tests for API interactions",
|
|
20
|
+
zodSchema: GenerationInputSchema,
|
|
21
|
+
handler: "generate",
|
|
22
|
+
clients: ["pactflow"], // ONLY pactflow
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
title: "Review Pact Tests",
|
|
26
|
+
summary: "Review Pact tests using PactFlow AI. You can provide the following inputs: (1) Pact tests to be reviewed along with metadata",
|
|
27
|
+
purpose: "Review Pact tests for API interactions",
|
|
28
|
+
zodSchema: RefineInputSchema,
|
|
29
|
+
handler: "review",
|
|
30
|
+
clients: ["pactflow"]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
title: "Get Provider States",
|
|
34
|
+
summary: "Retrieve the states of a specific provider",
|
|
35
|
+
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.",
|
|
36
|
+
parameters: [
|
|
37
|
+
{
|
|
38
|
+
name: "provider",
|
|
39
|
+
type: z.string(),
|
|
40
|
+
description: "name of the provider to retrieve states for",
|
|
41
|
+
required: true
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
handler: "getProviderStates",
|
|
45
|
+
clients: ["pactflow", "pact_broker"]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
title: "Can I Deploy",
|
|
49
|
+
summary: "Performs a comprehensive compatibility check to determine whether a specific version of a service (pacticipant) can be safely deployed into a given environment. It analyzes the complete contract matrix of consumer-provider relationships to confirm that all required integrations are verified and compatible.",
|
|
50
|
+
purpose: "To serve as a deployment safety check within the PactBroker and PactFlow ecosystem, leveraging contract testing results to validate whether a specific service / pacticipant version is compatible with all integrated services. This feature prevents unsafe releases, reduces integration risks, and enables teams to confidently automate deployments across environments with a clear, auditable record of verification results.",
|
|
51
|
+
zodSchema: CanIDeploySchema,
|
|
52
|
+
handler: "canIDeploy",
|
|
53
|
+
clients: ["pactflow", "pact_broker"]
|
|
54
|
+
}
|
|
55
|
+
];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { RemoteOpenAPIDocumentSchema, } from "./ai.js";
|
|
2
|
+
import yaml from "js-yaml";
|
|
3
|
+
// @ts-expect-error missing type declarations
|
|
4
|
+
import Swagger from "swagger-client";
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the OpenAPI specification from the provided input.
|
|
7
|
+
*
|
|
8
|
+
* @param remoteOpenAPIDocument The remote OpenAPI document to resolve.
|
|
9
|
+
* @returns The resolved OpenAPI document.
|
|
10
|
+
* @throws Error if the resolution fails.
|
|
11
|
+
*/
|
|
12
|
+
export async function resolveOpenAPISpec(remoteOpenAPIDocument) {
|
|
13
|
+
const openAPISchema = RemoteOpenAPIDocumentSchema.safeParse(remoteOpenAPIDocument);
|
|
14
|
+
if (openAPISchema.error || !remoteOpenAPIDocument) {
|
|
15
|
+
throw new Error(`Invalid RemoteOpenAPIDocument: ${JSON.stringify(openAPISchema.error?.issues)}`);
|
|
16
|
+
}
|
|
17
|
+
const unresolvedSpec = await getRemoteSpecContents(openAPISchema.data);
|
|
18
|
+
const resolvedSpec = await Swagger.resolve({ spec: unresolvedSpec });
|
|
19
|
+
if (resolvedSpec.errors?.length) {
|
|
20
|
+
throw new Error(`Failed to resolve OpenAPI document: ${resolvedSpec.errors?.join(", ")}`);
|
|
21
|
+
}
|
|
22
|
+
return resolvedSpec.spec;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Fetch the contents of a remote OpenAPI document.
|
|
26
|
+
*
|
|
27
|
+
* @param openAPISchema The schema for the remote OpenAPI document.
|
|
28
|
+
* @returns A promise that resolves to a map of the OpenAPI document contents.
|
|
29
|
+
* @throws Error if the URL is not provided or the fetch fails.
|
|
30
|
+
*/
|
|
31
|
+
export async function getRemoteSpecContents(openAPISchema) {
|
|
32
|
+
if (!openAPISchema.url) {
|
|
33
|
+
throw new Error("'url' must be provided.");
|
|
34
|
+
}
|
|
35
|
+
let headers = {};
|
|
36
|
+
if (openAPISchema.authToken) {
|
|
37
|
+
headers = {
|
|
38
|
+
Authorization: `${openAPISchema.authScheme ?? "Bearer"} ${openAPISchema.authToken}`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const remoteSpec = await fetch(openAPISchema.url, {
|
|
42
|
+
headers,
|
|
43
|
+
method: "GET",
|
|
44
|
+
});
|
|
45
|
+
const specRawBody = await remoteSpec.text();
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(specRawBody);
|
|
48
|
+
}
|
|
49
|
+
catch (jsonError) {
|
|
50
|
+
try {
|
|
51
|
+
return yaml.load(specRawBody);
|
|
52
|
+
}
|
|
53
|
+
catch (yamlError) {
|
|
54
|
+
throw new Error(`Unsupported Content-Type: ${remoteSpec.headers.get("Content-Type")} for remote OpenAPI document. Found following parse errors:-\nJSON parse error: ${jsonError}\nYAML parse error: ${yamlError}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Adds the OpenAPI specification to the input schema if a remote document is provided.
|
|
60
|
+
*
|
|
61
|
+
* @param inputSchema The input schema to modify.
|
|
62
|
+
* @returns The modified input schema with the OpenAPI specification added.
|
|
63
|
+
*/
|
|
64
|
+
export async function addOpenAPISpecToSchema(inputSchema) {
|
|
65
|
+
if (inputSchema.remoteDocument) {
|
|
66
|
+
const resolvedSpec = await resolveOpenAPISpec(inputSchema.remoteDocument);
|
|
67
|
+
inputSchema.document = resolvedSpec;
|
|
68
|
+
}
|
|
69
|
+
return inputSchema;
|
|
70
|
+
}
|