mock-mcp 0.3.1 → 0.5.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 (40) hide show
  1. package/README.md +212 -124
  2. package/dist/adapter/index.cjs +875 -0
  3. package/dist/adapter/index.d.cts +142 -0
  4. package/dist/adapter/index.d.ts +142 -0
  5. package/dist/adapter/index.js +835 -0
  6. package/dist/client/connect.cjs +991 -0
  7. package/dist/client/connect.d.cts +218 -0
  8. package/dist/client/connect.d.ts +211 -7
  9. package/dist/client/connect.js +941 -20
  10. package/dist/client/index.cjs +992 -0
  11. package/dist/client/index.d.cts +3 -0
  12. package/dist/client/index.d.ts +3 -2
  13. package/dist/client/index.js +951 -2
  14. package/dist/daemon/index.cjs +717 -0
  15. package/dist/daemon/index.d.cts +62 -0
  16. package/dist/daemon/index.d.ts +62 -0
  17. package/dist/daemon/index.js +678 -0
  18. package/dist/index.cjs +2708 -0
  19. package/dist/index.d.cts +602 -0
  20. package/dist/index.d.ts +602 -11
  21. package/dist/index.js +2651 -53
  22. package/dist/shared/index.cjs +506 -0
  23. package/dist/shared/index.d.cts +241 -0
  24. package/dist/shared/index.d.ts +241 -0
  25. package/dist/shared/index.js +423 -0
  26. package/dist/types-bEGXLBF0.d.cts +190 -0
  27. package/dist/types-bEGXLBF0.d.ts +190 -0
  28. package/package.json +45 -4
  29. package/dist/client/batch-mock-collector.d.ts +0 -111
  30. package/dist/client/batch-mock-collector.js +0 -308
  31. package/dist/client/util.d.ts +0 -1
  32. package/dist/client/util.js +0 -3
  33. package/dist/connect.cjs +0 -400
  34. package/dist/connect.d.cts +0 -82
  35. package/dist/server/index.d.ts +0 -1
  36. package/dist/server/index.js +0 -1
  37. package/dist/server/test-mock-mcp-server.d.ts +0 -73
  38. package/dist/server/test-mock-mcp-server.js +0 -419
  39. package/dist/types.d.ts +0 -45
  40. package/dist/types.js +0 -2
@@ -1,2 +1,951 @@
1
- export { BatchMockCollector, } from "./batch-mock-collector.js";
2
- export { connect } from "./connect.js";
1
+ import http from 'http';
2
+ import fssync from 'fs';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import { fileURLToPath, pathToFileURL } from 'url';
6
+ import WebSocket from 'ws';
7
+ import fs from 'fs/promises';
8
+ import os from 'os';
9
+ import { spawn } from 'child_process';
10
+ import { createRequire } from 'module';
11
+
12
+ // src/client/batch-mock-collector.ts
13
+ function debugLog(_msg) {
14
+ }
15
+ var __curDirname = (() => {
16
+ try {
17
+ const metaUrl = import.meta.url;
18
+ if (metaUrl && typeof metaUrl === "string" && metaUrl.startsWith("file://")) {
19
+ return path.dirname(fileURLToPath(metaUrl));
20
+ }
21
+ } catch {
22
+ }
23
+ return process.cwd();
24
+ })();
25
+ function hasValidProjectMarker(dir) {
26
+ try {
27
+ const gitPath = path.join(dir, ".git");
28
+ try {
29
+ const stat = fssync.statSync(gitPath);
30
+ if (stat.isDirectory() || stat.isFile()) {
31
+ return true;
32
+ }
33
+ } catch {
34
+ }
35
+ const pkgPath = path.join(dir, "package.json");
36
+ try {
37
+ fssync.accessSync(pkgPath, fssync.constants.F_OK);
38
+ return true;
39
+ } catch {
40
+ }
41
+ return false;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+ function resolveProjectRoot(startDir = process.cwd()) {
47
+ let current = path.resolve(startDir);
48
+ const root = path.parse(current).root;
49
+ while (current !== root) {
50
+ const gitPath = path.join(current, ".git");
51
+ try {
52
+ const stat = fssync.statSync(gitPath);
53
+ if (stat.isDirectory() || stat.isFile()) {
54
+ return current;
55
+ }
56
+ } catch {
57
+ }
58
+ const pkgPath = path.join(current, "package.json");
59
+ try {
60
+ fssync.accessSync(pkgPath, fssync.constants.F_OK);
61
+ return current;
62
+ } catch {
63
+ }
64
+ current = path.dirname(current);
65
+ }
66
+ return path.resolve(startDir);
67
+ }
68
+ function computeProjectId(projectRoot) {
69
+ const real = fssync.realpathSync(projectRoot);
70
+ return crypto.createHash("sha256").update(real).digest("hex").slice(0, 16);
71
+ }
72
+ function getCacheDir(override) {
73
+ if (override) {
74
+ return override;
75
+ }
76
+ const envCacheDir = process.env.MOCK_MCP_CACHE_DIR;
77
+ if (envCacheDir) {
78
+ return envCacheDir;
79
+ }
80
+ const xdg = process.env.XDG_CACHE_HOME;
81
+ if (xdg) {
82
+ return xdg;
83
+ }
84
+ if (process.platform === "win32" && process.env.LOCALAPPDATA) {
85
+ return process.env.LOCALAPPDATA;
86
+ }
87
+ const home = os.homedir();
88
+ if (home) {
89
+ return path.join(home, ".cache");
90
+ }
91
+ return os.tmpdir();
92
+ }
93
+ function getPaths(projectId, cacheDir) {
94
+ const base = path.join(getCacheDir(cacheDir), "mock-mcp");
95
+ const registryPath = path.join(base, `${projectId}.json`);
96
+ const lockPath = path.join(base, `${projectId}.lock`);
97
+ const ipcPath = process.platform === "win32" ? `\\\\.\\pipe\\mock-mcp-${projectId}` : path.join(base, `${projectId}.sock`);
98
+ return { base, registryPath, lockPath, ipcPath };
99
+ }
100
+ async function readRegistry(registryPath) {
101
+ try {
102
+ const txt = await fs.readFile(registryPath, "utf-8");
103
+ return JSON.parse(txt);
104
+ } catch (error) {
105
+ debugLog(`readRegistry error for ${registryPath}: ${error instanceof Error ? error.message : String(error)}`);
106
+ return null;
107
+ }
108
+ }
109
+ async function healthCheck(ipcPath, timeoutMs = 2e3) {
110
+ return new Promise((resolve) => {
111
+ const req = http.request(
112
+ {
113
+ method: "GET",
114
+ socketPath: ipcPath,
115
+ path: "/health",
116
+ timeout: timeoutMs
117
+ },
118
+ (res) => {
119
+ resolve(res.statusCode === 200);
120
+ }
121
+ );
122
+ req.on("error", () => resolve(false));
123
+ req.on("timeout", () => {
124
+ req.destroy();
125
+ resolve(false);
126
+ });
127
+ req.end();
128
+ });
129
+ }
130
+ async function tryAcquireLock(lockPath) {
131
+ try {
132
+ const fh = await fs.open(lockPath, "wx");
133
+ await fh.write(`${process.pid}
134
+ `);
135
+ return fh;
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+ async function releaseLock(lockPath, fh) {
141
+ await fh.close();
142
+ await fs.rm(lockPath).catch(() => {
143
+ });
144
+ }
145
+ function randomToken() {
146
+ return crypto.randomBytes(24).toString("base64url");
147
+ }
148
+ function getDaemonEntryPath() {
149
+ try {
150
+ const cwdRequire = createRequire(pathToFileURL(path.join(process.cwd(), "index.js")).href);
151
+ const resolved = cwdRequire.resolve("mock-mcp");
152
+ const distDir = path.dirname(resolved);
153
+ const daemonEntry = path.join(distDir, "index.js");
154
+ if (fssync.existsSync(daemonEntry)) {
155
+ return daemonEntry;
156
+ }
157
+ } catch {
158
+ }
159
+ try {
160
+ const packageRoot = resolveProjectRoot(__curDirname);
161
+ const distPath = path.join(packageRoot, "dist", "index.js");
162
+ if (fssync.existsSync(distPath)) {
163
+ return distPath;
164
+ }
165
+ } catch {
166
+ }
167
+ if (process.argv[1]) {
168
+ return process.argv[1];
169
+ }
170
+ return path.join(process.cwd(), "dist", "index.js");
171
+ }
172
+ async function ensureDaemonRunning(opts = {}) {
173
+ let projectRoot = opts.projectRoot ?? resolveProjectRoot();
174
+ if (!hasValidProjectMarker(projectRoot)) {
175
+ const resolved = resolveProjectRoot(projectRoot);
176
+ if (resolved !== projectRoot && hasValidProjectMarker(resolved)) {
177
+ console.error(`[mock-mcp] Warning: projectRoot "${projectRoot}" doesn't look like a project root`);
178
+ console.error(`[mock-mcp] Found .git/package.json at: "${resolved}"`);
179
+ projectRoot = resolved;
180
+ } else {
181
+ console.error(`[mock-mcp] \u26A0\uFE0F WARNING: Could not find a valid project root!`);
182
+ console.error(`[mock-mcp] Current path: "${projectRoot}"`);
183
+ console.error(`[mock-mcp] This path doesn't contain .git or package.json.`);
184
+ console.error(`[mock-mcp] This may cause project mismatch issues.`);
185
+ console.error(`[mock-mcp] `);
186
+ console.error(`[mock-mcp] For MCP adapters, please specify --project-root explicitly:`);
187
+ console.error(`[mock-mcp] mock-mcp adapter --project-root /path/to/your/project`);
188
+ console.error(`[mock-mcp] `);
189
+ console.error(`[mock-mcp] In your MCP client config (Cursor, Claude Desktop, etc.):`);
190
+ console.error(`[mock-mcp] {`);
191
+ console.error(`[mock-mcp] "args": ["-y", "mock-mcp", "adapter", "--project-root", "/path/to/your/project"]`);
192
+ console.error(`[mock-mcp] }`);
193
+ }
194
+ }
195
+ const projectId = computeProjectId(projectRoot);
196
+ const { base, registryPath, lockPath, ipcPath } = getPaths(
197
+ projectId,
198
+ opts.cacheDir
199
+ );
200
+ const timeoutMs = opts.timeoutMs ?? 1e4;
201
+ await fs.mkdir(base, { recursive: true });
202
+ const existing = await readRegistry(registryPath);
203
+ debugLog(`Registry read result: ${existing ? "Found (PID " + existing.pid + ")" : "Null"}`);
204
+ if (existing) {
205
+ let healthy = false;
206
+ for (let i = 0; i < 3; i++) {
207
+ debugLog(`Checking health attempt ${i + 1}/3 on ${existing.ipcPath}`);
208
+ healthy = await healthCheck(existing.ipcPath);
209
+ if (healthy) break;
210
+ await new Promise((r) => setTimeout(r, 200));
211
+ }
212
+ if (healthy) {
213
+ return existing;
214
+ }
215
+ }
216
+ if (process.platform !== "win32") {
217
+ try {
218
+ await fs.rm(ipcPath);
219
+ } catch {
220
+ }
221
+ }
222
+ const lock = await tryAcquireLock(lockPath);
223
+ if (lock) {
224
+ try {
225
+ const recheckReg = await readRegistry(registryPath);
226
+ if (recheckReg && await healthCheck(recheckReg.ipcPath)) {
227
+ return recheckReg;
228
+ }
229
+ const token = randomToken();
230
+ const daemonEntry = getDaemonEntryPath();
231
+ const child = spawn(
232
+ process.execPath,
233
+ [daemonEntry, "daemon", "--project-root", projectRoot, "--token", token],
234
+ {
235
+ detached: true,
236
+ stdio: ["ignore", "pipe", "pipe"],
237
+ env: {
238
+ ...process.env,
239
+ MOCK_MCP_CACHE_DIR: opts.cacheDir ?? ""
240
+ }
241
+ }
242
+ );
243
+ let daemonStderr = "";
244
+ let daemonStdout = "";
245
+ child.stdout?.on("data", (data) => {
246
+ const str = data.toString();
247
+ debugLog(`Daemon stdout: ${str}`);
248
+ });
249
+ child.stderr?.on("data", (data) => {
250
+ daemonStderr += data.toString();
251
+ debugLog(`Daemon stderr: ${data.toString()}`);
252
+ });
253
+ child.on("error", (err) => {
254
+ console.error(`[mock-mcp] Daemon spawn error: ${err.message}`);
255
+ });
256
+ child.on("exit", (code, signal) => {
257
+ if (code !== null && code !== 0) {
258
+ console.error(`[mock-mcp] Daemon exited with code: ${code}`);
259
+ if (daemonStderr) {
260
+ console.error(`[mock-mcp] Daemon stderr: ${daemonStderr.slice(0, 500)}`);
261
+ }
262
+ } else if (signal) {
263
+ console.error(`[mock-mcp] Daemon killed by signal: ${signal}`);
264
+ }
265
+ });
266
+ child.unref();
267
+ const deadline2 = Date.now() + timeoutMs;
268
+ while (Date.now() < deadline2) {
269
+ const reg = await readRegistry(registryPath);
270
+ if (reg && await healthCheck(reg.ipcPath)) {
271
+ return reg;
272
+ }
273
+ await sleep(50);
274
+ }
275
+ console.error("[mock-mcp] Daemon failed to start within timeout");
276
+ if (daemonStderr) {
277
+ console.error(`[mock-mcp] Daemon stderr:
278
+ ${daemonStderr}`);
279
+ }
280
+ throw new Error(
281
+ `Daemon start timeout after ${timeoutMs}ms. Check logs for details.`
282
+ );
283
+ } finally {
284
+ await releaseLock(lockPath, lock);
285
+ }
286
+ }
287
+ const deadline = Date.now() + timeoutMs;
288
+ while (Date.now() < deadline) {
289
+ const reg = await readRegistry(registryPath);
290
+ if (reg && await healthCheck(reg.ipcPath)) {
291
+ return reg;
292
+ }
293
+ await sleep(50);
294
+ }
295
+ throw new Error(
296
+ `Waiting for daemon timed out after ${timeoutMs}ms. Another process may have failed to start it.`
297
+ );
298
+ }
299
+ function sleep(ms) {
300
+ return new Promise((resolve) => setTimeout(resolve, ms));
301
+ }
302
+
303
+ // src/shared/protocol.ts
304
+ var HELLO_TEST = "HELLO_TEST";
305
+ var HELLO_ACK = "HELLO_ACK";
306
+ var BATCH_MOCK_REQUEST = "BATCH_MOCK_REQUEST";
307
+ var BATCH_MOCK_RESULT = "BATCH_MOCK_RESULT";
308
+
309
+ // src/client/util.ts
310
+ var isEnabled = () => {
311
+ return process.env.MOCK_MCP !== void 0 && process.env.MOCK_MCP !== "0";
312
+ };
313
+
314
+ // src/client/batch-mock-collector.ts
315
+ var DEFAULT_TIMEOUT = 6e4;
316
+ var DEFAULT_BATCH_DEBOUNCE_MS = 0;
317
+ var DEFAULT_MAX_BATCH_SIZE = 50;
318
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 15e3;
319
+ var BatchMockCollector = class {
320
+ ws;
321
+ registry;
322
+ runId = crypto.randomUUID();
323
+ pendingRequests = /* @__PURE__ */ new Map();
324
+ queuedRequestIds = /* @__PURE__ */ new Set();
325
+ timeout;
326
+ batchDebounceMs;
327
+ maxBatchSize;
328
+ logger;
329
+ heartbeatIntervalMs;
330
+ enableReconnect;
331
+ projectRoot;
332
+ testMeta;
333
+ batchTimer = null;
334
+ heartbeatTimer = null;
335
+ reconnectTimer = null;
336
+ requestIdCounter = 0;
337
+ closed = false;
338
+ authed = false;
339
+ readyResolve;
340
+ readyReject;
341
+ readyPromise = Promise.resolve();
342
+ constructor(options = {}) {
343
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
344
+ this.batchDebounceMs = options.batchDebounceMs ?? DEFAULT_BATCH_DEBOUNCE_MS;
345
+ this.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
346
+ this.logger = options.logger ?? console;
347
+ this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
348
+ this.enableReconnect = options.enableReconnect ?? true;
349
+ this.testMeta = options.testMeta;
350
+ this.projectRoot = this.resolveProjectRootFromOptions(options);
351
+ this.logger.log(`[mock-mcp] BatchMockCollector created`);
352
+ this.logger.log(`[mock-mcp] runId: ${this.runId}`);
353
+ this.logger.log(`[mock-mcp] timeout: ${this.timeout}ms`);
354
+ this.logger.log(`[mock-mcp] batchDebounceMs: ${this.batchDebounceMs}ms`);
355
+ this.logger.log(`[mock-mcp] maxBatchSize: ${this.maxBatchSize}`);
356
+ this.logger.log(`[mock-mcp] heartbeatIntervalMs: ${this.heartbeatIntervalMs}ms`);
357
+ this.logger.log(`[mock-mcp] enableReconnect: ${this.enableReconnect}`);
358
+ this.logger.log(`[mock-mcp] projectRoot: ${this.projectRoot ?? "(auto-detect)"}`);
359
+ if (options.filePath) {
360
+ this.logger.log(`[mock-mcp] filePath: ${options.filePath}`);
361
+ }
362
+ this.resetReadyPromise();
363
+ this.initConnection({
364
+ timeout: this.timeout
365
+ });
366
+ }
367
+ /**
368
+ * Resolve projectRoot from options.
369
+ * Priority: projectRoot (if valid) > filePath > projectRoot (fallback) > undefined (auto-detect)
370
+ *
371
+ * A projectRoot is "valid" if it contains .git or package.json. This prevents
372
+ * accidentally using a wrong directory (e.g., user's home directory) when the
373
+ * caller mistakenly passes process.cwd() as projectRoot.
374
+ */
375
+ resolveProjectRootFromOptions(options) {
376
+ if (options.projectRoot) {
377
+ const hasGit = this.hasGitOrPackageJson(options.projectRoot);
378
+ if (hasGit) {
379
+ return options.projectRoot;
380
+ }
381
+ this.logger.warn(`[mock-mcp] Warning: projectRoot "${options.projectRoot}" doesn't contain .git or package.json`);
382
+ }
383
+ if (options.filePath) {
384
+ let filePath = options.filePath;
385
+ if (filePath.startsWith("file://")) {
386
+ try {
387
+ filePath = fileURLToPath(filePath);
388
+ } catch {
389
+ filePath = filePath.replace(/^file:\/\//, "");
390
+ }
391
+ }
392
+ const dir = path.dirname(filePath);
393
+ const resolved = resolveProjectRoot(dir);
394
+ this.logger.log(`[mock-mcp] Resolved projectRoot from filePath:`);
395
+ this.logger.log(`[mock-mcp] filePath: ${options.filePath}`);
396
+ this.logger.log(`[mock-mcp] dir: ${dir}`);
397
+ this.logger.log(`[mock-mcp] projectRoot: ${resolved}`);
398
+ return resolved;
399
+ }
400
+ if (options.projectRoot) {
401
+ this.logger.warn(`[mock-mcp] Warning: Using projectRoot "${options.projectRoot}" despite missing .git/package.json`);
402
+ return options.projectRoot;
403
+ }
404
+ return void 0;
405
+ }
406
+ /**
407
+ * Check if a directory contains .git or package.json
408
+ */
409
+ hasGitOrPackageJson(dir) {
410
+ try {
411
+ const gitPath = path.join(dir, ".git");
412
+ const pkgPath = path.join(dir, "package.json");
413
+ try {
414
+ const stat = fssync.statSync(gitPath);
415
+ if (stat.isDirectory() || stat.isFile()) {
416
+ return true;
417
+ }
418
+ } catch {
419
+ }
420
+ try {
421
+ fssync.accessSync(pkgPath, fssync.constants.F_OK);
422
+ return true;
423
+ } catch {
424
+ }
425
+ return false;
426
+ } catch {
427
+ return false;
428
+ }
429
+ }
430
+ /**
431
+ * Ensures the underlying connection is ready for use.
432
+ */
433
+ async waitUntilReady() {
434
+ return this.readyPromise;
435
+ }
436
+ /**
437
+ * Request mock data for a specific endpoint/method pair.
438
+ */
439
+ async requestMock(endpoint, method, options = {}) {
440
+ if (this.closed) {
441
+ throw new Error("BatchMockCollector has been closed");
442
+ }
443
+ await this.waitUntilReady();
444
+ const requestId = `req-${++this.requestIdCounter}`;
445
+ const request = {
446
+ requestId,
447
+ endpoint,
448
+ method,
449
+ body: options.body,
450
+ headers: options.headers,
451
+ metadata: options.metadata
452
+ };
453
+ let settleCompletion;
454
+ const completion = new Promise((resolve) => {
455
+ settleCompletion = resolve;
456
+ });
457
+ return new Promise((resolve, reject) => {
458
+ const timeoutId = setTimeout(() => {
459
+ this.rejectRequest(
460
+ requestId,
461
+ new Error(`Mock request timed out after ${this.timeout}ms: ${method} ${endpoint}`)
462
+ );
463
+ }, this.timeout);
464
+ this.pendingRequests.set(requestId, {
465
+ request,
466
+ resolve: (mock) => {
467
+ settleCompletion({ status: "fulfilled", value: void 0 });
468
+ resolve(this.buildResolvedMock(mock));
469
+ },
470
+ reject: (error) => {
471
+ settleCompletion({ status: "rejected", reason: error });
472
+ reject(error);
473
+ },
474
+ timeoutId,
475
+ completion
476
+ });
477
+ this.enqueueRequest(requestId);
478
+ });
479
+ }
480
+ /**
481
+ * Wait for all currently pending requests to settle.
482
+ */
483
+ async waitForPendingRequests() {
484
+ if (!isEnabled()) {
485
+ return;
486
+ }
487
+ const pendingCompletions = Array.from(this.pendingRequests.values()).map(
488
+ (p) => p.completion
489
+ );
490
+ const results = await Promise.all(pendingCompletions);
491
+ const rejected = results.find(
492
+ (r) => r.status === "rejected"
493
+ );
494
+ if (rejected) {
495
+ throw rejected.reason;
496
+ }
497
+ }
498
+ /**
499
+ * Close the connection and fail all pending requests.
500
+ */
501
+ async close(code) {
502
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] close() called with code: ${code ?? "(default)"}`);
503
+ if (this.closed) {
504
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Already closed, returning`);
505
+ return;
506
+ }
507
+ this.closed = true;
508
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Cleaning up timers...`);
509
+ if (this.batchTimer) {
510
+ clearTimeout(this.batchTimer);
511
+ this.batchTimer = null;
512
+ }
513
+ if (this.heartbeatTimer) {
514
+ clearInterval(this.heartbeatTimer);
515
+ this.heartbeatTimer = null;
516
+ }
517
+ if (this.reconnectTimer) {
518
+ clearTimeout(this.reconnectTimer);
519
+ this.reconnectTimer = null;
520
+ }
521
+ const pendingCount = this.pendingRequests.size;
522
+ const queuedCount = this.queuedRequestIds.size;
523
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Pending requests: ${pendingCount}, Queued: ${queuedCount}`);
524
+ this.queuedRequestIds.clear();
525
+ const closePromise = new Promise((resolve) => {
526
+ if (!this.ws) {
527
+ resolve();
528
+ return;
529
+ }
530
+ this.ws.once("close", () => resolve());
531
+ });
532
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Closing WebSocket...`);
533
+ this.ws?.close(code);
534
+ this.failAllPending(new Error("BatchMockCollector has been closed"));
535
+ await closePromise;
536
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] \u2705 Connection closed`);
537
+ }
538
+ // ===========================================================================
539
+ // Connection Management
540
+ // ===========================================================================
541
+ async initConnection({
542
+ timeout = 1e4
543
+ }) {
544
+ const initStartTime = Date.now();
545
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Initializing connection...`);
546
+ try {
547
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Ensuring daemon is running...`);
548
+ const daemonStartTime = Date.now();
549
+ this.registry = await ensureDaemonRunning({
550
+ projectRoot: this.projectRoot,
551
+ timeoutMs: timeout
552
+ });
553
+ const daemonElapsed = Date.now() - daemonStartTime;
554
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Daemon ready (${daemonElapsed}ms)`);
555
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Project ID: ${this.registry.projectId}`);
556
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Daemon PID: ${this.registry.pid}`);
557
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] IPC Path: ${this.registry.ipcPath}`);
558
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Creating WebSocket connection...`);
559
+ const wsStartTime = Date.now();
560
+ this.ws = await this.createWebSocket(this.registry.ipcPath);
561
+ const wsElapsed = Date.now() - wsStartTime;
562
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] WebSocket created (${wsElapsed}ms)`);
563
+ this.setupWebSocket();
564
+ const totalElapsed = Date.now() - initStartTime;
565
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Connection initialized (total: ${totalElapsed}ms)`);
566
+ } catch (error) {
567
+ const elapsed = Date.now() - initStartTime;
568
+ this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] Connection init failed after ${elapsed}ms:`, error);
569
+ this.readyReject?.(error instanceof Error ? error : new Error(String(error)));
570
+ }
571
+ }
572
+ createWebSocket(ipcPath) {
573
+ return new Promise((resolve, reject) => {
574
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Creating WebSocket to IPC: ${ipcPath}`);
575
+ const agent = new http.Agent({
576
+ // @ts-expect-error: Node.js supports socketPath for Unix sockets
577
+ socketPath: ipcPath
578
+ });
579
+ const ws = new WebSocket("ws://localhost/test", {
580
+ agent
581
+ });
582
+ ws.once("open", () => {
583
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] WebSocket opened`);
584
+ resolve(ws);
585
+ });
586
+ ws.once("error", (err) => {
587
+ this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] WebSocket connection error:`, err);
588
+ reject(err);
589
+ });
590
+ });
591
+ }
592
+ setupWebSocket() {
593
+ if (!this.ws || !this.registry) {
594
+ this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] setupWebSocket called but ws or registry is missing`);
595
+ return;
596
+ }
597
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Setting up WebSocket event handlers...`);
598
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Current readyState: ${this.ws.readyState}`);
599
+ this.ws.on("message", (data) => this.handleMessage(data));
600
+ this.ws.on("error", (error) => {
601
+ this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] \u274C WebSocket ERROR:`, error);
602
+ this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] authed: ${this.authed}`);
603
+ this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] readyState: ${this.ws?.readyState}`);
604
+ if (!this.authed) {
605
+ this.readyReject?.(error instanceof Error ? error : new Error(String(error)));
606
+ }
607
+ this.failAllPending(error instanceof Error ? error : new Error(String(error)));
608
+ });
609
+ this.ws.on("close", (code, reason) => {
610
+ this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] \u{1F50C} WebSocket CLOSE`);
611
+ this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] code: ${code}`);
612
+ this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] reason: ${reason?.toString() || "(none)"}`);
613
+ this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] authed: ${this.authed}`);
614
+ this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] closed: ${this.closed}`);
615
+ this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] enableReconnect: ${this.enableReconnect}`);
616
+ this.authed = false;
617
+ this.stopHeartbeat();
618
+ this.failAllPending(new Error(`Daemon connection closed (code: ${code})`));
619
+ if (!this.closed && this.enableReconnect) {
620
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Will attempt reconnect...`);
621
+ this.scheduleReconnect();
622
+ }
623
+ });
624
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] WebSocket event handlers configured`);
625
+ if (this.ws.readyState === WebSocket.OPEN) {
626
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] \u{1F50C} WebSocket already OPEN - sending HELLO`);
627
+ this.sendHello();
628
+ }
629
+ }
630
+ sendHello() {
631
+ if (!this.ws || !this.registry) {
632
+ this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] sendHello called but ws or registry is missing`);
633
+ return;
634
+ }
635
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Sending HELLO handshake...`);
636
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] runId: ${this.runId}`);
637
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] pid: ${process.pid}`);
638
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] cwd: ${process.cwd()}`);
639
+ if (this.testMeta) {
640
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] testFile: ${this.testMeta.testFile ?? "(none)"}`);
641
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] testName: ${this.testMeta.testName ?? "(none)"}`);
642
+ }
643
+ const hello = {
644
+ type: HELLO_TEST,
645
+ token: this.registry.token,
646
+ runId: this.runId,
647
+ pid: process.pid,
648
+ cwd: process.cwd(),
649
+ testMeta: this.testMeta
650
+ };
651
+ this.ws.send(JSON.stringify(hello));
652
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] HELLO sent, waiting for HELLO_ACK...`);
653
+ }
654
+ handleMessage(data) {
655
+ let msg;
656
+ try {
657
+ msg = JSON.parse(data.toString());
658
+ } catch {
659
+ this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] Failed to parse server message`);
660
+ return;
661
+ }
662
+ const msgType = msg?.type;
663
+ if (this.isHelloAck(msg)) {
664
+ this.authed = true;
665
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] \u2705 Received HELLO_ACK - Authenticated!`);
666
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Connection is now READY`);
667
+ this.readyResolve?.();
668
+ this.startHeartbeat();
669
+ return;
670
+ }
671
+ if (this.isBatchMockResult(msg)) {
672
+ this.logger.log(
673
+ `[mock-mcp] [${this.runId.slice(0, 8)}] \u{1F4E6} Received BATCH_MOCK_RESULT`
674
+ );
675
+ this.logger.log(
676
+ `[mock-mcp] [${this.runId.slice(0, 8)}] batchId: ${msg.batchId}`
677
+ );
678
+ this.logger.log(
679
+ `[mock-mcp] [${this.runId.slice(0, 8)}] mocks count: ${msg.mocks.length}`
680
+ );
681
+ for (const mock of msg.mocks) {
682
+ this.logger.log(
683
+ `[mock-mcp] [${this.runId.slice(0, 8)}] - ${mock.requestId}: status=${mock.status ?? 200}`
684
+ );
685
+ this.resolveRequest(mock);
686
+ }
687
+ return;
688
+ }
689
+ if (msgType === "HEARTBEAT_ACK") {
690
+ return;
691
+ }
692
+ this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] Received unknown message type: ${msgType}`);
693
+ }
694
+ isHelloAck(msg) {
695
+ return msg !== null && typeof msg === "object" && msg.type === HELLO_ACK;
696
+ }
697
+ isBatchMockResult(msg) {
698
+ return msg !== null && typeof msg === "object" && msg.type === BATCH_MOCK_RESULT && Array.isArray(msg.mocks);
699
+ }
700
+ // ===========================================================================
701
+ // Request Management
702
+ // ===========================================================================
703
+ resolveRequest(mock) {
704
+ const pending = this.pendingRequests.get(mock.requestId);
705
+ if (!pending) {
706
+ this.logger.warn(`Received mock for unknown request: ${mock.requestId}`);
707
+ return;
708
+ }
709
+ clearTimeout(pending.timeoutId);
710
+ this.pendingRequests.delete(mock.requestId);
711
+ const resolve = () => pending.resolve(mock);
712
+ if (mock.delayMs && mock.delayMs > 0) {
713
+ setTimeout(resolve, mock.delayMs);
714
+ } else {
715
+ resolve();
716
+ }
717
+ }
718
+ rejectRequest(requestId, error) {
719
+ const pending = this.pendingRequests.get(requestId);
720
+ if (!pending) return;
721
+ clearTimeout(pending.timeoutId);
722
+ this.pendingRequests.delete(requestId);
723
+ pending.reject(error);
724
+ }
725
+ failAllPending(error) {
726
+ for (const requestId of Array.from(this.pendingRequests.keys())) {
727
+ this.rejectRequest(requestId, error);
728
+ }
729
+ }
730
+ // ===========================================================================
731
+ // Batching
732
+ // ===========================================================================
733
+ enqueueRequest(requestId) {
734
+ this.queuedRequestIds.add(requestId);
735
+ if (this.batchTimer) {
736
+ return;
737
+ }
738
+ this.batchTimer = setTimeout(() => {
739
+ this.batchTimer = null;
740
+ this.flushQueue();
741
+ }, this.batchDebounceMs);
742
+ }
743
+ flushQueue() {
744
+ const queuedIds = Array.from(this.queuedRequestIds);
745
+ this.queuedRequestIds.clear();
746
+ if (queuedIds.length === 0) {
747
+ return;
748
+ }
749
+ for (let i = 0; i < queuedIds.length; i += this.maxBatchSize) {
750
+ const chunkIds = queuedIds.slice(i, i + this.maxBatchSize);
751
+ const requests = [];
752
+ for (const id of chunkIds) {
753
+ const pending = this.pendingRequests.get(id);
754
+ if (pending) {
755
+ requests.push(pending.request);
756
+ }
757
+ }
758
+ if (requests.length > 0) {
759
+ this.sendBatch(requests);
760
+ }
761
+ }
762
+ }
763
+ sendBatch(requests) {
764
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
765
+ this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] Cannot send batch - WebSocket not open`);
766
+ this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] ws exists: ${!!this.ws}`);
767
+ this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] readyState: ${this.ws?.readyState}`);
768
+ const error = new Error("WebSocket is not open");
769
+ for (const req of requests) {
770
+ this.rejectRequest(req.requestId, error);
771
+ }
772
+ return;
773
+ }
774
+ const payload = {
775
+ type: BATCH_MOCK_REQUEST,
776
+ runId: this.runId,
777
+ requests
778
+ };
779
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] \u{1F4E4} Sending BATCH_MOCK_REQUEST`);
780
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] requests: ${requests.length}`);
781
+ for (const req of requests) {
782
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] - ${req.requestId}: ${req.method} ${req.endpoint}`);
783
+ }
784
+ this.ws.send(JSON.stringify(payload));
785
+ }
786
+ // ===========================================================================
787
+ // Heartbeat
788
+ // ===========================================================================
789
+ startHeartbeat() {
790
+ if (this.heartbeatIntervalMs <= 0 || this.heartbeatTimer) {
791
+ return;
792
+ }
793
+ let lastPong = Date.now();
794
+ this.ws?.on("pong", () => {
795
+ lastPong = Date.now();
796
+ });
797
+ this.heartbeatTimer = setInterval(() => {
798
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
799
+ return;
800
+ }
801
+ const now = Date.now();
802
+ if (now - lastPong > this.heartbeatIntervalMs * 2) {
803
+ this.logger.warn("Heartbeat missed; closing socket to trigger reconnect...");
804
+ this.ws.close();
805
+ return;
806
+ }
807
+ this.ws.ping();
808
+ }, this.heartbeatIntervalMs);
809
+ this.heartbeatTimer.unref?.();
810
+ }
811
+ stopHeartbeat() {
812
+ if (this.heartbeatTimer) {
813
+ clearInterval(this.heartbeatTimer);
814
+ this.heartbeatTimer = null;
815
+ }
816
+ }
817
+ // ===========================================================================
818
+ // Reconnection
819
+ // ===========================================================================
820
+ scheduleReconnect() {
821
+ if (this.reconnectTimer) {
822
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Reconnect already scheduled, skipping`);
823
+ return;
824
+ }
825
+ if (this.closed) {
826
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Client is closed, not reconnecting`);
827
+ return;
828
+ }
829
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Scheduling reconnect in 1000ms...`);
830
+ this.reconnectTimer = setTimeout(async () => {
831
+ this.reconnectTimer = null;
832
+ this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] \u{1F504} Attempting reconnect to daemon...`);
833
+ this.stopHeartbeat();
834
+ this.resetReadyPromise();
835
+ this.authed = false;
836
+ const reconnectStartTime = Date.now();
837
+ try {
838
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Re-discovering daemon...`);
839
+ this.registry = await ensureDaemonRunning({
840
+ projectRoot: this.projectRoot
841
+ });
842
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Daemon found, creating new WebSocket...`);
843
+ this.ws = await this.createWebSocket(this.registry.ipcPath);
844
+ this.setupWebSocket();
845
+ const elapsed = Date.now() - reconnectStartTime;
846
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] \u2705 Reconnect successful (${elapsed}ms)`);
847
+ } catch (error) {
848
+ const elapsed = Date.now() - reconnectStartTime;
849
+ this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] \u274C Reconnection failed after ${elapsed}ms:`, error);
850
+ this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Will retry reconnect...`);
851
+ this.scheduleReconnect();
852
+ }
853
+ }, 1e3);
854
+ this.reconnectTimer.unref?.();
855
+ }
856
+ // ===========================================================================
857
+ // Utilities
858
+ // ===========================================================================
859
+ resetReadyPromise() {
860
+ this.readyPromise = new Promise((resolve, reject) => {
861
+ this.readyResolve = resolve;
862
+ this.readyReject = reject;
863
+ });
864
+ }
865
+ buildResolvedMock(mock) {
866
+ return {
867
+ requestId: mock.requestId,
868
+ data: mock.data,
869
+ status: mock.status,
870
+ headers: mock.headers,
871
+ delayMs: mock.delayMs
872
+ };
873
+ }
874
+ // ===========================================================================
875
+ // Public Getters
876
+ // ===========================================================================
877
+ /**
878
+ * Get the run ID for this collector instance.
879
+ */
880
+ getRunId() {
881
+ return this.runId;
882
+ }
883
+ /**
884
+ * Get the daemon registry information (after connection).
885
+ */
886
+ getRegistry() {
887
+ return this.registry;
888
+ }
889
+ };
890
+
891
+ // src/client/connect.ts
892
+ var DisabledMockClient = class {
893
+ runId = "disabled";
894
+ async waitUntilReady() {
895
+ return;
896
+ }
897
+ async requestMock() {
898
+ throw new Error(
899
+ "[mock-mcp] MOCK_MCP is not enabled. Set MOCK_MCP=1 to enable mock generation."
900
+ );
901
+ }
902
+ async waitForPendingRequests() {
903
+ return;
904
+ }
905
+ async close() {
906
+ return;
907
+ }
908
+ getRunId() {
909
+ return this.runId;
910
+ }
911
+ };
912
+ var connect = async (options) => {
913
+ const logger = options?.logger ?? console;
914
+ const startTime = Date.now();
915
+ logger.log("[mock-mcp] connect() called");
916
+ logger.log(`[mock-mcp] PID: ${process.pid}`);
917
+ logger.log(`[mock-mcp] CWD: ${process.cwd()}`);
918
+ logger.log(`[mock-mcp] MOCK_MCP env: ${process.env.MOCK_MCP ?? "(not set)"}`);
919
+ if (!isEnabled()) {
920
+ logger.log("[mock-mcp] Skipping (set MOCK_MCP=1 to enable)");
921
+ return new DisabledMockClient();
922
+ }
923
+ logger.log("[mock-mcp] Creating BatchMockCollector...");
924
+ const collector = new BatchMockCollector(options ?? {});
925
+ const runId = collector.getRunId();
926
+ logger.log(`[mock-mcp] Run ID: ${runId}`);
927
+ logger.log("[mock-mcp] Waiting for connection to be ready...");
928
+ try {
929
+ await collector.waitUntilReady();
930
+ const elapsed = Date.now() - startTime;
931
+ const registry = collector.getRegistry();
932
+ logger.log("[mock-mcp] ========== Connection Established ==========");
933
+ logger.log(`[mock-mcp] Run ID: ${runId}`);
934
+ logger.log(`[mock-mcp] Daemon PID: ${registry?.pid ?? "unknown"}`);
935
+ logger.log(`[mock-mcp] Project ID: ${registry?.projectId ?? "unknown"}`);
936
+ logger.log(`[mock-mcp] IPC Path: ${registry?.ipcPath ?? "unknown"}`);
937
+ logger.log(`[mock-mcp] Connection time: ${elapsed}ms`);
938
+ logger.log("[mock-mcp] ==============================================");
939
+ return collector;
940
+ } catch (error) {
941
+ const elapsed = Date.now() - startTime;
942
+ logger.error("[mock-mcp] ========== Connection Failed ==========");
943
+ logger.error(`[mock-mcp] Run ID: ${runId}`);
944
+ logger.error(`[mock-mcp] Error: ${error instanceof Error ? error.message : String(error)}`);
945
+ logger.error(`[mock-mcp] Elapsed time: ${elapsed}ms`);
946
+ logger.error("[mock-mcp] =========================================");
947
+ throw error;
948
+ }
949
+ };
950
+
951
+ export { BatchMockCollector, connect };