@smartbear/mcp 0.6.0 → 0.7.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 +20 -3
- package/dist/api-hub/client/api.js +253 -0
- package/dist/api-hub/client/configuration.js +27 -0
- package/dist/api-hub/client/index.js +5 -0
- package/dist/api-hub/client/portal-types.js +131 -0
- package/dist/api-hub/client/registry-types.js +55 -0
- package/dist/api-hub/client/tools.js +86 -0
- package/dist/api-hub/client.js +64 -404
- package/dist/bugsnag/client/api/CurrentUser.js +16 -10
- package/dist/bugsnag/client/api/Error.js +35 -35
- package/dist/bugsnag/client/api/Project.js +21 -9
- package/dist/bugsnag/client/api/base.js +7 -4
- package/dist/bugsnag/client/api/filters.js +9 -9
- package/dist/bugsnag/client.js +165 -140
- package/dist/common/info.js +1 -1
- package/dist/common/server.js +35 -27
- package/dist/index.js +11 -4
- package/dist/pactflow/client/ai.js +20 -20
- package/dist/pactflow/client/base.js +48 -13
- package/dist/pactflow/client/prompts.js +10 -12
- package/dist/pactflow/client/tools.js +10 -10
- package/dist/pactflow/client/utils.js +1 -1
- package/dist/pactflow/client.js +16 -9
- package/dist/qmetry/client/api/client-api.js +39 -0
- package/dist/qmetry/client/handlers.js +11 -0
- package/dist/qmetry/client/project.js +27 -0
- package/dist/qmetry/client/testcase.js +104 -0
- package/dist/qmetry/client/tools.js +222 -0
- package/dist/qmetry/client.js +95 -0
- package/dist/qmetry/config/constants.js +12 -0
- package/dist/qmetry/config/rest-endpoints.js +11 -0
- package/dist/qmetry/types/common.js +174 -0
- package/dist/qmetry/types/testcase.js +19 -0
- package/dist/reflect/client.js +14 -14
- package/package.json +6 -5
package/dist/common/info.js
CHANGED
package/dist/common/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1
|
+
import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { ZodAny, ZodArray, ZodBoolean, ZodEnum, ZodLiteral, ZodNumber, ZodObject, ZodString, ZodUnion, } from "zod";
|
|
3
3
|
import Bugsnag from "../common/bugsnag.js";
|
|
4
4
|
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "./info.js";
|
|
@@ -41,7 +41,9 @@ export class SmartBearMcpServer extends McpServer {
|
|
|
41
41
|
});
|
|
42
42
|
if (client.registerResources) {
|
|
43
43
|
client.registerResources((name, path, cb) => {
|
|
44
|
-
return super.registerResource(name, new ResourceTemplate(`${client.prefix}://${name}/${path}`, {
|
|
44
|
+
return super.registerResource(name, new ResourceTemplate(`${client.prefix}://${name}/${path}`, {
|
|
45
|
+
list: undefined,
|
|
46
|
+
}), {}, async (url, variables, extra) => {
|
|
45
47
|
try {
|
|
46
48
|
return await cb(url, variables, extra);
|
|
47
49
|
}
|
|
@@ -94,64 +96,70 @@ export class SmartBearMcpServer extends McpServer {
|
|
|
94
96
|
return args;
|
|
95
97
|
}
|
|
96
98
|
getDescription(params) {
|
|
97
|
-
const { summary, useCases, examples, parameters, zodSchema, hints, outputFormat } = params;
|
|
99
|
+
const { summary, useCases, examples, parameters, zodSchema, hints, outputFormat, } = params;
|
|
98
100
|
let description = summary;
|
|
99
101
|
// Parameters if available otherwise use zodSchema
|
|
100
102
|
if ((parameters ?? []).length > 0) {
|
|
101
|
-
description += `\n\n**Parameters:**\n${parameters
|
|
102
|
-
|
|
103
|
-
`${p.
|
|
104
|
-
`${p.
|
|
103
|
+
description += `\n\n**Parameters:**\n${parameters
|
|
104
|
+
?.map((p) => `- ${p.name} (${this.getReadableTypeName(p.type)})${p.required ? " *required*" : ""}` +
|
|
105
|
+
`${p.description ? `: ${p.description}` : ""}` +
|
|
106
|
+
`${p.examples ? ` (e.g. ${p.examples.join(", ")})` : ""}` +
|
|
107
|
+
`${p.constraints ? `\n - ${p.constraints.join("\n - ")}` : ""}`)
|
|
108
|
+
.join("\n")}`;
|
|
105
109
|
}
|
|
106
110
|
if (zodSchema && zodSchema instanceof ZodObject) {
|
|
107
111
|
description += "\n\n**Parameters:**\n";
|
|
108
112
|
description += Object.keys(zodSchema.shape)
|
|
109
|
-
.map(key => this.formatParameterDescription(key, zodSchema.shape[key]))
|
|
110
|
-
.join(
|
|
113
|
+
.map((key) => this.formatParameterDescription(key, zodSchema.shape[key]))
|
|
114
|
+
.join("\n");
|
|
111
115
|
}
|
|
112
116
|
if (outputFormat) {
|
|
113
117
|
description += `\n\n**Output Format:** ${outputFormat}`;
|
|
114
118
|
}
|
|
115
119
|
// Use Cases
|
|
116
120
|
if (useCases && useCases.length > 0) {
|
|
117
|
-
description += `\n\n**Use Cases:** ${useCases.map((uc, i) => `${i + 1}. ${uc}`).join(
|
|
121
|
+
description += `\n\n**Use Cases:** ${useCases.map((uc, i) => `${i + 1}. ${uc}`).join(" ")}`;
|
|
118
122
|
}
|
|
119
123
|
// Examples
|
|
120
124
|
if (examples && examples.length > 0) {
|
|
121
|
-
description +=
|
|
125
|
+
description +=
|
|
126
|
+
`\n\n**Examples:**\n` +
|
|
127
|
+
examples
|
|
128
|
+
.map((ex, idx) => `${idx + 1}. ${ex.description}\n\`\`\`json\n${JSON.stringify(ex.parameters, null, 2)}\n\`\`\`${ex.expectedOutput ? `\nExpected Output: ${ex.expectedOutput}` : ""}`)
|
|
129
|
+
.join("\n\n");
|
|
122
130
|
}
|
|
123
131
|
// Hints
|
|
124
132
|
if (hints && hints.length > 0) {
|
|
125
|
-
description += `\n\n**Hints:** ${hints.map((hint, i) => `${i + 1}. ${hint}`).join(
|
|
133
|
+
description += `\n\n**Hints:** ${hints.map((hint, i) => `${i + 1}. ${hint}`).join(" ")}`;
|
|
126
134
|
}
|
|
127
135
|
return description.trim();
|
|
128
136
|
}
|
|
129
137
|
formatParameterDescription(key, field) {
|
|
130
|
-
return `- ${key} (${this.getReadableTypeName(field)})` +
|
|
131
|
-
`${field.isOptional() ?
|
|
132
|
-
`${field.description ? `: ${field.description}` :
|
|
133
|
-
`${key === "examples" && field instanceof ZodEnum ? ` (e.g. ${Object.keys(field.enum).join(
|
|
134
|
-
`${key === "constraints" && field instanceof ZodEnum ? `\n - ${Object.keys(field.enum).join(
|
|
138
|
+
return (`- ${key} (${this.getReadableTypeName(field)})` +
|
|
139
|
+
`${field.isOptional() ? "" : " *required*"}` +
|
|
140
|
+
`${field.description ? `: ${field.description}` : ""}` +
|
|
141
|
+
`${key === "examples" && field instanceof ZodEnum ? ` (e.g. ${Object.keys(field.enum).join(", ")})` : ""}` +
|
|
142
|
+
`${key === "constraints" && field instanceof ZodEnum ? `\n - ${Object.keys(field.enum).join("\n - ")}` : ""}`);
|
|
135
143
|
}
|
|
136
144
|
getReadableTypeName(zodType) {
|
|
137
145
|
if (zodType instanceof ZodString)
|
|
138
|
-
return
|
|
146
|
+
return "string";
|
|
139
147
|
if (zodType instanceof ZodNumber)
|
|
140
|
-
return
|
|
148
|
+
return "number";
|
|
141
149
|
if (zodType instanceof ZodBoolean)
|
|
142
|
-
return
|
|
150
|
+
return "boolean";
|
|
143
151
|
if (zodType instanceof ZodArray)
|
|
144
|
-
return
|
|
152
|
+
return "array";
|
|
145
153
|
if (zodType instanceof ZodObject)
|
|
146
|
-
return
|
|
154
|
+
return "object";
|
|
147
155
|
if (zodType instanceof ZodEnum)
|
|
148
|
-
return
|
|
156
|
+
return "enum";
|
|
149
157
|
if (zodType instanceof ZodLiteral)
|
|
150
|
-
return
|
|
158
|
+
return "literal";
|
|
151
159
|
if (zodType instanceof ZodUnion)
|
|
152
|
-
return
|
|
160
|
+
return "union";
|
|
153
161
|
if (zodType instanceof ZodAny)
|
|
154
|
-
return
|
|
155
|
-
return
|
|
162
|
+
return "any";
|
|
163
|
+
return "any";
|
|
156
164
|
}
|
|
157
165
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import Bugsnag from "./common/bugsnag.js";
|
|
4
|
-
import { BugsnagClient } from "./bugsnag/client.js";
|
|
5
|
-
import { ReflectClient } from "./reflect/client.js";
|
|
6
3
|
import { ApiHubClient } from "./api-hub/client.js";
|
|
4
|
+
import { BugsnagClient } from "./bugsnag/client.js";
|
|
5
|
+
import Bugsnag from "./common/bugsnag.js";
|
|
7
6
|
import { SmartBearMcpServer } from "./common/server.js";
|
|
8
7
|
import { PactflowClient } from "./pactflow/client.js";
|
|
8
|
+
import { QmetryClient } from "./qmetry/client.js";
|
|
9
|
+
import { ReflectClient } from "./reflect/client.js";
|
|
9
10
|
// This is used to report errors in the MCP server itself
|
|
10
11
|
// If you want to use your own BugSnag API key, set the MCP_SERVER_BUGSNAG_API_KEY environment variable
|
|
11
12
|
const McpServerBugsnagAPIKey = process.env.MCP_SERVER_BUGSNAG_API_KEY;
|
|
@@ -21,6 +22,8 @@ async function main() {
|
|
|
21
22
|
const pactBrokerUrl = process.env.PACT_BROKER_BASE_URL;
|
|
22
23
|
const pactBrokerUsername = process.env.PACT_BROKER_USERNAME;
|
|
23
24
|
const pactBrokerPassword = process.env.PACT_BROKER_PASSWORD;
|
|
25
|
+
const qmetryToken = process.env.QMETRY_API_KEY;
|
|
26
|
+
const qmetryBaseUrl = process.env.QMETRY_BASE_URL;
|
|
24
27
|
let client_defined = false;
|
|
25
28
|
if (reflectToken) {
|
|
26
29
|
server.addClient(new ReflectClient(reflectToken));
|
|
@@ -49,8 +52,12 @@ async function main() {
|
|
|
49
52
|
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
53
|
}
|
|
51
54
|
}
|
|
55
|
+
if (qmetryToken) {
|
|
56
|
+
server.addClient(new QmetryClient(qmetryToken, qmetryBaseUrl));
|
|
57
|
+
client_defined = true;
|
|
58
|
+
}
|
|
52
59
|
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");
|
|
60
|
+
console.error("Please set one of REFLECT_API_TOKEN, BUGSNAG_AUTH_TOKEN, API_HUB_API_KEY, QMETRY_API_KEY or PACT_BROKER_BASE_URL / (and relevant Pact auth) environment variables");
|
|
54
61
|
process.exit(1);
|
|
55
62
|
}
|
|
56
63
|
const transport = new StdioServerTransport();
|
|
@@ -146,18 +146,15 @@ export const GenerationInputSchema = z.object({
|
|
|
146
146
|
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"),
|
|
147
147
|
});
|
|
148
148
|
export const MatcherRecommendationInputSchema = z.array(EndpointMatcherSchema);
|
|
149
|
-
export const AiCreditsSchema = z
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
name: z
|
|
159
|
-
.string()
|
|
160
|
-
.describe("The name of the organization."),
|
|
149
|
+
export const AiCreditsSchema = z
|
|
150
|
+
.object({
|
|
151
|
+
total: z.number().describe("The total number of AI credits available."),
|
|
152
|
+
used: z.number().describe("The number of AI credits used."),
|
|
153
|
+
})
|
|
154
|
+
.describe("AI credits information.");
|
|
155
|
+
export const OrganizationEntitlementsSchema = z
|
|
156
|
+
.object({
|
|
157
|
+
name: z.string().describe("The name of the organization."),
|
|
161
158
|
planAiEnabled: z
|
|
162
159
|
.boolean()
|
|
163
160
|
.describe("Whether AI features are enabled at the plan level."),
|
|
@@ -165,13 +162,16 @@ export const OrganizationEntitlementsSchema = z.object({
|
|
|
165
162
|
.boolean()
|
|
166
163
|
.describe("Whether AI features are enabled at the preferences level."),
|
|
167
164
|
aiCredits: AiCreditsSchema.describe("AI credits information."),
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
})
|
|
174
|
-
|
|
165
|
+
})
|
|
166
|
+
.describe("Organization entitlements information.");
|
|
167
|
+
export const UserEntitlementsSchema = z
|
|
168
|
+
.object({
|
|
169
|
+
aiPermissions: z.array(z.string()).describe("List of AI permissions."),
|
|
170
|
+
})
|
|
171
|
+
.describe("User entitlements information.");
|
|
172
|
+
export const EntitlementsSchema = z
|
|
173
|
+
.object({
|
|
175
174
|
organizationEntitlements: OrganizationEntitlementsSchema.describe("Organization entitlements information."),
|
|
176
175
|
userEntitlements: UserEntitlementsSchema.describe("User entitlements information."),
|
|
177
|
-
})
|
|
176
|
+
})
|
|
177
|
+
.describe("Entitlements information.");
|
|
@@ -1,19 +1,54 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export const CanIDeploySchema = z.object({
|
|
3
|
-
pacticipant: z
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
pacticipant: z
|
|
4
|
+
.string()
|
|
5
|
+
.describe("The name of the pacticipant (application/service) being evaluated for deployment"),
|
|
6
|
+
version: z
|
|
7
|
+
.string()
|
|
8
|
+
.describe("The version of the pacticipant that you want to check if it's safe to deploy"),
|
|
9
|
+
environment: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe("The target environment where the pacticipant version will be deployed (e.g., 'production', 'staging', 'test')"),
|
|
6
12
|
});
|
|
7
13
|
export const MatrixSchema = z.object({
|
|
8
|
-
latestby: z
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
latestby: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("This property removes the rows for the overridden pacts/verifications from the results. The options are cvp (show only the latest row for each consumer version and provider) and cvpv (show only the latest row each consumer version and provider version). For a can-i-deploy query with one selector, it should be set to cvp. For a can-i-deploy query with two selectors, it should be set to cvpv."),
|
|
18
|
+
limit: z
|
|
19
|
+
.number()
|
|
20
|
+
.min(1)
|
|
21
|
+
.max(1000)
|
|
22
|
+
.default(100)
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("The limit on the number of results to return (1-1000, default: 100)"),
|
|
25
|
+
q: z
|
|
26
|
+
.array(z.object({
|
|
27
|
+
pacticipant: z
|
|
28
|
+
.string()
|
|
29
|
+
.describe("Name of the pacticipant (application)"),
|
|
12
30
|
version: z.string().optional().describe("Version number"),
|
|
13
|
-
branch: z
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
31
|
+
branch: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Name of the pacticipant version branch"),
|
|
35
|
+
environment: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("The name of the environment that the pacticipant version is deployed to"),
|
|
39
|
+
latest: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Used in conjunction with other properties to indicate whether the selector is describing the latest version from a branch/with a tag/for a pacticipant, or all of them. Note that when used with tags, the 'latest' is calculated using the creation date of the pacticipant version, NOT the creation date of the tag."),
|
|
43
|
+
tag: z
|
|
44
|
+
.string()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("The name of the pacticipant version tag (superseded by branch and environments)"),
|
|
47
|
+
mainBranch: z
|
|
48
|
+
.boolean()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("Whether or not the version(s) described are from the main branch of the pacticipant, as set in the mainBranch property of the pacticipant resource."),
|
|
51
|
+
}))
|
|
52
|
+
.min(1)
|
|
53
|
+
.max(2),
|
|
19
54
|
});
|
|
@@ -116,18 +116,16 @@ export const PROMPTS = [
|
|
|
116
116
|
openAPI: z.string(),
|
|
117
117
|
},
|
|
118
118
|
},
|
|
119
|
-
callback:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
text: OADMatcherPrompt.replace("{0}", openAPI),
|
|
127
|
-
},
|
|
119
|
+
callback: ({ openAPI }) => ({
|
|
120
|
+
messages: [
|
|
121
|
+
{
|
|
122
|
+
role: "user",
|
|
123
|
+
content: {
|
|
124
|
+
type: "text",
|
|
125
|
+
text: OADMatcherPrompt.replace("{0}", openAPI),
|
|
128
126
|
},
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
},
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
}),
|
|
132
130
|
},
|
|
133
131
|
];
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* and registered with their corresponding handler.
|
|
11
11
|
*/
|
|
12
12
|
import { z } from "zod";
|
|
13
|
-
import { GenerationInputSchema, RefineInputSchema
|
|
13
|
+
import { GenerationInputSchema, RefineInputSchema } from "./ai.js";
|
|
14
14
|
import { CanIDeploySchema, MatrixSchema } from "./base.js";
|
|
15
15
|
export const TOOLS = [
|
|
16
16
|
{
|
|
@@ -40,11 +40,11 @@ export const TOOLS = [
|
|
|
40
40
|
name: "provider",
|
|
41
41
|
type: z.string(),
|
|
42
42
|
description: "name of the provider to retrieve states for",
|
|
43
|
-
required: true
|
|
44
|
-
}
|
|
43
|
+
required: true,
|
|
44
|
+
},
|
|
45
45
|
],
|
|
46
46
|
handler: "getProviderStates",
|
|
47
|
-
clients: ["pactflow", "pact_broker"]
|
|
47
|
+
clients: ["pactflow", "pact_broker"],
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
title: "Can I Deploy",
|
|
@@ -52,7 +52,7 @@ export const TOOLS = [
|
|
|
52
52
|
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.",
|
|
53
53
|
zodSchema: CanIDeploySchema,
|
|
54
54
|
handler: "canIDeploy",
|
|
55
|
-
clients: ["pactflow", "pact_broker"]
|
|
55
|
+
clients: ["pactflow", "pact_broker"],
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
title: "Matrix",
|
|
@@ -64,11 +64,11 @@ export const TOOLS = [
|
|
|
64
64
|
"Visualize the overall contract compatibility across two pacticipants / services.",
|
|
65
65
|
"Perform advanced queries using selectors to understand compatibility within specific branches, environments, or version ranges.",
|
|
66
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."
|
|
67
|
+
"Expose contract verification details to non-frequent API users in a more accessible format.",
|
|
68
68
|
],
|
|
69
69
|
zodSchema: MatrixSchema,
|
|
70
70
|
handler: "getMatrix",
|
|
71
|
-
clients: ["pactflow", "pact_broker"]
|
|
71
|
+
clients: ["pactflow", "pact_broker"],
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
title: "PactFlow AI Status",
|
|
@@ -79,9 +79,9 @@ export const TOOLS = [
|
|
|
79
79
|
"Monitor remaining and consumed AI credits to manage usage and avoid unexpected disruptions",
|
|
80
80
|
"Detect entitlement or permission issues when a user tries to access AI features and guide corrective actions",
|
|
81
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"
|
|
82
|
+
"Fetches usage and entitlement reports for auditing, budgeting, and compliance purposes",
|
|
83
83
|
],
|
|
84
84
|
handler: "getAIStatus",
|
|
85
|
-
clients: ["pactflow"]
|
|
86
|
-
}
|
|
85
|
+
clients: ["pactflow"],
|
|
86
|
+
},
|
|
87
87
|
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { RemoteOpenAPIDocumentSchema, } from "./ai.js";
|
|
2
1
|
import yaml from "js-yaml";
|
|
3
2
|
// @ts-expect-error missing type declarations
|
|
4
3
|
import Swagger from "swagger-client";
|
|
4
|
+
import { RemoteOpenAPIDocumentSchema, } from "./ai.js";
|
|
5
5
|
/**
|
|
6
6
|
* Resolve the OpenAPI specification from the provided input.
|
|
7
7
|
*
|
package/dist/pactflow/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
|
|
2
|
-
import {
|
|
3
|
-
import { getOADMatcherRecommendations, getUserMatcherSelection } from "./client/prompt-utils.js";
|
|
2
|
+
import { getOADMatcherRecommendations, getUserMatcherSelection, } from "./client/prompt-utils.js";
|
|
4
3
|
import { PROMPTS } from "./client/prompts.js";
|
|
4
|
+
import { TOOLS } from "./client/tools.js";
|
|
5
5
|
// Tool definitions for PactFlow AI API client
|
|
6
6
|
export class PactflowClient {
|
|
7
7
|
name = "Contract Testing";
|
|
@@ -51,7 +51,9 @@ export class PactflowClient {
|
|
|
51
51
|
* @throws Error if the HTTP request fails or the operation times out.
|
|
52
52
|
*/
|
|
53
53
|
async generate(toolInput, getInput) {
|
|
54
|
-
if (toolInput.openapi?.document &&
|
|
54
|
+
if (toolInput.openapi?.document &&
|
|
55
|
+
(!toolInput.openapi?.matcher ||
|
|
56
|
+
Object.keys(toolInput.openapi.matcher).length === 0)) {
|
|
55
57
|
const matcherResponse = await getOADMatcherRecommendations(toolInput.openapi.document, this.server);
|
|
56
58
|
const userSelection = await getUserMatcherSelection(matcherResponse, getInput);
|
|
57
59
|
toolInput.openapi.matcher = userSelection;
|
|
@@ -77,7 +79,9 @@ export class PactflowClient {
|
|
|
77
79
|
* @throws Error if the HTTP request fails or the operation times out.
|
|
78
80
|
*/
|
|
79
81
|
async review(toolInput, getInput) {
|
|
80
|
-
if (toolInput.openapi?.document &&
|
|
82
|
+
if (toolInput.openapi?.document &&
|
|
83
|
+
(!toolInput.openapi?.matcher ||
|
|
84
|
+
Object.keys(toolInput.openapi.matcher).length === 0)) {
|
|
81
85
|
const matcherResponse = await getOADMatcherRecommendations(toolInput.openapi.document, this.server);
|
|
82
86
|
const userSelection = await getUserMatcherSelection(matcherResponse, getInput);
|
|
83
87
|
toolInput.openapi.matcher = userSelection;
|
|
@@ -130,6 +134,9 @@ export class PactflowClient {
|
|
|
130
134
|
isComplete: response.status === 200,
|
|
131
135
|
};
|
|
132
136
|
}
|
|
137
|
+
get requestHeaders() {
|
|
138
|
+
return this.headers;
|
|
139
|
+
}
|
|
133
140
|
async getResult(resultUrl) {
|
|
134
141
|
const response = await fetch(resultUrl, {
|
|
135
142
|
method: "GET",
|
|
@@ -161,7 +168,7 @@ export class PactflowClient {
|
|
|
161
168
|
throw new Error(`${operationName} timed out after ${timeout / 1000} seconds`);
|
|
162
169
|
}
|
|
163
170
|
// PactFlow / Pact_Broker client methods
|
|
164
|
-
async getProviderStates({ provider }) {
|
|
171
|
+
async getProviderStates({ provider, }) {
|
|
165
172
|
const uri_encoded_provider_name = encodeURIComponent(provider);
|
|
166
173
|
const response = await fetch(`${this.baseUrl}/pacts/provider/${uri_encoded_provider_name}/provider-states`, {
|
|
167
174
|
method: "GET",
|
|
@@ -249,7 +256,7 @@ export class PactflowClient {
|
|
|
249
256
|
queryParts.push(`q[]mainBranch=${selector.mainBranch}`);
|
|
250
257
|
}
|
|
251
258
|
});
|
|
252
|
-
const url = `${this.baseUrl}/matrix?${queryParts.join(
|
|
259
|
+
const url = `${this.baseUrl}/matrix?${queryParts.join("&")}`;
|
|
253
260
|
try {
|
|
254
261
|
const response = await fetch(url, {
|
|
255
262
|
method: "GET",
|
|
@@ -273,8 +280,8 @@ export class PactflowClient {
|
|
|
273
280
|
* @param getInput - The function used to get input for tools.
|
|
274
281
|
*/
|
|
275
282
|
registerTools(register, getInput) {
|
|
276
|
-
for (const tool of TOOLS.filter(t => t.clients.includes(this.clientType))) {
|
|
277
|
-
const { handler, clients: _, formatResponse, ...toolparams } = tool;
|
|
283
|
+
for (const tool of TOOLS.filter((t) => t.clients.includes(this.clientType))) {
|
|
284
|
+
const { handler, clients: _, formatResponse, ...toolparams } = tool;
|
|
278
285
|
register(toolparams, async (args, _extra) => {
|
|
279
286
|
const handler_fn = this[handler];
|
|
280
287
|
if (typeof handler_fn !== "function") {
|
|
@@ -304,7 +311,7 @@ export class PactflowClient {
|
|
|
304
311
|
* @param register - The function used to register prompts.
|
|
305
312
|
*/
|
|
306
313
|
registerPrompts(register) {
|
|
307
|
-
PROMPTS.forEach(prompt => {
|
|
314
|
+
PROMPTS.forEach((prompt) => {
|
|
308
315
|
register(prompt.name, prompt.params, prompt.callback);
|
|
309
316
|
});
|
|
310
317
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../../../common/info.js";
|
|
2
|
+
import { QMETRY_DEFAULTS } from "../../config/constants.js";
|
|
3
|
+
export async function qmetryRequest({ method = "GET", path, token, project, baseUrl, body, }) {
|
|
4
|
+
const url = `${baseUrl}${path}`;
|
|
5
|
+
const headers = {
|
|
6
|
+
apikey: token,
|
|
7
|
+
project: project || QMETRY_DEFAULTS.PROJECT_KEY,
|
|
8
|
+
"User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
|
|
9
|
+
};
|
|
10
|
+
if (body) {
|
|
11
|
+
headers["Content-Type"] = "application/json";
|
|
12
|
+
}
|
|
13
|
+
const init = {
|
|
14
|
+
method,
|
|
15
|
+
headers,
|
|
16
|
+
};
|
|
17
|
+
if (body && ["POST", "PUT", "PATCH"].includes(method)) {
|
|
18
|
+
init.body = JSON.stringify(body);
|
|
19
|
+
}
|
|
20
|
+
const res = await fetch(url, init);
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
let errorText;
|
|
23
|
+
try {
|
|
24
|
+
const contentType = res.headers.get("content-type");
|
|
25
|
+
if (contentType?.includes("application/json")) {
|
|
26
|
+
const json = await res.json();
|
|
27
|
+
errorText = JSON.stringify(json);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
errorText = await res.text();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
errorText = res.statusText;
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`QMetry API request failed (${res.status}): ${errorText}`);
|
|
37
|
+
}
|
|
38
|
+
return (await res.json());
|
|
39
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { QMetryToolsHandlers } from "../config/constants.js";
|
|
2
|
+
import { getProjectInfo } from "./project.js";
|
|
3
|
+
import { fetchTestCaseDetails, fetchTestCaseSteps, fetchTestCases, fetchTestCaseVersionDetails, } from "./testcase.js";
|
|
4
|
+
export const QMETRY_HANDLER_MAP = {
|
|
5
|
+
[QMetryToolsHandlers.SET_PROJECT_INFO]: getProjectInfo,
|
|
6
|
+
[QMetryToolsHandlers.FETCH_PROJECT_INFO]: getProjectInfo,
|
|
7
|
+
[QMetryToolsHandlers.FETCH_TEST_CASES]: fetchTestCases,
|
|
8
|
+
[QMetryToolsHandlers.FETCH_TEST_CASE_DETAILS]: fetchTestCaseDetails,
|
|
9
|
+
[QMetryToolsHandlers.FETCH_TEST_CASE_VERSION_DETAILS]: fetchTestCaseVersionDetails,
|
|
10
|
+
[QMetryToolsHandlers.FETCH_TEST_CASE_STEPS]: fetchTestCaseSteps,
|
|
11
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { QMETRY_DEFAULTS } from "../config/constants.js";
|
|
2
|
+
import { QMETRY_PATHS } from "../config/rest-endpoints.js";
|
|
3
|
+
import { qmetryRequest } from "./api/client-api.js";
|
|
4
|
+
/**
|
|
5
|
+
* Retrieves project information from QMetry
|
|
6
|
+
*
|
|
7
|
+
* This function serves dual purpose:
|
|
8
|
+
* 1. SET_PROJECT_INFO - Sets/switches the current project context
|
|
9
|
+
* 2. FETCH_PROJECT_INFO - Retrieves project details and configuration
|
|
10
|
+
*
|
|
11
|
+
* Both operations use the same API endpoint as QMetry handles project context
|
|
12
|
+
* switching and information retrieval through the same GET request.
|
|
13
|
+
*
|
|
14
|
+
* @param token - QMetry API authentication token
|
|
15
|
+
* @param baseUrl - QMetry instance base URL (defaults to configured URL)
|
|
16
|
+
* @param project - Project key to retrieve info for (defaults to configured project)
|
|
17
|
+
* @returns Promise resolving to project information including viewIds, folders, and configuration
|
|
18
|
+
*/
|
|
19
|
+
export async function getProjectInfo(token, baseUrl, project) {
|
|
20
|
+
return qmetryRequest({
|
|
21
|
+
method: "GET",
|
|
22
|
+
path: QMETRY_PATHS.PROJECT.GET_INFO,
|
|
23
|
+
token,
|
|
24
|
+
baseUrl: baseUrl || QMETRY_DEFAULTS.BASE_URL,
|
|
25
|
+
project: project || QMETRY_DEFAULTS.PROJECT_KEY,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { QMETRY_DEFAULTS } from "../config/constants.js";
|
|
2
|
+
import { QMETRY_PATHS } from "../config/rest-endpoints.js";
|
|
3
|
+
import { DEFAULT_FETCH_TESTCASE_DETAILS_PAYLOAD, DEFAULT_FETCH_TESTCASE_STEPS_PAYLOAD, DEFAULT_FETCH_TESTCASE_VERSION_DETAILS_PAYLOAD, DEFAULT_FETCH_TESTCASES_PAYLOAD, } from "../types/testcase.js";
|
|
4
|
+
import { qmetryRequest } from "./api/client-api.js";
|
|
5
|
+
function resolveDefaults(baseUrl, project) {
|
|
6
|
+
return {
|
|
7
|
+
resolvedBaseUrl: baseUrl || QMETRY_DEFAULTS.BASE_URL,
|
|
8
|
+
resolvedProject: project || QMETRY_DEFAULTS.PROJECT_KEY,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Fetches a list of test cases.
|
|
13
|
+
* @throws If `viewId` or `folderPath` are missing/invalid.
|
|
14
|
+
*/
|
|
15
|
+
export async function fetchTestCases(token, baseUrl, project, payload) {
|
|
16
|
+
const { resolvedBaseUrl, resolvedProject } = resolveDefaults(baseUrl, project);
|
|
17
|
+
const body = {
|
|
18
|
+
...DEFAULT_FETCH_TESTCASES_PAYLOAD,
|
|
19
|
+
...payload,
|
|
20
|
+
};
|
|
21
|
+
if (typeof body.viewId !== "number") {
|
|
22
|
+
throw new Error("[fetchTestCases] Missing or invalid required parameter: 'viewId'.");
|
|
23
|
+
}
|
|
24
|
+
if (typeof body.folderPath !== "string") {
|
|
25
|
+
throw new Error("[fetchTestCases] Missing or invalid required parameter: 'folderPath'.");
|
|
26
|
+
}
|
|
27
|
+
return qmetryRequest({
|
|
28
|
+
method: "POST",
|
|
29
|
+
path: QMETRY_PATHS.TESTCASE.GET_TC_LIST,
|
|
30
|
+
token,
|
|
31
|
+
project: resolvedProject,
|
|
32
|
+
baseUrl: resolvedBaseUrl,
|
|
33
|
+
body,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Fetches a test case details.
|
|
38
|
+
* @throws If `tcID` is missing/invalid.
|
|
39
|
+
*/
|
|
40
|
+
export async function fetchTestCaseDetails(token, baseUrl, project, payload) {
|
|
41
|
+
const { resolvedBaseUrl, resolvedProject } = resolveDefaults(baseUrl, project);
|
|
42
|
+
const body = {
|
|
43
|
+
...DEFAULT_FETCH_TESTCASE_DETAILS_PAYLOAD,
|
|
44
|
+
...payload,
|
|
45
|
+
};
|
|
46
|
+
if (typeof body.tcID !== "number") {
|
|
47
|
+
throw new Error("[fetchTestCaseDetails] Missing or invalid required parameter: 'tcID'.");
|
|
48
|
+
}
|
|
49
|
+
return qmetryRequest({
|
|
50
|
+
method: "POST",
|
|
51
|
+
path: QMETRY_PATHS.TESTCASE.GET_TC_DETAILS,
|
|
52
|
+
token,
|
|
53
|
+
project: resolvedProject,
|
|
54
|
+
baseUrl: resolvedBaseUrl,
|
|
55
|
+
body,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Fetches a test case details by version.
|
|
60
|
+
* @throws If `id` is missing/invalid.
|
|
61
|
+
*/
|
|
62
|
+
export async function fetchTestCaseVersionDetails(token, baseUrl, project, payload) {
|
|
63
|
+
const { resolvedBaseUrl, resolvedProject } = resolveDefaults(baseUrl, project);
|
|
64
|
+
const body = {
|
|
65
|
+
...DEFAULT_FETCH_TESTCASE_VERSION_DETAILS_PAYLOAD,
|
|
66
|
+
...payload,
|
|
67
|
+
};
|
|
68
|
+
if (!body.id) {
|
|
69
|
+
throw new Error("[fetchTestCaseVersionDetails] Missing or invalid required parameter: 'id'.");
|
|
70
|
+
}
|
|
71
|
+
if (typeof body.version !== "number") {
|
|
72
|
+
throw new Error("[fetchTestCaseVersionDetails] Missing or invalid required parameter: 'version'.");
|
|
73
|
+
}
|
|
74
|
+
return qmetryRequest({
|
|
75
|
+
method: "POST",
|
|
76
|
+
path: QMETRY_PATHS.TESTCASE.GET_TC_DETAILS_BY_VERSION,
|
|
77
|
+
token,
|
|
78
|
+
project: resolvedProject,
|
|
79
|
+
baseUrl: resolvedBaseUrl,
|
|
80
|
+
body,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Fetches a test case steps.
|
|
85
|
+
* @throws If `id` is missing/invalid.
|
|
86
|
+
*/
|
|
87
|
+
export async function fetchTestCaseSteps(token, baseUrl, project, payload) {
|
|
88
|
+
const { resolvedBaseUrl, resolvedProject } = resolveDefaults(baseUrl, project);
|
|
89
|
+
const body = {
|
|
90
|
+
...DEFAULT_FETCH_TESTCASE_STEPS_PAYLOAD,
|
|
91
|
+
...payload,
|
|
92
|
+
};
|
|
93
|
+
if (typeof body.id !== "number") {
|
|
94
|
+
throw new Error("[fetchTestCaseSteps] Missing or invalid required parameter: 'id'.");
|
|
95
|
+
}
|
|
96
|
+
return qmetryRequest({
|
|
97
|
+
method: "POST",
|
|
98
|
+
path: QMETRY_PATHS.TESTCASE.GET_TC_STEPS,
|
|
99
|
+
token,
|
|
100
|
+
project: resolvedProject,
|
|
101
|
+
baseUrl: resolvedBaseUrl,
|
|
102
|
+
body,
|
|
103
|
+
});
|
|
104
|
+
}
|