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
@@ -0,0 +1,678 @@
1
+ import http2 from 'http';
2
+ import fs from 'fs/promises';
3
+ import crypto from 'crypto';
4
+ import { WebSocketServer, WebSocket } from 'ws';
5
+ import fssync from 'fs';
6
+ import os from 'os';
7
+ import path from 'path';
8
+ import 'child_process';
9
+ import { fileURLToPath } from 'url';
10
+ import 'module';
11
+
12
+ // src/daemon/daemon.ts
13
+ function debugLog(_msg) {
14
+ }
15
+ (() => {
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 computeProjectId(projectRoot) {
26
+ const real = fssync.realpathSync(projectRoot);
27
+ return crypto.createHash("sha256").update(real).digest("hex").slice(0, 16);
28
+ }
29
+ function getCacheDir(override) {
30
+ if (override) {
31
+ return override;
32
+ }
33
+ const envCacheDir = process.env.MOCK_MCP_CACHE_DIR;
34
+ if (envCacheDir) {
35
+ return envCacheDir;
36
+ }
37
+ const xdg = process.env.XDG_CACHE_HOME;
38
+ if (xdg) {
39
+ return xdg;
40
+ }
41
+ if (process.platform === "win32" && process.env.LOCALAPPDATA) {
42
+ return process.env.LOCALAPPDATA;
43
+ }
44
+ const home = os.homedir();
45
+ if (home) {
46
+ return path.join(home, ".cache");
47
+ }
48
+ return os.tmpdir();
49
+ }
50
+ function getPaths(projectId, cacheDir) {
51
+ const base = path.join(getCacheDir(cacheDir), "mock-mcp");
52
+ const registryPath = path.join(base, `${projectId}.json`);
53
+ const lockPath = path.join(base, `${projectId}.lock`);
54
+ const ipcPath = process.platform === "win32" ? `\\\\.\\pipe\\mock-mcp-${projectId}` : path.join(base, `${projectId}.sock`);
55
+ return { base, registryPath, lockPath, ipcPath };
56
+ }
57
+ async function writeRegistry(registryPath, registry) {
58
+ await fs.writeFile(registryPath, JSON.stringify(registry, null, 2), {
59
+ encoding: "utf-8",
60
+ mode: 384
61
+ // Read/write for owner only
62
+ });
63
+ }
64
+ function getGlobalIndexPath(cacheDir) {
65
+ const base = path.join(getCacheDir(cacheDir), "mock-mcp");
66
+ return path.join(base, "active-daemons.json");
67
+ }
68
+ async function readGlobalIndex(cacheDir) {
69
+ const indexPath = getGlobalIndexPath(cacheDir);
70
+ try {
71
+ const txt = await fs.readFile(indexPath, "utf-8");
72
+ return JSON.parse(txt);
73
+ } catch {
74
+ return { daemons: [], updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
75
+ }
76
+ }
77
+ async function writeGlobalIndex(index, cacheDir) {
78
+ const indexPath = getGlobalIndexPath(cacheDir);
79
+ const base = path.dirname(indexPath);
80
+ await fs.mkdir(base, { recursive: true });
81
+ await fs.writeFile(indexPath, JSON.stringify(index, null, 2), {
82
+ encoding: "utf-8",
83
+ mode: 384
84
+ });
85
+ }
86
+ async function registerDaemonGlobally(entry, cacheDir) {
87
+ const index = await readGlobalIndex(cacheDir);
88
+ index.daemons = index.daemons.filter((d) => d.projectId !== entry.projectId);
89
+ index.daemons.push(entry);
90
+ index.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
91
+ await writeGlobalIndex(index, cacheDir);
92
+ debugLog(`Registered daemon ${entry.projectId} in global index`);
93
+ }
94
+ async function unregisterDaemonGlobally(projectId, cacheDir) {
95
+ const index = await readGlobalIndex(cacheDir);
96
+ index.daemons = index.daemons.filter((d) => d.projectId !== projectId);
97
+ index.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
98
+ await writeGlobalIndex(index, cacheDir);
99
+ }
100
+
101
+ // src/shared/protocol.ts
102
+ var HELLO_TEST = "HELLO_TEST";
103
+ var HELLO_ACK = "HELLO_ACK";
104
+ var BATCH_MOCK_REQUEST = "BATCH_MOCK_REQUEST";
105
+ var BATCH_MOCK_RESULT = "BATCH_MOCK_RESULT";
106
+ var HEARTBEAT = "HEARTBEAT";
107
+ var HEARTBEAT_ACK = "HEARTBEAT_ACK";
108
+ var RPC_GET_STATUS = "getStatus";
109
+ var RPC_LIST_RUNS = "listRuns";
110
+ var RPC_CLAIM_NEXT_BATCH = "claimNextBatch";
111
+ var RPC_PROVIDE_BATCH = "provideBatch";
112
+ var RPC_RELEASE_BATCH = "releaseBatch";
113
+ var RPC_GET_BATCH = "getBatch";
114
+ var RPC_ERROR_METHOD_NOT_FOUND = -32601;
115
+ var RPC_ERROR_INTERNAL = -32603;
116
+ var RPC_ERROR_NOT_FOUND = -32e3;
117
+ var RPC_ERROR_UNAUTHORIZED = -32001;
118
+ var RPC_ERROR_CONFLICT = -32002;
119
+ var RPC_ERROR_EXPIRED = -32003;
120
+ function isHelloTestMessage(msg) {
121
+ if (!msg || typeof msg !== "object") return false;
122
+ const m = msg;
123
+ return m.type === HELLO_TEST && typeof m.token === "string" && typeof m.runId === "string" && typeof m.pid === "number" && typeof m.cwd === "string";
124
+ }
125
+ function isBatchMockRequestMessage(msg) {
126
+ if (!msg || typeof msg !== "object") return false;
127
+ const m = msg;
128
+ return m.type === BATCH_MOCK_REQUEST && typeof m.runId === "string" && Array.isArray(m.requests);
129
+ }
130
+ function isHeartbeatMessage(msg) {
131
+ if (!msg || typeof msg !== "object") return false;
132
+ const m = msg;
133
+ return m.type === HEARTBEAT && typeof m.runId === "string";
134
+ }
135
+ function isJsonRpcRequest(msg) {
136
+ if (!msg || typeof msg !== "object") return false;
137
+ const m = msg;
138
+ return m.jsonrpc === "2.0" && (typeof m.id === "string" || typeof m.id === "number") && typeof m.method === "string";
139
+ }
140
+
141
+ // src/daemon/daemon.ts
142
+ var MockMcpDaemon = class {
143
+ logger;
144
+ opts;
145
+ server;
146
+ wss;
147
+ sweepTimer;
148
+ idleTimer;
149
+ startedAt;
150
+ // State management
151
+ runs = /* @__PURE__ */ new Map();
152
+ batches = /* @__PURE__ */ new Map();
153
+ pendingQueue = [];
154
+ // batchIds in order
155
+ batchSeq = 0;
156
+ constructor(options) {
157
+ this.logger = options.logger ?? console;
158
+ this.opts = {
159
+ projectRoot: options.projectRoot,
160
+ token: options.token,
161
+ version: options.version,
162
+ cacheDir: options.cacheDir,
163
+ logger: this.logger,
164
+ defaultLeaseMs: options.defaultLeaseMs ?? 3e4,
165
+ sweepIntervalMs: options.sweepIntervalMs ?? 5e3,
166
+ idleShutdownMs: options.idleShutdownMs ?? 6e5
167
+ };
168
+ }
169
+ // ===========================================================================
170
+ // Lifecycle
171
+ // ===========================================================================
172
+ async start() {
173
+ const projectId = computeProjectId(this.opts.projectRoot);
174
+ const { base, registryPath, ipcPath } = getPaths(projectId, this.opts.cacheDir);
175
+ await fs.mkdir(base, { recursive: true });
176
+ if (process.platform !== "win32") {
177
+ try {
178
+ await fs.rm(ipcPath);
179
+ } catch {
180
+ }
181
+ }
182
+ const server = http2.createServer((req, res) => this.handleHttp(req, res));
183
+ this.server = server;
184
+ const wss = new WebSocketServer({ noServer: true });
185
+ this.wss = wss;
186
+ server.on("upgrade", (req, socket, head) => {
187
+ if (!req.url?.startsWith("/test")) {
188
+ socket.destroy();
189
+ return;
190
+ }
191
+ wss.handleUpgrade(req, socket, head, (ws) => {
192
+ wss.emit("connection", ws, req);
193
+ });
194
+ });
195
+ wss.on("connection", (ws, req) => this.handleWsConnection(ws, req));
196
+ await new Promise((resolve, reject) => {
197
+ server.once("error", reject);
198
+ server.listen(ipcPath, () => {
199
+ this.logger.error(`\u{1F680} Daemon listening on ${ipcPath}`);
200
+ resolve();
201
+ });
202
+ });
203
+ this.startedAt = Date.now();
204
+ const registry = {
205
+ projectId,
206
+ projectRoot: this.opts.projectRoot,
207
+ ipcPath,
208
+ token: this.opts.token,
209
+ pid: process.pid,
210
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
211
+ version: this.opts.version
212
+ };
213
+ await writeRegistry(registryPath, registry);
214
+ const globalEntry = {
215
+ projectId,
216
+ projectRoot: this.opts.projectRoot,
217
+ ipcPath,
218
+ registryPath,
219
+ pid: process.pid,
220
+ startedAt: registry.startedAt,
221
+ version: this.opts.version
222
+ };
223
+ await registerDaemonGlobally(globalEntry, this.opts.cacheDir);
224
+ this.sweepTimer = setInterval(() => this.sweepExpiredClaims(), this.opts.sweepIntervalMs);
225
+ this.sweepTimer.unref?.();
226
+ this.resetIdleTimer();
227
+ this.logger.error(`\u2705 Daemon ready (project: ${projectId}, pid: ${process.pid})`);
228
+ }
229
+ async stop() {
230
+ if (this.sweepTimer) {
231
+ clearInterval(this.sweepTimer);
232
+ this.sweepTimer = void 0;
233
+ }
234
+ if (this.idleTimer) {
235
+ clearTimeout(this.idleTimer);
236
+ this.idleTimer = void 0;
237
+ }
238
+ for (const run of this.runs.values()) {
239
+ run.ws.close(1001, "Daemon shutting down");
240
+ }
241
+ this.runs.clear();
242
+ this.wss?.close();
243
+ await new Promise((resolve) => {
244
+ if (!this.server) {
245
+ resolve();
246
+ return;
247
+ }
248
+ this.server.close(() => resolve());
249
+ });
250
+ this.batches.clear();
251
+ this.pendingQueue.length = 0;
252
+ const projectId = computeProjectId(this.opts.projectRoot);
253
+ await unregisterDaemonGlobally(projectId, this.opts.cacheDir);
254
+ this.logger.error("\u{1F44B} Daemon stopped");
255
+ }
256
+ // ===========================================================================
257
+ // HTTP Handler (/health, /control)
258
+ // ===========================================================================
259
+ async handleHttp(req, res) {
260
+ try {
261
+ if (req.method === "GET" && req.url === "/health") {
262
+ res.writeHead(200, { "content-type": "application/json" });
263
+ res.end(
264
+ JSON.stringify({
265
+ ok: true,
266
+ pid: process.pid,
267
+ version: this.opts.version,
268
+ projectId: computeProjectId(this.opts.projectRoot)
269
+ })
270
+ );
271
+ return;
272
+ }
273
+ if (req.method === "POST" && req.url === "/control") {
274
+ const token = req.headers["x-mock-mcp-token"];
275
+ if (token !== this.opts.token) {
276
+ res.writeHead(401, { "content-type": "application/json" });
277
+ res.end(JSON.stringify({ error: "Unauthorized" }));
278
+ return;
279
+ }
280
+ const body = await this.readBody(req);
281
+ let rpcReq;
282
+ try {
283
+ rpcReq = JSON.parse(body);
284
+ } catch {
285
+ res.writeHead(400, { "content-type": "application/json" });
286
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
287
+ return;
288
+ }
289
+ if (!isJsonRpcRequest(rpcReq)) {
290
+ res.writeHead(400, { "content-type": "application/json" });
291
+ res.end(JSON.stringify({ error: "Invalid JSON-RPC request" }));
292
+ return;
293
+ }
294
+ const rpcRes = await this.handleRpc(rpcReq);
295
+ res.writeHead(200, { "content-type": "application/json" });
296
+ res.end(JSON.stringify(rpcRes));
297
+ return;
298
+ }
299
+ res.writeHead(404, { "content-type": "text/plain" });
300
+ res.end("Not Found");
301
+ } catch (e) {
302
+ this.logger.error("HTTP handler error:", e);
303
+ res.writeHead(500, { "content-type": "text/plain" });
304
+ res.end(e instanceof Error ? e.message : String(e));
305
+ }
306
+ }
307
+ readBody(req) {
308
+ return new Promise((resolve, reject) => {
309
+ let buf = "";
310
+ req.on("data", (chunk) => buf += chunk);
311
+ req.on("end", () => resolve(buf));
312
+ req.on("error", reject);
313
+ });
314
+ }
315
+ // ===========================================================================
316
+ // WebSocket Handler (/test)
317
+ // ===========================================================================
318
+ handleWsConnection(ws, _req) {
319
+ let authed = false;
320
+ let runId = null;
321
+ const helloTimeout = setTimeout(() => {
322
+ if (!authed) {
323
+ ws.close(1008, "HELLO timeout");
324
+ }
325
+ }, 5e3);
326
+ ws.on("message", (data) => {
327
+ let msg;
328
+ try {
329
+ msg = JSON.parse(data.toString());
330
+ } catch {
331
+ this.logger.warn("Invalid JSON from test process");
332
+ return;
333
+ }
334
+ if (!authed) {
335
+ if (!isHelloTestMessage(msg)) {
336
+ ws.close(1008, "Expected HELLO_TEST");
337
+ return;
338
+ }
339
+ if (msg.token !== this.opts.token) {
340
+ ws.close(1008, "Invalid token");
341
+ return;
342
+ }
343
+ authed = true;
344
+ clearTimeout(helloTimeout);
345
+ runId = msg.runId;
346
+ const runState = {
347
+ runId,
348
+ pid: msg.pid,
349
+ cwd: msg.cwd,
350
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
351
+ lastSeen: Date.now(),
352
+ testMeta: msg.testMeta,
353
+ ws
354
+ };
355
+ this.runs.set(runId, runState);
356
+ const ack = { type: HELLO_ACK, runId };
357
+ ws.send(JSON.stringify(ack));
358
+ this.logger.error(`\u{1F50C} Test run connected: ${runId} (pid: ${msg.pid})`);
359
+ this.resetIdleTimer();
360
+ return;
361
+ }
362
+ if (isBatchMockRequestMessage(msg)) {
363
+ this.handleBatchRequest(runId, msg.requests, ws);
364
+ return;
365
+ }
366
+ if (isHeartbeatMessage(msg)) {
367
+ const run = this.runs.get(msg.runId);
368
+ if (run) {
369
+ run.lastSeen = Date.now();
370
+ }
371
+ const ack = { type: HEARTBEAT_ACK, runId: msg.runId };
372
+ ws.send(JSON.stringify(ack));
373
+ return;
374
+ }
375
+ });
376
+ ws.on("close", () => {
377
+ clearTimeout(helloTimeout);
378
+ if (runId) {
379
+ this.cleanupRun(runId);
380
+ }
381
+ });
382
+ ws.on("error", (err) => {
383
+ this.logger.error("WebSocket error:", err);
384
+ if (runId) {
385
+ this.cleanupRun(runId);
386
+ }
387
+ });
388
+ }
389
+ handleBatchRequest(runId, requests, ws) {
390
+ const batchId = `batch:${runId}:${++this.batchSeq}`;
391
+ const batch = {
392
+ batchId,
393
+ runId,
394
+ requests,
395
+ createdAt: Date.now(),
396
+ status: "pending"
397
+ };
398
+ this.batches.set(batchId, batch);
399
+ this.pendingQueue.push(batchId);
400
+ this.logger.error(
401
+ [
402
+ `\u{1F4E5} Received ${requests.length} request(s) (${batchId})`,
403
+ ...requests.map(
404
+ (req, i) => ` ${i + 1}. ${req.method} ${req.endpoint} (${req.requestId})`
405
+ )
406
+ ].join("\n")
407
+ );
408
+ this.logger.error("\u23F3 Awaiting mock data from MCP adapter...");
409
+ }
410
+ cleanupRun(runId) {
411
+ const run = this.runs.get(runId);
412
+ if (!run) return;
413
+ this.runs.delete(runId);
414
+ for (const [batchId, batch] of this.batches) {
415
+ if (batch.runId === runId) {
416
+ this.batches.delete(batchId);
417
+ }
418
+ }
419
+ for (let i = this.pendingQueue.length - 1; i >= 0; i--) {
420
+ const bid = this.pendingQueue[i];
421
+ if (!this.batches.has(bid)) {
422
+ this.pendingQueue.splice(i, 1);
423
+ }
424
+ }
425
+ this.logger.error(`\u{1F50C} Test run disconnected: ${runId}`);
426
+ this.resetIdleTimer();
427
+ }
428
+ // ===========================================================================
429
+ // JSON-RPC Handler
430
+ // ===========================================================================
431
+ async handleRpc(req) {
432
+ try {
433
+ this.sweepExpiredClaims();
434
+ const params = req.params ?? {};
435
+ switch (req.method) {
436
+ case RPC_GET_STATUS:
437
+ return this.rpcSuccess(req.id, this.getStatus());
438
+ case RPC_LIST_RUNS:
439
+ return this.rpcSuccess(req.id, this.listRuns());
440
+ case RPC_CLAIM_NEXT_BATCH:
441
+ return this.rpcSuccess(req.id, this.claimNextBatch(params));
442
+ case RPC_PROVIDE_BATCH:
443
+ return this.rpcSuccess(req.id, await this.provideBatch(params));
444
+ case RPC_RELEASE_BATCH:
445
+ return this.rpcSuccess(req.id, this.releaseBatch(params));
446
+ case RPC_GET_BATCH:
447
+ return this.rpcSuccess(req.id, this.getBatch(params));
448
+ default:
449
+ return this.rpcError(req.id, RPC_ERROR_METHOD_NOT_FOUND, `Unknown method: ${req.method}`);
450
+ }
451
+ } catch (e) {
452
+ const msg = e instanceof Error ? e.message : String(e);
453
+ const code = this.getErrorCode(e);
454
+ return this.rpcError(req.id, code, msg);
455
+ }
456
+ }
457
+ getErrorCode(e) {
458
+ if (e instanceof RpcError) {
459
+ return e.code;
460
+ }
461
+ return RPC_ERROR_INTERNAL;
462
+ }
463
+ rpcSuccess(id, result) {
464
+ return { jsonrpc: "2.0", id, result };
465
+ }
466
+ rpcError(id, code, message) {
467
+ return { jsonrpc: "2.0", id, error: { code, message } };
468
+ }
469
+ // ===========================================================================
470
+ // RPC Methods
471
+ // ===========================================================================
472
+ getStatus() {
473
+ const pending = this.pendingQueue.filter((bid) => {
474
+ const b = this.batches.get(bid);
475
+ return b && b.status === "pending";
476
+ }).length;
477
+ const claimed = Array.from(this.batches.values()).filter(
478
+ (b) => b.status === "claimed"
479
+ ).length;
480
+ return {
481
+ version: this.opts.version,
482
+ projectId: computeProjectId(this.opts.projectRoot),
483
+ projectRoot: this.opts.projectRoot,
484
+ pid: process.pid,
485
+ uptime: this.startedAt ? Date.now() - this.startedAt : 0,
486
+ runs: this.runs.size,
487
+ pending,
488
+ claimed,
489
+ totalBatches: this.batches.size
490
+ };
491
+ }
492
+ listRuns() {
493
+ const runs = Array.from(this.runs.values()).map((r) => {
494
+ const pendingBatches = Array.from(this.batches.values()).filter(
495
+ (b) => b.runId === r.runId && b.status === "pending"
496
+ ).length;
497
+ return {
498
+ runId: r.runId,
499
+ pid: r.pid,
500
+ cwd: r.cwd,
501
+ startedAt: r.startedAt,
502
+ lastSeen: r.lastSeen,
503
+ pendingBatches,
504
+ testMeta: r.testMeta
505
+ };
506
+ });
507
+ return { runs };
508
+ }
509
+ claimNextBatch(params) {
510
+ const { adapterId, runId, leaseMs = this.opts.defaultLeaseMs } = params;
511
+ if (!adapterId) {
512
+ throw new RpcError(RPC_ERROR_UNAUTHORIZED, "adapterId required");
513
+ }
514
+ for (let i = 0; i < this.pendingQueue.length; i++) {
515
+ const batchId = this.pendingQueue[i];
516
+ const batch = this.batches.get(batchId);
517
+ if (!batch || batch.status !== "pending") {
518
+ continue;
519
+ }
520
+ if (runId && batch.runId !== runId) {
521
+ continue;
522
+ }
523
+ this.pendingQueue.splice(i, 1);
524
+ batch.status = "claimed";
525
+ batch.claim = {
526
+ adapterId,
527
+ claimToken: crypto.randomUUID(),
528
+ leaseUntil: Date.now() + leaseMs
529
+ };
530
+ this.logger.error(`\u{1F512} Batch ${batchId} claimed by adapter ${adapterId.slice(0, 8)}...`);
531
+ return {
532
+ batchId: batch.batchId,
533
+ runId: batch.runId,
534
+ requests: batch.requests,
535
+ claimToken: batch.claim.claimToken,
536
+ leaseUntil: batch.claim.leaseUntil
537
+ };
538
+ }
539
+ return null;
540
+ }
541
+ async provideBatch(params) {
542
+ const { adapterId, batchId, claimToken, mocks } = params;
543
+ const batch = this.batches.get(batchId);
544
+ if (!batch) {
545
+ throw new RpcError(RPC_ERROR_NOT_FOUND, `Batch not found: ${batchId}`);
546
+ }
547
+ if (batch.status !== "claimed" || !batch.claim) {
548
+ throw new RpcError(RPC_ERROR_CONFLICT, `Batch not in claimed state: ${batchId}`);
549
+ }
550
+ if (batch.claim.adapterId !== adapterId) {
551
+ throw new RpcError(RPC_ERROR_UNAUTHORIZED, "Not the owner of this batch");
552
+ }
553
+ if (batch.claim.claimToken !== claimToken) {
554
+ throw new RpcError(RPC_ERROR_UNAUTHORIZED, "Invalid claim token");
555
+ }
556
+ if (batch.claim.leaseUntil <= Date.now()) {
557
+ batch.status = "pending";
558
+ batch.claim = void 0;
559
+ this.pendingQueue.push(batchId);
560
+ throw new RpcError(RPC_ERROR_EXPIRED, "Claim lease expired");
561
+ }
562
+ this.validateMocks(batch, mocks);
563
+ const run = this.runs.get(batch.runId);
564
+ if (!run) {
565
+ this.batches.delete(batchId);
566
+ throw new RpcError(RPC_ERROR_NOT_FOUND, "Run is gone");
567
+ }
568
+ if (run.ws.readyState !== WebSocket.OPEN) {
569
+ this.batches.delete(batchId);
570
+ throw new RpcError(RPC_ERROR_NOT_FOUND, "Test process disconnected");
571
+ }
572
+ const result = {
573
+ type: BATCH_MOCK_RESULT,
574
+ batchId,
575
+ mocks
576
+ };
577
+ run.ws.send(JSON.stringify(result));
578
+ batch.status = "fulfilled";
579
+ this.batches.delete(batchId);
580
+ this.logger.error(`\u2705 Delivered ${mocks.length} mock(s) for ${batchId}`);
581
+ return { ok: true, message: `Provided ${mocks.length} mock(s) for ${batchId}` };
582
+ }
583
+ validateMocks(batch, mocks) {
584
+ const expectedIds = new Set(batch.requests.map((r) => r.requestId));
585
+ const providedIds = /* @__PURE__ */ new Set();
586
+ for (const mock of mocks) {
587
+ if (!expectedIds.has(mock.requestId)) {
588
+ throw new RpcError(
589
+ RPC_ERROR_CONFLICT,
590
+ `Mock references unknown requestId: ${mock.requestId}`
591
+ );
592
+ }
593
+ if (providedIds.has(mock.requestId)) {
594
+ throw new RpcError(
595
+ RPC_ERROR_CONFLICT,
596
+ `Duplicate mock for requestId: ${mock.requestId}`
597
+ );
598
+ }
599
+ providedIds.add(mock.requestId);
600
+ }
601
+ const missing = Array.from(expectedIds).filter((id) => !providedIds.has(id));
602
+ if (missing.length > 0) {
603
+ throw new RpcError(
604
+ RPC_ERROR_CONFLICT,
605
+ `Missing mocks for requestId(s): ${missing.join(", ")}`
606
+ );
607
+ }
608
+ }
609
+ releaseBatch(params) {
610
+ const { adapterId, batchId, claimToken } = params;
611
+ const batch = this.batches.get(batchId);
612
+ if (!batch) {
613
+ throw new RpcError(RPC_ERROR_NOT_FOUND, `Batch not found: ${batchId}`);
614
+ }
615
+ if (batch.status !== "claimed" || !batch.claim) {
616
+ throw new RpcError(RPC_ERROR_CONFLICT, `Batch not in claimed state: ${batchId}`);
617
+ }
618
+ if (batch.claim.adapterId !== adapterId || batch.claim.claimToken !== claimToken) {
619
+ throw new RpcError(RPC_ERROR_UNAUTHORIZED, "Not the owner of this batch");
620
+ }
621
+ batch.status = "pending";
622
+ batch.claim = void 0;
623
+ this.pendingQueue.push(batchId);
624
+ this.logger.error(`\u{1F513} Batch ${batchId} released by adapter`);
625
+ return { ok: true };
626
+ }
627
+ getBatch(params) {
628
+ const batch = this.batches.get(params.batchId);
629
+ if (!batch) {
630
+ throw new RpcError(RPC_ERROR_NOT_FOUND, `Batch not found: ${params.batchId}`);
631
+ }
632
+ return {
633
+ batchId: batch.batchId,
634
+ runId: batch.runId,
635
+ requests: batch.requests,
636
+ status: batch.status,
637
+ createdAt: batch.createdAt,
638
+ claim: batch.claim ? { adapterId: batch.claim.adapterId, leaseUntil: batch.claim.leaseUntil } : void 0
639
+ };
640
+ }
641
+ // ===========================================================================
642
+ // Maintenance
643
+ // ===========================================================================
644
+ sweepExpiredClaims() {
645
+ const now = Date.now();
646
+ for (const batch of this.batches.values()) {
647
+ if (batch.status === "claimed" && batch.claim && batch.claim.leaseUntil <= now) {
648
+ this.logger.warn(`\u23F0 Claim expired for ${batch.batchId}, returning to pending`);
649
+ batch.status = "pending";
650
+ batch.claim = void 0;
651
+ this.pendingQueue.push(batch.batchId);
652
+ }
653
+ }
654
+ }
655
+ resetIdleTimer() {
656
+ if (this.idleTimer) {
657
+ clearTimeout(this.idleTimer);
658
+ }
659
+ if (this.runs.size === 0) {
660
+ this.idleTimer = setTimeout(() => {
661
+ if (this.runs.size === 0) {
662
+ this.logger.error("\u{1F4A4} No activity, shutting down daemon...");
663
+ this.stop().then(() => process.exit(0));
664
+ }
665
+ }, this.opts.idleShutdownMs);
666
+ this.idleTimer.unref?.();
667
+ }
668
+ }
669
+ };
670
+ var RpcError = class extends Error {
671
+ constructor(code, message) {
672
+ super(message);
673
+ this.code = code;
674
+ this.name = "RpcError";
675
+ }
676
+ };
677
+
678
+ export { MockMcpDaemon };