mock-mcp 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +217 -128
  2. package/dist/adapter/index.cjs +712 -0
  3. package/dist/adapter/index.d.cts +55 -0
  4. package/dist/adapter/index.d.ts +55 -0
  5. package/dist/adapter/index.js +672 -0
  6. package/dist/client/connect.cjs +913 -0
  7. package/dist/client/connect.d.cts +211 -0
  8. package/dist/client/connect.d.ts +209 -6
  9. package/dist/client/connect.js +867 -10
  10. package/dist/client/index.cjs +914 -0
  11. package/dist/client/index.d.cts +4 -0
  12. package/dist/client/index.d.ts +4 -2
  13. package/dist/client/index.js +873 -2
  14. package/dist/daemon/index.cjs +667 -0
  15. package/dist/daemon/index.d.cts +62 -0
  16. package/dist/daemon/index.d.ts +62 -0
  17. package/dist/daemon/index.js +628 -0
  18. package/dist/discovery-Dc2LdF8q.d.cts +105 -0
  19. package/dist/discovery-Dc2LdF8q.d.ts +105 -0
  20. package/dist/index.cjs +2238 -0
  21. package/dist/index.d.cts +472 -0
  22. package/dist/index.d.ts +472 -10
  23. package/dist/index.js +2185 -53
  24. package/dist/protocol-CiwaQFOt.d.ts +239 -0
  25. package/dist/protocol-xZu-wb0n.d.cts +239 -0
  26. package/dist/shared/index.cjs +386 -0
  27. package/dist/shared/index.d.cts +4 -0
  28. package/dist/shared/index.d.ts +4 -0
  29. package/dist/shared/index.js +310 -0
  30. package/dist/types-BKREdsyr.d.cts +32 -0
  31. package/dist/types-BKREdsyr.d.ts +32 -0
  32. package/package.json +44 -4
  33. package/dist/client/batch-mock-collector.d.ts +0 -87
  34. package/dist/client/batch-mock-collector.js +0 -223
  35. package/dist/client/util.d.ts +0 -1
  36. package/dist/client/util.js +0 -3
  37. package/dist/connect.cjs +0 -299
  38. package/dist/connect.d.cts +0 -95
  39. package/dist/server/index.d.ts +0 -1
  40. package/dist/server/index.js +0 -1
  41. package/dist/server/test-mock-mcp-server.d.ts +0 -73
  42. package/dist/server/test-mock-mcp-server.js +0 -392
  43. package/dist/types.d.ts +0 -42
  44. package/dist/types.js +0 -2
@@ -0,0 +1,310 @@
1
+ import fs from 'fs/promises';
2
+ import fssync from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import crypto from 'crypto';
6
+ import { spawn } from 'child_process';
7
+ import http from 'http';
8
+ import { fileURLToPath, pathToFileURL } from 'url';
9
+ import { createRequire } from 'module';
10
+
11
+ // src/shared/discovery.ts
12
+ function debugLog(_msg) {
13
+ }
14
+ var __curDirname = (() => {
15
+ try {
16
+ const metaUrl = import.meta.url;
17
+ if (metaUrl && typeof metaUrl === "string" && metaUrl.startsWith("file://")) {
18
+ return path.dirname(fileURLToPath(metaUrl));
19
+ }
20
+ } catch {
21
+ }
22
+ return process.cwd();
23
+ })();
24
+ function resolveProjectRoot(startDir = process.cwd()) {
25
+ let current = path.resolve(startDir);
26
+ const root = path.parse(current).root;
27
+ while (current !== root) {
28
+ const gitPath = path.join(current, ".git");
29
+ try {
30
+ const stat = fssync.statSync(gitPath);
31
+ if (stat.isDirectory() || stat.isFile()) {
32
+ return current;
33
+ }
34
+ } catch {
35
+ }
36
+ const pkgPath = path.join(current, "package.json");
37
+ try {
38
+ fssync.accessSync(pkgPath, fssync.constants.F_OK);
39
+ return current;
40
+ } catch {
41
+ }
42
+ current = path.dirname(current);
43
+ }
44
+ return path.resolve(startDir);
45
+ }
46
+ function computeProjectId(projectRoot) {
47
+ const real = fssync.realpathSync(projectRoot);
48
+ return crypto.createHash("sha256").update(real).digest("hex").slice(0, 16);
49
+ }
50
+ function getCacheDir(override) {
51
+ if (override) {
52
+ return override;
53
+ }
54
+ const envCacheDir = process.env.MOCK_MCP_CACHE_DIR;
55
+ if (envCacheDir) {
56
+ return envCacheDir;
57
+ }
58
+ const xdg = process.env.XDG_CACHE_HOME;
59
+ if (xdg) {
60
+ return xdg;
61
+ }
62
+ if (process.platform === "win32" && process.env.LOCALAPPDATA) {
63
+ return process.env.LOCALAPPDATA;
64
+ }
65
+ const home = os.homedir();
66
+ if (home) {
67
+ return path.join(home, ".cache");
68
+ }
69
+ return os.tmpdir();
70
+ }
71
+ function getPaths(projectId, cacheDir) {
72
+ const base = path.join(getCacheDir(cacheDir), "mock-mcp");
73
+ const registryPath = path.join(base, `${projectId}.json`);
74
+ const lockPath = path.join(base, `${projectId}.lock`);
75
+ const ipcPath = process.platform === "win32" ? `\\\\.\\pipe\\mock-mcp-${projectId}` : path.join(base, `${projectId}.sock`);
76
+ return { base, registryPath, lockPath, ipcPath };
77
+ }
78
+ async function readRegistry(registryPath) {
79
+ try {
80
+ const txt = await fs.readFile(registryPath, "utf-8");
81
+ return JSON.parse(txt);
82
+ } catch (error) {
83
+ debugLog(`readRegistry error for ${registryPath}: ${error instanceof Error ? error.message : String(error)}`);
84
+ return null;
85
+ }
86
+ }
87
+ async function writeRegistry(registryPath, registry) {
88
+ await fs.writeFile(registryPath, JSON.stringify(registry, null, 2), {
89
+ encoding: "utf-8",
90
+ mode: 384
91
+ // Read/write for owner only
92
+ });
93
+ }
94
+ async function healthCheck(ipcPath, timeoutMs = 2e3) {
95
+ return new Promise((resolve) => {
96
+ const req = http.request(
97
+ {
98
+ method: "GET",
99
+ socketPath: ipcPath,
100
+ path: "/health",
101
+ timeout: timeoutMs
102
+ },
103
+ (res) => {
104
+ resolve(res.statusCode === 200);
105
+ }
106
+ );
107
+ req.on("error", () => resolve(false));
108
+ req.on("timeout", () => {
109
+ req.destroy();
110
+ resolve(false);
111
+ });
112
+ req.end();
113
+ });
114
+ }
115
+ async function tryAcquireLock(lockPath) {
116
+ try {
117
+ const fh = await fs.open(lockPath, "wx");
118
+ await fh.write(`${process.pid}
119
+ `);
120
+ return fh;
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+ async function releaseLock(lockPath, fh) {
126
+ await fh.close();
127
+ await fs.rm(lockPath).catch(() => {
128
+ });
129
+ }
130
+ function randomToken() {
131
+ return crypto.randomBytes(24).toString("base64url");
132
+ }
133
+ function getDaemonEntryPath() {
134
+ try {
135
+ const cwdRequire = createRequire(pathToFileURL(path.join(process.cwd(), "index.js")).href);
136
+ const resolved = cwdRequire.resolve("mock-mcp");
137
+ const distDir = path.dirname(resolved);
138
+ const daemonEntry = path.join(distDir, "index.js");
139
+ if (fssync.existsSync(daemonEntry)) {
140
+ return daemonEntry;
141
+ }
142
+ } catch {
143
+ }
144
+ try {
145
+ const packageRoot = resolveProjectRoot(__curDirname);
146
+ const distPath = path.join(packageRoot, "dist", "index.js");
147
+ if (fssync.existsSync(distPath)) {
148
+ return distPath;
149
+ }
150
+ } catch {
151
+ }
152
+ if (process.argv[1]) {
153
+ return process.argv[1];
154
+ }
155
+ return path.join(process.cwd(), "dist", "index.js");
156
+ }
157
+ async function ensureDaemonRunning(opts = {}) {
158
+ const projectRoot = opts.projectRoot ?? resolveProjectRoot();
159
+ const projectId = computeProjectId(projectRoot);
160
+ const { base, registryPath, lockPath, ipcPath } = getPaths(
161
+ projectId,
162
+ opts.cacheDir
163
+ );
164
+ const timeoutMs = opts.timeoutMs ?? 1e4;
165
+ await fs.mkdir(base, { recursive: true });
166
+ const existing = await readRegistry(registryPath);
167
+ debugLog(`Registry read result: ${existing ? "Found (PID " + existing.pid + ")" : "Null"}`);
168
+ if (existing) {
169
+ let healthy = false;
170
+ for (let i = 0; i < 3; i++) {
171
+ debugLog(`Checking health attempt ${i + 1}/3 on ${existing.ipcPath}`);
172
+ healthy = await healthCheck(existing.ipcPath);
173
+ if (healthy) break;
174
+ await new Promise((r) => setTimeout(r, 200));
175
+ }
176
+ if (healthy) {
177
+ return existing;
178
+ }
179
+ }
180
+ if (process.platform !== "win32") {
181
+ try {
182
+ await fs.rm(ipcPath);
183
+ } catch {
184
+ }
185
+ }
186
+ const lock = await tryAcquireLock(lockPath);
187
+ if (lock) {
188
+ try {
189
+ const recheckReg = await readRegistry(registryPath);
190
+ if (recheckReg && await healthCheck(recheckReg.ipcPath)) {
191
+ return recheckReg;
192
+ }
193
+ const token = randomToken();
194
+ const daemonEntry = getDaemonEntryPath();
195
+ const child = spawn(
196
+ process.execPath,
197
+ [daemonEntry, "daemon", "--project-root", projectRoot, "--token", token],
198
+ {
199
+ detached: true,
200
+ stdio: ["ignore", "pipe", "pipe"],
201
+ env: {
202
+ ...process.env,
203
+ MOCK_MCP_CACHE_DIR: opts.cacheDir ?? ""
204
+ }
205
+ }
206
+ );
207
+ let daemonStderr = "";
208
+ let daemonStdout = "";
209
+ child.stdout?.on("data", (data) => {
210
+ const str = data.toString();
211
+ debugLog(`Daemon stdout: ${str}`);
212
+ });
213
+ child.stderr?.on("data", (data) => {
214
+ daemonStderr += data.toString();
215
+ debugLog(`Daemon stderr: ${data.toString()}`);
216
+ });
217
+ child.on("error", (err) => {
218
+ console.error(`[mock-mcp] Daemon spawn error: ${err.message}`);
219
+ });
220
+ child.on("exit", (code, signal) => {
221
+ if (code !== null && code !== 0) {
222
+ console.error(`[mock-mcp] Daemon exited with code: ${code}`);
223
+ if (daemonStderr) {
224
+ console.error(`[mock-mcp] Daemon stderr: ${daemonStderr.slice(0, 500)}`);
225
+ }
226
+ } else if (signal) {
227
+ console.error(`[mock-mcp] Daemon killed by signal: ${signal}`);
228
+ }
229
+ });
230
+ child.unref();
231
+ const deadline2 = Date.now() + timeoutMs;
232
+ while (Date.now() < deadline2) {
233
+ const reg = await readRegistry(registryPath);
234
+ if (reg && await healthCheck(reg.ipcPath)) {
235
+ return reg;
236
+ }
237
+ await sleep(50);
238
+ }
239
+ console.error("[mock-mcp] Daemon failed to start within timeout");
240
+ if (daemonStderr) {
241
+ console.error(`[mock-mcp] Daemon stderr:
242
+ ${daemonStderr}`);
243
+ }
244
+ throw new Error(
245
+ `Daemon start timeout after ${timeoutMs}ms. Check logs for details.`
246
+ );
247
+ } finally {
248
+ await releaseLock(lockPath, lock);
249
+ }
250
+ }
251
+ const deadline = Date.now() + timeoutMs;
252
+ while (Date.now() < deadline) {
253
+ const reg = await readRegistry(registryPath);
254
+ if (reg && await healthCheck(reg.ipcPath)) {
255
+ return reg;
256
+ }
257
+ await sleep(50);
258
+ }
259
+ throw new Error(
260
+ `Waiting for daemon timed out after ${timeoutMs}ms. Another process may have failed to start it.`
261
+ );
262
+ }
263
+ function sleep(ms) {
264
+ return new Promise((resolve) => setTimeout(resolve, ms));
265
+ }
266
+
267
+ // src/shared/protocol.ts
268
+ var HELLO_TEST = "HELLO_TEST";
269
+ var HELLO_ACK = "HELLO_ACK";
270
+ var BATCH_MOCK_REQUEST = "BATCH_MOCK_REQUEST";
271
+ var BATCH_MOCK_RESULT = "BATCH_MOCK_RESULT";
272
+ var HEARTBEAT = "HEARTBEAT";
273
+ var HEARTBEAT_ACK = "HEARTBEAT_ACK";
274
+ var RPC_GET_STATUS = "getStatus";
275
+ var RPC_LIST_RUNS = "listRuns";
276
+ var RPC_CLAIM_NEXT_BATCH = "claimNextBatch";
277
+ var RPC_PROVIDE_BATCH = "provideBatch";
278
+ var RPC_RELEASE_BATCH = "releaseBatch";
279
+ var RPC_GET_BATCH = "getBatch";
280
+ var RPC_ERROR_PARSE = -32700;
281
+ var RPC_ERROR_INVALID_REQUEST = -32600;
282
+ var RPC_ERROR_METHOD_NOT_FOUND = -32601;
283
+ var RPC_ERROR_INVALID_PARAMS = -32602;
284
+ var RPC_ERROR_INTERNAL = -32603;
285
+ var RPC_ERROR_NOT_FOUND = -32e3;
286
+ var RPC_ERROR_UNAUTHORIZED = -32001;
287
+ var RPC_ERROR_CONFLICT = -32002;
288
+ var RPC_ERROR_EXPIRED = -32003;
289
+ function isHelloTestMessage(msg) {
290
+ if (!msg || typeof msg !== "object") return false;
291
+ const m = msg;
292
+ return m.type === HELLO_TEST && typeof m.token === "string" && typeof m.runId === "string" && typeof m.pid === "number" && typeof m.cwd === "string";
293
+ }
294
+ function isBatchMockRequestMessage(msg) {
295
+ if (!msg || typeof msg !== "object") return false;
296
+ const m = msg;
297
+ return m.type === BATCH_MOCK_REQUEST && typeof m.runId === "string" && Array.isArray(m.requests);
298
+ }
299
+ function isHeartbeatMessage(msg) {
300
+ if (!msg || typeof msg !== "object") return false;
301
+ const m = msg;
302
+ return m.type === HEARTBEAT && typeof m.runId === "string";
303
+ }
304
+ function isJsonRpcRequest(msg) {
305
+ if (!msg || typeof msg !== "object") return false;
306
+ const m = msg;
307
+ return m.jsonrpc === "2.0" && (typeof m.id === "string" || typeof m.id === "number") && typeof m.method === "string";
308
+ }
309
+
310
+ export { BATCH_MOCK_REQUEST, BATCH_MOCK_RESULT, HEARTBEAT, HEARTBEAT_ACK, HELLO_ACK, HELLO_TEST, RPC_CLAIM_NEXT_BATCH, RPC_ERROR_CONFLICT, RPC_ERROR_EXPIRED, RPC_ERROR_INTERNAL, RPC_ERROR_INVALID_PARAMS, RPC_ERROR_INVALID_REQUEST, RPC_ERROR_METHOD_NOT_FOUND, RPC_ERROR_NOT_FOUND, RPC_ERROR_PARSE, RPC_ERROR_UNAUTHORIZED, RPC_GET_BATCH, RPC_GET_STATUS, RPC_LIST_RUNS, RPC_PROVIDE_BATCH, RPC_RELEASE_BATCH, computeProjectId, ensureDaemonRunning, getCacheDir, getDaemonEntryPath, getPaths, healthCheck, isBatchMockRequestMessage, isHeartbeatMessage, isHelloTestMessage, isJsonRpcRequest, randomToken, readRegistry, releaseLock, resolveProjectRoot, sleep, tryAcquireLock, writeRegistry };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Core types for mock-mcp.
3
+ */
4
+ /**
5
+ * Shape of a mock request emitted by the test process.
6
+ */
7
+ interface MockRequestDescriptor {
8
+ requestId: string;
9
+ endpoint: string;
10
+ method: string;
11
+ body?: unknown;
12
+ headers?: Record<string, string>;
13
+ metadata?: Record<string, unknown>;
14
+ }
15
+ /**
16
+ * Shape of the mock data that needs to be returned for a request.
17
+ */
18
+ interface MockResponseDescriptor {
19
+ requestId: string;
20
+ data: unknown;
21
+ status?: number;
22
+ headers?: Record<string, string>;
23
+ delayMs?: number;
24
+ }
25
+ /**
26
+ * Resolved mock with typed data.
27
+ */
28
+ interface ResolvedMock<T = unknown> extends Omit<MockResponseDescriptor, "data"> {
29
+ data: T;
30
+ }
31
+
32
+ export type { MockRequestDescriptor as M, ResolvedMock as R, MockResponseDescriptor as a };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Core types for mock-mcp.
3
+ */
4
+ /**
5
+ * Shape of a mock request emitted by the test process.
6
+ */
7
+ interface MockRequestDescriptor {
8
+ requestId: string;
9
+ endpoint: string;
10
+ method: string;
11
+ body?: unknown;
12
+ headers?: Record<string, string>;
13
+ metadata?: Record<string, unknown>;
14
+ }
15
+ /**
16
+ * Shape of the mock data that needs to be returned for a request.
17
+ */
18
+ interface MockResponseDescriptor {
19
+ requestId: string;
20
+ data: unknown;
21
+ status?: number;
22
+ headers?: Record<string, string>;
23
+ delayMs?: number;
24
+ }
25
+ /**
26
+ * Resolved mock with typed data.
27
+ */
28
+ interface ResolvedMock<T = unknown> extends Omit<MockResponseDescriptor, "data"> {
29
+ data: T;
30
+ }
31
+
32
+ export type { MockRequestDescriptor as M, ResolvedMock as R, MockResponseDescriptor as a };
package/package.json CHANGED
@@ -1,12 +1,46 @@
1
1
  {
2
2
  "name": "mock-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "An MCP server enabling LLMs to write integration tests through live test environment interaction",
5
- "main": "./dist/connect.cjs",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
6
8
  "type": "module",
7
9
  "bin": {
8
10
  "mock-mcp": "./dist/index.js"
9
11
  },
12
+ "exports": {
13
+ ".": {
14
+ "import": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "require": {
19
+ "types": "./dist/index.d.cts",
20
+ "default": "./dist/index.cjs"
21
+ }
22
+ },
23
+ "./client": {
24
+ "import": {
25
+ "types": "./dist/client/index.d.ts",
26
+ "default": "./dist/client/index.js"
27
+ },
28
+ "require": {
29
+ "types": "./dist/client/index.d.cts",
30
+ "default": "./dist/client/index.cjs"
31
+ }
32
+ },
33
+ "./connect": {
34
+ "import": {
35
+ "types": "./dist/client/connect.d.ts",
36
+ "default": "./dist/client/connect.js"
37
+ },
38
+ "require": {
39
+ "types": "./dist/client/connect.d.cts",
40
+ "default": "./dist/client/connect.cjs"
41
+ }
42
+ }
43
+ },
10
44
  "files": [
11
45
  "dist"
12
46
  ],
@@ -17,12 +51,17 @@
17
51
  "node": ">=18.0.0"
18
52
  },
19
53
  "scripts": {
20
- "build": "tsc && tsup src/client/connect.ts --dts",
54
+ "build": "tsup",
55
+ "clean": "rimraf dist",
21
56
  "start": "node dist/index.js",
57
+ "start:adapter": "node dist/index.js adapter",
22
58
  "dev": "tsx src/index.ts",
59
+ "dev:adapter": "tsx src/index.ts adapter",
23
60
  "lint": "eslint . --ext .ts",
24
61
  "test": "vitest run",
25
62
  "test:watch": "vitest --watch",
63
+ "test:concurrency": "vitest run test/concurrency.test.ts",
64
+ "test:inspector": "vitest run test/inspector-integration.test.ts",
26
65
  "prepublishOnly": "npm run build",
27
66
  "link": "npm link",
28
67
  "unlink": "npm unlink",
@@ -56,6 +95,7 @@
56
95
  "commitizen": "^4.3.1",
57
96
  "eslint": "^9.31.0",
58
97
  "node-fetch": "^3.3.2",
98
+ "rimraf": "^6.1.2",
59
99
  "tsup": "^8.5.0",
60
100
  "tsx": "^4.20.3",
61
101
  "typescript": "^5.8.3",
@@ -70,4 +110,4 @@
70
110
  "path": "cz-conventional-changelog"
71
111
  }
72
112
  }
73
- }
113
+ }
@@ -1,87 +0,0 @@
1
- type Logger = Pick<Console, "log" | "warn" | "error"> & {
2
- debug?: (...args: unknown[]) => void;
3
- };
4
- export interface BatchMockCollectorOptions {
5
- /**
6
- * TCP port exposed by {@link TestMockMCPServer}.
7
- *
8
- * @default 3002
9
- */
10
- port?: number;
11
- /**
12
- * Timeout for individual mock requests in milliseconds.
13
- *
14
- * @default 60000
15
- */
16
- timeout?: number;
17
- /**
18
- * Delay (in milliseconds) that determines how long the collector waits before
19
- * flushing the current batch. Setting this to 0 mirrors the "flush on the next
20
- * macrotask" approach described in the technical design document.
21
- *
22
- * @default 0
23
- */
24
- batchDebounceMs?: number;
25
- /**
26
- * Maximum number of requests that may be included in a single batch payload.
27
- * Requests that exceed this limit will be split into multiple batches.
28
- *
29
- * @default 50
30
- */
31
- maxBatchSize?: number;
32
- /**
33
- * Optional custom logger. Defaults to `console`.
34
- */
35
- logger?: Logger;
36
- }
37
- export interface RequestMockOptions {
38
- body?: unknown;
39
- headers?: Record<string, string>;
40
- metadata?: Record<string, unknown>;
41
- }
42
- /**
43
- * Collects HTTP requests issued during a single macrotask and forwards them to
44
- * the MCP server as a batch for AI-assisted mock generation.
45
- */
46
- export declare class BatchMockCollector {
47
- private readonly ws;
48
- private readonly pendingRequests;
49
- private readonly queuedRequestIds;
50
- private readonly timeout;
51
- private readonly batchDebounceMs;
52
- private readonly maxBatchSize;
53
- private readonly logger;
54
- private batchTimer;
55
- private requestIdCounter;
56
- private closed;
57
- private readyResolve?;
58
- private readyReject?;
59
- private readonly readyPromise;
60
- constructor(options?: BatchMockCollectorOptions);
61
- /**
62
- * Ensures the underlying WebSocket connection is ready for use.
63
- */
64
- waitUntilReady(): Promise<void>;
65
- /**
66
- * Request mock data for a specific endpoint/method pair.
67
- */
68
- requestMock<T = unknown>(endpoint: string, method: string, options?: RequestMockOptions): Promise<T>;
69
- /**
70
- * Wait for all requests that are currently pending to settle. Requests
71
- * created after this method is called are not included.
72
- */
73
- waitForPendingRequests(): Promise<void>;
74
- /**
75
- * Close the underlying connection and fail all pending requests.
76
- */
77
- close(code?: number): Promise<void>;
78
- private setupWebSocket;
79
- private handleMessage;
80
- private resolveRequest;
81
- private enqueueRequest;
82
- private flushQueue;
83
- private sendBatch;
84
- private rejectRequest;
85
- private failAllPending;
86
- }
87
- export {};