mock-mcp 0.3.0 → 0.3.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.
package/README.md CHANGED
@@ -70,9 +70,10 @@ it("example", async () => {
70
70
  instructions: "Respond with a single user described by the schema.",
71
71
  };
72
72
 
73
- fetchMock.get("/user", () =>
74
- mockClient.requestMock("/user", "GET", { metadata }) // add mock via mock-mcp
75
- );
73
+ fetchMock.get("/user", async () => {
74
+ const response = await mockClient.requestMock("/user", "GET", { metadata }) // add mock via mock-mcp
75
+ return response.data
76
+ });
76
77
 
77
78
  const result = await fetch("/user");
78
79
  const data = await result.json();
@@ -183,7 +184,7 @@ const mockClient = await connect({
183
184
 
184
185
  await page.route("**/api/users", async (route) => {
185
186
  const url = new URL(route.request().url());
186
- const data = await mockClient.requestMock(
187
+ const { data } = await mockClient.requestMock(
187
188
  url.pathname,
188
189
  route.request().method()
189
190
  );
@@ -1,3 +1,4 @@
1
+ import { type ResolvedMock } from "../types.js";
1
2
  type Logger = Pick<Console, "log" | "warn" | "error"> & {
2
3
  debug?: (...args: unknown[]) => void;
3
4
  };
@@ -33,6 +34,18 @@ export interface BatchMockCollectorOptions {
33
34
  * Optional custom logger. Defaults to `console`.
34
35
  */
35
36
  logger?: Logger;
37
+ /**
38
+ * Interval for WebSocket heartbeats in milliseconds. Set to 0 to disable.
39
+ *
40
+ * @default 15000
41
+ */
42
+ heartbeatIntervalMs?: number;
43
+ /**
44
+ * Automatically attempt to reconnect when the WebSocket closes unexpectedly.
45
+ *
46
+ * @default true
47
+ */
48
+ enableReconnect?: boolean;
36
49
  }
37
50
  export interface RequestMockOptions {
38
51
  body?: unknown;
@@ -44,19 +57,24 @@ export interface RequestMockOptions {
44
57
  * the MCP server as a batch for AI-assisted mock generation.
45
58
  */
46
59
  export declare class BatchMockCollector {
47
- private readonly ws;
60
+ private ws;
48
61
  private readonly pendingRequests;
49
62
  private readonly queuedRequestIds;
50
63
  private readonly timeout;
51
64
  private readonly batchDebounceMs;
52
65
  private readonly maxBatchSize;
53
66
  private readonly logger;
67
+ private readonly heartbeatIntervalMs;
68
+ private readonly enableReconnect;
69
+ private readonly port;
54
70
  private batchTimer;
71
+ private heartbeatTimer;
72
+ private reconnectTimer;
55
73
  private requestIdCounter;
56
74
  private closed;
57
75
  private readyResolve?;
58
76
  private readyReject?;
59
- private readonly readyPromise;
77
+ private readyPromise;
60
78
  constructor(options?: BatchMockCollectorOptions);
61
79
  /**
62
80
  * Ensures the underlying WebSocket connection is ready for use.
@@ -65,7 +83,7 @@ export declare class BatchMockCollector {
65
83
  /**
66
84
  * Request mock data for a specific endpoint/method pair.
67
85
  */
68
- requestMock<T = unknown>(endpoint: string, method: string, options?: RequestMockOptions): Promise<T>;
86
+ requestMock<T = unknown>(endpoint: string, method: string, options?: RequestMockOptions): Promise<ResolvedMock<T>>;
69
87
  /**
70
88
  * Wait for all requests that are currently pending to settle. Requests
71
89
  * created after this method is called are not included.
@@ -76,11 +94,17 @@ export declare class BatchMockCollector {
76
94
  */
77
95
  close(code?: number): Promise<void>;
78
96
  private setupWebSocket;
97
+ private createWebSocket;
98
+ private resetReadyPromise;
99
+ private startHeartbeat;
100
+ private stopHeartbeat;
101
+ private scheduleReconnect;
79
102
  private handleMessage;
80
103
  private resolveRequest;
81
104
  private enqueueRequest;
82
105
  private flushQueue;
83
106
  private sendBatch;
107
+ private buildResolvedMock;
84
108
  private rejectRequest;
85
109
  private failAllPending;
86
110
  }
@@ -5,6 +5,7 @@ const DEFAULT_TIMEOUT = 60_000;
5
5
  const DEFAULT_BATCH_DEBOUNCE_MS = 0;
6
6
  const DEFAULT_MAX_BATCH_SIZE = 50;
7
7
  const DEFAULT_PORT = 3002;
8
+ const DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000;
8
9
  /**
9
10
  * Collects HTTP requests issued during a single macrotask and forwards them to
10
11
  * the MCP server as a batch for AI-assisted mock generation.
@@ -17,24 +18,28 @@ export class BatchMockCollector {
17
18
  batchDebounceMs;
18
19
  maxBatchSize;
19
20
  logger;
21
+ heartbeatIntervalMs;
22
+ enableReconnect;
23
+ port;
20
24
  batchTimer = null;
25
+ heartbeatTimer = null;
26
+ reconnectTimer = null;
21
27
  requestIdCounter = 0;
22
28
  closed = false;
23
29
  readyResolve;
24
30
  readyReject;
25
- readyPromise;
31
+ readyPromise = Promise.resolve();
26
32
  constructor(options = {}) {
27
33
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
28
34
  this.batchDebounceMs = options.batchDebounceMs ?? DEFAULT_BATCH_DEBOUNCE_MS;
29
35
  this.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
30
36
  this.logger = options.logger ?? console;
31
- const port = options.port ?? DEFAULT_PORT;
32
- this.readyPromise = new Promise((resolve, reject) => {
33
- this.readyResolve = resolve;
34
- this.readyReject = reject;
35
- });
36
- const wsUrl = `ws://localhost:${port}`;
37
- this.ws = new WebSocket(wsUrl);
37
+ this.heartbeatIntervalMs =
38
+ options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
39
+ this.enableReconnect = options.enableReconnect ?? true;
40
+ this.port = options.port ?? DEFAULT_PORT;
41
+ this.resetReadyPromise();
42
+ this.ws = this.createWebSocket();
38
43
  this.setupWebSocket();
39
44
  }
40
45
  /**
@@ -70,9 +75,9 @@ export class BatchMockCollector {
70
75
  }, this.timeout);
71
76
  this.pendingRequests.set(requestId, {
72
77
  request,
73
- resolve: (data) => {
78
+ resolve: (mock) => {
74
79
  settleCompletion({ status: "fulfilled", value: undefined });
75
- resolve(data);
80
+ resolve(this.buildResolvedMock(mock));
76
81
  },
77
82
  reject: (error) => {
78
83
  settleCompletion({ status: "rejected", reason: error });
@@ -111,6 +116,14 @@ export class BatchMockCollector {
111
116
  clearTimeout(this.batchTimer);
112
117
  this.batchTimer = null;
113
118
  }
119
+ if (this.heartbeatTimer) {
120
+ clearInterval(this.heartbeatTimer);
121
+ this.heartbeatTimer = null;
122
+ }
123
+ if (this.reconnectTimer) {
124
+ clearTimeout(this.reconnectTimer);
125
+ this.reconnectTimer = null;
126
+ }
114
127
  this.queuedRequestIds.clear();
115
128
  const closePromise = new Promise((resolve) => {
116
129
  this.ws.once("close", () => resolve());
@@ -123,6 +136,7 @@ export class BatchMockCollector {
123
136
  this.ws.on("open", () => {
124
137
  this.logger.log("🔌 Connected to mock MCP WebSocket endpoint");
125
138
  this.readyResolve?.();
139
+ this.startHeartbeat();
126
140
  });
127
141
  this.ws.on("message", (data) => this.handleMessage(data));
128
142
  this.ws.on("error", (error) => {
@@ -132,8 +146,64 @@ export class BatchMockCollector {
132
146
  });
133
147
  this.ws.on("close", () => {
134
148
  this.logger.warn("🔌 WebSocket connection closed");
149
+ this.stopHeartbeat();
135
150
  this.failAllPending(new Error("WebSocket connection closed"));
151
+ if (!this.closed && this.enableReconnect) {
152
+ this.scheduleReconnect();
153
+ }
154
+ });
155
+ }
156
+ createWebSocket() {
157
+ const wsUrl = `ws://localhost:${this.port}`;
158
+ return new WebSocket(wsUrl);
159
+ }
160
+ resetReadyPromise() {
161
+ this.readyPromise = new Promise((resolve, reject) => {
162
+ this.readyResolve = resolve;
163
+ this.readyReject = reject;
164
+ });
165
+ }
166
+ startHeartbeat() {
167
+ if (this.heartbeatIntervalMs <= 0 || this.heartbeatTimer) {
168
+ return;
169
+ }
170
+ let lastPong = Date.now();
171
+ this.ws.on("pong", () => {
172
+ lastPong = Date.now();
136
173
  });
174
+ this.heartbeatTimer = setInterval(() => {
175
+ if (this.ws.readyState !== WebSocket.OPEN) {
176
+ return;
177
+ }
178
+ const now = Date.now();
179
+ if (now - lastPong > this.heartbeatIntervalMs * 2) {
180
+ this.logger.warn("Heartbeat missed; closing socket to trigger reconnect...");
181
+ this.ws.close();
182
+ return;
183
+ }
184
+ this.ws.ping();
185
+ }, this.heartbeatIntervalMs);
186
+ this.heartbeatTimer.unref?.();
187
+ }
188
+ stopHeartbeat() {
189
+ if (this.heartbeatTimer) {
190
+ clearInterval(this.heartbeatTimer);
191
+ this.heartbeatTimer = null;
192
+ }
193
+ }
194
+ scheduleReconnect() {
195
+ if (this.reconnectTimer || this.closed) {
196
+ return;
197
+ }
198
+ this.reconnectTimer = setTimeout(() => {
199
+ this.reconnectTimer = null;
200
+ this.logger.warn("🔄 Reconnecting to mock MCP WebSocket endpoint...");
201
+ this.stopHeartbeat();
202
+ this.resetReadyPromise();
203
+ this.ws = this.createWebSocket();
204
+ this.setupWebSocket();
205
+ }, 1_000);
206
+ this.reconnectTimer.unref?.();
137
207
  }
138
208
  handleMessage(data) {
139
209
  let parsed;
@@ -161,7 +231,13 @@ export class BatchMockCollector {
161
231
  }
162
232
  clearTimeout(pending.timeoutId);
163
233
  this.pendingRequests.delete(mock.requestId);
164
- pending.resolve(mock.data);
234
+ const resolve = () => pending.resolve(mock);
235
+ if (mock.delayMs && mock.delayMs > 0) {
236
+ setTimeout(resolve, mock.delayMs);
237
+ }
238
+ else {
239
+ resolve();
240
+ }
165
241
  }
166
242
  enqueueRequest(requestId) {
167
243
  this.queuedRequestIds.add(requestId);
@@ -206,6 +282,15 @@ export class BatchMockCollector {
206
282
  this.logger.debug?.(`📤 Sending batch with ${requests.length} request(s) to MCP server`);
207
283
  this.ws.send(JSON.stringify(payload));
208
284
  }
285
+ buildResolvedMock(mock) {
286
+ return {
287
+ requestId: mock.requestId,
288
+ data: mock.data,
289
+ status: mock.status,
290
+ headers: mock.headers,
291
+ delayMs: mock.delayMs,
292
+ };
293
+ }
209
294
  rejectRequest(requestId, error) {
210
295
  const pending = this.pendingRequests.get(requestId);
211
296
  if (!pending) {
@@ -1,8 +1,14 @@
1
- import { BatchMockCollector } from "./batch-mock-collector.js";
2
- import type { BatchMockCollectorOptions } from "./batch-mock-collector.js";
1
+ import type { BatchMockCollectorOptions, RequestMockOptions } from "./batch-mock-collector.js";
2
+ import type { ResolvedMock } from "../types.js";
3
3
  export type ConnectOptions = number | BatchMockCollectorOptions | undefined;
4
+ export interface MockClient {
5
+ waitUntilReady(): Promise<void>;
6
+ requestMock<T = unknown>(endpoint: string, method: string, options?: RequestMockOptions): Promise<ResolvedMock<T>>;
7
+ waitForPendingRequests(): Promise<void>;
8
+ close(code?: number): Promise<void>;
9
+ }
4
10
  /**
5
11
  * Convenience helper that creates a {@link BatchMockCollector} and waits for the
6
12
  * underlying WebSocket connection to become ready before resolving.
7
13
  */
8
- export declare const connect: (options?: ConnectOptions) => Promise<BatchMockCollector | void>;
14
+ export declare const connect: (options?: ConnectOptions) => Promise<MockClient>;
@@ -1,15 +1,29 @@
1
1
  import { BatchMockCollector } from "./batch-mock-collector.js";
2
2
  import { isEnabled } from "./util.js";
3
+ class DisabledMockClient {
4
+ async waitUntilReady() {
5
+ return;
6
+ }
7
+ async requestMock() {
8
+ throw new Error("[mock-mcp] MOCK_MCP is not enabled. Set MOCK_MCP=1 to enable mock generation.");
9
+ }
10
+ async waitForPendingRequests() {
11
+ return;
12
+ }
13
+ async close() {
14
+ return;
15
+ }
16
+ }
3
17
  /**
4
18
  * Convenience helper that creates a {@link BatchMockCollector} and waits for the
5
19
  * underlying WebSocket connection to become ready before resolving.
6
20
  */
7
21
  export const connect = async (options) => {
22
+ const resolvedOptions = typeof options === "number" ? { port: options } : options ?? {};
8
23
  if (!isEnabled()) {
9
24
  console.log("[mock-mcp] Skipping (set MOCK_MCP=1 to enable)");
10
- return;
25
+ return new DisabledMockClient();
11
26
  }
12
- const resolvedOptions = typeof options === "number" ? { port: options } : options ?? {};
13
27
  const collector = new BatchMockCollector(resolvedOptions);
14
28
  await collector.waitUntilReady();
15
29
  return collector;
@@ -1,2 +1,2 @@
1
1
  export { BatchMockCollector, type BatchMockCollectorOptions, type RequestMockOptions, } from "./batch-mock-collector.js";
2
- export { connect, type ConnectOptions } from "./connect.js";
2
+ export { connect, type ConnectOptions, type MockClient } from "./connect.js";
package/dist/connect.cjs CHANGED
@@ -51,6 +51,7 @@ var DEFAULT_TIMEOUT = 6e4;
51
51
  var DEFAULT_BATCH_DEBOUNCE_MS = 0;
52
52
  var DEFAULT_MAX_BATCH_SIZE = 50;
53
53
  var DEFAULT_PORT = 3002;
54
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 15e3;
54
55
  var BatchMockCollector = class {
55
56
  ws;
56
57
  pendingRequests = /* @__PURE__ */ new Map();
@@ -59,24 +60,27 @@ var BatchMockCollector = class {
59
60
  batchDebounceMs;
60
61
  maxBatchSize;
61
62
  logger;
63
+ heartbeatIntervalMs;
64
+ enableReconnect;
65
+ port;
62
66
  batchTimer = null;
67
+ heartbeatTimer = null;
68
+ reconnectTimer = null;
63
69
  requestIdCounter = 0;
64
70
  closed = false;
65
71
  readyResolve;
66
72
  readyReject;
67
- readyPromise;
73
+ readyPromise = Promise.resolve();
68
74
  constructor(options = {}) {
69
75
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
70
76
  this.batchDebounceMs = options.batchDebounceMs ?? DEFAULT_BATCH_DEBOUNCE_MS;
71
77
  this.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
72
78
  this.logger = options.logger ?? console;
73
- const port = options.port ?? DEFAULT_PORT;
74
- this.readyPromise = new Promise((resolve, reject) => {
75
- this.readyResolve = resolve;
76
- this.readyReject = reject;
77
- });
78
- const wsUrl = `ws://localhost:${port}`;
79
- this.ws = new import_ws.default(wsUrl);
79
+ this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
80
+ this.enableReconnect = options.enableReconnect ?? true;
81
+ this.port = options.port ?? DEFAULT_PORT;
82
+ this.resetReadyPromise();
83
+ this.ws = this.createWebSocket();
80
84
  this.setupWebSocket();
81
85
  }
82
86
  /**
@@ -117,9 +121,9 @@ var BatchMockCollector = class {
117
121
  }, this.timeout);
118
122
  this.pendingRequests.set(requestId, {
119
123
  request,
120
- resolve: (data) => {
124
+ resolve: (mock) => {
121
125
  settleCompletion({ status: "fulfilled", value: void 0 });
122
- resolve(data);
126
+ resolve(this.buildResolvedMock(mock));
123
127
  },
124
128
  reject: (error) => {
125
129
  settleCompletion({ status: "rejected", reason: error });
@@ -162,6 +166,14 @@ var BatchMockCollector = class {
162
166
  clearTimeout(this.batchTimer);
163
167
  this.batchTimer = null;
164
168
  }
169
+ if (this.heartbeatTimer) {
170
+ clearInterval(this.heartbeatTimer);
171
+ this.heartbeatTimer = null;
172
+ }
173
+ if (this.reconnectTimer) {
174
+ clearTimeout(this.reconnectTimer);
175
+ this.reconnectTimer = null;
176
+ }
165
177
  this.queuedRequestIds.clear();
166
178
  const closePromise = new Promise((resolve) => {
167
179
  this.ws.once("close", () => resolve());
@@ -174,6 +186,7 @@ var BatchMockCollector = class {
174
186
  this.ws.on("open", () => {
175
187
  this.logger.log("\u{1F50C} Connected to mock MCP WebSocket endpoint");
176
188
  this.readyResolve?.();
189
+ this.startHeartbeat();
177
190
  });
178
191
  this.ws.on("message", (data) => this.handleMessage(data));
179
192
  this.ws.on("error", (error) => {
@@ -187,9 +200,67 @@ var BatchMockCollector = class {
187
200
  });
188
201
  this.ws.on("close", () => {
189
202
  this.logger.warn("\u{1F50C} WebSocket connection closed");
203
+ this.stopHeartbeat();
190
204
  this.failAllPending(new Error("WebSocket connection closed"));
205
+ if (!this.closed && this.enableReconnect) {
206
+ this.scheduleReconnect();
207
+ }
191
208
  });
192
209
  }
210
+ createWebSocket() {
211
+ const wsUrl = `ws://localhost:${this.port}`;
212
+ return new import_ws.default(wsUrl);
213
+ }
214
+ resetReadyPromise() {
215
+ this.readyPromise = new Promise((resolve, reject) => {
216
+ this.readyResolve = resolve;
217
+ this.readyReject = reject;
218
+ });
219
+ }
220
+ startHeartbeat() {
221
+ if (this.heartbeatIntervalMs <= 0 || this.heartbeatTimer) {
222
+ return;
223
+ }
224
+ let lastPong = Date.now();
225
+ this.ws.on("pong", () => {
226
+ lastPong = Date.now();
227
+ });
228
+ this.heartbeatTimer = setInterval(() => {
229
+ if (this.ws.readyState !== import_ws.default.OPEN) {
230
+ return;
231
+ }
232
+ const now = Date.now();
233
+ if (now - lastPong > this.heartbeatIntervalMs * 2) {
234
+ this.logger.warn(
235
+ "Heartbeat missed; closing socket to trigger reconnect..."
236
+ );
237
+ this.ws.close();
238
+ return;
239
+ }
240
+ this.ws.ping();
241
+ }, this.heartbeatIntervalMs);
242
+ this.heartbeatTimer.unref?.();
243
+ }
244
+ stopHeartbeat() {
245
+ if (this.heartbeatTimer) {
246
+ clearInterval(this.heartbeatTimer);
247
+ this.heartbeatTimer = null;
248
+ }
249
+ }
250
+ scheduleReconnect() {
251
+ if (this.reconnectTimer || this.closed) {
252
+ return;
253
+ }
254
+ this.reconnectTimer = setTimeout(() => {
255
+ this.reconnectTimer = null;
256
+ this.logger.warn("\u{1F504} Reconnecting to mock MCP WebSocket endpoint...");
257
+ this.stopHeartbeat();
258
+ this.resetReadyPromise();
259
+ this.ws = this.createWebSocket();
260
+ this.setupWebSocket();
261
+ }, 1e3);
262
+ this.reconnectTimer.unref?.();
263
+ }
193
264
  handleMessage(data) {
194
265
  let parsed;
195
266
  try {
@@ -217,7 +288,12 @@ var BatchMockCollector = class {
217
288
  }
218
289
  clearTimeout(pending.timeoutId);
219
290
  this.pendingRequests.delete(mock.requestId);
220
- pending.resolve(mock.data);
291
+ const resolve = () => pending.resolve(mock);
292
+ if (mock.delayMs && mock.delayMs > 0) {
293
+ setTimeout(resolve, mock.delayMs);
294
+ } else {
295
+ resolve();
296
+ }
221
297
  }
222
298
  enqueueRequest(requestId) {
223
299
  this.queuedRequestIds.add(requestId);
@@ -266,6 +342,15 @@ var BatchMockCollector = class {
266
342
  );
267
343
  this.ws.send(JSON.stringify(payload));
268
344
  }
345
+ buildResolvedMock(mock) {
346
+ return {
347
+ requestId: mock.requestId,
348
+ data: mock.data,
349
+ status: mock.status,
350
+ headers: mock.headers,
351
+ delayMs: mock.delayMs
352
+ };
353
+ }
269
354
  rejectRequest(requestId, error) {
270
355
  const pending = this.pendingRequests.get(requestId);
271
356
  if (!pending) {
@@ -283,12 +368,28 @@ var BatchMockCollector = class {
283
368
  };
284
369
 
285
370
  // src/client/connect.ts
371
+ var DisabledMockClient = class {
372
+ async waitUntilReady() {
373
+ return;
374
+ }
375
+ async requestMock() {
376
+ throw new Error(
377
+ "[mock-mcp] MOCK_MCP is not enabled. Set MOCK_MCP=1 to enable mock generation."
378
+ );
379
+ }
380
+ async waitForPendingRequests() {
381
+ return;
382
+ }
383
+ async close() {
384
+ return;
385
+ }
386
+ };
286
387
  var connect = async (options) => {
388
+ const resolvedOptions = typeof options === "number" ? { port: options } : options ?? {};
287
389
  if (!isEnabled()) {
288
390
  console.log("[mock-mcp] Skipping (set MOCK_MCP=1 to enable)");
289
- return;
391
+ return new DisabledMockClient();
290
392
  }
291
- const resolvedOptions = typeof options === "number" ? { port: options } : options ?? {};
292
393
  const collector = new BatchMockCollector(resolvedOptions);
293
394
  await collector.waitUntilReady();
294
395
  return collector;
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Shape of the mock data that needs to be returned for a request.
3
+ */
4
+ interface MockResponseDescriptor {
5
+ requestId: string;
6
+ data: unknown;
7
+ status?: number;
8
+ headers?: Record<string, string>;
9
+ delayMs?: number;
10
+ }
11
+ interface ResolvedMock<T = unknown> extends Omit<MockResponseDescriptor, "data"> {
12
+ data: T;
13
+ }
14
+
1
15
  type Logger = Pick<Console, "log" | "warn" | "error"> & {
2
16
  debug?: (...args: unknown[]) => void;
3
17
  };
@@ -33,63 +47,36 @@ interface BatchMockCollectorOptions {
33
47
  * Optional custom logger. Defaults to `console`.
34
48
  */
35
49
  logger?: Logger;
50
+ /**
51
+ * Interval for WebSocket heartbeats in milliseconds. Set to 0 to disable.
52
+ *
53
+ * @default 15000
54
+ */
55
+ heartbeatIntervalMs?: number;
56
+ /**
57
+ * Automatically attempt to reconnect when the WebSocket closes unexpectedly.
58
+ *
59
+ * @default true
60
+ */
61
+ enableReconnect?: boolean;
36
62
  }
37
63
  interface RequestMockOptions {
38
64
  body?: unknown;
39
65
  headers?: Record<string, string>;
40
66
  metadata?: Record<string, unknown>;
41
67
  }
42
- /**
43
- * Collects HTTP requests issued during a single macrotask and forwards them to
44
- * the MCP server as a batch for AI-assisted mock generation.
45
- */
46
- declare class BatchMockCollector {
47
- private readonly ws;
48
- private readonly pendingRequests;
49
- private readonly queuedRequestIds;
50
- private readonly timeout;
51
- private readonly batchDebounceMs;
52
- private readonly maxBatchSize;
53
- private readonly logger;
54
- private batchTimer;
55
- private requestIdCounter;
56
- private closed;
57
- private readyResolve?;
58
- private readyReject?;
59
- private readonly readyPromise;
60
- constructor(options?: BatchMockCollectorOptions);
61
- /**
62
- * Ensures the underlying WebSocket connection is ready for use.
63
- */
68
+
69
+ type ConnectOptions = number | BatchMockCollectorOptions | undefined;
70
+ interface MockClient {
64
71
  waitUntilReady(): Promise<void>;
65
- /**
66
- * Request mock data for a specific endpoint/method pair.
67
- */
68
- requestMock<T = unknown>(endpoint: string, method: string, options?: RequestMockOptions): Promise<T>;
69
- /**
70
- * Wait for all requests that are currently pending to settle. Requests
71
- * created after this method is called are not included.
72
- */
72
+ requestMock<T = unknown>(endpoint: string, method: string, options?: RequestMockOptions): Promise<ResolvedMock<T>>;
73
73
  waitForPendingRequests(): Promise<void>;
74
- /**
75
- * Close the underlying connection and fail all pending requests.
76
- */
77
74
  close(code?: number): Promise<void>;
78
- private setupWebSocket;
79
- private handleMessage;
80
- private resolveRequest;
81
- private enqueueRequest;
82
- private flushQueue;
83
- private sendBatch;
84
- private rejectRequest;
85
- private failAllPending;
86
75
  }
87
-
88
- type ConnectOptions = number | BatchMockCollectorOptions | undefined;
89
76
  /**
90
77
  * Convenience helper that creates a {@link BatchMockCollector} and waits for the
91
78
  * underlying WebSocket connection to become ready before resolving.
92
79
  */
93
- declare const connect: (options?: ConnectOptions) => Promise<BatchMockCollector | void>;
80
+ declare const connect: (options?: ConnectOptions) => Promise<MockClient>;
94
81
 
95
- export { type ConnectOptions, connect };
82
+ export { type ConnectOptions, type MockClient, connect };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { TestMockMCPServer, type TestMockMCPServerOptions } from "./server/test-mock-mcp-server.js";
3
3
  import { BatchMockCollector, type BatchMockCollectorOptions, type RequestMockOptions } from "./client/batch-mock-collector.js";
4
- import { connect, type ConnectOptions } from "./client/connect.js";
4
+ import { connect, type ConnectOptions, type MockClient } from "./client/connect.js";
5
+ import type { ResolvedMock } from "./types.js";
5
6
  export { TestMockMCPServer };
6
7
  export type { TestMockMCPServerOptions };
7
8
  export { BatchMockCollector };
8
9
  export type { BatchMockCollectorOptions, RequestMockOptions };
9
10
  export { connect };
10
- export type { ConnectOptions };
11
+ export type { ConnectOptions, MockClient, ResolvedMock };
@@ -109,9 +109,21 @@ export class TestMockMCPServer {
109
109
  if (!batch) {
110
110
  throw new Error(`Batch not found: ${batchId}`);
111
111
  }
112
- const missing = mocks.find((mock) => !batch.requests.some((request) => request.requestId === mock.requestId));
113
- if (missing) {
114
- throw new Error(`Mock data references unknown requestId: ${missing.requestId}`);
112
+ const expectedIds = new Set(batch.requests.map((request) => request.requestId));
113
+ const providedIds = new Set();
114
+ const unknownMock = mocks.find((mock) => !expectedIds.has(mock.requestId));
115
+ if (unknownMock) {
116
+ throw new Error(`Mock data references unknown requestId: ${unknownMock.requestId}`);
117
+ }
118
+ for (const mock of mocks) {
119
+ if (providedIds.has(mock.requestId)) {
120
+ throw new Error(`Duplicate mock data provided for requestId: ${mock.requestId}`);
121
+ }
122
+ providedIds.add(mock.requestId);
123
+ }
124
+ const missingIds = Array.from(expectedIds).filter((requestId) => !providedIds.has(requestId));
125
+ if (missingIds.length > 0) {
126
+ throw new Error(`Missing mock data for requestId(s): ${missingIds.join(", ")}`);
115
127
  }
116
128
  if (batch.ws.readyState !== WebSocket.OPEN) {
117
129
  this.pendingBatches.delete(batchId);
@@ -204,7 +216,14 @@ export class TestMockMCPServer {
204
216
  properties: {
205
217
  requestId: { type: "string" },
206
218
  data: {
207
- type: "object",
219
+ anyOf: [
220
+ { type: "object" },
221
+ { type: "array" },
222
+ { type: "string" },
223
+ { type: "number" },
224
+ { type: "boolean" },
225
+ { type: "null" },
226
+ ],
208
227
  },
209
228
  status: {
210
229
  type: "number",
@@ -249,6 +268,14 @@ export class TestMockMCPServer {
249
268
  this.logger.error("🔌 Test process connected");
250
269
  this.clients.add(ws);
251
270
  ws.on("message", (data) => this.handleClientMessage(ws, data));
271
+ ws.on("ping", () => {
272
+ try {
273
+ ws.pong();
274
+ }
275
+ catch (error) {
276
+ this.logger.warn("Failed to respond to ping:", error);
277
+ }
278
+ });
252
279
  ws.on("close", () => {
253
280
  this.logger.error("🔌 Test process disconnected");
254
281
  this.clients.delete(ws);
package/dist/types.d.ts CHANGED
@@ -21,6 +21,9 @@ export interface MockResponseDescriptor {
21
21
  headers?: Record<string, string>;
22
22
  delayMs?: number;
23
23
  }
24
+ export interface ResolvedMock<T = unknown> extends Omit<MockResponseDescriptor, "data"> {
25
+ data: T;
26
+ }
24
27
  export interface BatchMockRequestMessage {
25
28
  type: typeof BATCH_MOCK_REQUEST;
26
29
  requests: MockRequestDescriptor[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mock-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "An MCP server enabling LLMs to write integration tests through live test environment interaction",
5
5
  "main": "./dist/connect.cjs",
6
6
  "type": "module",