mock-mcp 0.5.0 → 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/dist/adapter/index.cjs +465 -302
- package/dist/adapter/index.d.cts +93 -6
- package/dist/adapter/index.d.ts +93 -6
- package/dist/adapter/index.js +465 -302
- package/dist/client/connect.cjs +83 -5
- package/dist/client/connect.d.cts +10 -3
- package/dist/client/connect.d.ts +10 -3
- package/dist/client/connect.js +82 -4
- package/dist/client/index.cjs +83 -5
- package/dist/client/index.d.cts +1 -2
- package/dist/client/index.d.ts +1 -2
- package/dist/client/index.js +82 -4
- package/dist/daemon/index.cjs +55 -5
- package/dist/daemon/index.js +54 -4
- package/dist/index.cjs +559 -89
- package/dist/index.d.cts +137 -7
- package/dist/index.d.ts +137 -7
- package/dist/index.js +556 -90
- package/dist/shared/index.cjs +121 -1
- package/dist/shared/index.d.cts +240 -3
- package/dist/shared/index.d.ts +240 -3
- package/dist/shared/index.js +115 -2
- package/dist/{discovery-Dc2LdF8q.d.cts → types-bEGXLBF0.d.cts} +86 -1
- package/dist/{discovery-Dc2LdF8q.d.ts → types-bEGXLBF0.d.ts} +86 -1
- package/package.json +2 -1
- package/dist/protocol-CiwaQFOt.d.ts +0 -239
- package/dist/protocol-xZu-wb0n.d.cts +0 -239
- package/dist/types-BKREdsyr.d.cts +0 -32
- package/dist/types-BKREdsyr.d.ts +0 -32
package/dist/adapter/index.js
CHANGED
|
@@ -1,111 +1,20 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
-
import crypto2 from 'crypto';
|
|
5
4
|
import http from 'http';
|
|
5
|
+
import crypto2 from 'crypto';
|
|
6
6
|
import fs from 'fs/promises';
|
|
7
|
-
import
|
|
7
|
+
import 'fs';
|
|
8
8
|
import os from 'os';
|
|
9
9
|
import path from 'path';
|
|
10
|
-
import
|
|
11
|
-
import { fileURLToPath
|
|
12
|
-
import
|
|
10
|
+
import 'child_process';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import 'module';
|
|
13
13
|
|
|
14
14
|
// src/adapter/adapter.ts
|
|
15
|
-
var DaemonClient = class {
|
|
16
|
-
constructor(ipcPath, token, adapterId) {
|
|
17
|
-
this.ipcPath = ipcPath;
|
|
18
|
-
this.token = token;
|
|
19
|
-
this.adapterId = adapterId;
|
|
20
|
-
}
|
|
21
|
-
// ===========================================================================
|
|
22
|
-
// RPC Methods
|
|
23
|
-
// ===========================================================================
|
|
24
|
-
async getStatus() {
|
|
25
|
-
return this.rpc("getStatus", {});
|
|
26
|
-
}
|
|
27
|
-
async listRuns() {
|
|
28
|
-
return this.rpc("listRuns", {});
|
|
29
|
-
}
|
|
30
|
-
async claimNextBatch(args) {
|
|
31
|
-
return this.rpc("claimNextBatch", {
|
|
32
|
-
adapterId: this.adapterId,
|
|
33
|
-
runId: args.runId,
|
|
34
|
-
leaseMs: args.leaseMs
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
async provideBatch(args) {
|
|
38
|
-
return this.rpc("provideBatch", {
|
|
39
|
-
adapterId: this.adapterId,
|
|
40
|
-
batchId: args.batchId,
|
|
41
|
-
claimToken: args.claimToken,
|
|
42
|
-
mocks: args.mocks
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
async releaseBatch(args) {
|
|
46
|
-
return this.rpc("releaseBatch", {
|
|
47
|
-
adapterId: this.adapterId,
|
|
48
|
-
batchId: args.batchId,
|
|
49
|
-
claimToken: args.claimToken,
|
|
50
|
-
reason: args.reason
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
async getBatch(batchId) {
|
|
54
|
-
return this.rpc("getBatch", { batchId });
|
|
55
|
-
}
|
|
56
|
-
// ===========================================================================
|
|
57
|
-
// Internal
|
|
58
|
-
// ===========================================================================
|
|
59
|
-
rpc(method, params) {
|
|
60
|
-
const payload = {
|
|
61
|
-
jsonrpc: "2.0",
|
|
62
|
-
id: crypto2.randomUUID(),
|
|
63
|
-
method,
|
|
64
|
-
params
|
|
65
|
-
};
|
|
66
|
-
return new Promise((resolve, reject) => {
|
|
67
|
-
const req = http.request(
|
|
68
|
-
{
|
|
69
|
-
method: "POST",
|
|
70
|
-
socketPath: this.ipcPath,
|
|
71
|
-
path: "/control",
|
|
72
|
-
headers: {
|
|
73
|
-
"content-type": "application/json",
|
|
74
|
-
"x-mock-mcp-token": this.token
|
|
75
|
-
},
|
|
76
|
-
timeout: 3e4
|
|
77
|
-
},
|
|
78
|
-
(res) => {
|
|
79
|
-
let buf = "";
|
|
80
|
-
res.on("data", (chunk) => buf += chunk);
|
|
81
|
-
res.on("end", () => {
|
|
82
|
-
try {
|
|
83
|
-
const response = JSON.parse(buf);
|
|
84
|
-
if (response.error) {
|
|
85
|
-
reject(new Error(response.error.message));
|
|
86
|
-
} else {
|
|
87
|
-
resolve(response.result);
|
|
88
|
-
}
|
|
89
|
-
} catch (e) {
|
|
90
|
-
reject(e);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
);
|
|
95
|
-
req.on("error", (err) => {
|
|
96
|
-
reject(new Error(`Daemon connection failed: ${err.message}`));
|
|
97
|
-
});
|
|
98
|
-
req.on("timeout", () => {
|
|
99
|
-
req.destroy();
|
|
100
|
-
reject(new Error("Daemon request timeout"));
|
|
101
|
-
});
|
|
102
|
-
req.end(JSON.stringify(payload));
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
15
|
function debugLog(_msg) {
|
|
107
16
|
}
|
|
108
|
-
|
|
17
|
+
(() => {
|
|
109
18
|
try {
|
|
110
19
|
const metaUrl = import.meta.url;
|
|
111
20
|
if (metaUrl && typeof metaUrl === "string" && metaUrl.startsWith("file://")) {
|
|
@@ -115,32 +24,6 @@ var __curDirname = (() => {
|
|
|
115
24
|
}
|
|
116
25
|
return process.cwd();
|
|
117
26
|
})();
|
|
118
|
-
function resolveProjectRoot(startDir = process.cwd()) {
|
|
119
|
-
let current = path.resolve(startDir);
|
|
120
|
-
const root = path.parse(current).root;
|
|
121
|
-
while (current !== root) {
|
|
122
|
-
const gitPath = path.join(current, ".git");
|
|
123
|
-
try {
|
|
124
|
-
const stat = fssync.statSync(gitPath);
|
|
125
|
-
if (stat.isDirectory() || stat.isFile()) {
|
|
126
|
-
return current;
|
|
127
|
-
}
|
|
128
|
-
} catch {
|
|
129
|
-
}
|
|
130
|
-
const pkgPath = path.join(current, "package.json");
|
|
131
|
-
try {
|
|
132
|
-
fssync.accessSync(pkgPath, fssync.constants.F_OK);
|
|
133
|
-
return current;
|
|
134
|
-
} catch {
|
|
135
|
-
}
|
|
136
|
-
current = path.dirname(current);
|
|
137
|
-
}
|
|
138
|
-
return path.resolve(startDir);
|
|
139
|
-
}
|
|
140
|
-
function computeProjectId(projectRoot) {
|
|
141
|
-
const real = fssync.realpathSync(projectRoot);
|
|
142
|
-
return crypto2.createHash("sha256").update(real).digest("hex").slice(0, 16);
|
|
143
|
-
}
|
|
144
27
|
function getCacheDir(override) {
|
|
145
28
|
if (override) {
|
|
146
29
|
return override;
|
|
@@ -162,13 +45,6 @@ function getCacheDir(override) {
|
|
|
162
45
|
}
|
|
163
46
|
return os.tmpdir();
|
|
164
47
|
}
|
|
165
|
-
function getPaths(projectId, cacheDir) {
|
|
166
|
-
const base = path.join(getCacheDir(cacheDir), "mock-mcp");
|
|
167
|
-
const registryPath = path.join(base, `${projectId}.json`);
|
|
168
|
-
const lockPath = path.join(base, `${projectId}.lock`);
|
|
169
|
-
const ipcPath = process.platform === "win32" ? `\\\\.\\pipe\\mock-mcp-${projectId}` : path.join(base, `${projectId}.sock`);
|
|
170
|
-
return { base, registryPath, lockPath, ipcPath };
|
|
171
|
-
}
|
|
172
48
|
async function readRegistry(registryPath) {
|
|
173
49
|
try {
|
|
174
50
|
const txt = await fs.readFile(registryPath, "utf-8");
|
|
@@ -199,157 +75,310 @@ async function healthCheck(ipcPath, timeoutMs = 2e3) {
|
|
|
199
75
|
req.end();
|
|
200
76
|
});
|
|
201
77
|
}
|
|
202
|
-
|
|
78
|
+
function getGlobalIndexPath(cacheDir) {
|
|
79
|
+
const base = path.join(getCacheDir(cacheDir), "mock-mcp");
|
|
80
|
+
return path.join(base, "active-daemons.json");
|
|
81
|
+
}
|
|
82
|
+
async function readGlobalIndex(cacheDir) {
|
|
83
|
+
const indexPath = getGlobalIndexPath(cacheDir);
|
|
203
84
|
try {
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
`);
|
|
207
|
-
return fh;
|
|
85
|
+
const txt = await fs.readFile(indexPath, "utf-8");
|
|
86
|
+
return JSON.parse(txt);
|
|
208
87
|
} catch {
|
|
209
|
-
return
|
|
88
|
+
return { daemons: [], updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
210
89
|
}
|
|
211
90
|
}
|
|
212
|
-
async function
|
|
213
|
-
|
|
214
|
-
|
|
91
|
+
async function writeGlobalIndex(index, cacheDir) {
|
|
92
|
+
const indexPath = getGlobalIndexPath(cacheDir);
|
|
93
|
+
const base = path.dirname(indexPath);
|
|
94
|
+
await fs.mkdir(base, { recursive: true });
|
|
95
|
+
await fs.writeFile(indexPath, JSON.stringify(index, null, 2), {
|
|
96
|
+
encoding: "utf-8",
|
|
97
|
+
mode: 384
|
|
215
98
|
});
|
|
216
99
|
}
|
|
217
|
-
function
|
|
218
|
-
|
|
100
|
+
async function cleanupGlobalIndex(cacheDir) {
|
|
101
|
+
const index = await readGlobalIndex(cacheDir);
|
|
102
|
+
const validDaemons = [];
|
|
103
|
+
for (const entry of index.daemons) {
|
|
104
|
+
try {
|
|
105
|
+
process.kill(entry.pid, 0);
|
|
106
|
+
const healthy = await healthCheck(entry.ipcPath, 1e3);
|
|
107
|
+
if (healthy) {
|
|
108
|
+
validDaemons.push(entry);
|
|
109
|
+
} else {
|
|
110
|
+
debugLog(`Removing unhealthy daemon ${entry.projectId} (pid ${entry.pid})`);
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
debugLog(`Removing dead daemon ${entry.projectId} (pid ${entry.pid})`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (validDaemons.length !== index.daemons.length) {
|
|
117
|
+
index.daemons = validDaemons;
|
|
118
|
+
index.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
119
|
+
await writeGlobalIndex(index, cacheDir);
|
|
120
|
+
}
|
|
219
121
|
}
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
if (
|
|
227
|
-
|
|
122
|
+
async function discoverAllDaemons(cacheDir) {
|
|
123
|
+
await cleanupGlobalIndex(cacheDir);
|
|
124
|
+
const index = await readGlobalIndex(cacheDir);
|
|
125
|
+
const results = [];
|
|
126
|
+
for (const entry of index.daemons) {
|
|
127
|
+
const registry = await readRegistry(entry.registryPath);
|
|
128
|
+
if (registry) {
|
|
129
|
+
const healthy = await healthCheck(entry.ipcPath, 2e3);
|
|
130
|
+
results.push({ registry, healthy });
|
|
228
131
|
}
|
|
229
|
-
} catch {
|
|
230
132
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
133
|
+
return results;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/adapter/multi-daemon-client.ts
|
|
137
|
+
var MultiDaemonClient = class {
|
|
138
|
+
logger;
|
|
139
|
+
cacheDir;
|
|
140
|
+
adapterId;
|
|
141
|
+
constructor(opts = {}) {
|
|
142
|
+
this.logger = opts.logger ?? console;
|
|
143
|
+
this.cacheDir = opts.cacheDir;
|
|
144
|
+
this.adapterId = crypto2.randomUUID();
|
|
145
|
+
}
|
|
146
|
+
// ===========================================================================
|
|
147
|
+
// Discovery
|
|
148
|
+
// ===========================================================================
|
|
149
|
+
/**
|
|
150
|
+
* Discover all active and healthy daemons.
|
|
151
|
+
*/
|
|
152
|
+
async discoverDaemons() {
|
|
153
|
+
return discoverAllDaemons(this.cacheDir);
|
|
154
|
+
}
|
|
155
|
+
// ===========================================================================
|
|
156
|
+
// Aggregated RPC Methods
|
|
157
|
+
// ===========================================================================
|
|
158
|
+
/**
|
|
159
|
+
* Get aggregated status from all daemons.
|
|
160
|
+
*/
|
|
161
|
+
async getAggregatedStatus() {
|
|
162
|
+
const daemons = await this.discoverDaemons();
|
|
163
|
+
const statuses = [];
|
|
164
|
+
let totalRuns = 0;
|
|
165
|
+
let totalPending = 0;
|
|
166
|
+
let totalClaimed = 0;
|
|
167
|
+
for (const { registry, healthy } of daemons) {
|
|
168
|
+
if (!healthy) {
|
|
169
|
+
statuses.push({
|
|
170
|
+
version: registry.version,
|
|
171
|
+
projectId: registry.projectId,
|
|
172
|
+
projectRoot: registry.projectRoot,
|
|
173
|
+
pid: registry.pid,
|
|
174
|
+
uptime: 0,
|
|
175
|
+
runs: 0,
|
|
176
|
+
pending: 0,
|
|
177
|
+
claimed: 0,
|
|
178
|
+
totalBatches: 0,
|
|
179
|
+
healthy: false
|
|
180
|
+
});
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const status = await this.rpc(registry, "getStatus", {});
|
|
185
|
+
statuses.push({ ...status, healthy: true });
|
|
186
|
+
totalRuns += status.runs;
|
|
187
|
+
totalPending += status.pending;
|
|
188
|
+
totalClaimed += status.claimed;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
this.logger.warn(`Failed to get status from daemon ${registry.projectId}: ${error}`);
|
|
191
|
+
statuses.push({
|
|
192
|
+
version: registry.version,
|
|
193
|
+
projectId: registry.projectId,
|
|
194
|
+
projectRoot: registry.projectRoot,
|
|
195
|
+
pid: registry.pid,
|
|
196
|
+
uptime: 0,
|
|
197
|
+
runs: 0,
|
|
198
|
+
pending: 0,
|
|
199
|
+
claimed: 0,
|
|
200
|
+
totalBatches: 0,
|
|
201
|
+
healthy: false
|
|
202
|
+
});
|
|
203
|
+
}
|
|
236
204
|
}
|
|
237
|
-
|
|
205
|
+
return { daemons: statuses, totalRuns, totalPending, totalClaimed };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* List all runs across all daemons.
|
|
209
|
+
*/
|
|
210
|
+
async listAllRuns() {
|
|
211
|
+
const daemons = await this.discoverDaemons();
|
|
212
|
+
const allRuns = [];
|
|
213
|
+
for (const { registry, healthy } of daemons) {
|
|
214
|
+
if (!healthy) continue;
|
|
215
|
+
try {
|
|
216
|
+
const result = await this.rpc(registry, "listRuns", {});
|
|
217
|
+
for (const run of result.runs) {
|
|
218
|
+
allRuns.push({
|
|
219
|
+
...run,
|
|
220
|
+
projectId: registry.projectId,
|
|
221
|
+
projectRoot: registry.projectRoot
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
this.logger.warn(`Failed to list runs from daemon ${registry.projectId}: ${error}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return allRuns;
|
|
238
229
|
}
|
|
239
|
-
|
|
240
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Claim the next available batch from any daemon.
|
|
232
|
+
* Searches through all daemons in order until finding one with a pending batch.
|
|
233
|
+
*/
|
|
234
|
+
async claimNextBatch(args) {
|
|
235
|
+
const daemons = await this.discoverDaemons();
|
|
236
|
+
for (const { registry, healthy } of daemons) {
|
|
237
|
+
if (!healthy) continue;
|
|
238
|
+
try {
|
|
239
|
+
const result = await this.rpc(registry, "claimNextBatch", {
|
|
240
|
+
adapterId: this.adapterId,
|
|
241
|
+
runId: args.runId,
|
|
242
|
+
leaseMs: args.leaseMs
|
|
243
|
+
});
|
|
244
|
+
if (result) {
|
|
245
|
+
return {
|
|
246
|
+
...result,
|
|
247
|
+
projectId: registry.projectId,
|
|
248
|
+
projectRoot: registry.projectRoot
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
this.logger.warn(`Failed to claim batch from daemon ${registry.projectId}: ${error}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
241
256
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
);
|
|
251
|
-
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
252
|
-
await fs.mkdir(base, { recursive: true });
|
|
253
|
-
const existing = await readRegistry(registryPath);
|
|
254
|
-
debugLog(`Registry read result: ${existing ? "Found (PID " + existing.pid + ")" : "Null"}`);
|
|
255
|
-
if (existing) {
|
|
256
|
-
let healthy = false;
|
|
257
|
-
for (let i = 0; i < 3; i++) {
|
|
258
|
-
debugLog(`Checking health attempt ${i + 1}/3 on ${existing.ipcPath}`);
|
|
259
|
-
healthy = await healthCheck(existing.ipcPath);
|
|
260
|
-
if (healthy) break;
|
|
261
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
257
|
+
/**
|
|
258
|
+
* Provide mock data for a batch.
|
|
259
|
+
* Automatically routes to the correct daemon based on batchId.
|
|
260
|
+
*/
|
|
261
|
+
async provideBatch(args) {
|
|
262
|
+
const parts = args.batchId.split(":");
|
|
263
|
+
if (parts.length < 2) {
|
|
264
|
+
return { ok: false, message: `Invalid batchId format: ${args.batchId}` };
|
|
262
265
|
}
|
|
263
|
-
|
|
264
|
-
|
|
266
|
+
const daemons = await this.discoverDaemons();
|
|
267
|
+
for (const { registry, healthy } of daemons) {
|
|
268
|
+
if (!healthy) continue;
|
|
269
|
+
try {
|
|
270
|
+
const result = await this.rpc(registry, "provideBatch", {
|
|
271
|
+
adapterId: this.adapterId,
|
|
272
|
+
batchId: args.batchId,
|
|
273
|
+
claimToken: args.claimToken,
|
|
274
|
+
mocks: args.mocks
|
|
275
|
+
});
|
|
276
|
+
return result;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
279
|
+
if (msg.includes("not found") || msg.includes("Not found")) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
return { ok: false, message: msg };
|
|
283
|
+
}
|
|
265
284
|
}
|
|
285
|
+
return { ok: false, message: `Batch not found: ${args.batchId}` };
|
|
266
286
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
287
|
+
/**
|
|
288
|
+
* Release a batch.
|
|
289
|
+
*/
|
|
290
|
+
async releaseBatch(args) {
|
|
291
|
+
const daemons = await this.discoverDaemons();
|
|
292
|
+
for (const { registry, healthy } of daemons) {
|
|
293
|
+
if (!healthy) continue;
|
|
294
|
+
try {
|
|
295
|
+
const result = await this.rpc(registry, "releaseBatch", {
|
|
296
|
+
adapterId: this.adapterId,
|
|
297
|
+
batchId: args.batchId,
|
|
298
|
+
claimToken: args.claimToken,
|
|
299
|
+
reason: args.reason
|
|
300
|
+
});
|
|
301
|
+
return result;
|
|
302
|
+
} catch (error) {
|
|
303
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
304
|
+
if (msg.includes("not found") || msg.includes("Not found")) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
return { ok: false, message: msg };
|
|
308
|
+
}
|
|
271
309
|
}
|
|
310
|
+
return { ok: false, message: `Batch not found: ${args.batchId}` };
|
|
272
311
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
312
|
+
/**
|
|
313
|
+
* Get a specific batch by ID.
|
|
314
|
+
*/
|
|
315
|
+
async getBatch(batchId) {
|
|
316
|
+
const daemons = await this.discoverDaemons();
|
|
317
|
+
for (const { registry, healthy } of daemons) {
|
|
318
|
+
if (!healthy) continue;
|
|
319
|
+
try {
|
|
320
|
+
const result = await this.rpc(registry, "getBatch", { batchId });
|
|
321
|
+
return result;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
324
|
+
if (msg.includes("not found") || msg.includes("Not found")) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
throw error;
|
|
279
328
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
// ===========================================================================
|
|
333
|
+
// Internal RPC
|
|
334
|
+
// ===========================================================================
|
|
335
|
+
rpc(registry, method, params) {
|
|
336
|
+
const payload = {
|
|
337
|
+
jsonrpc: "2.0",
|
|
338
|
+
id: crypto2.randomUUID(),
|
|
339
|
+
method,
|
|
340
|
+
params
|
|
341
|
+
};
|
|
342
|
+
return new Promise((resolve, reject) => {
|
|
343
|
+
const req = http.request(
|
|
285
344
|
{
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
345
|
+
method: "POST",
|
|
346
|
+
socketPath: registry.ipcPath,
|
|
347
|
+
path: "/control",
|
|
348
|
+
headers: {
|
|
349
|
+
"content-type": "application/json",
|
|
350
|
+
"x-mock-mcp-token": registry.token
|
|
351
|
+
},
|
|
352
|
+
timeout: 3e4
|
|
353
|
+
},
|
|
354
|
+
(res) => {
|
|
355
|
+
let buf = "";
|
|
356
|
+
res.on("data", (chunk) => buf += chunk);
|
|
357
|
+
res.on("end", () => {
|
|
358
|
+
try {
|
|
359
|
+
const response = JSON.parse(buf);
|
|
360
|
+
if (response.error) {
|
|
361
|
+
reject(new Error(response.error.message));
|
|
362
|
+
} else {
|
|
363
|
+
resolve(response.result);
|
|
364
|
+
}
|
|
365
|
+
} catch (e) {
|
|
366
|
+
reject(e);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
292
369
|
}
|
|
293
370
|
);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
child.stdout?.on("data", (data) => {
|
|
297
|
-
const str = data.toString();
|
|
298
|
-
debugLog(`Daemon stdout: ${str}`);
|
|
299
|
-
});
|
|
300
|
-
child.stderr?.on("data", (data) => {
|
|
301
|
-
daemonStderr += data.toString();
|
|
302
|
-
debugLog(`Daemon stderr: ${data.toString()}`);
|
|
303
|
-
});
|
|
304
|
-
child.on("error", (err) => {
|
|
305
|
-
console.error(`[mock-mcp] Daemon spawn error: ${err.message}`);
|
|
371
|
+
req.on("error", (err) => {
|
|
372
|
+
reject(new Error(`Daemon connection failed: ${err.message}`));
|
|
306
373
|
});
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (daemonStderr) {
|
|
311
|
-
console.error(`[mock-mcp] Daemon stderr: ${daemonStderr.slice(0, 500)}`);
|
|
312
|
-
}
|
|
313
|
-
} else if (signal) {
|
|
314
|
-
console.error(`[mock-mcp] Daemon killed by signal: ${signal}`);
|
|
315
|
-
}
|
|
374
|
+
req.on("timeout", () => {
|
|
375
|
+
req.destroy();
|
|
376
|
+
reject(new Error("Daemon request timeout"));
|
|
316
377
|
});
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
while (Date.now() < deadline2) {
|
|
320
|
-
const reg = await readRegistry(registryPath);
|
|
321
|
-
if (reg && await healthCheck(reg.ipcPath)) {
|
|
322
|
-
return reg;
|
|
323
|
-
}
|
|
324
|
-
await sleep(50);
|
|
325
|
-
}
|
|
326
|
-
console.error("[mock-mcp] Daemon failed to start within timeout");
|
|
327
|
-
if (daemonStderr) {
|
|
328
|
-
console.error(`[mock-mcp] Daemon stderr:
|
|
329
|
-
${daemonStderr}`);
|
|
330
|
-
}
|
|
331
|
-
throw new Error(
|
|
332
|
-
`Daemon start timeout after ${timeoutMs}ms. Check logs for details.`
|
|
333
|
-
);
|
|
334
|
-
} finally {
|
|
335
|
-
await releaseLock(lockPath, lock);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
const deadline = Date.now() + timeoutMs;
|
|
339
|
-
while (Date.now() < deadline) {
|
|
340
|
-
const reg = await readRegistry(registryPath);
|
|
341
|
-
if (reg && await healthCheck(reg.ipcPath)) {
|
|
342
|
-
return reg;
|
|
343
|
-
}
|
|
344
|
-
await sleep(50);
|
|
378
|
+
req.end(JSON.stringify(payload));
|
|
379
|
+
});
|
|
345
380
|
}
|
|
346
|
-
|
|
347
|
-
`Waiting for daemon timed out after ${timeoutMs}ms. Another process may have failed to start it.`
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
function sleep(ms) {
|
|
351
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
352
|
-
}
|
|
381
|
+
};
|
|
353
382
|
|
|
354
383
|
// src/adapter/adapter.ts
|
|
355
384
|
var TOOLS = [
|
|
@@ -481,12 +510,19 @@ The mocks array must contain exactly one mock for each request in the batch.`,
|
|
|
481
510
|
];
|
|
482
511
|
async function runAdapter(opts = {}) {
|
|
483
512
|
const logger = opts.logger ?? console;
|
|
484
|
-
const version = opts.version ?? "0.
|
|
485
|
-
logger.error("\u{1F50D}
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
513
|
+
const version = opts.version ?? "0.5.0";
|
|
514
|
+
logger.error("\u{1F50D} Initializing mock-mcp adapter (multi-daemon mode)...");
|
|
515
|
+
const multiDaemon = new MultiDaemonClient({ logger });
|
|
516
|
+
const daemons = await multiDaemon.discoverDaemons();
|
|
517
|
+
if (daemons.length > 0) {
|
|
518
|
+
logger.error(`\u2705 Found ${daemons.length} active daemon(s):`);
|
|
519
|
+
for (const d of daemons) {
|
|
520
|
+
const status = d.healthy ? "healthy" : "unhealthy";
|
|
521
|
+
logger.error(` - ${d.registry.projectId}: ${d.registry.projectRoot} (${status})`);
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
logger.error("\u2139\uFE0F No active daemons found. Waiting for test processes to start...");
|
|
525
|
+
}
|
|
490
526
|
const server = new Server(
|
|
491
527
|
{
|
|
492
528
|
name: "mock-mcp-adapter",
|
|
@@ -504,15 +540,15 @@ async function runAdapter(opts = {}) {
|
|
|
504
540
|
try {
|
|
505
541
|
switch (name) {
|
|
506
542
|
case "get_status": {
|
|
507
|
-
const result = await
|
|
508
|
-
return buildToolResponse(
|
|
543
|
+
const result = await multiDaemon.getAggregatedStatus();
|
|
544
|
+
return buildToolResponse(formatAggregatedStatus(result));
|
|
509
545
|
}
|
|
510
546
|
case "list_runs": {
|
|
511
|
-
const result = await
|
|
512
|
-
return buildToolResponse(
|
|
547
|
+
const result = await multiDaemon.listAllRuns();
|
|
548
|
+
return buildToolResponse(formatExtendedRuns(result));
|
|
513
549
|
}
|
|
514
550
|
case "claim_next_batch": {
|
|
515
|
-
const result = await
|
|
551
|
+
const result = await multiDaemon.claimNextBatch({
|
|
516
552
|
runId: args?.runId,
|
|
517
553
|
leaseMs: args?.leaseMs
|
|
518
554
|
});
|
|
@@ -522,14 +558,17 @@ async function runAdapter(opts = {}) {
|
|
|
522
558
|
if (!args?.batchId) {
|
|
523
559
|
throw new Error("batchId is required");
|
|
524
560
|
}
|
|
525
|
-
const result = await
|
|
561
|
+
const result = await multiDaemon.getBatch(args.batchId);
|
|
562
|
+
if (!result) {
|
|
563
|
+
throw new Error(`Batch not found: ${args.batchId}`);
|
|
564
|
+
}
|
|
526
565
|
return buildToolResponse(formatBatch(result));
|
|
527
566
|
}
|
|
528
567
|
case "provide_batch_mock_data": {
|
|
529
568
|
if (!args?.batchId || !args?.claimToken || !args?.mocks) {
|
|
530
569
|
throw new Error("batchId, claimToken, and mocks are required");
|
|
531
570
|
}
|
|
532
|
-
const result = await
|
|
571
|
+
const result = await multiDaemon.provideBatch({
|
|
533
572
|
batchId: args.batchId,
|
|
534
573
|
claimToken: args.claimToken,
|
|
535
574
|
mocks: args.mocks
|
|
@@ -540,7 +579,7 @@ async function runAdapter(opts = {}) {
|
|
|
540
579
|
if (!args?.batchId || !args?.claimToken) {
|
|
541
580
|
throw new Error("batchId and claimToken are required");
|
|
542
581
|
}
|
|
543
|
-
const result = await
|
|
582
|
+
const result = await multiDaemon.releaseBatch({
|
|
544
583
|
batchId: args.batchId,
|
|
545
584
|
claimToken: args.claimToken,
|
|
546
585
|
reason: args?.reason
|
|
@@ -552,7 +591,7 @@ async function runAdapter(opts = {}) {
|
|
|
552
591
|
}
|
|
553
592
|
} catch (error) {
|
|
554
593
|
const message = error instanceof Error ? error.message : String(error);
|
|
555
|
-
logger.error(`Tool error (${name})
|
|
594
|
+
logger.error(`Tool error (${name}): ${message}`);
|
|
556
595
|
return buildToolResponse(`Error: ${message}`, true);
|
|
557
596
|
}
|
|
558
597
|
});
|
|
@@ -566,53 +605,86 @@ function buildToolResponse(text, isError = false) {
|
|
|
566
605
|
isError
|
|
567
606
|
};
|
|
568
607
|
}
|
|
569
|
-
function
|
|
570
|
-
|
|
608
|
+
function formatAggregatedStatus(status) {
|
|
609
|
+
if (status.daemons.length === 0) {
|
|
610
|
+
return `# Mock MCP Status
|
|
571
611
|
|
|
572
|
-
|
|
573
|
-
- **Project ID**: ${status.projectId}
|
|
574
|
-
- **Project Root**: ${status.projectRoot}
|
|
575
|
-
- **PID**: ${status.pid}
|
|
576
|
-
- **Uptime**: ${Math.round(status.uptime / 1e3)}s
|
|
577
|
-
|
|
578
|
-
## Batches
|
|
579
|
-
- **Pending**: ${status.pending}
|
|
580
|
-
- **Claimed**: ${status.claimed}
|
|
581
|
-
- **Active Runs**: ${status.runs}
|
|
612
|
+
No active daemons found. Start a test with \`MOCK_MCP=1\` to begin.
|
|
582
613
|
`;
|
|
614
|
+
}
|
|
615
|
+
const lines = [
|
|
616
|
+
"# Mock MCP Status\n",
|
|
617
|
+
"## Summary",
|
|
618
|
+
`- **Active Daemons**: ${status.daemons.filter((d) => d.healthy).length}`,
|
|
619
|
+
`- **Total Active Runs**: ${status.totalRuns}`,
|
|
620
|
+
`- **Total Pending Batches**: ${status.totalPending}`,
|
|
621
|
+
`- **Total Claimed Batches**: ${status.totalClaimed}`,
|
|
622
|
+
"",
|
|
623
|
+
"## Daemons\n"
|
|
624
|
+
];
|
|
625
|
+
for (const daemon of status.daemons) {
|
|
626
|
+
const healthIcon = daemon.healthy ? "\u2705" : "\u274C";
|
|
627
|
+
lines.push(`### ${healthIcon} ${daemon.projectRoot}`);
|
|
628
|
+
lines.push(`- **Project ID**: ${daemon.projectId}`);
|
|
629
|
+
lines.push(`- **Version**: ${daemon.version}`);
|
|
630
|
+
lines.push(`- **PID**: ${daemon.pid}`);
|
|
631
|
+
if (daemon.healthy) {
|
|
632
|
+
lines.push(`- **Uptime**: ${Math.round(daemon.uptime / 1e3)}s`);
|
|
633
|
+
lines.push(`- **Runs**: ${daemon.runs}`);
|
|
634
|
+
lines.push(`- **Pending**: ${daemon.pending}`);
|
|
635
|
+
lines.push(`- **Claimed**: ${daemon.claimed}`);
|
|
636
|
+
} else {
|
|
637
|
+
lines.push(`- **Status**: Not responding`);
|
|
638
|
+
}
|
|
639
|
+
lines.push("");
|
|
640
|
+
}
|
|
641
|
+
return lines.join("\n");
|
|
583
642
|
}
|
|
584
|
-
function
|
|
585
|
-
if (
|
|
586
|
-
return "No active test runs.";
|
|
643
|
+
function formatExtendedRuns(runs) {
|
|
644
|
+
if (runs.length === 0) {
|
|
645
|
+
return "No active test runs.\n\nStart a test with `MOCK_MCP=1` to begin.";
|
|
587
646
|
}
|
|
588
647
|
const lines = ["# Active Test Runs\n"];
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
648
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
649
|
+
for (const run of runs) {
|
|
650
|
+
const key = run.projectRoot;
|
|
651
|
+
if (!byProject.has(key)) {
|
|
652
|
+
byProject.set(key, []);
|
|
653
|
+
}
|
|
654
|
+
byProject.get(key).push(run);
|
|
655
|
+
}
|
|
656
|
+
for (const [projectRoot, projectRuns] of byProject) {
|
|
657
|
+
lines.push(`## Project: ${projectRoot}
|
|
658
|
+
`);
|
|
659
|
+
for (const run of projectRuns) {
|
|
660
|
+
lines.push(`### Run: ${run.runId}`);
|
|
661
|
+
lines.push(`- **PID**: ${run.pid}`);
|
|
662
|
+
lines.push(`- **CWD**: ${run.cwd}`);
|
|
663
|
+
lines.push(`- **Started**: ${run.startedAt}`);
|
|
664
|
+
lines.push(`- **Pending Batches**: ${run.pendingBatches}`);
|
|
665
|
+
if (run.testMeta) {
|
|
666
|
+
if (run.testMeta.testFile) {
|
|
667
|
+
lines.push(`- **Test File**: ${run.testMeta.testFile}`);
|
|
668
|
+
}
|
|
669
|
+
if (run.testMeta.testName) {
|
|
670
|
+
lines.push(`- **Test Name**: ${run.testMeta.testName}`);
|
|
671
|
+
}
|
|
601
672
|
}
|
|
673
|
+
lines.push("");
|
|
602
674
|
}
|
|
603
|
-
lines.push("");
|
|
604
675
|
}
|
|
605
676
|
return lines.join("\n");
|
|
606
677
|
}
|
|
607
678
|
function formatClaimResult(result) {
|
|
608
679
|
if (!result) {
|
|
609
|
-
return "No pending batches available to claim.";
|
|
680
|
+
return "No pending batches available to claim.\n\nMake sure a test is running with `MOCK_MCP=1` and has pending mock requests.";
|
|
610
681
|
}
|
|
611
682
|
const lines = [
|
|
612
683
|
"# Batch Claimed Successfully\n",
|
|
613
684
|
`**Batch ID**: \`${result.batchId}\``,
|
|
614
685
|
`**Claim Token**: \`${result.claimToken}\``,
|
|
615
686
|
`**Run ID**: ${result.runId}`,
|
|
687
|
+
`**Project**: ${result.projectRoot}`,
|
|
616
688
|
`**Lease Until**: ${new Date(result.leaseUntil).toISOString()}`,
|
|
617
689
|
"",
|
|
618
690
|
"## Requests\n"
|
|
@@ -668,5 +740,96 @@ function formatProvideResult(result) {
|
|
|
668
740
|
}
|
|
669
741
|
return `\u274C Failed to provide mock data: ${result.message}`;
|
|
670
742
|
}
|
|
743
|
+
var DaemonClient = class {
|
|
744
|
+
constructor(ipcPath, token, adapterId) {
|
|
745
|
+
this.ipcPath = ipcPath;
|
|
746
|
+
this.token = token;
|
|
747
|
+
this.adapterId = adapterId;
|
|
748
|
+
}
|
|
749
|
+
// ===========================================================================
|
|
750
|
+
// RPC Methods
|
|
751
|
+
// ===========================================================================
|
|
752
|
+
async getStatus() {
|
|
753
|
+
return this.rpc("getStatus", {});
|
|
754
|
+
}
|
|
755
|
+
async listRuns() {
|
|
756
|
+
return this.rpc("listRuns", {});
|
|
757
|
+
}
|
|
758
|
+
async claimNextBatch(args) {
|
|
759
|
+
return this.rpc("claimNextBatch", {
|
|
760
|
+
adapterId: this.adapterId,
|
|
761
|
+
runId: args.runId,
|
|
762
|
+
leaseMs: args.leaseMs
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
async provideBatch(args) {
|
|
766
|
+
return this.rpc("provideBatch", {
|
|
767
|
+
adapterId: this.adapterId,
|
|
768
|
+
batchId: args.batchId,
|
|
769
|
+
claimToken: args.claimToken,
|
|
770
|
+
mocks: args.mocks
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
async releaseBatch(args) {
|
|
774
|
+
return this.rpc("releaseBatch", {
|
|
775
|
+
adapterId: this.adapterId,
|
|
776
|
+
batchId: args.batchId,
|
|
777
|
+
claimToken: args.claimToken,
|
|
778
|
+
reason: args.reason
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
async getBatch(batchId) {
|
|
782
|
+
return this.rpc("getBatch", { batchId });
|
|
783
|
+
}
|
|
784
|
+
// ===========================================================================
|
|
785
|
+
// Internal
|
|
786
|
+
// ===========================================================================
|
|
787
|
+
rpc(method, params) {
|
|
788
|
+
const payload = {
|
|
789
|
+
jsonrpc: "2.0",
|
|
790
|
+
id: crypto2.randomUUID(),
|
|
791
|
+
method,
|
|
792
|
+
params
|
|
793
|
+
};
|
|
794
|
+
return new Promise((resolve, reject) => {
|
|
795
|
+
const req = http.request(
|
|
796
|
+
{
|
|
797
|
+
method: "POST",
|
|
798
|
+
socketPath: this.ipcPath,
|
|
799
|
+
path: "/control",
|
|
800
|
+
headers: {
|
|
801
|
+
"content-type": "application/json",
|
|
802
|
+
"x-mock-mcp-token": this.token
|
|
803
|
+
},
|
|
804
|
+
timeout: 3e4
|
|
805
|
+
},
|
|
806
|
+
(res) => {
|
|
807
|
+
let buf = "";
|
|
808
|
+
res.on("data", (chunk) => buf += chunk);
|
|
809
|
+
res.on("end", () => {
|
|
810
|
+
try {
|
|
811
|
+
const response = JSON.parse(buf);
|
|
812
|
+
if (response.error) {
|
|
813
|
+
reject(new Error(response.error.message));
|
|
814
|
+
} else {
|
|
815
|
+
resolve(response.result);
|
|
816
|
+
}
|
|
817
|
+
} catch (e) {
|
|
818
|
+
reject(e);
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
);
|
|
823
|
+
req.on("error", (err) => {
|
|
824
|
+
reject(new Error(`Daemon connection failed: ${err.message}`));
|
|
825
|
+
});
|
|
826
|
+
req.on("timeout", () => {
|
|
827
|
+
req.destroy();
|
|
828
|
+
reject(new Error("Daemon request timeout"));
|
|
829
|
+
});
|
|
830
|
+
req.end(JSON.stringify(payload));
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
};
|
|
671
834
|
|
|
672
|
-
export { DaemonClient, runAdapter };
|
|
835
|
+
export { DaemonClient, MultiDaemonClient, runAdapter };
|