@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.
Files changed (35) hide show
  1. package/README.md +20 -3
  2. package/dist/api-hub/client/api.js +253 -0
  3. package/dist/api-hub/client/configuration.js +27 -0
  4. package/dist/api-hub/client/index.js +5 -0
  5. package/dist/api-hub/client/portal-types.js +131 -0
  6. package/dist/api-hub/client/registry-types.js +55 -0
  7. package/dist/api-hub/client/tools.js +86 -0
  8. package/dist/api-hub/client.js +64 -404
  9. package/dist/bugsnag/client/api/CurrentUser.js +16 -10
  10. package/dist/bugsnag/client/api/Error.js +35 -35
  11. package/dist/bugsnag/client/api/Project.js +21 -9
  12. package/dist/bugsnag/client/api/base.js +7 -4
  13. package/dist/bugsnag/client/api/filters.js +9 -9
  14. package/dist/bugsnag/client.js +165 -140
  15. package/dist/common/info.js +1 -1
  16. package/dist/common/server.js +35 -27
  17. package/dist/index.js +11 -4
  18. package/dist/pactflow/client/ai.js +20 -20
  19. package/dist/pactflow/client/base.js +48 -13
  20. package/dist/pactflow/client/prompts.js +10 -12
  21. package/dist/pactflow/client/tools.js +10 -10
  22. package/dist/pactflow/client/utils.js +1 -1
  23. package/dist/pactflow/client.js +16 -9
  24. package/dist/qmetry/client/api/client-api.js +39 -0
  25. package/dist/qmetry/client/handlers.js +11 -0
  26. package/dist/qmetry/client/project.js +27 -0
  27. package/dist/qmetry/client/testcase.js +104 -0
  28. package/dist/qmetry/client/tools.js +222 -0
  29. package/dist/qmetry/client.js +95 -0
  30. package/dist/qmetry/config/constants.js +12 -0
  31. package/dist/qmetry/config/rest-endpoints.js +11 -0
  32. package/dist/qmetry/types/common.js +174 -0
  33. package/dist/qmetry/types/testcase.js +19 -0
  34. package/dist/reflect/client.js +14 -14
  35. package/package.json +6 -5
@@ -1,3 +1,3 @@
1
- import packageJson from '../../package.json' with { type: "json" };
1
+ import packageJson from "../../package.json" with { type: "json" };
2
2
  export const MCP_SERVER_NAME = packageJson.config.mcpServerName;
3
3
  export const MCP_SERVER_VERSION = packageJson.version;
@@ -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}`, { list: undefined }), {}, async (url, variables, extra) => {
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?.map(p => `- ${p.name} (${this.getReadableTypeName(p.type)})${p.required ? ' *required*' : ''}` +
102
- `${p.description ? `: ${p.description}` : ''}` +
103
- `${p.examples ? ` (e.g. ${p.examples.join(', ')})` : ''}` +
104
- `${p.constraints ? `\n - ${p.constraints.join('\n - ')}` : ''}`).join('\n')}`;
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('\n');
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 += `\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');
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() ? '' : ' *required*'}` +
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('\n - ')}` : ''}`;
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 'string';
146
+ return "string";
139
147
  if (zodType instanceof ZodNumber)
140
- return 'number';
148
+ return "number";
141
149
  if (zodType instanceof ZodBoolean)
142
- return 'boolean';
150
+ return "boolean";
143
151
  if (zodType instanceof ZodArray)
144
- return 'array';
152
+ return "array";
145
153
  if (zodType instanceof ZodObject)
146
- return 'object';
154
+ return "object";
147
155
  if (zodType instanceof ZodEnum)
148
- return 'enum';
156
+ return "enum";
149
157
  if (zodType instanceof ZodLiteral)
150
- return 'literal';
158
+ return "literal";
151
159
  if (zodType instanceof ZodUnion)
152
- return 'union';
160
+ return "union";
153
161
  if (zodType instanceof ZodAny)
154
- return 'any';
155
- return 'any';
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.object({
150
- total: z
151
- .number()
152
- .describe("The total number of AI credits available."),
153
- used: z
154
- .number()
155
- .describe("The number of AI credits used."),
156
- }).describe("AI credits information.");
157
- export const OrganizationEntitlementsSchema = z.object({
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
- }).describe("Organization entitlements information.");
169
- export const UserEntitlementsSchema = z.object({
170
- aiPermissions: z
171
- .array(z.string())
172
- .describe("List of AI permissions."),
173
- }).describe("User entitlements information.");
174
- export const EntitlementsSchema = z.object({
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
- }).describe("Entitlements information.");
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.string().describe("The name of the pacticipant (application/service) being evaluated for deployment"),
4
- version: z.string().describe("The version of the pacticipant that you want to check if it's safe to deploy"),
5
- environment: z.string().describe("The target environment where the pacticipant version will be deployed (e.g., 'production', 'staging', 'test')"),
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.string().optional().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."),
9
- limit: z.number().min(1).max(1000).default(100).optional().describe("The limit on the number of results to return (1-1000, default: 100)"),
10
- q: z.array(z.object({
11
- pacticipant: z.string().describe("Name of the pacticipant (application)"),
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.string().optional().describe("Name of the pacticipant version branch"),
14
- environment: z.string().optional().describe("The name of the environment that the pacticipant version is deployed to"),
15
- latest: z.boolean().optional().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."),
16
- tag: z.string().optional().describe("The name of the pacticipant version tag (superseded by branch and environments)"),
17
- mainBranch: z.boolean().optional().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."),
18
- })).min(1).max(2)
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: function ({ openAPI }) {
120
- return {
121
- messages: [
122
- {
123
- role: "user",
124
- content: {
125
- type: "text",
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, } from "./ai.js";
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
  *
@@ -1,7 +1,7 @@
1
1
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
2
- import { TOOLS } from "./client/tools.js";
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 && (!toolInput.openapi?.matcher || Object.keys(toolInput.openapi.matcher).length === 0)) {
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 && (!toolInput.openapi?.matcher || Object.keys(toolInput.openapi.matcher).length === 0)) {
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; // eslint-disable-line @typescript-eslint/no-unused-vars
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
+ }