@smartbear/mcp 0.6.0 → 0.8.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 (41) hide show
  1. package/README.md +37 -3
  2. package/dist/api-hub/client/api.js +387 -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 +69 -0
  7. package/dist/api-hub/client/tools.js +98 -0
  8. package/dist/api-hub/client.js +70 -404
  9. package/dist/bugsnag/client/api/CurrentUser.js +19 -13
  10. package/dist/bugsnag/client/api/Error.js +45 -57
  11. package/dist/bugsnag/client/api/Project.js +35 -30
  12. package/dist/bugsnag/client/api/base.js +24 -9
  13. package/dist/bugsnag/client/api/filters.js +9 -9
  14. package/dist/bugsnag/client.js +281 -373
  15. package/dist/common/info.js +1 -1
  16. package/dist/common/server.js +39 -28
  17. package/dist/index.js +18 -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 +18 -18
  22. package/dist/pactflow/client/utils.js +1 -1
  23. package/dist/pactflow/client.js +23 -15
  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/dist/zephyr/client.js +16 -0
  36. package/dist/zephyr/common/api-client.js +27 -0
  37. package/dist/zephyr/common/auth-service.js +14 -0
  38. package/dist/zephyr/common/types.js +35 -0
  39. package/dist/zephyr/tool/project/get-projects.js +54 -0
  40. package/dist/zephyr/tool/zephyr-tool.js +1 -0
  41. package/package.json +8 -6
@@ -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,5 +1,5 @@
1
- import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { ZodAny, ZodArray, ZodBoolean, ZodEnum, ZodLiteral, ZodNumber, ZodObject, ZodString, ZodUnion, } from "zod";
1
+ import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ZodAny, ZodArray, ZodBoolean, ZodEnum, ZodLiteral, ZodNumber, ZodObject, ZodOptional, ZodString, ZodUnion, } from "zod";
3
3
  import Bugsnag from "../common/bugsnag.js";
4
4
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "./info.js";
5
5
  export class SmartBearMcpServer extends McpServer {
@@ -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,73 @@ 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) {
145
+ if (zodType instanceof ZodOptional) {
146
+ zodType = zodType._def.innerType;
147
+ }
137
148
  if (zodType instanceof ZodString)
138
- return 'string';
149
+ return "string";
139
150
  if (zodType instanceof ZodNumber)
140
- return 'number';
151
+ return "number";
141
152
  if (zodType instanceof ZodBoolean)
142
- return 'boolean';
153
+ return "boolean";
143
154
  if (zodType instanceof ZodArray)
144
- return 'array';
155
+ return "array";
145
156
  if (zodType instanceof ZodObject)
146
- return 'object';
157
+ return "object";
147
158
  if (zodType instanceof ZodEnum)
148
- return 'enum';
159
+ return "enum";
149
160
  if (zodType instanceof ZodLiteral)
150
- return 'literal';
161
+ return "literal";
151
162
  if (zodType instanceof ZodUnion)
152
- return 'union';
163
+ return "union";
153
164
  if (zodType instanceof ZodAny)
154
- return 'any';
155
- return 'any';
165
+ return "any";
166
+ return "any";
156
167
  }
157
168
  }
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
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";
10
+ import { ZephyrClient } from "./zephyr/client.js";
9
11
  // This is used to report errors in the MCP server itself
10
12
  // If you want to use your own BugSnag API key, set the MCP_SERVER_BUGSNAG_API_KEY environment variable
11
13
  const McpServerBugsnagAPIKey = process.env.MCP_SERVER_BUGSNAG_API_KEY;
@@ -21,6 +23,10 @@ async function main() {
21
23
  const pactBrokerUrl = process.env.PACT_BROKER_BASE_URL;
22
24
  const pactBrokerUsername = process.env.PACT_BROKER_USERNAME;
23
25
  const pactBrokerPassword = process.env.PACT_BROKER_PASSWORD;
26
+ const qmetryToken = process.env.QMETRY_API_KEY;
27
+ const qmetryBaseUrl = process.env.QMETRY_BASE_URL;
28
+ const zephyrToken = process.env.ZEPHYR_API_TOKEN;
29
+ const zephyrBaseUrl = process.env.ZEPHYR_BASE_URL;
24
30
  let client_defined = false;
25
31
  if (reflectToken) {
26
32
  server.addClient(new ReflectClient(reflectToken));
@@ -49,8 +55,16 @@ async function main() {
49
55
  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
56
  }
51
57
  }
58
+ if (qmetryToken) {
59
+ server.addClient(new QmetryClient(qmetryToken, qmetryBaseUrl));
60
+ client_defined = true;
61
+ }
62
+ if (zephyrToken) {
63
+ server.addClient(new ZephyrClient(zephyrToken, zephyrBaseUrl));
64
+ client_defined = true;
65
+ }
52
66
  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");
67
+ console.error("Please set one of REFLECT_API_TOKEN, BUGSNAG_AUTH_TOKEN, API_HUB_API_KEY, QMETRY_API_KEY, ZEPHYR_API_TOKEN, or PACT_BROKER_BASE_URL / (and relevant Pact auth) environment variables");
54
68
  process.exit(1);
55
69
  }
56
70
  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,24 +64,24 @@ 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
- 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.",
74
+ title: "Check PactFlow AI Entitlements",
75
+ summary: "Check your PactFlow AI entitlements and credit balance if you encounter 401 Unauthorized errors or permission/credit issues when using PactFlow AI features.",
76
+ purpose: "Retrieve AI entitlement information when PactFlow AI operations fail with 401 unauthorized errors. Use this to diagnose permission issues, check remaining credits, and verify account eligibility for AI features.",
77
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"
78
+ "Diagnose 401 unauthorized errors when attempting to use PactFlow AI features",
79
+ "Check remaining AI credits when PactFlow AI operations are rejected due to insufficient credits",
80
+ "Verify account entitlements when users receive permission denied errors for PactFlow AI functionality",
81
+ "Troubleshoot PactFlow AI access issues by retrieving current entitlement status and credit balance",
82
+ "Provide detailed error context when PactFlow AI features are unavailable due to account limitations",
83
83
  ],
84
- handler: "getAIStatus",
85
- clients: ["pactflow"]
86
- }
84
+ handler: "checkAIEntitlements",
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;
@@ -95,14 +99,15 @@ export class PactflowClient {
95
99
  return await this.pollForCompletion(status_response, "Review Pacts");
96
100
  }
97
101
  /**
98
- * Retrieves AI status information for the current user
99
- * and organization.
102
+ * Retrieve PactFlow AI entitlement information for the current user
103
+ * and organization when encountering 401 unauthorized errors.
104
+ * Use this to check AI entitlements and credits when AI operations fail.
100
105
  *
101
- * @returns Entitlement containing AI status information, organization
106
+ * @returns Entitlement containing permissions, organization
102
107
  * entitlements, and user entitlements.
103
108
  * @throws Error if the request fails or returns a non-OK response.
104
109
  */
105
- async getAIStatus() {
110
+ async checkAIEntitlements() {
106
111
  const url = `${this.aiBaseUrl}/entitlement`;
107
112
  try {
108
113
  const response = await fetch(url, {
@@ -111,12 +116,12 @@ export class PactflowClient {
111
116
  });
112
117
  if (!response.ok) {
113
118
  const errorText = await response.text().catch(() => "");
114
- throw new Error(`PactFlow AI Status Request Failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
119
+ throw new Error(`PactFlow AI Entitlements Request Failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
115
120
  }
116
121
  return (await response.json());
117
122
  }
118
123
  catch (error) {
119
- process.stderr.write(`[GetAICredits] Unexpected error: ${error}\n`);
124
+ process.stderr.write(`[CheckAIEntitlements] Unexpected error: ${error}\n`);
120
125
  throw error;
121
126
  }
122
127
  }
@@ -130,6 +135,9 @@ export class PactflowClient {
130
135
  isComplete: response.status === 200,
131
136
  };
132
137
  }
138
+ get requestHeaders() {
139
+ return this.headers;
140
+ }
133
141
  async getResult(resultUrl) {
134
142
  const response = await fetch(resultUrl, {
135
143
  method: "GET",
@@ -161,7 +169,7 @@ export class PactflowClient {
161
169
  throw new Error(`${operationName} timed out after ${timeout / 1000} seconds`);
162
170
  }
163
171
  // PactFlow / Pact_Broker client methods
164
- async getProviderStates({ provider }) {
172
+ async getProviderStates({ provider, }) {
165
173
  const uri_encoded_provider_name = encodeURIComponent(provider);
166
174
  const response = await fetch(`${this.baseUrl}/pacts/provider/${uri_encoded_provider_name}/provider-states`, {
167
175
  method: "GET",
@@ -249,7 +257,7 @@ export class PactflowClient {
249
257
  queryParts.push(`q[]mainBranch=${selector.mainBranch}`);
250
258
  }
251
259
  });
252
- const url = `${this.baseUrl}/matrix?${queryParts.join('&')}`;
260
+ const url = `${this.baseUrl}/matrix?${queryParts.join("&")}`;
253
261
  try {
254
262
  const response = await fetch(url, {
255
263
  method: "GET",
@@ -273,8 +281,8 @@ export class PactflowClient {
273
281
  * @param getInput - The function used to get input for tools.
274
282
  */
275
283
  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
284
+ for (const tool of TOOLS.filter((t) => t.clients.includes(this.clientType))) {
285
+ const { handler, clients: _, formatResponse, ...toolparams } = tool;
278
286
  register(toolparams, async (args, _extra) => {
279
287
  const handler_fn = this[handler];
280
288
  if (typeof handler_fn !== "function") {
@@ -304,7 +312,7 @@ export class PactflowClient {
304
312
  * @param register - The function used to register prompts.
305
313
  */
306
314
  registerPrompts(register) {
307
- PROMPTS.forEach(prompt => {
315
+ PROMPTS.forEach((prompt) => {
308
316
  register(prompt.name, prompt.params, prompt.callback);
309
317
  });
310
318
  }
@@ -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
+ }