mock-mcp 0.3.0 → 0.5.0

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