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.
- package/README.md +212 -124
- package/dist/adapter/index.cjs +875 -0
- package/dist/adapter/index.d.cts +142 -0
- package/dist/adapter/index.d.ts +142 -0
- package/dist/adapter/index.js +835 -0
- package/dist/client/connect.cjs +991 -0
- package/dist/client/connect.d.cts +218 -0
- package/dist/client/connect.d.ts +211 -7
- package/dist/client/connect.js +941 -20
- package/dist/client/index.cjs +992 -0
- package/dist/client/index.d.cts +3 -0
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.js +951 -2
- package/dist/daemon/index.cjs +717 -0
- package/dist/daemon/index.d.cts +62 -0
- package/dist/daemon/index.d.ts +62 -0
- package/dist/daemon/index.js +678 -0
- package/dist/index.cjs +2708 -0
- package/dist/index.d.cts +602 -0
- package/dist/index.d.ts +602 -11
- package/dist/index.js +2651 -53
- package/dist/shared/index.cjs +506 -0
- package/dist/shared/index.d.cts +241 -0
- package/dist/shared/index.d.ts +241 -0
- package/dist/shared/index.js +423 -0
- package/dist/types-bEGXLBF0.d.cts +190 -0
- package/dist/types-bEGXLBF0.d.ts +190 -0
- package/package.json +45 -4
- package/dist/client/batch-mock-collector.d.ts +0 -111
- package/dist/client/batch-mock-collector.js +0 -308
- package/dist/client/util.d.ts +0 -1
- package/dist/client/util.js +0 -3
- package/dist/connect.cjs +0 -400
- package/dist/connect.d.cts +0 -82
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.js +0 -1
- package/dist/server/test-mock-mcp-server.d.ts +0 -73
- package/dist/server/test-mock-mcp-server.js +0 -419
- package/dist/types.d.ts +0 -45
- package/dist/types.js +0 -2
package/dist/index.js
CHANGED
|
@@ -1,58 +1,2656 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import { realpathSync } from
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import fssync, { realpathSync } from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import http from 'http';
|
|
9
|
+
import { pathToFileURL, fileURLToPath } from 'url';
|
|
10
|
+
import { createRequire } from 'module';
|
|
11
|
+
import WebSocket2, { WebSocketServer, WebSocket } from 'ws';
|
|
12
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
13
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
15
|
+
import process2 from 'process';
|
|
16
|
+
|
|
17
|
+
var __defProp = Object.defineProperty;
|
|
18
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
19
|
+
var __esm = (fn, res) => function __init() {
|
|
20
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
21
|
+
};
|
|
22
|
+
var __export = (target, all) => {
|
|
23
|
+
for (var name in all)
|
|
24
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/shared/discovery.ts
|
|
28
|
+
var discovery_exports = {};
|
|
29
|
+
__export(discovery_exports, {
|
|
30
|
+
cleanupGlobalIndex: () => cleanupGlobalIndex,
|
|
31
|
+
computeProjectId: () => computeProjectId,
|
|
32
|
+
discoverAllDaemons: () => discoverAllDaemons,
|
|
33
|
+
ensureDaemonRunning: () => ensureDaemonRunning,
|
|
34
|
+
getCacheDir: () => getCacheDir,
|
|
35
|
+
getDaemonEntryPath: () => getDaemonEntryPath,
|
|
36
|
+
getGlobalIndexPath: () => getGlobalIndexPath,
|
|
37
|
+
getPaths: () => getPaths,
|
|
38
|
+
healthCheck: () => healthCheck,
|
|
39
|
+
randomToken: () => randomToken,
|
|
40
|
+
readGlobalIndex: () => readGlobalIndex,
|
|
41
|
+
readRegistry: () => readRegistry,
|
|
42
|
+
registerDaemonGlobally: () => registerDaemonGlobally,
|
|
43
|
+
releaseLock: () => releaseLock,
|
|
44
|
+
resolveProjectRoot: () => resolveProjectRoot,
|
|
45
|
+
sleep: () => sleep,
|
|
46
|
+
tryAcquireLock: () => tryAcquireLock,
|
|
47
|
+
unregisterDaemonGlobally: () => unregisterDaemonGlobally,
|
|
48
|
+
writeGlobalIndex: () => writeGlobalIndex,
|
|
49
|
+
writeRegistry: () => writeRegistry
|
|
50
|
+
});
|
|
51
|
+
function debugLog(_msg) {
|
|
52
|
+
}
|
|
53
|
+
function hasValidProjectMarker(dir) {
|
|
54
|
+
try {
|
|
55
|
+
const gitPath = path.join(dir, ".git");
|
|
56
|
+
try {
|
|
57
|
+
const stat = fssync.statSync(gitPath);
|
|
58
|
+
if (stat.isDirectory() || stat.isFile()) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
const pkgPath = path.join(dir, "package.json");
|
|
64
|
+
try {
|
|
65
|
+
fssync.accessSync(pkgPath, fssync.constants.F_OK);
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function resolveProjectRoot(startDir = process.cwd()) {
|
|
75
|
+
let current = path.resolve(startDir);
|
|
76
|
+
const root = path.parse(current).root;
|
|
77
|
+
while (current !== root) {
|
|
78
|
+
const gitPath = path.join(current, ".git");
|
|
79
|
+
try {
|
|
80
|
+
const stat = fssync.statSync(gitPath);
|
|
81
|
+
if (stat.isDirectory() || stat.isFile()) {
|
|
82
|
+
return current;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
const pkgPath = path.join(current, "package.json");
|
|
87
|
+
try {
|
|
88
|
+
fssync.accessSync(pkgPath, fssync.constants.F_OK);
|
|
89
|
+
return current;
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
current = path.dirname(current);
|
|
93
|
+
}
|
|
94
|
+
return path.resolve(startDir);
|
|
95
|
+
}
|
|
96
|
+
function computeProjectId(projectRoot) {
|
|
97
|
+
const real = fssync.realpathSync(projectRoot);
|
|
98
|
+
return crypto.createHash("sha256").update(real).digest("hex").slice(0, 16);
|
|
99
|
+
}
|
|
100
|
+
function getCacheDir(override) {
|
|
101
|
+
if (override) {
|
|
102
|
+
return override;
|
|
103
|
+
}
|
|
104
|
+
const envCacheDir = process.env.MOCK_MCP_CACHE_DIR;
|
|
105
|
+
if (envCacheDir) {
|
|
106
|
+
return envCacheDir;
|
|
107
|
+
}
|
|
108
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
109
|
+
if (xdg) {
|
|
110
|
+
return xdg;
|
|
111
|
+
}
|
|
112
|
+
if (process.platform === "win32" && process.env.LOCALAPPDATA) {
|
|
113
|
+
return process.env.LOCALAPPDATA;
|
|
114
|
+
}
|
|
115
|
+
const home = os.homedir();
|
|
116
|
+
if (home) {
|
|
117
|
+
return path.join(home, ".cache");
|
|
118
|
+
}
|
|
119
|
+
return os.tmpdir();
|
|
120
|
+
}
|
|
121
|
+
function getPaths(projectId, cacheDir) {
|
|
122
|
+
const base = path.join(getCacheDir(cacheDir), "mock-mcp");
|
|
123
|
+
const registryPath = path.join(base, `${projectId}.json`);
|
|
124
|
+
const lockPath = path.join(base, `${projectId}.lock`);
|
|
125
|
+
const ipcPath = process.platform === "win32" ? `\\\\.\\pipe\\mock-mcp-${projectId}` : path.join(base, `${projectId}.sock`);
|
|
126
|
+
return { base, registryPath, lockPath, ipcPath };
|
|
127
|
+
}
|
|
128
|
+
async function readRegistry(registryPath) {
|
|
129
|
+
try {
|
|
130
|
+
const txt = await fs.readFile(registryPath, "utf-8");
|
|
131
|
+
return JSON.parse(txt);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
debugLog(`readRegistry error for ${registryPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function writeRegistry(registryPath, registry) {
|
|
138
|
+
await fs.writeFile(registryPath, JSON.stringify(registry, null, 2), {
|
|
139
|
+
encoding: "utf-8",
|
|
140
|
+
mode: 384
|
|
141
|
+
// Read/write for owner only
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async function healthCheck(ipcPath, timeoutMs = 2e3) {
|
|
145
|
+
return new Promise((resolve) => {
|
|
146
|
+
const req = http.request(
|
|
147
|
+
{
|
|
148
|
+
method: "GET",
|
|
149
|
+
socketPath: ipcPath,
|
|
150
|
+
path: "/health",
|
|
151
|
+
timeout: timeoutMs
|
|
152
|
+
},
|
|
153
|
+
(res) => {
|
|
154
|
+
resolve(res.statusCode === 200);
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
req.on("error", () => resolve(false));
|
|
158
|
+
req.on("timeout", () => {
|
|
159
|
+
req.destroy();
|
|
160
|
+
resolve(false);
|
|
161
|
+
});
|
|
162
|
+
req.end();
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async function tryAcquireLock(lockPath) {
|
|
166
|
+
try {
|
|
167
|
+
const fh = await fs.open(lockPath, "wx");
|
|
168
|
+
await fh.write(`${process.pid}
|
|
169
|
+
`);
|
|
170
|
+
return fh;
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function releaseLock(lockPath, fh) {
|
|
176
|
+
await fh.close();
|
|
177
|
+
await fs.rm(lockPath).catch(() => {
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function randomToken() {
|
|
181
|
+
return crypto.randomBytes(24).toString("base64url");
|
|
182
|
+
}
|
|
183
|
+
function getDaemonEntryPath() {
|
|
184
|
+
try {
|
|
185
|
+
const cwdRequire = createRequire(pathToFileURL(path.join(process.cwd(), "index.js")).href);
|
|
186
|
+
const resolved = cwdRequire.resolve("mock-mcp");
|
|
187
|
+
const distDir = path.dirname(resolved);
|
|
188
|
+
const daemonEntry = path.join(distDir, "index.js");
|
|
189
|
+
if (fssync.existsSync(daemonEntry)) {
|
|
190
|
+
return daemonEntry;
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const packageRoot = resolveProjectRoot(__curDirname);
|
|
196
|
+
const distPath = path.join(packageRoot, "dist", "index.js");
|
|
197
|
+
if (fssync.existsSync(distPath)) {
|
|
198
|
+
return distPath;
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
if (process.argv[1]) {
|
|
203
|
+
return process.argv[1];
|
|
204
|
+
}
|
|
205
|
+
return path.join(process.cwd(), "dist", "index.js");
|
|
206
|
+
}
|
|
207
|
+
async function ensureDaemonRunning(opts = {}) {
|
|
208
|
+
let projectRoot = opts.projectRoot ?? resolveProjectRoot();
|
|
209
|
+
if (!hasValidProjectMarker(projectRoot)) {
|
|
210
|
+
const resolved = resolveProjectRoot(projectRoot);
|
|
211
|
+
if (resolved !== projectRoot && hasValidProjectMarker(resolved)) {
|
|
212
|
+
console.error(`[mock-mcp] Warning: projectRoot "${projectRoot}" doesn't look like a project root`);
|
|
213
|
+
console.error(`[mock-mcp] Found .git/package.json at: "${resolved}"`);
|
|
214
|
+
projectRoot = resolved;
|
|
215
|
+
} else {
|
|
216
|
+
console.error(`[mock-mcp] \u26A0\uFE0F WARNING: Could not find a valid project root!`);
|
|
217
|
+
console.error(`[mock-mcp] Current path: "${projectRoot}"`);
|
|
218
|
+
console.error(`[mock-mcp] This path doesn't contain .git or package.json.`);
|
|
219
|
+
console.error(`[mock-mcp] This may cause project mismatch issues.`);
|
|
220
|
+
console.error(`[mock-mcp] `);
|
|
221
|
+
console.error(`[mock-mcp] For MCP adapters, please specify --project-root explicitly:`);
|
|
222
|
+
console.error(`[mock-mcp] mock-mcp adapter --project-root /path/to/your/project`);
|
|
223
|
+
console.error(`[mock-mcp] `);
|
|
224
|
+
console.error(`[mock-mcp] In your MCP client config (Cursor, Claude Desktop, etc.):`);
|
|
225
|
+
console.error(`[mock-mcp] {`);
|
|
226
|
+
console.error(`[mock-mcp] "args": ["-y", "mock-mcp", "adapter", "--project-root", "/path/to/your/project"]`);
|
|
227
|
+
console.error(`[mock-mcp] }`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const projectId = computeProjectId(projectRoot);
|
|
231
|
+
const { base, registryPath, lockPath, ipcPath } = getPaths(
|
|
232
|
+
projectId,
|
|
233
|
+
opts.cacheDir
|
|
234
|
+
);
|
|
235
|
+
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
236
|
+
await fs.mkdir(base, { recursive: true });
|
|
237
|
+
const existing = await readRegistry(registryPath);
|
|
238
|
+
debugLog(`Registry read result: ${existing ? "Found (PID " + existing.pid + ")" : "Null"}`);
|
|
239
|
+
if (existing) {
|
|
240
|
+
let healthy = false;
|
|
241
|
+
for (let i = 0; i < 3; i++) {
|
|
242
|
+
debugLog(`Checking health attempt ${i + 1}/3 on ${existing.ipcPath}`);
|
|
243
|
+
healthy = await healthCheck(existing.ipcPath);
|
|
244
|
+
if (healthy) break;
|
|
245
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
246
|
+
}
|
|
247
|
+
if (healthy) {
|
|
248
|
+
return existing;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (process.platform !== "win32") {
|
|
252
|
+
try {
|
|
253
|
+
await fs.rm(ipcPath);
|
|
254
|
+
} catch {
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const lock = await tryAcquireLock(lockPath);
|
|
258
|
+
if (lock) {
|
|
259
|
+
try {
|
|
260
|
+
const recheckReg = await readRegistry(registryPath);
|
|
261
|
+
if (recheckReg && await healthCheck(recheckReg.ipcPath)) {
|
|
262
|
+
return recheckReg;
|
|
263
|
+
}
|
|
264
|
+
const token = randomToken();
|
|
265
|
+
const daemonEntry = getDaemonEntryPath();
|
|
266
|
+
const child = spawn(
|
|
267
|
+
process.execPath,
|
|
268
|
+
[daemonEntry, "daemon", "--project-root", projectRoot, "--token", token],
|
|
269
|
+
{
|
|
270
|
+
detached: true,
|
|
271
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
272
|
+
env: {
|
|
273
|
+
...process.env,
|
|
274
|
+
MOCK_MCP_CACHE_DIR: opts.cacheDir ?? ""
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
let daemonStderr = "";
|
|
279
|
+
let daemonStdout = "";
|
|
280
|
+
child.stdout?.on("data", (data) => {
|
|
281
|
+
const str = data.toString();
|
|
282
|
+
debugLog(`Daemon stdout: ${str}`);
|
|
283
|
+
});
|
|
284
|
+
child.stderr?.on("data", (data) => {
|
|
285
|
+
daemonStderr += data.toString();
|
|
286
|
+
debugLog(`Daemon stderr: ${data.toString()}`);
|
|
287
|
+
});
|
|
288
|
+
child.on("error", (err) => {
|
|
289
|
+
console.error(`[mock-mcp] Daemon spawn error: ${err.message}`);
|
|
290
|
+
});
|
|
291
|
+
child.on("exit", (code, signal) => {
|
|
292
|
+
if (code !== null && code !== 0) {
|
|
293
|
+
console.error(`[mock-mcp] Daemon exited with code: ${code}`);
|
|
294
|
+
if (daemonStderr) {
|
|
295
|
+
console.error(`[mock-mcp] Daemon stderr: ${daemonStderr.slice(0, 500)}`);
|
|
296
|
+
}
|
|
297
|
+
} else if (signal) {
|
|
298
|
+
console.error(`[mock-mcp] Daemon killed by signal: ${signal}`);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
child.unref();
|
|
302
|
+
const deadline2 = Date.now() + timeoutMs;
|
|
303
|
+
while (Date.now() < deadline2) {
|
|
304
|
+
const reg = await readRegistry(registryPath);
|
|
305
|
+
if (reg && await healthCheck(reg.ipcPath)) {
|
|
306
|
+
return reg;
|
|
307
|
+
}
|
|
308
|
+
await sleep(50);
|
|
309
|
+
}
|
|
310
|
+
console.error("[mock-mcp] Daemon failed to start within timeout");
|
|
311
|
+
if (daemonStderr) {
|
|
312
|
+
console.error(`[mock-mcp] Daemon stderr:
|
|
313
|
+
${daemonStderr}`);
|
|
314
|
+
}
|
|
315
|
+
throw new Error(
|
|
316
|
+
`Daemon start timeout after ${timeoutMs}ms. Check logs for details.`
|
|
317
|
+
);
|
|
318
|
+
} finally {
|
|
319
|
+
await releaseLock(lockPath, lock);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const deadline = Date.now() + timeoutMs;
|
|
323
|
+
while (Date.now() < deadline) {
|
|
324
|
+
const reg = await readRegistry(registryPath);
|
|
325
|
+
if (reg && await healthCheck(reg.ipcPath)) {
|
|
326
|
+
return reg;
|
|
327
|
+
}
|
|
328
|
+
await sleep(50);
|
|
329
|
+
}
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Waiting for daemon timed out after ${timeoutMs}ms. Another process may have failed to start it.`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
function sleep(ms) {
|
|
335
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
336
|
+
}
|
|
337
|
+
function getGlobalIndexPath(cacheDir) {
|
|
338
|
+
const base = path.join(getCacheDir(cacheDir), "mock-mcp");
|
|
339
|
+
return path.join(base, "active-daemons.json");
|
|
340
|
+
}
|
|
341
|
+
async function readGlobalIndex(cacheDir) {
|
|
342
|
+
const indexPath = getGlobalIndexPath(cacheDir);
|
|
343
|
+
try {
|
|
344
|
+
const txt = await fs.readFile(indexPath, "utf-8");
|
|
345
|
+
return JSON.parse(txt);
|
|
346
|
+
} catch {
|
|
347
|
+
return { daemons: [], updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function writeGlobalIndex(index, cacheDir) {
|
|
351
|
+
const indexPath = getGlobalIndexPath(cacheDir);
|
|
352
|
+
const base = path.dirname(indexPath);
|
|
353
|
+
await fs.mkdir(base, { recursive: true });
|
|
354
|
+
await fs.writeFile(indexPath, JSON.stringify(index, null, 2), {
|
|
355
|
+
encoding: "utf-8",
|
|
356
|
+
mode: 384
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
async function registerDaemonGlobally(entry, cacheDir) {
|
|
360
|
+
const index = await readGlobalIndex(cacheDir);
|
|
361
|
+
index.daemons = index.daemons.filter((d) => d.projectId !== entry.projectId);
|
|
362
|
+
index.daemons.push(entry);
|
|
363
|
+
index.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
364
|
+
await writeGlobalIndex(index, cacheDir);
|
|
365
|
+
debugLog(`Registered daemon ${entry.projectId} in global index`);
|
|
366
|
+
}
|
|
367
|
+
async function unregisterDaemonGlobally(projectId, cacheDir) {
|
|
368
|
+
const index = await readGlobalIndex(cacheDir);
|
|
369
|
+
index.daemons = index.daemons.filter((d) => d.projectId !== projectId);
|
|
370
|
+
index.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
371
|
+
await writeGlobalIndex(index, cacheDir);
|
|
372
|
+
}
|
|
373
|
+
async function cleanupGlobalIndex(cacheDir) {
|
|
374
|
+
const index = await readGlobalIndex(cacheDir);
|
|
375
|
+
const validDaemons = [];
|
|
376
|
+
for (const entry of index.daemons) {
|
|
377
|
+
try {
|
|
378
|
+
process.kill(entry.pid, 0);
|
|
379
|
+
const healthy = await healthCheck(entry.ipcPath, 1e3);
|
|
380
|
+
if (healthy) {
|
|
381
|
+
validDaemons.push(entry);
|
|
382
|
+
} else {
|
|
383
|
+
debugLog(`Removing unhealthy daemon ${entry.projectId} (pid ${entry.pid})`);
|
|
384
|
+
}
|
|
385
|
+
} catch {
|
|
386
|
+
debugLog(`Removing dead daemon ${entry.projectId} (pid ${entry.pid})`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (validDaemons.length !== index.daemons.length) {
|
|
390
|
+
index.daemons = validDaemons;
|
|
391
|
+
index.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
392
|
+
await writeGlobalIndex(index, cacheDir);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function discoverAllDaemons(cacheDir) {
|
|
396
|
+
await cleanupGlobalIndex(cacheDir);
|
|
397
|
+
const index = await readGlobalIndex(cacheDir);
|
|
398
|
+
const results = [];
|
|
399
|
+
for (const entry of index.daemons) {
|
|
400
|
+
const registry = await readRegistry(entry.registryPath);
|
|
401
|
+
if (registry) {
|
|
402
|
+
const healthy = await healthCheck(entry.ipcPath, 2e3);
|
|
403
|
+
results.push({ registry, healthy });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return results;
|
|
407
|
+
}
|
|
408
|
+
var __curDirname;
|
|
409
|
+
var init_discovery = __esm({
|
|
410
|
+
"src/shared/discovery.ts"() {
|
|
411
|
+
__curDirname = (() => {
|
|
412
|
+
try {
|
|
413
|
+
const metaUrl = import.meta.url;
|
|
414
|
+
if (metaUrl && typeof metaUrl === "string" && metaUrl.startsWith("file://")) {
|
|
415
|
+
return path.dirname(fileURLToPath(metaUrl));
|
|
416
|
+
}
|
|
417
|
+
} catch {
|
|
418
|
+
}
|
|
419
|
+
return process.cwd();
|
|
420
|
+
})();
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// src/shared/protocol.ts
|
|
425
|
+
function isHelloTestMessage(msg) {
|
|
426
|
+
if (!msg || typeof msg !== "object") return false;
|
|
427
|
+
const m = msg;
|
|
428
|
+
return m.type === HELLO_TEST && typeof m.token === "string" && typeof m.runId === "string" && typeof m.pid === "number" && typeof m.cwd === "string";
|
|
429
|
+
}
|
|
430
|
+
function isBatchMockRequestMessage(msg) {
|
|
431
|
+
if (!msg || typeof msg !== "object") return false;
|
|
432
|
+
const m = msg;
|
|
433
|
+
return m.type === BATCH_MOCK_REQUEST && typeof m.runId === "string" && Array.isArray(m.requests);
|
|
434
|
+
}
|
|
435
|
+
function isHeartbeatMessage(msg) {
|
|
436
|
+
if (!msg || typeof msg !== "object") return false;
|
|
437
|
+
const m = msg;
|
|
438
|
+
return m.type === HEARTBEAT && typeof m.runId === "string";
|
|
439
|
+
}
|
|
440
|
+
function isJsonRpcRequest(msg) {
|
|
441
|
+
if (!msg || typeof msg !== "object") return false;
|
|
442
|
+
const m = msg;
|
|
443
|
+
return m.jsonrpc === "2.0" && (typeof m.id === "string" || typeof m.id === "number") && typeof m.method === "string";
|
|
444
|
+
}
|
|
445
|
+
var HELLO_TEST, HELLO_ACK, BATCH_MOCK_REQUEST, BATCH_MOCK_RESULT, HEARTBEAT, HEARTBEAT_ACK, RPC_GET_STATUS, RPC_LIST_RUNS, RPC_CLAIM_NEXT_BATCH, RPC_PROVIDE_BATCH, RPC_RELEASE_BATCH, RPC_GET_BATCH, RPC_ERROR_METHOD_NOT_FOUND, RPC_ERROR_INTERNAL, RPC_ERROR_NOT_FOUND, RPC_ERROR_UNAUTHORIZED, RPC_ERROR_CONFLICT, RPC_ERROR_EXPIRED;
|
|
446
|
+
var init_protocol = __esm({
|
|
447
|
+
"src/shared/protocol.ts"() {
|
|
448
|
+
HELLO_TEST = "HELLO_TEST";
|
|
449
|
+
HELLO_ACK = "HELLO_ACK";
|
|
450
|
+
BATCH_MOCK_REQUEST = "BATCH_MOCK_REQUEST";
|
|
451
|
+
BATCH_MOCK_RESULT = "BATCH_MOCK_RESULT";
|
|
452
|
+
HEARTBEAT = "HEARTBEAT";
|
|
453
|
+
HEARTBEAT_ACK = "HEARTBEAT_ACK";
|
|
454
|
+
RPC_GET_STATUS = "getStatus";
|
|
455
|
+
RPC_LIST_RUNS = "listRuns";
|
|
456
|
+
RPC_CLAIM_NEXT_BATCH = "claimNextBatch";
|
|
457
|
+
RPC_PROVIDE_BATCH = "provideBatch";
|
|
458
|
+
RPC_RELEASE_BATCH = "releaseBatch";
|
|
459
|
+
RPC_GET_BATCH = "getBatch";
|
|
460
|
+
RPC_ERROR_METHOD_NOT_FOUND = -32601;
|
|
461
|
+
RPC_ERROR_INTERNAL = -32603;
|
|
462
|
+
RPC_ERROR_NOT_FOUND = -32e3;
|
|
463
|
+
RPC_ERROR_UNAUTHORIZED = -32001;
|
|
464
|
+
RPC_ERROR_CONFLICT = -32002;
|
|
465
|
+
RPC_ERROR_EXPIRED = -32003;
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// src/daemon/daemon.ts
|
|
470
|
+
var daemon_exports = {};
|
|
471
|
+
__export(daemon_exports, {
|
|
472
|
+
MockMcpDaemon: () => MockMcpDaemon
|
|
473
|
+
});
|
|
474
|
+
var MockMcpDaemon, RpcError;
|
|
475
|
+
var init_daemon = __esm({
|
|
476
|
+
"src/daemon/daemon.ts"() {
|
|
477
|
+
init_discovery();
|
|
478
|
+
init_protocol();
|
|
479
|
+
MockMcpDaemon = class {
|
|
480
|
+
logger;
|
|
481
|
+
opts;
|
|
482
|
+
server;
|
|
483
|
+
wss;
|
|
484
|
+
sweepTimer;
|
|
485
|
+
idleTimer;
|
|
486
|
+
startedAt;
|
|
487
|
+
// State management
|
|
488
|
+
runs = /* @__PURE__ */ new Map();
|
|
489
|
+
batches = /* @__PURE__ */ new Map();
|
|
490
|
+
pendingQueue = [];
|
|
491
|
+
// batchIds in order
|
|
492
|
+
batchSeq = 0;
|
|
493
|
+
constructor(options) {
|
|
494
|
+
this.logger = options.logger ?? console;
|
|
495
|
+
this.opts = {
|
|
496
|
+
projectRoot: options.projectRoot,
|
|
497
|
+
token: options.token,
|
|
498
|
+
version: options.version,
|
|
499
|
+
cacheDir: options.cacheDir,
|
|
500
|
+
logger: this.logger,
|
|
501
|
+
defaultLeaseMs: options.defaultLeaseMs ?? 3e4,
|
|
502
|
+
sweepIntervalMs: options.sweepIntervalMs ?? 5e3,
|
|
503
|
+
idleShutdownMs: options.idleShutdownMs ?? 6e5
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
// ===========================================================================
|
|
507
|
+
// Lifecycle
|
|
508
|
+
// ===========================================================================
|
|
509
|
+
async start() {
|
|
510
|
+
const projectId = computeProjectId(this.opts.projectRoot);
|
|
511
|
+
const { base, registryPath, ipcPath } = getPaths(projectId, this.opts.cacheDir);
|
|
512
|
+
await fs.mkdir(base, { recursive: true });
|
|
513
|
+
if (process.platform !== "win32") {
|
|
514
|
+
try {
|
|
515
|
+
await fs.rm(ipcPath);
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const server = http.createServer((req, res) => this.handleHttp(req, res));
|
|
520
|
+
this.server = server;
|
|
521
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
522
|
+
this.wss = wss;
|
|
523
|
+
server.on("upgrade", (req, socket, head) => {
|
|
524
|
+
if (!req.url?.startsWith("/test")) {
|
|
525
|
+
socket.destroy();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
529
|
+
wss.emit("connection", ws, req);
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
wss.on("connection", (ws, req) => this.handleWsConnection(ws, req));
|
|
533
|
+
await new Promise((resolve, reject) => {
|
|
534
|
+
server.once("error", reject);
|
|
535
|
+
server.listen(ipcPath, () => {
|
|
536
|
+
this.logger.error(`\u{1F680} Daemon listening on ${ipcPath}`);
|
|
537
|
+
resolve();
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
this.startedAt = Date.now();
|
|
541
|
+
const registry = {
|
|
542
|
+
projectId,
|
|
543
|
+
projectRoot: this.opts.projectRoot,
|
|
544
|
+
ipcPath,
|
|
545
|
+
token: this.opts.token,
|
|
546
|
+
pid: process.pid,
|
|
547
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
548
|
+
version: this.opts.version
|
|
549
|
+
};
|
|
550
|
+
await writeRegistry(registryPath, registry);
|
|
551
|
+
const globalEntry = {
|
|
552
|
+
projectId,
|
|
553
|
+
projectRoot: this.opts.projectRoot,
|
|
554
|
+
ipcPath,
|
|
555
|
+
registryPath,
|
|
556
|
+
pid: process.pid,
|
|
557
|
+
startedAt: registry.startedAt,
|
|
558
|
+
version: this.opts.version
|
|
559
|
+
};
|
|
560
|
+
await registerDaemonGlobally(globalEntry, this.opts.cacheDir);
|
|
561
|
+
this.sweepTimer = setInterval(() => this.sweepExpiredClaims(), this.opts.sweepIntervalMs);
|
|
562
|
+
this.sweepTimer.unref?.();
|
|
563
|
+
this.resetIdleTimer();
|
|
564
|
+
this.logger.error(`\u2705 Daemon ready (project: ${projectId}, pid: ${process.pid})`);
|
|
565
|
+
}
|
|
566
|
+
async stop() {
|
|
567
|
+
if (this.sweepTimer) {
|
|
568
|
+
clearInterval(this.sweepTimer);
|
|
569
|
+
this.sweepTimer = void 0;
|
|
570
|
+
}
|
|
571
|
+
if (this.idleTimer) {
|
|
572
|
+
clearTimeout(this.idleTimer);
|
|
573
|
+
this.idleTimer = void 0;
|
|
574
|
+
}
|
|
575
|
+
for (const run of this.runs.values()) {
|
|
576
|
+
run.ws.close(1001, "Daemon shutting down");
|
|
577
|
+
}
|
|
578
|
+
this.runs.clear();
|
|
579
|
+
this.wss?.close();
|
|
580
|
+
await new Promise((resolve) => {
|
|
581
|
+
if (!this.server) {
|
|
582
|
+
resolve();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
this.server.close(() => resolve());
|
|
586
|
+
});
|
|
587
|
+
this.batches.clear();
|
|
588
|
+
this.pendingQueue.length = 0;
|
|
589
|
+
const projectId = computeProjectId(this.opts.projectRoot);
|
|
590
|
+
await unregisterDaemonGlobally(projectId, this.opts.cacheDir);
|
|
591
|
+
this.logger.error("\u{1F44B} Daemon stopped");
|
|
592
|
+
}
|
|
593
|
+
// ===========================================================================
|
|
594
|
+
// HTTP Handler (/health, /control)
|
|
595
|
+
// ===========================================================================
|
|
596
|
+
async handleHttp(req, res) {
|
|
597
|
+
try {
|
|
598
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
599
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
600
|
+
res.end(
|
|
601
|
+
JSON.stringify({
|
|
602
|
+
ok: true,
|
|
603
|
+
pid: process.pid,
|
|
604
|
+
version: this.opts.version,
|
|
605
|
+
projectId: computeProjectId(this.opts.projectRoot)
|
|
606
|
+
})
|
|
607
|
+
);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (req.method === "POST" && req.url === "/control") {
|
|
611
|
+
const token = req.headers["x-mock-mcp-token"];
|
|
612
|
+
if (token !== this.opts.token) {
|
|
613
|
+
res.writeHead(401, { "content-type": "application/json" });
|
|
614
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const body = await this.readBody(req);
|
|
618
|
+
let rpcReq;
|
|
619
|
+
try {
|
|
620
|
+
rpcReq = JSON.parse(body);
|
|
621
|
+
} catch {
|
|
622
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
623
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (!isJsonRpcRequest(rpcReq)) {
|
|
627
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
628
|
+
res.end(JSON.stringify({ error: "Invalid JSON-RPC request" }));
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const rpcRes = await this.handleRpc(rpcReq);
|
|
632
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
633
|
+
res.end(JSON.stringify(rpcRes));
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
637
|
+
res.end("Not Found");
|
|
638
|
+
} catch (e) {
|
|
639
|
+
this.logger.error("HTTP handler error:", e);
|
|
640
|
+
res.writeHead(500, { "content-type": "text/plain" });
|
|
641
|
+
res.end(e instanceof Error ? e.message : String(e));
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
readBody(req) {
|
|
645
|
+
return new Promise((resolve, reject) => {
|
|
646
|
+
let buf = "";
|
|
647
|
+
req.on("data", (chunk) => buf += chunk);
|
|
648
|
+
req.on("end", () => resolve(buf));
|
|
649
|
+
req.on("error", reject);
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
// ===========================================================================
|
|
653
|
+
// WebSocket Handler (/test)
|
|
654
|
+
// ===========================================================================
|
|
655
|
+
handleWsConnection(ws, _req) {
|
|
656
|
+
let authed = false;
|
|
657
|
+
let runId = null;
|
|
658
|
+
const helloTimeout = setTimeout(() => {
|
|
659
|
+
if (!authed) {
|
|
660
|
+
ws.close(1008, "HELLO timeout");
|
|
661
|
+
}
|
|
662
|
+
}, 5e3);
|
|
663
|
+
ws.on("message", (data) => {
|
|
664
|
+
let msg;
|
|
665
|
+
try {
|
|
666
|
+
msg = JSON.parse(data.toString());
|
|
667
|
+
} catch {
|
|
668
|
+
this.logger.warn("Invalid JSON from test process");
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (!authed) {
|
|
672
|
+
if (!isHelloTestMessage(msg)) {
|
|
673
|
+
ws.close(1008, "Expected HELLO_TEST");
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (msg.token !== this.opts.token) {
|
|
677
|
+
ws.close(1008, "Invalid token");
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
authed = true;
|
|
681
|
+
clearTimeout(helloTimeout);
|
|
682
|
+
runId = msg.runId;
|
|
683
|
+
const runState = {
|
|
684
|
+
runId,
|
|
685
|
+
pid: msg.pid,
|
|
686
|
+
cwd: msg.cwd,
|
|
687
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
688
|
+
lastSeen: Date.now(),
|
|
689
|
+
testMeta: msg.testMeta,
|
|
690
|
+
ws
|
|
691
|
+
};
|
|
692
|
+
this.runs.set(runId, runState);
|
|
693
|
+
const ack = { type: HELLO_ACK, runId };
|
|
694
|
+
ws.send(JSON.stringify(ack));
|
|
695
|
+
this.logger.error(`\u{1F50C} Test run connected: ${runId} (pid: ${msg.pid})`);
|
|
696
|
+
this.resetIdleTimer();
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (isBatchMockRequestMessage(msg)) {
|
|
700
|
+
this.handleBatchRequest(runId, msg.requests, ws);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (isHeartbeatMessage(msg)) {
|
|
704
|
+
const run = this.runs.get(msg.runId);
|
|
705
|
+
if (run) {
|
|
706
|
+
run.lastSeen = Date.now();
|
|
707
|
+
}
|
|
708
|
+
const ack = { type: HEARTBEAT_ACK, runId: msg.runId };
|
|
709
|
+
ws.send(JSON.stringify(ack));
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
ws.on("close", () => {
|
|
714
|
+
clearTimeout(helloTimeout);
|
|
715
|
+
if (runId) {
|
|
716
|
+
this.cleanupRun(runId);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
ws.on("error", (err) => {
|
|
720
|
+
this.logger.error("WebSocket error:", err);
|
|
721
|
+
if (runId) {
|
|
722
|
+
this.cleanupRun(runId);
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
handleBatchRequest(runId, requests, ws) {
|
|
727
|
+
const batchId = `batch:${runId}:${++this.batchSeq}`;
|
|
728
|
+
const batch = {
|
|
729
|
+
batchId,
|
|
730
|
+
runId,
|
|
731
|
+
requests,
|
|
732
|
+
createdAt: Date.now(),
|
|
733
|
+
status: "pending"
|
|
734
|
+
};
|
|
735
|
+
this.batches.set(batchId, batch);
|
|
736
|
+
this.pendingQueue.push(batchId);
|
|
737
|
+
this.logger.error(
|
|
738
|
+
[
|
|
739
|
+
`\u{1F4E5} Received ${requests.length} request(s) (${batchId})`,
|
|
740
|
+
...requests.map(
|
|
741
|
+
(req, i) => ` ${i + 1}. ${req.method} ${req.endpoint} (${req.requestId})`
|
|
742
|
+
)
|
|
743
|
+
].join("\n")
|
|
744
|
+
);
|
|
745
|
+
this.logger.error("\u23F3 Awaiting mock data from MCP adapter...");
|
|
746
|
+
}
|
|
747
|
+
cleanupRun(runId) {
|
|
748
|
+
const run = this.runs.get(runId);
|
|
749
|
+
if (!run) return;
|
|
750
|
+
this.runs.delete(runId);
|
|
751
|
+
for (const [batchId, batch] of this.batches) {
|
|
752
|
+
if (batch.runId === runId) {
|
|
753
|
+
this.batches.delete(batchId);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
for (let i = this.pendingQueue.length - 1; i >= 0; i--) {
|
|
757
|
+
const bid = this.pendingQueue[i];
|
|
758
|
+
if (!this.batches.has(bid)) {
|
|
759
|
+
this.pendingQueue.splice(i, 1);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
this.logger.error(`\u{1F50C} Test run disconnected: ${runId}`);
|
|
763
|
+
this.resetIdleTimer();
|
|
764
|
+
}
|
|
765
|
+
// ===========================================================================
|
|
766
|
+
// JSON-RPC Handler
|
|
767
|
+
// ===========================================================================
|
|
768
|
+
async handleRpc(req) {
|
|
769
|
+
try {
|
|
770
|
+
this.sweepExpiredClaims();
|
|
771
|
+
const params = req.params ?? {};
|
|
772
|
+
switch (req.method) {
|
|
773
|
+
case RPC_GET_STATUS:
|
|
774
|
+
return this.rpcSuccess(req.id, this.getStatus());
|
|
775
|
+
case RPC_LIST_RUNS:
|
|
776
|
+
return this.rpcSuccess(req.id, this.listRuns());
|
|
777
|
+
case RPC_CLAIM_NEXT_BATCH:
|
|
778
|
+
return this.rpcSuccess(req.id, this.claimNextBatch(params));
|
|
779
|
+
case RPC_PROVIDE_BATCH:
|
|
780
|
+
return this.rpcSuccess(req.id, await this.provideBatch(params));
|
|
781
|
+
case RPC_RELEASE_BATCH:
|
|
782
|
+
return this.rpcSuccess(req.id, this.releaseBatch(params));
|
|
783
|
+
case RPC_GET_BATCH:
|
|
784
|
+
return this.rpcSuccess(req.id, this.getBatch(params));
|
|
785
|
+
default:
|
|
786
|
+
return this.rpcError(req.id, RPC_ERROR_METHOD_NOT_FOUND, `Unknown method: ${req.method}`);
|
|
787
|
+
}
|
|
788
|
+
} catch (e) {
|
|
789
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
790
|
+
const code = this.getErrorCode(e);
|
|
791
|
+
return this.rpcError(req.id, code, msg);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
getErrorCode(e) {
|
|
795
|
+
if (e instanceof RpcError) {
|
|
796
|
+
return e.code;
|
|
797
|
+
}
|
|
798
|
+
return RPC_ERROR_INTERNAL;
|
|
799
|
+
}
|
|
800
|
+
rpcSuccess(id, result) {
|
|
801
|
+
return { jsonrpc: "2.0", id, result };
|
|
802
|
+
}
|
|
803
|
+
rpcError(id, code, message) {
|
|
804
|
+
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
805
|
+
}
|
|
806
|
+
// ===========================================================================
|
|
807
|
+
// RPC Methods
|
|
808
|
+
// ===========================================================================
|
|
809
|
+
getStatus() {
|
|
810
|
+
const pending = this.pendingQueue.filter((bid) => {
|
|
811
|
+
const b = this.batches.get(bid);
|
|
812
|
+
return b && b.status === "pending";
|
|
813
|
+
}).length;
|
|
814
|
+
const claimed = Array.from(this.batches.values()).filter(
|
|
815
|
+
(b) => b.status === "claimed"
|
|
816
|
+
).length;
|
|
817
|
+
return {
|
|
818
|
+
version: this.opts.version,
|
|
819
|
+
projectId: computeProjectId(this.opts.projectRoot),
|
|
820
|
+
projectRoot: this.opts.projectRoot,
|
|
821
|
+
pid: process.pid,
|
|
822
|
+
uptime: this.startedAt ? Date.now() - this.startedAt : 0,
|
|
823
|
+
runs: this.runs.size,
|
|
824
|
+
pending,
|
|
825
|
+
claimed,
|
|
826
|
+
totalBatches: this.batches.size
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
listRuns() {
|
|
830
|
+
const runs = Array.from(this.runs.values()).map((r) => {
|
|
831
|
+
const pendingBatches = Array.from(this.batches.values()).filter(
|
|
832
|
+
(b) => b.runId === r.runId && b.status === "pending"
|
|
833
|
+
).length;
|
|
834
|
+
return {
|
|
835
|
+
runId: r.runId,
|
|
836
|
+
pid: r.pid,
|
|
837
|
+
cwd: r.cwd,
|
|
838
|
+
startedAt: r.startedAt,
|
|
839
|
+
lastSeen: r.lastSeen,
|
|
840
|
+
pendingBatches,
|
|
841
|
+
testMeta: r.testMeta
|
|
842
|
+
};
|
|
843
|
+
});
|
|
844
|
+
return { runs };
|
|
845
|
+
}
|
|
846
|
+
claimNextBatch(params) {
|
|
847
|
+
const { adapterId, runId, leaseMs = this.opts.defaultLeaseMs } = params;
|
|
848
|
+
if (!adapterId) {
|
|
849
|
+
throw new RpcError(RPC_ERROR_UNAUTHORIZED, "adapterId required");
|
|
850
|
+
}
|
|
851
|
+
for (let i = 0; i < this.pendingQueue.length; i++) {
|
|
852
|
+
const batchId = this.pendingQueue[i];
|
|
853
|
+
const batch = this.batches.get(batchId);
|
|
854
|
+
if (!batch || batch.status !== "pending") {
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
if (runId && batch.runId !== runId) {
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
this.pendingQueue.splice(i, 1);
|
|
861
|
+
batch.status = "claimed";
|
|
862
|
+
batch.claim = {
|
|
863
|
+
adapterId,
|
|
864
|
+
claimToken: crypto.randomUUID(),
|
|
865
|
+
leaseUntil: Date.now() + leaseMs
|
|
866
|
+
};
|
|
867
|
+
this.logger.error(`\u{1F512} Batch ${batchId} claimed by adapter ${adapterId.slice(0, 8)}...`);
|
|
868
|
+
return {
|
|
869
|
+
batchId: batch.batchId,
|
|
870
|
+
runId: batch.runId,
|
|
871
|
+
requests: batch.requests,
|
|
872
|
+
claimToken: batch.claim.claimToken,
|
|
873
|
+
leaseUntil: batch.claim.leaseUntil
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
async provideBatch(params) {
|
|
879
|
+
const { adapterId, batchId, claimToken, mocks } = params;
|
|
880
|
+
const batch = this.batches.get(batchId);
|
|
881
|
+
if (!batch) {
|
|
882
|
+
throw new RpcError(RPC_ERROR_NOT_FOUND, `Batch not found: ${batchId}`);
|
|
883
|
+
}
|
|
884
|
+
if (batch.status !== "claimed" || !batch.claim) {
|
|
885
|
+
throw new RpcError(RPC_ERROR_CONFLICT, `Batch not in claimed state: ${batchId}`);
|
|
886
|
+
}
|
|
887
|
+
if (batch.claim.adapterId !== adapterId) {
|
|
888
|
+
throw new RpcError(RPC_ERROR_UNAUTHORIZED, "Not the owner of this batch");
|
|
889
|
+
}
|
|
890
|
+
if (batch.claim.claimToken !== claimToken) {
|
|
891
|
+
throw new RpcError(RPC_ERROR_UNAUTHORIZED, "Invalid claim token");
|
|
892
|
+
}
|
|
893
|
+
if (batch.claim.leaseUntil <= Date.now()) {
|
|
894
|
+
batch.status = "pending";
|
|
895
|
+
batch.claim = void 0;
|
|
896
|
+
this.pendingQueue.push(batchId);
|
|
897
|
+
throw new RpcError(RPC_ERROR_EXPIRED, "Claim lease expired");
|
|
898
|
+
}
|
|
899
|
+
this.validateMocks(batch, mocks);
|
|
900
|
+
const run = this.runs.get(batch.runId);
|
|
901
|
+
if (!run) {
|
|
902
|
+
this.batches.delete(batchId);
|
|
903
|
+
throw new RpcError(RPC_ERROR_NOT_FOUND, "Run is gone");
|
|
904
|
+
}
|
|
905
|
+
if (run.ws.readyState !== WebSocket.OPEN) {
|
|
906
|
+
this.batches.delete(batchId);
|
|
907
|
+
throw new RpcError(RPC_ERROR_NOT_FOUND, "Test process disconnected");
|
|
908
|
+
}
|
|
909
|
+
const result = {
|
|
910
|
+
type: BATCH_MOCK_RESULT,
|
|
911
|
+
batchId,
|
|
912
|
+
mocks
|
|
913
|
+
};
|
|
914
|
+
run.ws.send(JSON.stringify(result));
|
|
915
|
+
batch.status = "fulfilled";
|
|
916
|
+
this.batches.delete(batchId);
|
|
917
|
+
this.logger.error(`\u2705 Delivered ${mocks.length} mock(s) for ${batchId}`);
|
|
918
|
+
return { ok: true, message: `Provided ${mocks.length} mock(s) for ${batchId}` };
|
|
919
|
+
}
|
|
920
|
+
validateMocks(batch, mocks) {
|
|
921
|
+
const expectedIds = new Set(batch.requests.map((r) => r.requestId));
|
|
922
|
+
const providedIds = /* @__PURE__ */ new Set();
|
|
923
|
+
for (const mock of mocks) {
|
|
924
|
+
if (!expectedIds.has(mock.requestId)) {
|
|
925
|
+
throw new RpcError(
|
|
926
|
+
RPC_ERROR_CONFLICT,
|
|
927
|
+
`Mock references unknown requestId: ${mock.requestId}`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
if (providedIds.has(mock.requestId)) {
|
|
931
|
+
throw new RpcError(
|
|
932
|
+
RPC_ERROR_CONFLICT,
|
|
933
|
+
`Duplicate mock for requestId: ${mock.requestId}`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
providedIds.add(mock.requestId);
|
|
937
|
+
}
|
|
938
|
+
const missing = Array.from(expectedIds).filter((id) => !providedIds.has(id));
|
|
939
|
+
if (missing.length > 0) {
|
|
940
|
+
throw new RpcError(
|
|
941
|
+
RPC_ERROR_CONFLICT,
|
|
942
|
+
`Missing mocks for requestId(s): ${missing.join(", ")}`
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
releaseBatch(params) {
|
|
947
|
+
const { adapterId, batchId, claimToken } = params;
|
|
948
|
+
const batch = this.batches.get(batchId);
|
|
949
|
+
if (!batch) {
|
|
950
|
+
throw new RpcError(RPC_ERROR_NOT_FOUND, `Batch not found: ${batchId}`);
|
|
951
|
+
}
|
|
952
|
+
if (batch.status !== "claimed" || !batch.claim) {
|
|
953
|
+
throw new RpcError(RPC_ERROR_CONFLICT, `Batch not in claimed state: ${batchId}`);
|
|
954
|
+
}
|
|
955
|
+
if (batch.claim.adapterId !== adapterId || batch.claim.claimToken !== claimToken) {
|
|
956
|
+
throw new RpcError(RPC_ERROR_UNAUTHORIZED, "Not the owner of this batch");
|
|
957
|
+
}
|
|
958
|
+
batch.status = "pending";
|
|
959
|
+
batch.claim = void 0;
|
|
960
|
+
this.pendingQueue.push(batchId);
|
|
961
|
+
this.logger.error(`\u{1F513} Batch ${batchId} released by adapter`);
|
|
962
|
+
return { ok: true };
|
|
963
|
+
}
|
|
964
|
+
getBatch(params) {
|
|
965
|
+
const batch = this.batches.get(params.batchId);
|
|
966
|
+
if (!batch) {
|
|
967
|
+
throw new RpcError(RPC_ERROR_NOT_FOUND, `Batch not found: ${params.batchId}`);
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
batchId: batch.batchId,
|
|
971
|
+
runId: batch.runId,
|
|
972
|
+
requests: batch.requests,
|
|
973
|
+
status: batch.status,
|
|
974
|
+
createdAt: batch.createdAt,
|
|
975
|
+
claim: batch.claim ? { adapterId: batch.claim.adapterId, leaseUntil: batch.claim.leaseUntil } : void 0
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
// ===========================================================================
|
|
979
|
+
// Maintenance
|
|
980
|
+
// ===========================================================================
|
|
981
|
+
sweepExpiredClaims() {
|
|
982
|
+
const now = Date.now();
|
|
983
|
+
for (const batch of this.batches.values()) {
|
|
984
|
+
if (batch.status === "claimed" && batch.claim && batch.claim.leaseUntil <= now) {
|
|
985
|
+
this.logger.warn(`\u23F0 Claim expired for ${batch.batchId}, returning to pending`);
|
|
986
|
+
batch.status = "pending";
|
|
987
|
+
batch.claim = void 0;
|
|
988
|
+
this.pendingQueue.push(batch.batchId);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
resetIdleTimer() {
|
|
993
|
+
if (this.idleTimer) {
|
|
994
|
+
clearTimeout(this.idleTimer);
|
|
995
|
+
}
|
|
996
|
+
if (this.runs.size === 0) {
|
|
997
|
+
this.idleTimer = setTimeout(() => {
|
|
998
|
+
if (this.runs.size === 0) {
|
|
999
|
+
this.logger.error("\u{1F4A4} No activity, shutting down daemon...");
|
|
1000
|
+
this.stop().then(() => process.exit(0));
|
|
1001
|
+
}
|
|
1002
|
+
}, this.opts.idleShutdownMs);
|
|
1003
|
+
this.idleTimer.unref?.();
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
RpcError = class extends Error {
|
|
1008
|
+
constructor(code, message) {
|
|
1009
|
+
super(message);
|
|
1010
|
+
this.code = code;
|
|
1011
|
+
this.name = "RpcError";
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
var MultiDaemonClient;
|
|
1017
|
+
var init_multi_daemon_client = __esm({
|
|
1018
|
+
"src/adapter/multi-daemon-client.ts"() {
|
|
1019
|
+
init_discovery();
|
|
1020
|
+
MultiDaemonClient = class {
|
|
1021
|
+
logger;
|
|
1022
|
+
cacheDir;
|
|
1023
|
+
adapterId;
|
|
1024
|
+
constructor(opts = {}) {
|
|
1025
|
+
this.logger = opts.logger ?? console;
|
|
1026
|
+
this.cacheDir = opts.cacheDir;
|
|
1027
|
+
this.adapterId = crypto.randomUUID();
|
|
1028
|
+
}
|
|
1029
|
+
// ===========================================================================
|
|
1030
|
+
// Discovery
|
|
1031
|
+
// ===========================================================================
|
|
1032
|
+
/**
|
|
1033
|
+
* Discover all active and healthy daemons.
|
|
1034
|
+
*/
|
|
1035
|
+
async discoverDaemons() {
|
|
1036
|
+
return discoverAllDaemons(this.cacheDir);
|
|
1037
|
+
}
|
|
1038
|
+
// ===========================================================================
|
|
1039
|
+
// Aggregated RPC Methods
|
|
1040
|
+
// ===========================================================================
|
|
1041
|
+
/**
|
|
1042
|
+
* Get aggregated status from all daemons.
|
|
1043
|
+
*/
|
|
1044
|
+
async getAggregatedStatus() {
|
|
1045
|
+
const daemons = await this.discoverDaemons();
|
|
1046
|
+
const statuses = [];
|
|
1047
|
+
let totalRuns = 0;
|
|
1048
|
+
let totalPending = 0;
|
|
1049
|
+
let totalClaimed = 0;
|
|
1050
|
+
for (const { registry, healthy } of daemons) {
|
|
1051
|
+
if (!healthy) {
|
|
1052
|
+
statuses.push({
|
|
1053
|
+
version: registry.version,
|
|
1054
|
+
projectId: registry.projectId,
|
|
1055
|
+
projectRoot: registry.projectRoot,
|
|
1056
|
+
pid: registry.pid,
|
|
1057
|
+
uptime: 0,
|
|
1058
|
+
runs: 0,
|
|
1059
|
+
pending: 0,
|
|
1060
|
+
claimed: 0,
|
|
1061
|
+
totalBatches: 0,
|
|
1062
|
+
healthy: false
|
|
1063
|
+
});
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
try {
|
|
1067
|
+
const status = await this.rpc(registry, "getStatus", {});
|
|
1068
|
+
statuses.push({ ...status, healthy: true });
|
|
1069
|
+
totalRuns += status.runs;
|
|
1070
|
+
totalPending += status.pending;
|
|
1071
|
+
totalClaimed += status.claimed;
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
this.logger.warn(`Failed to get status from daemon ${registry.projectId}: ${error}`);
|
|
1074
|
+
statuses.push({
|
|
1075
|
+
version: registry.version,
|
|
1076
|
+
projectId: registry.projectId,
|
|
1077
|
+
projectRoot: registry.projectRoot,
|
|
1078
|
+
pid: registry.pid,
|
|
1079
|
+
uptime: 0,
|
|
1080
|
+
runs: 0,
|
|
1081
|
+
pending: 0,
|
|
1082
|
+
claimed: 0,
|
|
1083
|
+
totalBatches: 0,
|
|
1084
|
+
healthy: false
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return { daemons: statuses, totalRuns, totalPending, totalClaimed };
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* List all runs across all daemons.
|
|
1092
|
+
*/
|
|
1093
|
+
async listAllRuns() {
|
|
1094
|
+
const daemons = await this.discoverDaemons();
|
|
1095
|
+
const allRuns = [];
|
|
1096
|
+
for (const { registry, healthy } of daemons) {
|
|
1097
|
+
if (!healthy) continue;
|
|
1098
|
+
try {
|
|
1099
|
+
const result = await this.rpc(registry, "listRuns", {});
|
|
1100
|
+
for (const run of result.runs) {
|
|
1101
|
+
allRuns.push({
|
|
1102
|
+
...run,
|
|
1103
|
+
projectId: registry.projectId,
|
|
1104
|
+
projectRoot: registry.projectRoot
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
this.logger.warn(`Failed to list runs from daemon ${registry.projectId}: ${error}`);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return allRuns;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Claim the next available batch from any daemon.
|
|
1115
|
+
* Searches through all daemons in order until finding one with a pending batch.
|
|
1116
|
+
*/
|
|
1117
|
+
async claimNextBatch(args) {
|
|
1118
|
+
const daemons = await this.discoverDaemons();
|
|
1119
|
+
for (const { registry, healthy } of daemons) {
|
|
1120
|
+
if (!healthy) continue;
|
|
1121
|
+
try {
|
|
1122
|
+
const result = await this.rpc(registry, "claimNextBatch", {
|
|
1123
|
+
adapterId: this.adapterId,
|
|
1124
|
+
runId: args.runId,
|
|
1125
|
+
leaseMs: args.leaseMs
|
|
1126
|
+
});
|
|
1127
|
+
if (result) {
|
|
1128
|
+
return {
|
|
1129
|
+
...result,
|
|
1130
|
+
projectId: registry.projectId,
|
|
1131
|
+
projectRoot: registry.projectRoot
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
this.logger.warn(`Failed to claim batch from daemon ${registry.projectId}: ${error}`);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Provide mock data for a batch.
|
|
1142
|
+
* Automatically routes to the correct daemon based on batchId.
|
|
1143
|
+
*/
|
|
1144
|
+
async provideBatch(args) {
|
|
1145
|
+
const parts = args.batchId.split(":");
|
|
1146
|
+
if (parts.length < 2) {
|
|
1147
|
+
return { ok: false, message: `Invalid batchId format: ${args.batchId}` };
|
|
1148
|
+
}
|
|
1149
|
+
const daemons = await this.discoverDaemons();
|
|
1150
|
+
for (const { registry, healthy } of daemons) {
|
|
1151
|
+
if (!healthy) continue;
|
|
1152
|
+
try {
|
|
1153
|
+
const result = await this.rpc(registry, "provideBatch", {
|
|
1154
|
+
adapterId: this.adapterId,
|
|
1155
|
+
batchId: args.batchId,
|
|
1156
|
+
claimToken: args.claimToken,
|
|
1157
|
+
mocks: args.mocks
|
|
1158
|
+
});
|
|
1159
|
+
return result;
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1162
|
+
if (msg.includes("not found") || msg.includes("Not found")) {
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
return { ok: false, message: msg };
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return { ok: false, message: `Batch not found: ${args.batchId}` };
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Release a batch.
|
|
1172
|
+
*/
|
|
1173
|
+
async releaseBatch(args) {
|
|
1174
|
+
const daemons = await this.discoverDaemons();
|
|
1175
|
+
for (const { registry, healthy } of daemons) {
|
|
1176
|
+
if (!healthy) continue;
|
|
1177
|
+
try {
|
|
1178
|
+
const result = await this.rpc(registry, "releaseBatch", {
|
|
1179
|
+
adapterId: this.adapterId,
|
|
1180
|
+
batchId: args.batchId,
|
|
1181
|
+
claimToken: args.claimToken,
|
|
1182
|
+
reason: args.reason
|
|
1183
|
+
});
|
|
1184
|
+
return result;
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1187
|
+
if (msg.includes("not found") || msg.includes("Not found")) {
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
return { ok: false, message: msg };
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return { ok: false, message: `Batch not found: ${args.batchId}` };
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Get a specific batch by ID.
|
|
1197
|
+
*/
|
|
1198
|
+
async getBatch(batchId) {
|
|
1199
|
+
const daemons = await this.discoverDaemons();
|
|
1200
|
+
for (const { registry, healthy } of daemons) {
|
|
1201
|
+
if (!healthy) continue;
|
|
1202
|
+
try {
|
|
1203
|
+
const result = await this.rpc(registry, "getBatch", { batchId });
|
|
1204
|
+
return result;
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1207
|
+
if (msg.includes("not found") || msg.includes("Not found")) {
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
throw error;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
// ===========================================================================
|
|
1216
|
+
// Internal RPC
|
|
1217
|
+
// ===========================================================================
|
|
1218
|
+
rpc(registry, method, params) {
|
|
1219
|
+
const payload = {
|
|
1220
|
+
jsonrpc: "2.0",
|
|
1221
|
+
id: crypto.randomUUID(),
|
|
1222
|
+
method,
|
|
1223
|
+
params
|
|
1224
|
+
};
|
|
1225
|
+
return new Promise((resolve, reject) => {
|
|
1226
|
+
const req = http.request(
|
|
1227
|
+
{
|
|
1228
|
+
method: "POST",
|
|
1229
|
+
socketPath: registry.ipcPath,
|
|
1230
|
+
path: "/control",
|
|
1231
|
+
headers: {
|
|
1232
|
+
"content-type": "application/json",
|
|
1233
|
+
"x-mock-mcp-token": registry.token
|
|
1234
|
+
},
|
|
1235
|
+
timeout: 3e4
|
|
1236
|
+
},
|
|
1237
|
+
(res) => {
|
|
1238
|
+
let buf = "";
|
|
1239
|
+
res.on("data", (chunk) => buf += chunk);
|
|
1240
|
+
res.on("end", () => {
|
|
1241
|
+
try {
|
|
1242
|
+
const response = JSON.parse(buf);
|
|
1243
|
+
if (response.error) {
|
|
1244
|
+
reject(new Error(response.error.message));
|
|
1245
|
+
} else {
|
|
1246
|
+
resolve(response.result);
|
|
1247
|
+
}
|
|
1248
|
+
} catch (e) {
|
|
1249
|
+
reject(e);
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
);
|
|
1254
|
+
req.on("error", (err) => {
|
|
1255
|
+
reject(new Error(`Daemon connection failed: ${err.message}`));
|
|
1256
|
+
});
|
|
1257
|
+
req.on("timeout", () => {
|
|
1258
|
+
req.destroy();
|
|
1259
|
+
reject(new Error("Daemon request timeout"));
|
|
1260
|
+
});
|
|
1261
|
+
req.end(JSON.stringify(payload));
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
// src/adapter/adapter.ts
|
|
1269
|
+
var adapter_exports = {};
|
|
1270
|
+
__export(adapter_exports, {
|
|
1271
|
+
runAdapter: () => runAdapter
|
|
1272
|
+
});
|
|
1273
|
+
async function runAdapter(opts = {}) {
|
|
1274
|
+
const logger = opts.logger ?? console;
|
|
1275
|
+
const version = opts.version ?? "0.5.0";
|
|
1276
|
+
logger.error("\u{1F50D} Initializing mock-mcp adapter (multi-daemon mode)...");
|
|
1277
|
+
const multiDaemon = new MultiDaemonClient({ logger });
|
|
1278
|
+
const daemons = await multiDaemon.discoverDaemons();
|
|
1279
|
+
if (daemons.length > 0) {
|
|
1280
|
+
logger.error(`\u2705 Found ${daemons.length} active daemon(s):`);
|
|
1281
|
+
for (const d of daemons) {
|
|
1282
|
+
const status = d.healthy ? "healthy" : "unhealthy";
|
|
1283
|
+
logger.error(` - ${d.registry.projectId}: ${d.registry.projectRoot} (${status})`);
|
|
1284
|
+
}
|
|
1285
|
+
} else {
|
|
1286
|
+
logger.error("\u2139\uFE0F No active daemons found. Waiting for test processes to start...");
|
|
1287
|
+
}
|
|
1288
|
+
const server = new Server(
|
|
1289
|
+
{
|
|
1290
|
+
name: "mock-mcp-adapter",
|
|
1291
|
+
version
|
|
1292
|
+
},
|
|
1293
|
+
{
|
|
1294
|
+
capabilities: { tools: {} }
|
|
1295
|
+
}
|
|
1296
|
+
);
|
|
1297
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1298
|
+
tools: [...TOOLS]
|
|
1299
|
+
}));
|
|
1300
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1301
|
+
const { name, arguments: args } = request.params;
|
|
1302
|
+
try {
|
|
1303
|
+
switch (name) {
|
|
1304
|
+
case "get_status": {
|
|
1305
|
+
const result = await multiDaemon.getAggregatedStatus();
|
|
1306
|
+
return buildToolResponse(formatAggregatedStatus(result));
|
|
1307
|
+
}
|
|
1308
|
+
case "list_runs": {
|
|
1309
|
+
const result = await multiDaemon.listAllRuns();
|
|
1310
|
+
return buildToolResponse(formatExtendedRuns(result));
|
|
1311
|
+
}
|
|
1312
|
+
case "claim_next_batch": {
|
|
1313
|
+
const result = await multiDaemon.claimNextBatch({
|
|
1314
|
+
runId: args?.runId,
|
|
1315
|
+
leaseMs: args?.leaseMs
|
|
1316
|
+
});
|
|
1317
|
+
return buildToolResponse(formatClaimResult(result));
|
|
1318
|
+
}
|
|
1319
|
+
case "get_batch": {
|
|
1320
|
+
if (!args?.batchId) {
|
|
1321
|
+
throw new Error("batchId is required");
|
|
1322
|
+
}
|
|
1323
|
+
const result = await multiDaemon.getBatch(args.batchId);
|
|
1324
|
+
if (!result) {
|
|
1325
|
+
throw new Error(`Batch not found: ${args.batchId}`);
|
|
1326
|
+
}
|
|
1327
|
+
return buildToolResponse(formatBatch(result));
|
|
1328
|
+
}
|
|
1329
|
+
case "provide_batch_mock_data": {
|
|
1330
|
+
if (!args?.batchId || !args?.claimToken || !args?.mocks) {
|
|
1331
|
+
throw new Error("batchId, claimToken, and mocks are required");
|
|
1332
|
+
}
|
|
1333
|
+
const result = await multiDaemon.provideBatch({
|
|
1334
|
+
batchId: args.batchId,
|
|
1335
|
+
claimToken: args.claimToken,
|
|
1336
|
+
mocks: args.mocks
|
|
1337
|
+
});
|
|
1338
|
+
return buildToolResponse(formatProvideResult(result));
|
|
1339
|
+
}
|
|
1340
|
+
case "release_batch": {
|
|
1341
|
+
if (!args?.batchId || !args?.claimToken) {
|
|
1342
|
+
throw new Error("batchId and claimToken are required");
|
|
1343
|
+
}
|
|
1344
|
+
const result = await multiDaemon.releaseBatch({
|
|
1345
|
+
batchId: args.batchId,
|
|
1346
|
+
claimToken: args.claimToken,
|
|
1347
|
+
reason: args?.reason
|
|
1348
|
+
});
|
|
1349
|
+
return buildToolResponse(JSON.stringify(result, null, 2));
|
|
1350
|
+
}
|
|
1351
|
+
default:
|
|
1352
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1353
|
+
}
|
|
1354
|
+
} catch (error) {
|
|
1355
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1356
|
+
logger.error(`Tool error (${name}): ${message}`);
|
|
1357
|
+
return buildToolResponse(`Error: ${message}`, true);
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
const transport = new StdioServerTransport();
|
|
1361
|
+
await server.connect(transport);
|
|
1362
|
+
logger.error("\u2705 MCP adapter ready (stdio transport)");
|
|
1363
|
+
}
|
|
1364
|
+
function buildToolResponse(text, isError = false) {
|
|
1365
|
+
return {
|
|
1366
|
+
content: [{ type: "text", text }],
|
|
1367
|
+
isError
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
function formatAggregatedStatus(status) {
|
|
1371
|
+
if (status.daemons.length === 0) {
|
|
1372
|
+
return `# Mock MCP Status
|
|
1373
|
+
|
|
1374
|
+
No active daemons found. Start a test with \`MOCK_MCP=1\` to begin.
|
|
1375
|
+
`;
|
|
1376
|
+
}
|
|
1377
|
+
const lines = [
|
|
1378
|
+
"# Mock MCP Status\n",
|
|
1379
|
+
"## Summary",
|
|
1380
|
+
`- **Active Daemons**: ${status.daemons.filter((d) => d.healthy).length}`,
|
|
1381
|
+
`- **Total Active Runs**: ${status.totalRuns}`,
|
|
1382
|
+
`- **Total Pending Batches**: ${status.totalPending}`,
|
|
1383
|
+
`- **Total Claimed Batches**: ${status.totalClaimed}`,
|
|
1384
|
+
"",
|
|
1385
|
+
"## Daemons\n"
|
|
1386
|
+
];
|
|
1387
|
+
for (const daemon of status.daemons) {
|
|
1388
|
+
const healthIcon = daemon.healthy ? "\u2705" : "\u274C";
|
|
1389
|
+
lines.push(`### ${healthIcon} ${daemon.projectRoot}`);
|
|
1390
|
+
lines.push(`- **Project ID**: ${daemon.projectId}`);
|
|
1391
|
+
lines.push(`- **Version**: ${daemon.version}`);
|
|
1392
|
+
lines.push(`- **PID**: ${daemon.pid}`);
|
|
1393
|
+
if (daemon.healthy) {
|
|
1394
|
+
lines.push(`- **Uptime**: ${Math.round(daemon.uptime / 1e3)}s`);
|
|
1395
|
+
lines.push(`- **Runs**: ${daemon.runs}`);
|
|
1396
|
+
lines.push(`- **Pending**: ${daemon.pending}`);
|
|
1397
|
+
lines.push(`- **Claimed**: ${daemon.claimed}`);
|
|
1398
|
+
} else {
|
|
1399
|
+
lines.push(`- **Status**: Not responding`);
|
|
1400
|
+
}
|
|
1401
|
+
lines.push("");
|
|
1402
|
+
}
|
|
1403
|
+
return lines.join("\n");
|
|
1404
|
+
}
|
|
1405
|
+
function formatExtendedRuns(runs) {
|
|
1406
|
+
if (runs.length === 0) {
|
|
1407
|
+
return "No active test runs.\n\nStart a test with `MOCK_MCP=1` to begin.";
|
|
1408
|
+
}
|
|
1409
|
+
const lines = ["# Active Test Runs\n"];
|
|
1410
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
1411
|
+
for (const run of runs) {
|
|
1412
|
+
const key = run.projectRoot;
|
|
1413
|
+
if (!byProject.has(key)) {
|
|
1414
|
+
byProject.set(key, []);
|
|
1415
|
+
}
|
|
1416
|
+
byProject.get(key).push(run);
|
|
1417
|
+
}
|
|
1418
|
+
for (const [projectRoot, projectRuns] of byProject) {
|
|
1419
|
+
lines.push(`## Project: ${projectRoot}
|
|
1420
|
+
`);
|
|
1421
|
+
for (const run of projectRuns) {
|
|
1422
|
+
lines.push(`### Run: ${run.runId}`);
|
|
1423
|
+
lines.push(`- **PID**: ${run.pid}`);
|
|
1424
|
+
lines.push(`- **CWD**: ${run.cwd}`);
|
|
1425
|
+
lines.push(`- **Started**: ${run.startedAt}`);
|
|
1426
|
+
lines.push(`- **Pending Batches**: ${run.pendingBatches}`);
|
|
1427
|
+
if (run.testMeta) {
|
|
1428
|
+
if (run.testMeta.testFile) {
|
|
1429
|
+
lines.push(`- **Test File**: ${run.testMeta.testFile}`);
|
|
1430
|
+
}
|
|
1431
|
+
if (run.testMeta.testName) {
|
|
1432
|
+
lines.push(`- **Test Name**: ${run.testMeta.testName}`);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
lines.push("");
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return lines.join("\n");
|
|
1439
|
+
}
|
|
1440
|
+
function formatClaimResult(result) {
|
|
1441
|
+
if (!result) {
|
|
1442
|
+
return "No pending batches available to claim.\n\nMake sure a test is running with `MOCK_MCP=1` and has pending mock requests.";
|
|
1443
|
+
}
|
|
1444
|
+
const lines = [
|
|
1445
|
+
"# Batch Claimed Successfully\n",
|
|
1446
|
+
`**Batch ID**: \`${result.batchId}\``,
|
|
1447
|
+
`**Claim Token**: \`${result.claimToken}\``,
|
|
1448
|
+
`**Run ID**: ${result.runId}`,
|
|
1449
|
+
`**Project**: ${result.projectRoot}`,
|
|
1450
|
+
`**Lease Until**: ${new Date(result.leaseUntil).toISOString()}`,
|
|
1451
|
+
"",
|
|
1452
|
+
"## Requests\n"
|
|
1453
|
+
];
|
|
1454
|
+
for (const req of result.requests) {
|
|
1455
|
+
lines.push(`### ${req.method} ${req.endpoint}`);
|
|
1456
|
+
lines.push(`- **Request ID**: \`${req.requestId}\``);
|
|
1457
|
+
if (req.body !== void 0) {
|
|
1458
|
+
lines.push(`- **Body**: \`\`\`json
|
|
1459
|
+
${JSON.stringify(req.body, null, 2)}
|
|
1460
|
+
\`\`\``);
|
|
1461
|
+
}
|
|
1462
|
+
if (req.headers) {
|
|
1463
|
+
lines.push(`- **Headers**: ${JSON.stringify(req.headers)}`);
|
|
1464
|
+
}
|
|
1465
|
+
if (req.metadata) {
|
|
1466
|
+
lines.push(`- **Metadata**: ${JSON.stringify(req.metadata)}`);
|
|
1467
|
+
}
|
|
1468
|
+
lines.push("");
|
|
1469
|
+
}
|
|
1470
|
+
lines.push("---");
|
|
1471
|
+
lines.push("**Next step**: Call `provide_batch_mock_data` with the batch ID, claim token, and mock data for each request.");
|
|
1472
|
+
return lines.join("\n");
|
|
1473
|
+
}
|
|
1474
|
+
function formatBatch(result) {
|
|
1475
|
+
const lines = [
|
|
1476
|
+
`# Batch: ${result.batchId}
|
|
1477
|
+
`,
|
|
1478
|
+
`**Status**: ${result.status}`,
|
|
1479
|
+
`**Run ID**: ${result.runId}`,
|
|
1480
|
+
`**Created**: ${new Date(result.createdAt).toISOString()}`
|
|
1481
|
+
];
|
|
1482
|
+
if (result.claim) {
|
|
1483
|
+
lines.push(`**Claimed by**: ${result.claim.adapterId}`);
|
|
1484
|
+
lines.push(`**Lease until**: ${new Date(result.claim.leaseUntil).toISOString()}`);
|
|
1485
|
+
}
|
|
1486
|
+
lines.push("", "## Requests\n");
|
|
1487
|
+
for (const req of result.requests) {
|
|
1488
|
+
lines.push(`### ${req.method} ${req.endpoint}`);
|
|
1489
|
+
lines.push(`- **Request ID**: \`${req.requestId}\``);
|
|
1490
|
+
if (req.body !== void 0) {
|
|
1491
|
+
lines.push(`- **Body**: \`\`\`json
|
|
1492
|
+
${JSON.stringify(req.body, null, 2)}
|
|
1493
|
+
\`\`\``);
|
|
1494
|
+
}
|
|
1495
|
+
lines.push("");
|
|
1496
|
+
}
|
|
1497
|
+
return lines.join("\n");
|
|
1498
|
+
}
|
|
1499
|
+
function formatProvideResult(result) {
|
|
1500
|
+
if (result.ok) {
|
|
1501
|
+
return `\u2705 ${result.message ?? "Mock data provided successfully."}`;
|
|
1502
|
+
}
|
|
1503
|
+
return `\u274C Failed to provide mock data: ${result.message}`;
|
|
1504
|
+
}
|
|
1505
|
+
var TOOLS;
|
|
1506
|
+
var init_adapter = __esm({
|
|
1507
|
+
"src/adapter/adapter.ts"() {
|
|
1508
|
+
init_multi_daemon_client();
|
|
1509
|
+
TOOLS = [
|
|
1510
|
+
{
|
|
1511
|
+
name: "get_status",
|
|
1512
|
+
description: "Get the current status of the mock-mcp daemon, including active test runs and pending batches.",
|
|
1513
|
+
inputSchema: {
|
|
1514
|
+
type: "object",
|
|
1515
|
+
properties: {},
|
|
1516
|
+
required: []
|
|
1517
|
+
}
|
|
1518
|
+
},
|
|
1519
|
+
{
|
|
1520
|
+
name: "list_runs",
|
|
1521
|
+
description: "List all active test runs connected to the daemon.",
|
|
1522
|
+
inputSchema: {
|
|
1523
|
+
type: "object",
|
|
1524
|
+
properties: {},
|
|
1525
|
+
required: []
|
|
1526
|
+
}
|
|
1527
|
+
},
|
|
1528
|
+
{
|
|
1529
|
+
name: "claim_next_batch",
|
|
1530
|
+
description: `Claim the next pending mock batch for processing. This acquires a lease on the batch.
|
|
1531
|
+
|
|
1532
|
+
You MUST call this before provide_batch_mock_data. The batch will be locked for 30 seconds (configurable via leaseMs).
|
|
1533
|
+
If you don't provide mock data within the lease time, the batch will be released for another adapter to claim.`,
|
|
1534
|
+
inputSchema: {
|
|
1535
|
+
type: "object",
|
|
1536
|
+
properties: {
|
|
1537
|
+
runId: {
|
|
1538
|
+
type: "string",
|
|
1539
|
+
description: "Optional: Filter to only claim batches from a specific test run."
|
|
1540
|
+
},
|
|
1541
|
+
leaseMs: {
|
|
1542
|
+
type: "number",
|
|
1543
|
+
description: "Optional: Lease duration in milliseconds. Default: 30000 (30 seconds)."
|
|
1544
|
+
}
|
|
1545
|
+
},
|
|
1546
|
+
required: []
|
|
1547
|
+
}
|
|
1548
|
+
},
|
|
1549
|
+
{
|
|
1550
|
+
name: "get_batch",
|
|
1551
|
+
description: "Get details of a specific batch by ID (read-only, does not claim).",
|
|
1552
|
+
inputSchema: {
|
|
1553
|
+
type: "object",
|
|
1554
|
+
properties: {
|
|
1555
|
+
batchId: {
|
|
1556
|
+
type: "string",
|
|
1557
|
+
description: "The batch ID to retrieve."
|
|
1558
|
+
}
|
|
1559
|
+
},
|
|
1560
|
+
required: ["batchId"]
|
|
1561
|
+
}
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
name: "provide_batch_mock_data",
|
|
1565
|
+
description: `Provide mock response data for a claimed batch.
|
|
1566
|
+
|
|
1567
|
+
You MUST first call claim_next_batch to get the batchId and claimToken.
|
|
1568
|
+
The mocks array must contain exactly one mock for each request in the batch.`,
|
|
1569
|
+
inputSchema: {
|
|
1570
|
+
type: "object",
|
|
1571
|
+
properties: {
|
|
1572
|
+
batchId: {
|
|
1573
|
+
type: "string",
|
|
1574
|
+
description: "The batch ID (from claim_next_batch)."
|
|
1575
|
+
},
|
|
1576
|
+
claimToken: {
|
|
1577
|
+
type: "string",
|
|
1578
|
+
description: "The claim token (from claim_next_batch)."
|
|
1579
|
+
},
|
|
1580
|
+
mocks: {
|
|
1581
|
+
type: "array",
|
|
1582
|
+
description: "Array of mock responses, one for each request in the batch.",
|
|
1583
|
+
items: {
|
|
1584
|
+
type: "object",
|
|
1585
|
+
properties: {
|
|
1586
|
+
requestId: {
|
|
1587
|
+
type: "string",
|
|
1588
|
+
description: "The requestId from the original request."
|
|
1589
|
+
},
|
|
1590
|
+
data: {
|
|
1591
|
+
description: "The mock response data (any JSON value)."
|
|
1592
|
+
},
|
|
1593
|
+
status: {
|
|
1594
|
+
type: "number",
|
|
1595
|
+
description: "Optional HTTP status code. Default: 200."
|
|
1596
|
+
},
|
|
1597
|
+
headers: {
|
|
1598
|
+
type: "object",
|
|
1599
|
+
description: "Optional response headers."
|
|
1600
|
+
},
|
|
1601
|
+
delayMs: {
|
|
1602
|
+
type: "number",
|
|
1603
|
+
description: "Optional delay before returning the mock (ms)."
|
|
1604
|
+
}
|
|
1605
|
+
},
|
|
1606
|
+
required: ["requestId", "data"]
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
},
|
|
1610
|
+
required: ["batchId", "claimToken", "mocks"]
|
|
1611
|
+
}
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
name: "release_batch",
|
|
1615
|
+
description: "Release a claimed batch without providing mock data. Use this if you cannot generate appropriate mocks.",
|
|
1616
|
+
inputSchema: {
|
|
1617
|
+
type: "object",
|
|
1618
|
+
properties: {
|
|
1619
|
+
batchId: {
|
|
1620
|
+
type: "string",
|
|
1621
|
+
description: "The batch ID to release."
|
|
1622
|
+
},
|
|
1623
|
+
claimToken: {
|
|
1624
|
+
type: "string",
|
|
1625
|
+
description: "The claim token."
|
|
1626
|
+
},
|
|
1627
|
+
reason: {
|
|
1628
|
+
type: "string",
|
|
1629
|
+
description: "Optional reason for releasing."
|
|
1630
|
+
}
|
|
1631
|
+
},
|
|
1632
|
+
required: ["batchId", "claimToken"]
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
];
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
// src/index.ts
|
|
1640
|
+
init_daemon();
|
|
1641
|
+
|
|
1642
|
+
// src/adapter/index.ts
|
|
1643
|
+
init_adapter();
|
|
1644
|
+
var DaemonClient = class {
|
|
1645
|
+
constructor(ipcPath, token, adapterId) {
|
|
1646
|
+
this.ipcPath = ipcPath;
|
|
1647
|
+
this.token = token;
|
|
1648
|
+
this.adapterId = adapterId;
|
|
1649
|
+
}
|
|
1650
|
+
// ===========================================================================
|
|
1651
|
+
// RPC Methods
|
|
1652
|
+
// ===========================================================================
|
|
1653
|
+
async getStatus() {
|
|
1654
|
+
return this.rpc("getStatus", {});
|
|
1655
|
+
}
|
|
1656
|
+
async listRuns() {
|
|
1657
|
+
return this.rpc("listRuns", {});
|
|
1658
|
+
}
|
|
1659
|
+
async claimNextBatch(args) {
|
|
1660
|
+
return this.rpc("claimNextBatch", {
|
|
1661
|
+
adapterId: this.adapterId,
|
|
1662
|
+
runId: args.runId,
|
|
1663
|
+
leaseMs: args.leaseMs
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
async provideBatch(args) {
|
|
1667
|
+
return this.rpc("provideBatch", {
|
|
1668
|
+
adapterId: this.adapterId,
|
|
1669
|
+
batchId: args.batchId,
|
|
1670
|
+
claimToken: args.claimToken,
|
|
1671
|
+
mocks: args.mocks
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
async releaseBatch(args) {
|
|
1675
|
+
return this.rpc("releaseBatch", {
|
|
1676
|
+
adapterId: this.adapterId,
|
|
1677
|
+
batchId: args.batchId,
|
|
1678
|
+
claimToken: args.claimToken,
|
|
1679
|
+
reason: args.reason
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
async getBatch(batchId) {
|
|
1683
|
+
return this.rpc("getBatch", { batchId });
|
|
1684
|
+
}
|
|
1685
|
+
// ===========================================================================
|
|
1686
|
+
// Internal
|
|
1687
|
+
// ===========================================================================
|
|
1688
|
+
rpc(method, params) {
|
|
1689
|
+
const payload = {
|
|
1690
|
+
jsonrpc: "2.0",
|
|
1691
|
+
id: crypto.randomUUID(),
|
|
1692
|
+
method,
|
|
1693
|
+
params
|
|
1694
|
+
};
|
|
1695
|
+
return new Promise((resolve, reject) => {
|
|
1696
|
+
const req = http.request(
|
|
1697
|
+
{
|
|
1698
|
+
method: "POST",
|
|
1699
|
+
socketPath: this.ipcPath,
|
|
1700
|
+
path: "/control",
|
|
1701
|
+
headers: {
|
|
1702
|
+
"content-type": "application/json",
|
|
1703
|
+
"x-mock-mcp-token": this.token
|
|
1704
|
+
},
|
|
1705
|
+
timeout: 3e4
|
|
1706
|
+
},
|
|
1707
|
+
(res) => {
|
|
1708
|
+
let buf = "";
|
|
1709
|
+
res.on("data", (chunk) => buf += chunk);
|
|
1710
|
+
res.on("end", () => {
|
|
1711
|
+
try {
|
|
1712
|
+
const response = JSON.parse(buf);
|
|
1713
|
+
if (response.error) {
|
|
1714
|
+
reject(new Error(response.error.message));
|
|
1715
|
+
} else {
|
|
1716
|
+
resolve(response.result);
|
|
1717
|
+
}
|
|
1718
|
+
} catch (e) {
|
|
1719
|
+
reject(e);
|
|
1720
|
+
}
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
);
|
|
1724
|
+
req.on("error", (err) => {
|
|
1725
|
+
reject(new Error(`Daemon connection failed: ${err.message}`));
|
|
1726
|
+
});
|
|
1727
|
+
req.on("timeout", () => {
|
|
1728
|
+
req.destroy();
|
|
1729
|
+
reject(new Error("Daemon request timeout"));
|
|
1730
|
+
});
|
|
1731
|
+
req.end(JSON.stringify(payload));
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
|
|
1736
|
+
// src/adapter/index.ts
|
|
1737
|
+
init_multi_daemon_client();
|
|
1738
|
+
|
|
1739
|
+
// src/client/batch-mock-collector.ts
|
|
1740
|
+
init_discovery();
|
|
1741
|
+
init_protocol();
|
|
1742
|
+
|
|
1743
|
+
// src/client/util.ts
|
|
1744
|
+
var isEnabled = () => {
|
|
1745
|
+
return process.env.MOCK_MCP !== void 0 && process.env.MOCK_MCP !== "0";
|
|
1746
|
+
};
|
|
1747
|
+
|
|
1748
|
+
// src/client/batch-mock-collector.ts
|
|
1749
|
+
var DEFAULT_TIMEOUT = 6e4;
|
|
1750
|
+
var DEFAULT_BATCH_DEBOUNCE_MS = 0;
|
|
1751
|
+
var DEFAULT_MAX_BATCH_SIZE = 50;
|
|
1752
|
+
var DEFAULT_HEARTBEAT_INTERVAL_MS = 15e3;
|
|
1753
|
+
var BatchMockCollector = class {
|
|
1754
|
+
ws;
|
|
1755
|
+
registry;
|
|
1756
|
+
runId = crypto.randomUUID();
|
|
1757
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
1758
|
+
queuedRequestIds = /* @__PURE__ */ new Set();
|
|
1759
|
+
timeout;
|
|
1760
|
+
batchDebounceMs;
|
|
1761
|
+
maxBatchSize;
|
|
1762
|
+
logger;
|
|
1763
|
+
heartbeatIntervalMs;
|
|
1764
|
+
enableReconnect;
|
|
1765
|
+
projectRoot;
|
|
1766
|
+
testMeta;
|
|
1767
|
+
batchTimer = null;
|
|
1768
|
+
heartbeatTimer = null;
|
|
1769
|
+
reconnectTimer = null;
|
|
1770
|
+
requestIdCounter = 0;
|
|
1771
|
+
closed = false;
|
|
1772
|
+
authed = false;
|
|
1773
|
+
readyResolve;
|
|
1774
|
+
readyReject;
|
|
1775
|
+
readyPromise = Promise.resolve();
|
|
1776
|
+
constructor(options = {}) {
|
|
1777
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
1778
|
+
this.batchDebounceMs = options.batchDebounceMs ?? DEFAULT_BATCH_DEBOUNCE_MS;
|
|
1779
|
+
this.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
|
|
1780
|
+
this.logger = options.logger ?? console;
|
|
1781
|
+
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
1782
|
+
this.enableReconnect = options.enableReconnect ?? true;
|
|
1783
|
+
this.testMeta = options.testMeta;
|
|
1784
|
+
this.projectRoot = this.resolveProjectRootFromOptions(options);
|
|
1785
|
+
this.logger.log(`[mock-mcp] BatchMockCollector created`);
|
|
1786
|
+
this.logger.log(`[mock-mcp] runId: ${this.runId}`);
|
|
1787
|
+
this.logger.log(`[mock-mcp] timeout: ${this.timeout}ms`);
|
|
1788
|
+
this.logger.log(`[mock-mcp] batchDebounceMs: ${this.batchDebounceMs}ms`);
|
|
1789
|
+
this.logger.log(`[mock-mcp] maxBatchSize: ${this.maxBatchSize}`);
|
|
1790
|
+
this.logger.log(`[mock-mcp] heartbeatIntervalMs: ${this.heartbeatIntervalMs}ms`);
|
|
1791
|
+
this.logger.log(`[mock-mcp] enableReconnect: ${this.enableReconnect}`);
|
|
1792
|
+
this.logger.log(`[mock-mcp] projectRoot: ${this.projectRoot ?? "(auto-detect)"}`);
|
|
1793
|
+
if (options.filePath) {
|
|
1794
|
+
this.logger.log(`[mock-mcp] filePath: ${options.filePath}`);
|
|
1795
|
+
}
|
|
1796
|
+
this.resetReadyPromise();
|
|
1797
|
+
this.initConnection({
|
|
1798
|
+
timeout: this.timeout
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Resolve projectRoot from options.
|
|
1803
|
+
* Priority: projectRoot (if valid) > filePath > projectRoot (fallback) > undefined (auto-detect)
|
|
1804
|
+
*
|
|
1805
|
+
* A projectRoot is "valid" if it contains .git or package.json. This prevents
|
|
1806
|
+
* accidentally using a wrong directory (e.g., user's home directory) when the
|
|
1807
|
+
* caller mistakenly passes process.cwd() as projectRoot.
|
|
1808
|
+
*/
|
|
1809
|
+
resolveProjectRootFromOptions(options) {
|
|
1810
|
+
if (options.projectRoot) {
|
|
1811
|
+
const hasGit = this.hasGitOrPackageJson(options.projectRoot);
|
|
1812
|
+
if (hasGit) {
|
|
1813
|
+
return options.projectRoot;
|
|
1814
|
+
}
|
|
1815
|
+
this.logger.warn(`[mock-mcp] Warning: projectRoot "${options.projectRoot}" doesn't contain .git or package.json`);
|
|
1816
|
+
}
|
|
1817
|
+
if (options.filePath) {
|
|
1818
|
+
let filePath = options.filePath;
|
|
1819
|
+
if (filePath.startsWith("file://")) {
|
|
1820
|
+
try {
|
|
1821
|
+
filePath = fileURLToPath(filePath);
|
|
1822
|
+
} catch {
|
|
1823
|
+
filePath = filePath.replace(/^file:\/\//, "");
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
const dir = path.dirname(filePath);
|
|
1827
|
+
const resolved = resolveProjectRoot(dir);
|
|
1828
|
+
this.logger.log(`[mock-mcp] Resolved projectRoot from filePath:`);
|
|
1829
|
+
this.logger.log(`[mock-mcp] filePath: ${options.filePath}`);
|
|
1830
|
+
this.logger.log(`[mock-mcp] dir: ${dir}`);
|
|
1831
|
+
this.logger.log(`[mock-mcp] projectRoot: ${resolved}`);
|
|
1832
|
+
return resolved;
|
|
1833
|
+
}
|
|
1834
|
+
if (options.projectRoot) {
|
|
1835
|
+
this.logger.warn(`[mock-mcp] Warning: Using projectRoot "${options.projectRoot}" despite missing .git/package.json`);
|
|
1836
|
+
return options.projectRoot;
|
|
1837
|
+
}
|
|
1838
|
+
return void 0;
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Check if a directory contains .git or package.json
|
|
1842
|
+
*/
|
|
1843
|
+
hasGitOrPackageJson(dir) {
|
|
1844
|
+
try {
|
|
1845
|
+
const gitPath = path.join(dir, ".git");
|
|
1846
|
+
const pkgPath = path.join(dir, "package.json");
|
|
1847
|
+
try {
|
|
1848
|
+
const stat = fssync.statSync(gitPath);
|
|
1849
|
+
if (stat.isDirectory() || stat.isFile()) {
|
|
1850
|
+
return true;
|
|
1851
|
+
}
|
|
1852
|
+
} catch {
|
|
1853
|
+
}
|
|
1854
|
+
try {
|
|
1855
|
+
fssync.accessSync(pkgPath, fssync.constants.F_OK);
|
|
1856
|
+
return true;
|
|
1857
|
+
} catch {
|
|
1858
|
+
}
|
|
1859
|
+
return false;
|
|
1860
|
+
} catch {
|
|
1861
|
+
return false;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Ensures the underlying connection is ready for use.
|
|
1866
|
+
*/
|
|
1867
|
+
async waitUntilReady() {
|
|
1868
|
+
return this.readyPromise;
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Request mock data for a specific endpoint/method pair.
|
|
1872
|
+
*/
|
|
1873
|
+
async requestMock(endpoint, method, options = {}) {
|
|
1874
|
+
if (this.closed) {
|
|
1875
|
+
throw new Error("BatchMockCollector has been closed");
|
|
1876
|
+
}
|
|
1877
|
+
await this.waitUntilReady();
|
|
1878
|
+
const requestId = `req-${++this.requestIdCounter}`;
|
|
1879
|
+
const request = {
|
|
1880
|
+
requestId,
|
|
1881
|
+
endpoint,
|
|
1882
|
+
method,
|
|
1883
|
+
body: options.body,
|
|
1884
|
+
headers: options.headers,
|
|
1885
|
+
metadata: options.metadata
|
|
1886
|
+
};
|
|
1887
|
+
let settleCompletion;
|
|
1888
|
+
const completion = new Promise((resolve) => {
|
|
1889
|
+
settleCompletion = resolve;
|
|
1890
|
+
});
|
|
1891
|
+
return new Promise((resolve, reject) => {
|
|
1892
|
+
const timeoutId = setTimeout(() => {
|
|
1893
|
+
this.rejectRequest(
|
|
1894
|
+
requestId,
|
|
1895
|
+
new Error(`Mock request timed out after ${this.timeout}ms: ${method} ${endpoint}`)
|
|
1896
|
+
);
|
|
1897
|
+
}, this.timeout);
|
|
1898
|
+
this.pendingRequests.set(requestId, {
|
|
1899
|
+
request,
|
|
1900
|
+
resolve: (mock) => {
|
|
1901
|
+
settleCompletion({ status: "fulfilled", value: void 0 });
|
|
1902
|
+
resolve(this.buildResolvedMock(mock));
|
|
1903
|
+
},
|
|
1904
|
+
reject: (error) => {
|
|
1905
|
+
settleCompletion({ status: "rejected", reason: error });
|
|
1906
|
+
reject(error);
|
|
1907
|
+
},
|
|
1908
|
+
timeoutId,
|
|
1909
|
+
completion
|
|
1910
|
+
});
|
|
1911
|
+
this.enqueueRequest(requestId);
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Wait for all currently pending requests to settle.
|
|
1916
|
+
*/
|
|
1917
|
+
async waitForPendingRequests() {
|
|
1918
|
+
if (!isEnabled()) {
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
const pendingCompletions = Array.from(this.pendingRequests.values()).map(
|
|
1922
|
+
(p) => p.completion
|
|
1923
|
+
);
|
|
1924
|
+
const results = await Promise.all(pendingCompletions);
|
|
1925
|
+
const rejected = results.find(
|
|
1926
|
+
(r) => r.status === "rejected"
|
|
1927
|
+
);
|
|
1928
|
+
if (rejected) {
|
|
1929
|
+
throw rejected.reason;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Close the connection and fail all pending requests.
|
|
1934
|
+
*/
|
|
1935
|
+
async close(code) {
|
|
1936
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] close() called with code: ${code ?? "(default)"}`);
|
|
1937
|
+
if (this.closed) {
|
|
1938
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Already closed, returning`);
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
this.closed = true;
|
|
1942
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Cleaning up timers...`);
|
|
1943
|
+
if (this.batchTimer) {
|
|
1944
|
+
clearTimeout(this.batchTimer);
|
|
1945
|
+
this.batchTimer = null;
|
|
1946
|
+
}
|
|
1947
|
+
if (this.heartbeatTimer) {
|
|
1948
|
+
clearInterval(this.heartbeatTimer);
|
|
1949
|
+
this.heartbeatTimer = null;
|
|
1950
|
+
}
|
|
1951
|
+
if (this.reconnectTimer) {
|
|
1952
|
+
clearTimeout(this.reconnectTimer);
|
|
1953
|
+
this.reconnectTimer = null;
|
|
1954
|
+
}
|
|
1955
|
+
const pendingCount = this.pendingRequests.size;
|
|
1956
|
+
const queuedCount = this.queuedRequestIds.size;
|
|
1957
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Pending requests: ${pendingCount}, Queued: ${queuedCount}`);
|
|
1958
|
+
this.queuedRequestIds.clear();
|
|
1959
|
+
const closePromise = new Promise((resolve) => {
|
|
1960
|
+
if (!this.ws) {
|
|
1961
|
+
resolve();
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
this.ws.once("close", () => resolve());
|
|
1965
|
+
});
|
|
1966
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Closing WebSocket...`);
|
|
1967
|
+
this.ws?.close(code);
|
|
1968
|
+
this.failAllPending(new Error("BatchMockCollector has been closed"));
|
|
1969
|
+
await closePromise;
|
|
1970
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] \u2705 Connection closed`);
|
|
1971
|
+
}
|
|
1972
|
+
// ===========================================================================
|
|
1973
|
+
// Connection Management
|
|
1974
|
+
// ===========================================================================
|
|
1975
|
+
async initConnection({
|
|
1976
|
+
timeout = 1e4
|
|
1977
|
+
}) {
|
|
1978
|
+
const initStartTime = Date.now();
|
|
1979
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Initializing connection...`);
|
|
1980
|
+
try {
|
|
1981
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Ensuring daemon is running...`);
|
|
1982
|
+
const daemonStartTime = Date.now();
|
|
1983
|
+
this.registry = await ensureDaemonRunning({
|
|
1984
|
+
projectRoot: this.projectRoot,
|
|
1985
|
+
timeoutMs: timeout
|
|
1986
|
+
});
|
|
1987
|
+
const daemonElapsed = Date.now() - daemonStartTime;
|
|
1988
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Daemon ready (${daemonElapsed}ms)`);
|
|
1989
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Project ID: ${this.registry.projectId}`);
|
|
1990
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Daemon PID: ${this.registry.pid}`);
|
|
1991
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] IPC Path: ${this.registry.ipcPath}`);
|
|
1992
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Creating WebSocket connection...`);
|
|
1993
|
+
const wsStartTime = Date.now();
|
|
1994
|
+
this.ws = await this.createWebSocket(this.registry.ipcPath);
|
|
1995
|
+
const wsElapsed = Date.now() - wsStartTime;
|
|
1996
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] WebSocket created (${wsElapsed}ms)`);
|
|
1997
|
+
this.setupWebSocket();
|
|
1998
|
+
const totalElapsed = Date.now() - initStartTime;
|
|
1999
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Connection initialized (total: ${totalElapsed}ms)`);
|
|
2000
|
+
} catch (error) {
|
|
2001
|
+
const elapsed = Date.now() - initStartTime;
|
|
2002
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] Connection init failed after ${elapsed}ms:`, error);
|
|
2003
|
+
this.readyReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
createWebSocket(ipcPath) {
|
|
2007
|
+
return new Promise((resolve, reject) => {
|
|
2008
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Creating WebSocket to IPC: ${ipcPath}`);
|
|
2009
|
+
const agent = new http.Agent({
|
|
2010
|
+
// @ts-expect-error: Node.js supports socketPath for Unix sockets
|
|
2011
|
+
socketPath: ipcPath
|
|
2012
|
+
});
|
|
2013
|
+
const ws = new WebSocket2("ws://localhost/test", {
|
|
2014
|
+
agent
|
|
2015
|
+
});
|
|
2016
|
+
ws.once("open", () => {
|
|
2017
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] WebSocket opened`);
|
|
2018
|
+
resolve(ws);
|
|
2019
|
+
});
|
|
2020
|
+
ws.once("error", (err) => {
|
|
2021
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] WebSocket connection error:`, err);
|
|
2022
|
+
reject(err);
|
|
2023
|
+
});
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
setupWebSocket() {
|
|
2027
|
+
if (!this.ws || !this.registry) {
|
|
2028
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] setupWebSocket called but ws or registry is missing`);
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Setting up WebSocket event handlers...`);
|
|
2032
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Current readyState: ${this.ws.readyState}`);
|
|
2033
|
+
this.ws.on("message", (data) => this.handleMessage(data));
|
|
2034
|
+
this.ws.on("error", (error) => {
|
|
2035
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] \u274C WebSocket ERROR:`, error);
|
|
2036
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] authed: ${this.authed}`);
|
|
2037
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] readyState: ${this.ws?.readyState}`);
|
|
2038
|
+
if (!this.authed) {
|
|
2039
|
+
this.readyReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
2040
|
+
}
|
|
2041
|
+
this.failAllPending(error instanceof Error ? error : new Error(String(error)));
|
|
2042
|
+
});
|
|
2043
|
+
this.ws.on("close", (code, reason) => {
|
|
2044
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] \u{1F50C} WebSocket CLOSE`);
|
|
2045
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] code: ${code}`);
|
|
2046
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] reason: ${reason?.toString() || "(none)"}`);
|
|
2047
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] authed: ${this.authed}`);
|
|
2048
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] closed: ${this.closed}`);
|
|
2049
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] enableReconnect: ${this.enableReconnect}`);
|
|
2050
|
+
this.authed = false;
|
|
2051
|
+
this.stopHeartbeat();
|
|
2052
|
+
this.failAllPending(new Error(`Daemon connection closed (code: ${code})`));
|
|
2053
|
+
if (!this.closed && this.enableReconnect) {
|
|
2054
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Will attempt reconnect...`);
|
|
2055
|
+
this.scheduleReconnect();
|
|
2056
|
+
}
|
|
2057
|
+
});
|
|
2058
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] WebSocket event handlers configured`);
|
|
2059
|
+
if (this.ws.readyState === WebSocket2.OPEN) {
|
|
2060
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] \u{1F50C} WebSocket already OPEN - sending HELLO`);
|
|
2061
|
+
this.sendHello();
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
sendHello() {
|
|
2065
|
+
if (!this.ws || !this.registry) {
|
|
2066
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] sendHello called but ws or registry is missing`);
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Sending HELLO handshake...`);
|
|
2070
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] runId: ${this.runId}`);
|
|
2071
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] pid: ${process.pid}`);
|
|
2072
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] cwd: ${process.cwd()}`);
|
|
2073
|
+
if (this.testMeta) {
|
|
2074
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] testFile: ${this.testMeta.testFile ?? "(none)"}`);
|
|
2075
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] testName: ${this.testMeta.testName ?? "(none)"}`);
|
|
2076
|
+
}
|
|
2077
|
+
const hello = {
|
|
2078
|
+
type: HELLO_TEST,
|
|
2079
|
+
token: this.registry.token,
|
|
2080
|
+
runId: this.runId,
|
|
2081
|
+
pid: process.pid,
|
|
2082
|
+
cwd: process.cwd(),
|
|
2083
|
+
testMeta: this.testMeta
|
|
2084
|
+
};
|
|
2085
|
+
this.ws.send(JSON.stringify(hello));
|
|
2086
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] HELLO sent, waiting for HELLO_ACK...`);
|
|
2087
|
+
}
|
|
2088
|
+
handleMessage(data) {
|
|
2089
|
+
let msg;
|
|
2090
|
+
try {
|
|
2091
|
+
msg = JSON.parse(data.toString());
|
|
2092
|
+
} catch {
|
|
2093
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] Failed to parse server message`);
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
const msgType = msg?.type;
|
|
2097
|
+
if (this.isHelloAck(msg)) {
|
|
2098
|
+
this.authed = true;
|
|
2099
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] \u2705 Received HELLO_ACK - Authenticated!`);
|
|
2100
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Connection is now READY`);
|
|
2101
|
+
this.readyResolve?.();
|
|
2102
|
+
this.startHeartbeat();
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
if (this.isBatchMockResult(msg)) {
|
|
2106
|
+
this.logger.log(
|
|
2107
|
+
`[mock-mcp] [${this.runId.slice(0, 8)}] \u{1F4E6} Received BATCH_MOCK_RESULT`
|
|
2108
|
+
);
|
|
2109
|
+
this.logger.log(
|
|
2110
|
+
`[mock-mcp] [${this.runId.slice(0, 8)}] batchId: ${msg.batchId}`
|
|
2111
|
+
);
|
|
2112
|
+
this.logger.log(
|
|
2113
|
+
`[mock-mcp] [${this.runId.slice(0, 8)}] mocks count: ${msg.mocks.length}`
|
|
2114
|
+
);
|
|
2115
|
+
for (const mock of msg.mocks) {
|
|
2116
|
+
this.logger.log(
|
|
2117
|
+
`[mock-mcp] [${this.runId.slice(0, 8)}] - ${mock.requestId}: status=${mock.status ?? 200}`
|
|
2118
|
+
);
|
|
2119
|
+
this.resolveRequest(mock);
|
|
2120
|
+
}
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
if (msgType === "HEARTBEAT_ACK") {
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] Received unknown message type: ${msgType}`);
|
|
2127
|
+
}
|
|
2128
|
+
isHelloAck(msg) {
|
|
2129
|
+
return msg !== null && typeof msg === "object" && msg.type === HELLO_ACK;
|
|
2130
|
+
}
|
|
2131
|
+
isBatchMockResult(msg) {
|
|
2132
|
+
return msg !== null && typeof msg === "object" && msg.type === BATCH_MOCK_RESULT && Array.isArray(msg.mocks);
|
|
2133
|
+
}
|
|
2134
|
+
// ===========================================================================
|
|
2135
|
+
// Request Management
|
|
2136
|
+
// ===========================================================================
|
|
2137
|
+
resolveRequest(mock) {
|
|
2138
|
+
const pending = this.pendingRequests.get(mock.requestId);
|
|
2139
|
+
if (!pending) {
|
|
2140
|
+
this.logger.warn(`Received mock for unknown request: ${mock.requestId}`);
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
clearTimeout(pending.timeoutId);
|
|
2144
|
+
this.pendingRequests.delete(mock.requestId);
|
|
2145
|
+
const resolve = () => pending.resolve(mock);
|
|
2146
|
+
if (mock.delayMs && mock.delayMs > 0) {
|
|
2147
|
+
setTimeout(resolve, mock.delayMs);
|
|
2148
|
+
} else {
|
|
2149
|
+
resolve();
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
rejectRequest(requestId, error) {
|
|
2153
|
+
const pending = this.pendingRequests.get(requestId);
|
|
2154
|
+
if (!pending) return;
|
|
2155
|
+
clearTimeout(pending.timeoutId);
|
|
2156
|
+
this.pendingRequests.delete(requestId);
|
|
2157
|
+
pending.reject(error);
|
|
2158
|
+
}
|
|
2159
|
+
failAllPending(error) {
|
|
2160
|
+
for (const requestId of Array.from(this.pendingRequests.keys())) {
|
|
2161
|
+
this.rejectRequest(requestId, error);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
// ===========================================================================
|
|
2165
|
+
// Batching
|
|
2166
|
+
// ===========================================================================
|
|
2167
|
+
enqueueRequest(requestId) {
|
|
2168
|
+
this.queuedRequestIds.add(requestId);
|
|
2169
|
+
if (this.batchTimer) {
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
this.batchTimer = setTimeout(() => {
|
|
2173
|
+
this.batchTimer = null;
|
|
2174
|
+
this.flushQueue();
|
|
2175
|
+
}, this.batchDebounceMs);
|
|
2176
|
+
}
|
|
2177
|
+
flushQueue() {
|
|
2178
|
+
const queuedIds = Array.from(this.queuedRequestIds);
|
|
2179
|
+
this.queuedRequestIds.clear();
|
|
2180
|
+
if (queuedIds.length === 0) {
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
for (let i = 0; i < queuedIds.length; i += this.maxBatchSize) {
|
|
2184
|
+
const chunkIds = queuedIds.slice(i, i + this.maxBatchSize);
|
|
2185
|
+
const requests = [];
|
|
2186
|
+
for (const id of chunkIds) {
|
|
2187
|
+
const pending = this.pendingRequests.get(id);
|
|
2188
|
+
if (pending) {
|
|
2189
|
+
requests.push(pending.request);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
if (requests.length > 0) {
|
|
2193
|
+
this.sendBatch(requests);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
sendBatch(requests) {
|
|
2198
|
+
if (!this.ws || this.ws.readyState !== WebSocket2.OPEN) {
|
|
2199
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] Cannot send batch - WebSocket not open`);
|
|
2200
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] ws exists: ${!!this.ws}`);
|
|
2201
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] readyState: ${this.ws?.readyState}`);
|
|
2202
|
+
const error = new Error("WebSocket is not open");
|
|
2203
|
+
for (const req of requests) {
|
|
2204
|
+
this.rejectRequest(req.requestId, error);
|
|
2205
|
+
}
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
const payload = {
|
|
2209
|
+
type: BATCH_MOCK_REQUEST,
|
|
2210
|
+
runId: this.runId,
|
|
2211
|
+
requests
|
|
2212
|
+
};
|
|
2213
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] \u{1F4E4} Sending BATCH_MOCK_REQUEST`);
|
|
2214
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] requests: ${requests.length}`);
|
|
2215
|
+
for (const req of requests) {
|
|
2216
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] - ${req.requestId}: ${req.method} ${req.endpoint}`);
|
|
2217
|
+
}
|
|
2218
|
+
this.ws.send(JSON.stringify(payload));
|
|
2219
|
+
}
|
|
2220
|
+
// ===========================================================================
|
|
2221
|
+
// Heartbeat
|
|
2222
|
+
// ===========================================================================
|
|
2223
|
+
startHeartbeat() {
|
|
2224
|
+
if (this.heartbeatIntervalMs <= 0 || this.heartbeatTimer) {
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
let lastPong = Date.now();
|
|
2228
|
+
this.ws?.on("pong", () => {
|
|
2229
|
+
lastPong = Date.now();
|
|
2230
|
+
});
|
|
2231
|
+
this.heartbeatTimer = setInterval(() => {
|
|
2232
|
+
if (!this.ws || this.ws.readyState !== WebSocket2.OPEN) {
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
const now = Date.now();
|
|
2236
|
+
if (now - lastPong > this.heartbeatIntervalMs * 2) {
|
|
2237
|
+
this.logger.warn("Heartbeat missed; closing socket to trigger reconnect...");
|
|
2238
|
+
this.ws.close();
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
this.ws.ping();
|
|
2242
|
+
}, this.heartbeatIntervalMs);
|
|
2243
|
+
this.heartbeatTimer.unref?.();
|
|
2244
|
+
}
|
|
2245
|
+
stopHeartbeat() {
|
|
2246
|
+
if (this.heartbeatTimer) {
|
|
2247
|
+
clearInterval(this.heartbeatTimer);
|
|
2248
|
+
this.heartbeatTimer = null;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
// ===========================================================================
|
|
2252
|
+
// Reconnection
|
|
2253
|
+
// ===========================================================================
|
|
2254
|
+
scheduleReconnect() {
|
|
2255
|
+
if (this.reconnectTimer) {
|
|
2256
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Reconnect already scheduled, skipping`);
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
if (this.closed) {
|
|
2260
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Client is closed, not reconnecting`);
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Scheduling reconnect in 1000ms...`);
|
|
2264
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
2265
|
+
this.reconnectTimer = null;
|
|
2266
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] \u{1F504} Attempting reconnect to daemon...`);
|
|
2267
|
+
this.stopHeartbeat();
|
|
2268
|
+
this.resetReadyPromise();
|
|
2269
|
+
this.authed = false;
|
|
2270
|
+
const reconnectStartTime = Date.now();
|
|
2271
|
+
try {
|
|
2272
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Re-discovering daemon...`);
|
|
2273
|
+
this.registry = await ensureDaemonRunning({
|
|
2274
|
+
projectRoot: this.projectRoot
|
|
2275
|
+
});
|
|
2276
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Daemon found, creating new WebSocket...`);
|
|
2277
|
+
this.ws = await this.createWebSocket(this.registry.ipcPath);
|
|
2278
|
+
this.setupWebSocket();
|
|
2279
|
+
const elapsed = Date.now() - reconnectStartTime;
|
|
2280
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] \u2705 Reconnect successful (${elapsed}ms)`);
|
|
2281
|
+
} catch (error) {
|
|
2282
|
+
const elapsed = Date.now() - reconnectStartTime;
|
|
2283
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] \u274C Reconnection failed after ${elapsed}ms:`, error);
|
|
2284
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Will retry reconnect...`);
|
|
2285
|
+
this.scheduleReconnect();
|
|
2286
|
+
}
|
|
2287
|
+
}, 1e3);
|
|
2288
|
+
this.reconnectTimer.unref?.();
|
|
2289
|
+
}
|
|
2290
|
+
// ===========================================================================
|
|
2291
|
+
// Utilities
|
|
2292
|
+
// ===========================================================================
|
|
2293
|
+
resetReadyPromise() {
|
|
2294
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
2295
|
+
this.readyResolve = resolve;
|
|
2296
|
+
this.readyReject = reject;
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
buildResolvedMock(mock) {
|
|
2300
|
+
return {
|
|
2301
|
+
requestId: mock.requestId,
|
|
2302
|
+
data: mock.data,
|
|
2303
|
+
status: mock.status,
|
|
2304
|
+
headers: mock.headers,
|
|
2305
|
+
delayMs: mock.delayMs
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
// ===========================================================================
|
|
2309
|
+
// Public Getters
|
|
2310
|
+
// ===========================================================================
|
|
2311
|
+
/**
|
|
2312
|
+
* Get the run ID for this collector instance.
|
|
2313
|
+
*/
|
|
2314
|
+
getRunId() {
|
|
2315
|
+
return this.runId;
|
|
2316
|
+
}
|
|
2317
|
+
/**
|
|
2318
|
+
* Get the daemon registry information (after connection).
|
|
2319
|
+
*/
|
|
2320
|
+
getRegistry() {
|
|
2321
|
+
return this.registry;
|
|
2322
|
+
}
|
|
2323
|
+
};
|
|
2324
|
+
|
|
2325
|
+
// src/client/connect.ts
|
|
2326
|
+
var DisabledMockClient = class {
|
|
2327
|
+
runId = "disabled";
|
|
2328
|
+
async waitUntilReady() {
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
async requestMock() {
|
|
2332
|
+
throw new Error(
|
|
2333
|
+
"[mock-mcp] MOCK_MCP is not enabled. Set MOCK_MCP=1 to enable mock generation."
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
async waitForPendingRequests() {
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
async close() {
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
getRunId() {
|
|
2343
|
+
return this.runId;
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2346
|
+
var connect = async (options) => {
|
|
2347
|
+
const logger = options?.logger ?? console;
|
|
2348
|
+
const startTime = Date.now();
|
|
2349
|
+
logger.log("[mock-mcp] connect() called");
|
|
2350
|
+
logger.log(`[mock-mcp] PID: ${process.pid}`);
|
|
2351
|
+
logger.log(`[mock-mcp] CWD: ${process.cwd()}`);
|
|
2352
|
+
logger.log(`[mock-mcp] MOCK_MCP env: ${process.env.MOCK_MCP ?? "(not set)"}`);
|
|
2353
|
+
if (!isEnabled()) {
|
|
2354
|
+
logger.log("[mock-mcp] Skipping (set MOCK_MCP=1 to enable)");
|
|
2355
|
+
return new DisabledMockClient();
|
|
2356
|
+
}
|
|
2357
|
+
logger.log("[mock-mcp] Creating BatchMockCollector...");
|
|
2358
|
+
const collector = new BatchMockCollector(options ?? {});
|
|
2359
|
+
const runId = collector.getRunId();
|
|
2360
|
+
logger.log(`[mock-mcp] Run ID: ${runId}`);
|
|
2361
|
+
logger.log("[mock-mcp] Waiting for connection to be ready...");
|
|
2362
|
+
try {
|
|
2363
|
+
await collector.waitUntilReady();
|
|
2364
|
+
const elapsed = Date.now() - startTime;
|
|
2365
|
+
const registry = collector.getRegistry();
|
|
2366
|
+
logger.log("[mock-mcp] ========== Connection Established ==========");
|
|
2367
|
+
logger.log(`[mock-mcp] Run ID: ${runId}`);
|
|
2368
|
+
logger.log(`[mock-mcp] Daemon PID: ${registry?.pid ?? "unknown"}`);
|
|
2369
|
+
logger.log(`[mock-mcp] Project ID: ${registry?.projectId ?? "unknown"}`);
|
|
2370
|
+
logger.log(`[mock-mcp] IPC Path: ${registry?.ipcPath ?? "unknown"}`);
|
|
2371
|
+
logger.log(`[mock-mcp] Connection time: ${elapsed}ms`);
|
|
2372
|
+
logger.log("[mock-mcp] ==============================================");
|
|
2373
|
+
return collector;
|
|
2374
|
+
} catch (error) {
|
|
2375
|
+
const elapsed = Date.now() - startTime;
|
|
2376
|
+
logger.error("[mock-mcp] ========== Connection Failed ==========");
|
|
2377
|
+
logger.error(`[mock-mcp] Run ID: ${runId}`);
|
|
2378
|
+
logger.error(`[mock-mcp] Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
2379
|
+
logger.error(`[mock-mcp] Elapsed time: ${elapsed}ms`);
|
|
2380
|
+
logger.error("[mock-mcp] =========================================");
|
|
2381
|
+
throw error;
|
|
2382
|
+
}
|
|
2383
|
+
};
|
|
2384
|
+
|
|
2385
|
+
// src/index.ts
|
|
2386
|
+
init_discovery();
|
|
9
2387
|
async function runCli() {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
2388
|
+
const args = process2.argv.slice(2);
|
|
2389
|
+
const command = args[0] ?? "adapter";
|
|
2390
|
+
switch (command) {
|
|
2391
|
+
case "adapter":
|
|
2392
|
+
await runAdapterCommand(args.slice(1));
|
|
2393
|
+
break;
|
|
2394
|
+
case "daemon":
|
|
2395
|
+
await runDaemonCommand(args.slice(1));
|
|
2396
|
+
break;
|
|
2397
|
+
case "status":
|
|
2398
|
+
await runStatusCommand(args.slice(1));
|
|
2399
|
+
break;
|
|
2400
|
+
case "stop":
|
|
2401
|
+
await runStopCommand(args.slice(1));
|
|
2402
|
+
break;
|
|
2403
|
+
case "help":
|
|
2404
|
+
case "--help":
|
|
2405
|
+
case "-h":
|
|
2406
|
+
printHelp();
|
|
2407
|
+
break;
|
|
2408
|
+
case "version":
|
|
2409
|
+
case "--version":
|
|
2410
|
+
case "-v":
|
|
2411
|
+
await printVersion();
|
|
2412
|
+
break;
|
|
2413
|
+
default:
|
|
2414
|
+
await runAdapterCommand();
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
async function runAdapterCommand(_args) {
|
|
2418
|
+
const { runAdapter: runAdapter2 } = await Promise.resolve().then(() => (init_adapter(), adapter_exports));
|
|
2419
|
+
await runAdapter2();
|
|
2420
|
+
}
|
|
2421
|
+
async function runDaemonCommand(args) {
|
|
2422
|
+
const { MockMcpDaemon: MockMcpDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
2423
|
+
const { resolveProjectRoot: resolveProjectRoot2 } = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
|
|
2424
|
+
let projectRoot;
|
|
2425
|
+
let token;
|
|
2426
|
+
for (let i = 0; i < args.length; i++) {
|
|
2427
|
+
const arg = args[i];
|
|
2428
|
+
if (arg === "--project-root" && args[i + 1]) {
|
|
2429
|
+
projectRoot = args[i + 1];
|
|
2430
|
+
i++;
|
|
2431
|
+
} else if (arg === "--token" && args[i + 1]) {
|
|
2432
|
+
token = args[i + 1];
|
|
2433
|
+
i++;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
projectRoot = projectRoot ?? resolveProjectRoot2();
|
|
2437
|
+
const cacheDir = process2.env.MOCK_MCP_CACHE_DIR || void 0;
|
|
2438
|
+
if (!token) {
|
|
2439
|
+
console.error("Error: --token is required for daemon mode");
|
|
2440
|
+
process2.exitCode = 1;
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
const version = await getVersion();
|
|
2444
|
+
const daemon = new MockMcpDaemon2({
|
|
2445
|
+
projectRoot,
|
|
2446
|
+
token,
|
|
2447
|
+
version,
|
|
2448
|
+
cacheDir
|
|
2449
|
+
});
|
|
2450
|
+
await daemon.start();
|
|
2451
|
+
const shutdown = async (signal) => {
|
|
2452
|
+
console.error(`
|
|
2453
|
+
${signal} received, shutting down...`);
|
|
2454
|
+
await daemon.stop();
|
|
2455
|
+
process2.exit(0);
|
|
2456
|
+
};
|
|
2457
|
+
process2.on("SIGINT", () => shutdown("SIGINT"));
|
|
2458
|
+
process2.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
2459
|
+
}
|
|
2460
|
+
async function runStatusCommand(_args) {
|
|
2461
|
+
const {
|
|
2462
|
+
resolveProjectRoot: resolveProjectRoot2,
|
|
2463
|
+
computeProjectId: computeProjectId2,
|
|
2464
|
+
getPaths: getPaths2,
|
|
2465
|
+
readRegistry: readRegistry3
|
|
2466
|
+
} = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
|
|
2467
|
+
const projectRoot = resolveProjectRoot2();
|
|
2468
|
+
const projectId = computeProjectId2(projectRoot);
|
|
2469
|
+
const { registryPath, ipcPath } = getPaths2(projectId);
|
|
2470
|
+
console.log(`Project Root: ${projectRoot}`);
|
|
2471
|
+
console.log(`Project ID: ${projectId}`);
|
|
2472
|
+
console.log(`IPC Path: ${ipcPath}`);
|
|
2473
|
+
console.log("");
|
|
2474
|
+
const registry = await readRegistry3(registryPath);
|
|
2475
|
+
if (!registry) {
|
|
2476
|
+
console.log("\u274C Daemon is not running (no registry found)");
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
console.log(`Registry PID: ${registry.pid}`);
|
|
2480
|
+
console.log(`Started At: ${registry.startedAt}`);
|
|
2481
|
+
console.log("");
|
|
2482
|
+
try {
|
|
2483
|
+
const status = await getDaemonStatus(ipcPath, registry.token);
|
|
2484
|
+
console.log("\u2705 Daemon is running\n");
|
|
2485
|
+
console.log(`Version: ${status.version}`);
|
|
2486
|
+
console.log(`Uptime: ${Math.round(status.uptime / 1e3)}s`);
|
|
2487
|
+
console.log(`Active Runs: ${status.runs}`);
|
|
2488
|
+
console.log(`Pending Batches: ${status.pending}`);
|
|
2489
|
+
console.log(`Claimed Batches: ${status.claimed}`);
|
|
2490
|
+
} catch (error) {
|
|
2491
|
+
console.log("\u274C Daemon is not responding");
|
|
2492
|
+
console.log(` ${error instanceof Error ? error.message : String(error)}`);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
async function getDaemonStatus(ipcPath, token) {
|
|
2496
|
+
return new Promise((resolve, reject) => {
|
|
2497
|
+
const payload = JSON.stringify({
|
|
2498
|
+
jsonrpc: "2.0",
|
|
2499
|
+
id: "status",
|
|
2500
|
+
method: "getStatus",
|
|
2501
|
+
params: {}
|
|
2502
|
+
});
|
|
2503
|
+
const req = http.request(
|
|
2504
|
+
{
|
|
2505
|
+
method: "POST",
|
|
2506
|
+
socketPath: ipcPath,
|
|
2507
|
+
path: "/control",
|
|
2508
|
+
headers: {
|
|
2509
|
+
"content-type": "application/json",
|
|
2510
|
+
"x-mock-mcp-token": token
|
|
2511
|
+
},
|
|
2512
|
+
timeout: 5e3
|
|
2513
|
+
},
|
|
2514
|
+
(res) => {
|
|
2515
|
+
let buf = "";
|
|
2516
|
+
res.on("data", (c) => buf += c);
|
|
2517
|
+
res.on("end", () => {
|
|
2518
|
+
try {
|
|
2519
|
+
const result = JSON.parse(buf);
|
|
2520
|
+
if (result.error) {
|
|
2521
|
+
reject(new Error(result.error.message));
|
|
2522
|
+
} else {
|
|
2523
|
+
resolve(result.result);
|
|
2524
|
+
}
|
|
2525
|
+
} catch (e) {
|
|
2526
|
+
reject(e);
|
|
2527
|
+
}
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
);
|
|
2531
|
+
req.on("error", reject);
|
|
2532
|
+
req.on("timeout", () => {
|
|
2533
|
+
req.destroy();
|
|
2534
|
+
reject(new Error("Request timeout"));
|
|
2535
|
+
});
|
|
2536
|
+
req.end(payload);
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
async function runStopCommand(_args) {
|
|
2540
|
+
const {
|
|
2541
|
+
resolveProjectRoot: resolveProjectRoot2,
|
|
2542
|
+
computeProjectId: computeProjectId2,
|
|
2543
|
+
getPaths: getPaths2,
|
|
2544
|
+
readRegistry: readRegistry3
|
|
2545
|
+
} = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
|
|
2546
|
+
const projectRoot = resolveProjectRoot2();
|
|
2547
|
+
const projectId = computeProjectId2(projectRoot);
|
|
2548
|
+
const { registryPath, ipcPath } = getPaths2(projectId);
|
|
2549
|
+
const registry = await readRegistry3(registryPath);
|
|
2550
|
+
if (!registry) {
|
|
2551
|
+
console.log("Daemon is not running.");
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
try {
|
|
2555
|
+
process2.kill(registry.pid, "SIGTERM");
|
|
2556
|
+
console.log(`Sent SIGTERM to daemon (PID: ${registry.pid})`);
|
|
2557
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
2558
|
+
try {
|
|
2559
|
+
process2.kill(registry.pid, 0);
|
|
2560
|
+
console.log("Daemon is still running, sending SIGKILL...");
|
|
2561
|
+
process2.kill(registry.pid, "SIGKILL");
|
|
2562
|
+
} catch {
|
|
2563
|
+
}
|
|
2564
|
+
} catch (error) {
|
|
2565
|
+
console.log(`Daemon process (PID: ${registry.pid}) is not running.`);
|
|
2566
|
+
}
|
|
2567
|
+
try {
|
|
2568
|
+
await fs.rm(registryPath);
|
|
2569
|
+
console.log("Registry cleaned up.");
|
|
2570
|
+
} catch {
|
|
2571
|
+
}
|
|
2572
|
+
if (process2.platform !== "win32") {
|
|
2573
|
+
try {
|
|
2574
|
+
await fs.rm(ipcPath);
|
|
2575
|
+
console.log("Socket cleaned up.");
|
|
2576
|
+
} catch {
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
console.log("Done.");
|
|
2580
|
+
}
|
|
2581
|
+
function printHelp() {
|
|
2582
|
+
console.log(`
|
|
2583
|
+
mock-mcp - AI-assisted mock generation for integration tests
|
|
2584
|
+
|
|
2585
|
+
USAGE:
|
|
2586
|
+
mock-mcp [command] [options]
|
|
2587
|
+
|
|
2588
|
+
COMMANDS:
|
|
2589
|
+
adapter Start the MCP adapter (default)
|
|
2590
|
+
This is what you configure in your MCP client.
|
|
2591
|
+
The adapter automatically discovers ALL active daemons
|
|
2592
|
+
across all projects - no configuration needed!
|
|
2593
|
+
|
|
2594
|
+
daemon Start the daemon process
|
|
2595
|
+
Usually auto-started by adapter/test code.
|
|
2596
|
+
|
|
2597
|
+
status Show daemon status for current project
|
|
2598
|
+
|
|
2599
|
+
stop Stop the daemon for current project
|
|
2600
|
+
|
|
2601
|
+
help Show this help message
|
|
2602
|
+
|
|
2603
|
+
version Show version
|
|
2604
|
+
|
|
2605
|
+
EXAMPLES:
|
|
2606
|
+
# In your MCP client configuration (Cursor, Claude Desktop, etc.):
|
|
2607
|
+
# Simple configuration - works across all projects automatically!
|
|
2608
|
+
{
|
|
2609
|
+
"mcpServers": {
|
|
2610
|
+
"mock-mcp": {
|
|
2611
|
+
"command": "npx",
|
|
2612
|
+
"args": ["-y", "mock-mcp", "adapter"]
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
# Check daemon status:
|
|
2618
|
+
mock-mcp status
|
|
2619
|
+
|
|
2620
|
+
# Stop daemon:
|
|
2621
|
+
mock-mcp stop
|
|
2622
|
+
|
|
2623
|
+
HOW IT WORKS:
|
|
2624
|
+
1. Run your tests with MOCK_MCP=1 to start a daemon and make mock requests
|
|
2625
|
+
2. The MCP adapter discovers all active daemons automatically
|
|
2626
|
+
3. Use list_runs/claim_next_batch tools from any MCP client to provide mocks
|
|
2627
|
+
|
|
2628
|
+
ENVIRONMENT:
|
|
2629
|
+
MOCK_MCP=1 Enable mock generation in test code
|
|
2630
|
+
MOCK_MCP_CACHE_DIR Override cache directory for daemon files
|
|
2631
|
+
|
|
2632
|
+
For more information, visit: https://github.com/mcpland/mock-mcp
|
|
2633
|
+
`);
|
|
2634
|
+
}
|
|
2635
|
+
async function printVersion() {
|
|
2636
|
+
const version = await getVersion();
|
|
2637
|
+
console.log(`mock-mcp v${version}`);
|
|
2638
|
+
}
|
|
2639
|
+
async function getVersion() {
|
|
2640
|
+
return "0.5.0";
|
|
2641
|
+
}
|
|
2642
|
+
var isCliExecution = (() => {
|
|
2643
|
+
if (typeof process2 === "undefined" || !process2.argv?.[1]) {
|
|
2644
|
+
return false;
|
|
2645
|
+
}
|
|
2646
|
+
const scriptPath = realpathSync(process2.argv[1]);
|
|
2647
|
+
return import.meta.url === pathToFileURL(scriptPath).href;
|
|
49
2648
|
})();
|
|
50
2649
|
if (isCliExecution) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
2650
|
+
runCli().catch((error) => {
|
|
2651
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
2652
|
+
process2.exitCode = 1;
|
|
2653
|
+
});
|
|
55
2654
|
}
|
|
56
|
-
|
|
57
|
-
export { BatchMockCollector };
|
|
58
|
-
export { connect };
|
|
2655
|
+
|
|
2656
|
+
export { BatchMockCollector, DaemonClient, MockMcpDaemon, MultiDaemonClient, cleanupGlobalIndex, computeProjectId, connect, discoverAllDaemons, ensureDaemonRunning, readGlobalIndex, resolveProjectRoot, runAdapter };
|