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