@smartbear/mcp 0.19.2 → 0.21.1

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 (63) hide show
  1. package/README.md +37 -7
  2. package/assets/icon.png +0 -0
  3. package/dist/bugsnag/client.js +19 -12
  4. package/dist/collaborator/client.js +10 -10
  5. package/dist/common/client-registry.js +2 -2
  6. package/dist/common/register-clients.js +2 -0
  7. package/dist/common/server.js +74 -111
  8. package/dist/common/shutdown.js +165 -0
  9. package/dist/common/transport-http.js +94 -12
  10. package/dist/common/transport-stdio.js +16 -2
  11. package/dist/common/zod-utils.js +62 -7
  12. package/dist/index.js +2 -0
  13. package/dist/package.json.js +1 -1
  14. package/dist/pactflow/client/prompts.js +19 -18
  15. package/dist/pactflow/client/tools.js +8 -13
  16. package/dist/pactflow/client.js +26 -12
  17. package/dist/qmetry/client/tools/testsuite-tools.js +2 -2
  18. package/dist/qmetry/client.js +1 -1
  19. package/dist/qtm4j/client.js +109 -0
  20. package/dist/qtm4j/config/constants.js +169 -0
  21. package/dist/qtm4j/config/field-resolution.types.js +34 -0
  22. package/dist/qtm4j/http/api-client.js +123 -0
  23. package/dist/qtm4j/http/auth-service.js +23 -0
  24. package/dist/qtm4j/resolver/cache/cache.js +52 -0
  25. package/dist/qtm4j/resolver/resolver-registry.js +70 -0
  26. package/dist/qtm4j/resolver/resolvers/common-attribute-resolver.js +56 -0
  27. package/dist/qtm4j/resolver/resolvers/component-resolver.js +56 -0
  28. package/dist/qtm4j/resolver/resolvers/label-resolver.js +56 -0
  29. package/dist/qtm4j/resolver/resolvers/resolver.js +6 -0
  30. package/dist/qtm4j/resolver/resolvers/test-case-uid-resolver.js +28 -0
  31. package/dist/qtm4j/schema/get-test-case.schema.js +153 -0
  32. package/dist/qtm4j/schema/get-test-steps.schema.js +74 -0
  33. package/dist/qtm4j/schema/project.schema.js +43 -0
  34. package/dist/qtm4j/schema/test-case.schema.js +41 -0
  35. package/dist/qtm4j/schema/update-test-case.schema.js +45 -0
  36. package/dist/qtm4j/tool/project/get-projects.js +111 -0
  37. package/dist/qtm4j/tool/project/set-project-context.js +99 -0
  38. package/dist/qtm4j/tool/test-case/create-test-case.js +113 -0
  39. package/dist/qtm4j/tool/test-case/get-test-cases.js +295 -0
  40. package/dist/qtm4j/tool/test-case/get-test-steps.js +111 -0
  41. package/dist/qtm4j/tool/test-case/update-test-case.js +158 -0
  42. package/dist/reflect/client.js +3 -3
  43. package/dist/reflect/prompt/sap-test.js +5 -5
  44. package/dist/reflect/tool/recording/add-prompt-step.js +6 -14
  45. package/dist/reflect/tool/recording/add-segment.js +4 -14
  46. package/dist/reflect/tool/recording/connect-to-session.js +3 -8
  47. package/dist/reflect/tool/recording/delete-previous-step.js +3 -8
  48. package/dist/reflect/tool/recording/get-screenshot.js +4 -14
  49. package/dist/reflect/tool/suites/cancel-suite-execution.js +4 -14
  50. package/dist/reflect/tool/suites/execute-suite.js +3 -8
  51. package/dist/reflect/tool/suites/get-suite-execution-status.js +4 -14
  52. package/dist/reflect/tool/suites/list-suite-executions.js +3 -8
  53. package/dist/reflect/tool/suites/list-suites.js +2 -1
  54. package/dist/reflect/tool/tests/get-test-status.js +3 -8
  55. package/dist/reflect/tool/tests/list-segments.js +5 -20
  56. package/dist/reflect/tool/tests/list-tests.js +2 -1
  57. package/dist/reflect/tool/tests/run-test.js +3 -8
  58. package/dist/swagger/client/api.js +11 -2
  59. package/dist/swagger/client/portal-types.js +0 -3
  60. package/dist/swagger/client/tools.js +0 -1
  61. package/dist/swagger/client.js +1 -1
  62. package/dist/zephyr/client.js +1 -1
  63. package/package.json +6 -4
@@ -0,0 +1,165 @@
1
+ const DEFAULT_TIMEOUT_MS = 25e3;
2
+ function defaultTimeout() {
3
+ const fromEnv = process.env.MCP_SHUTDOWN_TIMEOUT_MS;
4
+ if (fromEnv) {
5
+ const parsed = Number.parseInt(fromEnv, 10);
6
+ if (Number.isFinite(parsed) && parsed > 0) {
7
+ return parsed;
8
+ }
9
+ }
10
+ return DEFAULT_TIMEOUT_MS;
11
+ }
12
+ class ShutdownManager {
13
+ handlers = [];
14
+ state = "idle";
15
+ signalsInstalled = false;
16
+ installedSignalListeners = [];
17
+ signals;
18
+ timeoutMs;
19
+ proc;
20
+ exitFn;
21
+ logger;
22
+ constructor(options = {}) {
23
+ this.signals = options.signals ?? ["SIGTERM", "SIGINT"];
24
+ this.timeoutMs = options.timeoutMs ?? defaultTimeout();
25
+ this.proc = options.process ?? process;
26
+ this.exitFn = options.exit ?? ((code) => process.exit(code));
27
+ this.logger = options.logger ?? console;
28
+ }
29
+ /**
30
+ * Register a cleanup handler. Handlers run in LIFO order on shutdown,
31
+ * so subsystems registered later (closer to where they are used) tear
32
+ * down before subsystems registered earlier (closer to startup).
33
+ */
34
+ register(name, fn) {
35
+ if (this.state !== "idle") {
36
+ this.logger.warn(
37
+ `[MCP][shutdown] Refusing to register handler "${name}" — already ${this.state}`
38
+ );
39
+ return;
40
+ }
41
+ this.handlers.push({ name, fn });
42
+ }
43
+ /** True from the moment the first shutdown signal is received. */
44
+ isDraining() {
45
+ return this.state !== "idle";
46
+ }
47
+ /**
48
+ * Wire SIGTERM / SIGINT listeners. Idempotent — calling more than once
49
+ * is a no-op.
50
+ */
51
+ installSignalHandlers() {
52
+ if (this.signalsInstalled) return;
53
+ this.signalsInstalled = true;
54
+ for (const signal of this.signals) {
55
+ const listener = () => {
56
+ this.handleSignal(signal);
57
+ };
58
+ this.proc.on(signal, listener);
59
+ this.installedSignalListeners.push({ signal, listener });
60
+ }
61
+ }
62
+ /** Tear down installed signal listeners (test-only). */
63
+ uninstallSignalHandlers() {
64
+ for (const { signal, listener } of this.installedSignalListeners) {
65
+ this.proc.off(signal, listener);
66
+ }
67
+ this.installedSignalListeners = [];
68
+ this.signalsInstalled = false;
69
+ }
70
+ /**
71
+ * Run all registered handlers in LIFO order with the configured deadline.
72
+ * Returns the drain result without exiting the process — exposed for tests
73
+ * and for callers that want to log / report before exiting.
74
+ */
75
+ async drain() {
76
+ if (this.state === "complete") {
77
+ return { status: "clean", durationMs: 0, remainingHandlers: [] };
78
+ }
79
+ this.state = "draining";
80
+ const start = Date.now();
81
+ const reversed = [...this.handlers].reverse();
82
+ const remaining = new Set(reversed.map((h) => h.name));
83
+ this.logger.log(
84
+ `[MCP][shutdown] Draining ${reversed.length} handler(s), deadline ${this.timeoutMs}ms`
85
+ );
86
+ let timeoutHandle;
87
+ const drainPromise = (async () => {
88
+ for (const h of reversed) {
89
+ try {
90
+ await h.fn();
91
+ } catch (err) {
92
+ this.logger.error(
93
+ `[MCP][shutdown] Handler "${h.name}" threw during drain:`,
94
+ err
95
+ );
96
+ }
97
+ remaining.delete(h.name);
98
+ }
99
+ })();
100
+ const timeoutPromise = new Promise((resolve) => {
101
+ timeoutHandle = setTimeout(() => resolve("deadline"), this.timeoutMs);
102
+ timeoutHandle.unref?.();
103
+ });
104
+ const winner = await Promise.race([
105
+ drainPromise.then(() => "clean"),
106
+ timeoutPromise
107
+ ]);
108
+ if (timeoutHandle) clearTimeout(timeoutHandle);
109
+ this.state = "complete";
110
+ const durationMs = Date.now() - start;
111
+ const remainingHandlers = [...remaining];
112
+ if (winner === "clean") {
113
+ this.logger.log(`[MCP][shutdown] Drained cleanly in ${durationMs}ms`);
114
+ } else {
115
+ this.logger.error(
116
+ `[MCP][shutdown] Deadline exceeded after ${durationMs}ms, ${remainingHandlers.length} handler(s) outstanding: ${remainingHandlers.join(", ") || "(none)"}`
117
+ );
118
+ }
119
+ return { status: winner, durationMs, remainingHandlers };
120
+ }
121
+ /** Exposed for tests; not part of the production call path. */
122
+ getState() {
123
+ return this.state;
124
+ }
125
+ handleSignal(signal) {
126
+ if (this.state === "draining") {
127
+ this.logger.warn(
128
+ `[MCP][shutdown] Received ${signal} while draining — forcing immediate exit(1)`
129
+ );
130
+ this.exitFn(1);
131
+ return;
132
+ }
133
+ if (this.state === "complete") {
134
+ this.exitFn(0);
135
+ return;
136
+ }
137
+ this.logger.log(`[MCP][shutdown] Received ${signal}, starting drain`);
138
+ this.drain().then((result) => {
139
+ this.exitFn(result.status === "clean" ? 0 : 1);
140
+ }).catch((err) => {
141
+ this.logger.error(
142
+ "[MCP][shutdown] Unexpected error from drain():",
143
+ err
144
+ );
145
+ this.exitFn(1);
146
+ });
147
+ }
148
+ }
149
+ const shutdownManager = new ShutdownManager();
150
+ function registerShutdownHandler(name, fn) {
151
+ shutdownManager.register(name, fn);
152
+ }
153
+ function installSignalHandlers() {
154
+ shutdownManager.installSignalHandlers();
155
+ }
156
+ function isDraining() {
157
+ return shutdownManager.isDraining();
158
+ }
159
+ export {
160
+ ShutdownManager,
161
+ installSignalHandlers,
162
+ isDraining,
163
+ registerShutdownHandler,
164
+ shutdownManager
165
+ };
@@ -1,13 +1,41 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
+ import querystring from "node:querystring";
3
4
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4
5
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
6
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
6
7
  import { clientRegistry } from "./client-registry.js";
7
8
  import { withRequestContext } from "./request-context.js";
8
9
  import { SmartBearMcpServer } from "./server.js";
10
+ import { isDraining, registerShutdownHandler } from "./shutdown.js";
9
11
  import { getEnvVarName } from "./transport-stdio.js";
10
- import { isOptionalType } from "./zod-utils.js";
12
+ import { isOptionalType, getTypeDescription } from "./zod-utils.js";
13
+ const PROBE_HEADERS = {
14
+ "Content-Type": "application/json",
15
+ "Cache-Control": "no-store"
16
+ };
17
+ function handleHealthRequest(res) {
18
+ res.writeHead(200, PROBE_HEADERS);
19
+ res.end(
20
+ JSON.stringify({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() })
21
+ );
22
+ }
23
+ function handleReadyRequest(res, draining = isDraining) {
24
+ if (draining()) {
25
+ res.writeHead(503, PROBE_HEADERS);
26
+ res.end(
27
+ JSON.stringify({
28
+ status: "draining",
29
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
30
+ })
31
+ );
32
+ return;
33
+ }
34
+ res.writeHead(200, PROBE_HEADERS);
35
+ res.end(
36
+ JSON.stringify({ status: "ready", timestamp: (/* @__PURE__ */ new Date()).toISOString() })
37
+ );
38
+ }
11
39
  function getBaseUrl(req) {
12
40
  const baseUrlOverride = process.env.BASE_URL;
13
41
  if (baseUrlOverride) {
@@ -54,10 +82,11 @@ async function runHttpMode() {
54
82
  const baseUrl = getBaseUrl(req);
55
83
  const url = new URL(req.url || "/", baseUrl);
56
84
  if (req.method === "GET" && url.pathname === "/health") {
57
- res.writeHead(200, { "Content-Type": "application/json" });
58
- res.end(
59
- JSON.stringify({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() })
60
- );
85
+ handleHealthRequest(res);
86
+ return;
87
+ }
88
+ if (req.method === "GET" && url.pathname === "/ready") {
89
+ handleReadyRequest(res);
61
90
  return;
62
91
  }
63
92
  if (req.method === "GET" && (url.pathname === "/.well-known/oauth-protected-resource" || url.pathname === "/.well-known/oauth-protected-resource/mcp")) {
@@ -89,9 +118,8 @@ async function runHttpMode() {
89
118
  );
90
119
  httpServer.listen(PORT, () => {
91
120
  console.log(`[MCP HTTP Server] Listening on http://localhost:${PORT}`);
92
- console.log(
93
- `[MCP HTTP Server] Health check: http://localhost:${PORT}/health`
94
- );
121
+ console.log(`[MCP HTTP Server] Liveness: http://localhost:${PORT}/health`);
122
+ console.log(`[MCP HTTP Server] Readiness: http://localhost:${PORT}/ready`);
95
123
  console.log(
96
124
  `[MCP HTTP Server] Modern endpoint: http://localhost:${PORT}/mcp (Streamable HTTP)`
97
125
  );
@@ -110,6 +138,29 @@ ${headerHelp.join("\n")}`
110
138
  );
111
139
  }
112
140
  });
141
+ registerShutdownHandler("http-transport", async () => {
142
+ await drainHttpTransport(httpServer, transports);
143
+ });
144
+ }
145
+ async function drainHttpTransport(httpServer, transports) {
146
+ console.log(
147
+ `[MCP][shutdown] Draining HTTP transport (${transports.size} active session(s))`
148
+ );
149
+ const serverClosed = new Promise((resolve) => {
150
+ httpServer.close(() => resolve());
151
+ });
152
+ httpServer.closeIdleConnections?.();
153
+ const transportCloses = [...transports.values()].map(async (entry) => {
154
+ try {
155
+ await entry.transport.close();
156
+ } catch (err) {
157
+ console.error("[MCP][shutdown] Error closing transport:", err);
158
+ }
159
+ });
160
+ await Promise.all(transportCloses);
161
+ httpServer.closeAllConnections?.();
162
+ await serverClosed;
163
+ console.log("[MCP][shutdown] HTTP transport drained");
113
164
  }
114
165
  async function parseRequestBody(req) {
115
166
  if (req.method !== "POST") {
@@ -186,9 +237,24 @@ async function createNewTransport(req, res, transports) {
186
237
  async function handleStreamableHttpRequest(req, res, transports) {
187
238
  try {
188
239
  const sessionId = req.headers["mcp-session-id"];
240
+ if (sessionId && !transports.has(sessionId)) {
241
+ req.resume();
242
+ res.writeHead(404, { "Content-Type": "application/json" });
243
+ res.end(
244
+ JSON.stringify({
245
+ jsonrpc: "2.0",
246
+ error: {
247
+ code: -32001,
248
+ message: "Session not found"
249
+ },
250
+ id: null
251
+ })
252
+ );
253
+ return;
254
+ }
189
255
  const parsedBody = await parseRequestBody(req);
190
256
  let transport;
191
- if (sessionId && transports.has(sessionId)) {
257
+ if (sessionId) {
192
258
  const existingTransport = getExistingTransport(
193
259
  sessionId,
194
260
  transports,
@@ -196,7 +262,7 @@ async function handleStreamableHttpRequest(req, res, transports) {
196
262
  );
197
263
  if (!existingTransport) return;
198
264
  transport = existingTransport;
199
- } else if (!sessionId && req.method === "POST" && parsedBody && isInitializeRequest(parsedBody)) {
265
+ } else if (req.method === "POST" && parsedBody && isInitializeRequest(parsedBody)) {
200
266
  const newTransport = await createNewTransport(req, res, transports);
201
267
  if (!newTransport) return;
202
268
  transport = newTransport;
@@ -285,8 +351,14 @@ async function newServer(req, res) {
285
351
  () => clientRegistry.configure(
286
352
  server,
287
353
  (client, key) => {
354
+ const queryStringName = getQueryStringName(client, key);
355
+ const queryParams = querystring.parse(req.url?.split("?")[1] || "");
356
+ let value = queryParams[queryStringName] || queryParams[queryStringName.toLowerCase()];
357
+ if (typeof value === "string") {
358
+ return value;
359
+ }
288
360
  const headerName = getHeaderName(client, key);
289
- const value = req.headers[headerName] || req.headers[headerName.toLowerCase()];
361
+ value = req.headers[headerName] || req.headers[headerName.toLowerCase()];
290
362
  if (typeof value === "string") {
291
363
  return value;
292
364
  }
@@ -338,6 +410,11 @@ function getHeaderName(client, key) {
338
410
  (part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
339
411
  ).join("-")}`;
340
412
  }
413
+ function getQueryStringName(client, key) {
414
+ return `${client.configPrefix.toLowerCase()}${key.split("_").map(
415
+ (part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
416
+ ).join("")}`;
417
+ }
341
418
  function getHttpHeaders() {
342
419
  const headers = /* @__PURE__ */ new Set();
343
420
  for (const entry of clientRegistry.getAll()) {
@@ -355,15 +432,20 @@ function getHttpHeadersHelp() {
355
432
  const headerName = getHeaderName(entry, configKey);
356
433
  const requiredTag = isOptionalType(requirement) ? " (optional)" : " (required)";
357
434
  messages.push(
358
- ` - ${headerName}${requiredTag}: ${requirement.description}`
435
+ ` - ${headerName}${requiredTag}: ${getTypeDescription(requirement)}`
359
436
  );
360
437
  }
361
438
  }
362
439
  return messages;
363
440
  }
364
441
  export {
442
+ drainHttpTransport,
365
443
  getBaseUrl,
366
444
  getHeaderName,
445
+ getQueryStringName,
446
+ handleHealthRequest,
447
+ handleReadyRequest,
448
+ handleStreamableHttpRequest,
367
449
  newServer,
368
450
  runHttpMode
369
451
  };
@@ -2,7 +2,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
2
2
  import { clientRegistry } from "./client-registry.js";
3
3
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "./info.js";
4
4
  import { SmartBearMcpServer } from "./server.js";
5
- import { isOptionalType } from "./zod-utils.js";
5
+ import { registerShutdownHandler } from "./shutdown.js";
6
+ import { isOptionalType, getTypeDescription } from "./zod-utils.js";
6
7
  function getNoConfigMessage() {
7
8
  const messages = [];
8
9
  for (const entry of clientRegistry.getAll()) {
@@ -11,7 +12,7 @@ function getNoConfigMessage() {
11
12
  const envVarName = getEnvVarName(entry, configKey);
12
13
  const requiredTag = isOptionalType(requirement) ? " (optional)" : " (required)";
13
14
  messages.push(
14
- ` - ${envVarName}${requiredTag}: ${requirement.description}`
15
+ ` - ${envVarName}${requiredTag}: ${getTypeDescription(requirement)}`
15
16
  );
16
17
  }
17
18
  }
@@ -21,6 +22,12 @@ async function runStdioMode() {
21
22
  if (process.argv.includes("--version")) {
22
23
  console.log(`${MCP_SERVER_NAME}: v${MCP_SERVER_VERSION}`);
23
24
  process.exit(0);
25
+ } else if (process.argv.includes("--help")) {
26
+ console.log(
27
+ "The following environment variables can be set to configure each of the SmartBear clients:"
28
+ );
29
+ console.log(getNoConfigMessage().join("\n"));
30
+ process.exit(0);
24
31
  }
25
32
  const server = new SmartBearMcpServer();
26
33
  const configuredCount = await clientRegistry.configure(
@@ -41,6 +48,13 @@ ${message.join("\n")}` : "No clients support environment variable configuration.
41
48
  }
42
49
  }
43
50
  const transport = new StdioServerTransport();
51
+ registerShutdownHandler("stdio-transport", async () => {
52
+ try {
53
+ await transport.close();
54
+ } catch (err) {
55
+ console.error("[MCP][shutdown] Error closing stdio transport:", err);
56
+ }
57
+ });
44
58
  transport.onmessage = (message) => {
45
59
  if ("method" in message && message.method === "initialize") {
46
60
  if (message.params?.protocolVersion === "2025-11-25") {
@@ -1,20 +1,75 @@
1
- import { ZodOptional, ZodDefault, ZodNullable } from "zod";
2
- function isOptionalType(zodType) {
3
- return zodType instanceof ZodOptional || zodType instanceof ZodDefault || zodType instanceof ZodNullable;
4
- }
1
+ import { ZodOptional, ZodDefault, ZodNullable, ZodRecord, ZodString, ZodNumber, ZodBoolean, ZodArray, ZodObject, ZodEnum, ZodLiteral, ZodUnion, ZodAny } from "zod";
5
2
  function unwrapZodType(zodType) {
6
3
  if (zodType instanceof ZodOptional) {
7
- return unwrapZodType(zodType.unwrap());
4
+ return zodType.unwrap();
8
5
  }
9
6
  if (zodType instanceof ZodDefault) {
10
- return unwrapZodType(zodType.unwrap());
7
+ return zodType.unwrap();
11
8
  }
12
9
  if (zodType instanceof ZodNullable) {
13
- return unwrapZodType(zodType.unwrap());
10
+ return zodType.unwrap();
14
11
  }
15
12
  return zodType;
16
13
  }
14
+ function isOptionalType(zodType) {
15
+ const isOptional = zodType instanceof ZodOptional || zodType instanceof ZodDefault || zodType instanceof ZodNullable;
16
+ if (!isOptional) {
17
+ const unwrapped = unwrapZodType(zodType);
18
+ if (unwrapped !== zodType) {
19
+ return isOptionalType(unwrapped);
20
+ }
21
+ }
22
+ return isOptional;
23
+ }
24
+ function getDefaultValue(zodType) {
25
+ if (zodType instanceof ZodDefault) {
26
+ return zodType.def.defaultValue;
27
+ }
28
+ const unwrapped = unwrapZodType(zodType);
29
+ if (unwrapped !== zodType) {
30
+ return getDefaultValue(unwrapped);
31
+ }
32
+ return null;
33
+ }
34
+ function fullyUnwrapZodType(zodType) {
35
+ const unwrappedType = unwrapZodType(zodType);
36
+ if (unwrappedType === zodType) {
37
+ return unwrappedType;
38
+ }
39
+ return fullyUnwrapZodType(unwrappedType);
40
+ }
41
+ function getTypeDescription(zodType) {
42
+ if (zodType.description) {
43
+ return zodType.description;
44
+ }
45
+ const unwrapped = unwrapZodType(zodType);
46
+ if (unwrapped !== zodType) {
47
+ return getTypeDescription(unwrapped);
48
+ }
49
+ return null;
50
+ }
51
+ function getReadableTypeName(zodType) {
52
+ zodType = fullyUnwrapZodType(zodType);
53
+ if (zodType instanceof ZodRecord) {
54
+ const record = zodType;
55
+ return `record<${getReadableTypeName(record.def.keyType)}, ${getReadableTypeName(record.def.valueType)}>`;
56
+ }
57
+ if (zodType instanceof ZodString) return "string";
58
+ if (zodType instanceof ZodNumber) return "number";
59
+ if (zodType instanceof ZodBoolean) return "boolean";
60
+ if (zodType instanceof ZodArray) return "array";
61
+ if (zodType instanceof ZodObject) return "object";
62
+ if (zodType instanceof ZodEnum) return "enum";
63
+ if (zodType instanceof ZodLiteral) return "literal";
64
+ if (zodType instanceof ZodUnion) return "union";
65
+ if (zodType instanceof ZodAny) return "any";
66
+ return "any";
67
+ }
17
68
  export {
69
+ fullyUnwrapZodType,
70
+ getDefaultValue,
71
+ getReadableTypeName,
72
+ getTypeDescription,
18
73
  isOptionalType,
19
74
  unwrapZodType
20
75
  };
package/dist/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import Bugsnag from "./common/bugsnag.js";
3
3
  import "./common/register-clients.js";
4
+ import { installSignalHandlers } from "./common/shutdown.js";
4
5
  import { runHttpMode } from "./common/transport-http.js";
5
6
  import { runStdioMode } from "./common/transport-stdio.js";
6
7
  const McpServerBugsnagAPIKey = process.env.MCP_SERVER_BUGSNAG_API_KEY;
7
8
  if (McpServerBugsnagAPIKey) {
8
9
  Bugsnag.start(McpServerBugsnagAPIKey);
9
10
  }
11
+ installSignalHandlers();
10
12
  async function main() {
11
13
  const transportMode = process.env.MCP_TRANSPORT?.toLowerCase() || "stdio";
12
14
  if (transportMode === "http") {
@@ -1,4 +1,4 @@
1
- const version = "0.19.2";
1
+ const version = "0.21.1";
2
2
  const config = { "mcpServerName": "SmartBear MCP Server" };
3
3
  const packageJson = {
4
4
  version,
@@ -105,27 +105,28 @@ Now provided the below OpenAPI document:-
105
105
 
106
106
  Give JSON recommendations only provide the JSON block in markdown don't include any additional text.
107
107
  `;
108
+ const argsSchema = z.object({
109
+ openAPI: z.string().describe("The OpenAPI document to generate matcher recommendations for")
110
+ });
108
111
  const PROMPTS = [
109
112
  {
110
- name: "OpenAPI Matcher recommendations",
111
- params: {
112
- description: "Get OpenAPI matcher recommendations using sampling",
113
- title: "OpenAPI Matcher recommendations",
114
- argsSchema: {
115
- openAPI: z.string()
116
- }
117
- },
118
- callback: ({ openAPI }) => ({
119
- messages: [
120
- {
121
- role: "user",
122
- content: {
123
- type: "text",
124
- text: OADMatcherPrompt.replace("{0}", openAPI)
113
+ title: "OpenAPI Matcher recommendations",
114
+ description: "Get OpenAPI matcher recommendations using sampling",
115
+ argsSchema,
116
+ callback: (args) => {
117
+ const params = argsSchema.parse(args);
118
+ return {
119
+ messages: [
120
+ {
121
+ role: "user",
122
+ content: {
123
+ type: "text",
124
+ text: OADMatcherPrompt.replace("{0}", params.openAPI)
125
+ }
125
126
  }
126
- }
127
- ]
128
- })
127
+ ]
128
+ };
129
+ }
129
130
  }
130
131
  ];
131
132
  export {
@@ -27,14 +27,9 @@ const TOOLS = [
27
27
  title: "Get Provider States",
28
28
  summary: "Retrieve the states of a specific provider",
29
29
  purpose: "A provider state in Pact defines the specific preconditions that must be met on the provider side before a consumer-provider interaction can be tested. It sets up the provider in the right context—such as ensuring a particular user or record exists—so that the provider can return the response the consumer expects. This makes contract tests reliable, repeatable, and isolated by injecting or configuring the necessary data and conditions directly into the provider before each test runs.",
30
- parameters: [
31
- {
32
- name: "provider",
33
- type: z.string(),
34
- description: "name of the provider to retrieve states for",
35
- required: true
36
- }
37
- ],
30
+ inputSchema: z.object({
31
+ provider: z.string().describe("name of the provider to retrieve states for")
32
+ }),
38
33
  handler: "getProviderStates",
39
34
  clients: ["pactflow", "pact_broker"]
40
35
  },
@@ -256,7 +251,7 @@ const TOOLS = [
256
251
  clients: ["pactflow"]
257
252
  },
258
253
  {
259
- title: "Get BDCT Consumer Contract by Consumer Version",
254
+ title: "Get BDCT Consumer by Consumer Version",
260
255
  summary: "Fetch the consumer Pact contract for a specific consumer-provider version pair in Bi-Directional Contract Testing.",
261
256
  purpose: "Retrieve the Pact contract published by a specific consumer version, in the context of a specific provider version. Use this when you need the exact consumer contract that was compared against a given provider spec.",
262
257
  inputSchema: GetBiDirectionalConsumerProviderVersionSchema,
@@ -264,7 +259,7 @@ const TOOLS = [
264
259
  clients: ["pactflow"]
265
260
  },
266
261
  {
267
- title: "Get BDCT Provider Contract by Consumer Version",
262
+ title: "Get BDCT Provider by Consumer Version",
268
263
  summary: "Fetch the provider OpenAPI contract for a specific consumer-provider version pair in Bi-Directional Contract Testing.",
269
264
  purpose: "Retrieve the provider's OpenAPI spec in the context of a specific consumer version pair. Useful when investigating why a particular consumer-provider combination failed cross-contract verification.",
270
265
  inputSchema: GetBiDirectionalConsumerProviderVersionSchema,
@@ -272,7 +267,7 @@ const TOOLS = [
272
267
  clients: ["pactflow"]
273
268
  },
274
269
  {
275
- title: "Get BDCT Provider Contract Verification Results by Consumer Version",
270
+ title: "Get BDCT Provider Check Results by Consumer",
276
271
  summary: "Fetch the provider contract self-verification results for a specific consumer-provider version pair in Bi-Directional Contract Testing.",
277
272
  purpose: "Retrieve the provider's self-verification results in the context of a specific consumer version pair. Use when diagnosing failures for a particular consumer-provider combination.",
278
273
  inputSchema: GetBiDirectionalConsumerProviderVersionSchema,
@@ -280,7 +275,7 @@ const TOOLS = [
280
275
  clients: ["pactflow"]
281
276
  },
282
277
  {
283
- title: "Get BDCT Consumer Contract Verification Results by Consumer Version",
278
+ title: "Get BDCT Consumer Pact Test Results by Consumer",
284
279
  summary: "Fetch the consumer contract verification results for a specific consumer-provider version pair in Bi-Directional Contract Testing.",
285
280
  purpose: "Retrieve the results of comparing a specific consumer version's Pact against the provider's OpenAPI spec. Shows exactly which interactions passed or failed the cross-contract comparison.",
286
281
  inputSchema: GetBiDirectionalConsumerProviderVersionSchema,
@@ -288,7 +283,7 @@ const TOOLS = [
288
283
  clients: ["pactflow"]
289
284
  },
290
285
  {
291
- title: "Get BDCT Cross-Contract Verification Results by Consumer Version",
286
+ title: "Get BDCT X-Contract Test Results by Consumer",
292
287
  summary: "Fetch the cross-contract verification results for a specific consumer-provider version pair in Bi-Directional Contract Testing.",
293
288
  purpose: "Retrieve the precise cross-contract comparison outcome between a specific consumer version and provider version. This is the most granular BDCT result — use it to understand exactly why a specific consumer-provider pairing succeeded or failed, and which interactions were incompatible.",
294
289
  inputSchema: GetBiDirectionalConsumerProviderVersionSchema,
@@ -16,7 +16,7 @@ const ConfigurationSchema = zod__default.object({
16
16
  });
17
17
  class PactflowClient {
18
18
  name = "Contract Testing";
19
- toolPrefix = "contract-testing";
19
+ capabilityPrefix = "contract-testing";
20
20
  configPrefix = "Pact-Broker";
21
21
  config = ConfigurationSchema;
22
22
  token;
@@ -136,10 +136,17 @@ class PactflowClient {
136
136
  * @throws Error if the request fails or returns a non-OK response.
137
137
  */
138
138
  async checkAIEntitlements() {
139
- return await this.fetchJson(`${this.aiBaseUrl}/entitlement`, {
140
- method: "GET",
141
- errorContext: "PactFlow AI Entitlements Request"
142
- });
139
+ if (this.aiBaseUrl) {
140
+ return await this.fetchJson(
141
+ `${this.aiBaseUrl}/entitlement`,
142
+ {
143
+ method: "GET",
144
+ errorContext: "PactFlow AI Entitlements Request"
145
+ }
146
+ );
147
+ } else {
148
+ return null;
149
+ }
143
150
  }
144
151
  /**
145
152
  * Polls the given status URL with a HEAD request to check operation progress.
@@ -2060,7 +2067,7 @@ class PactflowClient {
2060
2067
  let disablePactflowAItools = false;
2061
2068
  try {
2062
2069
  const entitlement = await this.checkAIEntitlements();
2063
- if (!entitlement.aiEnabled) {
2070
+ if (entitlement && !entitlement.aiEnabled) {
2064
2071
  disablePactflowAItools = true;
2065
2072
  }
2066
2073
  } catch (error) {
@@ -2074,8 +2081,8 @@ class PactflowClient {
2074
2081
  if (tool.tags && disablePactflowAItools && tool.tags.includes("pactflow-ai")) {
2075
2082
  continue;
2076
2083
  }
2077
- const { handler, clients: _, formatResponse, ...toolparams } = tool;
2078
- register(toolparams, async (args, _extra) => {
2084
+ const { handler, clients: _, formatResponse, ...toolParams } = tool;
2085
+ register(toolParams, async (args, _extra) => {
2079
2086
  const handler_fn = this[handler];
2080
2087
  if (typeof handler_fn !== "function") {
2081
2088
  throw new Error(`Handler '${handler}' not found on PactClient`);
@@ -2100,10 +2107,17 @@ class PactflowClient {
2100
2107
  *
2101
2108
  * @param register - The function used to register prompts.
2102
2109
  */
2103
- registerPrompts(register) {
2104
- PROMPTS.forEach((prompt) => {
2105
- register(prompt.name, prompt.params, prompt.callback);
2106
- });
2110
+ async registerPrompts(register) {
2111
+ for (const prompt of PROMPTS) {
2112
+ register(
2113
+ {
2114
+ title: prompt.title,
2115
+ description: prompt.description,
2116
+ argsSchema: prompt.argsSchema
2117
+ },
2118
+ prompt.callback
2119
+ );
2120
+ }
2107
2121
  }
2108
2122
  }
2109
2123
  export {