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.
Files changed (40) hide show
  1. package/README.md +212 -124
  2. package/dist/adapter/index.cjs +875 -0
  3. package/dist/adapter/index.d.cts +142 -0
  4. package/dist/adapter/index.d.ts +142 -0
  5. package/dist/adapter/index.js +835 -0
  6. package/dist/client/connect.cjs +991 -0
  7. package/dist/client/connect.d.cts +218 -0
  8. package/dist/client/connect.d.ts +211 -7
  9. package/dist/client/connect.js +941 -20
  10. package/dist/client/index.cjs +992 -0
  11. package/dist/client/index.d.cts +3 -0
  12. package/dist/client/index.d.ts +3 -2
  13. package/dist/client/index.js +951 -2
  14. package/dist/daemon/index.cjs +717 -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 +678 -0
  18. package/dist/index.cjs +2708 -0
  19. package/dist/index.d.cts +602 -0
  20. package/dist/index.d.ts +602 -11
  21. package/dist/index.js +2651 -53
  22. package/dist/shared/index.cjs +506 -0
  23. package/dist/shared/index.d.cts +241 -0
  24. package/dist/shared/index.d.ts +241 -0
  25. package/dist/shared/index.js +423 -0
  26. package/dist/types-bEGXLBF0.d.cts +190 -0
  27. package/dist/types-bEGXLBF0.d.ts +190 -0
  28. package/package.json +45 -4
  29. package/dist/client/batch-mock-collector.d.ts +0 -111
  30. package/dist/client/batch-mock-collector.js +0 -308
  31. package/dist/client/util.d.ts +0 -1
  32. package/dist/client/util.js +0 -3
  33. package/dist/connect.cjs +0 -400
  34. package/dist/connect.d.cts +0 -82
  35. package/dist/server/index.d.ts +0 -1
  36. package/dist/server/index.js +0 -1
  37. package/dist/server/test-mock-mcp-server.d.ts +0 -73
  38. package/dist/server/test-mock-mcp-server.js +0 -419
  39. package/dist/types.d.ts +0 -45
  40. package/dist/types.js +0 -2
@@ -0,0 +1,190 @@
1
+ import fssync from 'node:fs';
2
+
3
+ /**
4
+ * Discovery module for mock-mcp Daemon + Adapter architecture.
5
+ *
6
+ * Provides:
7
+ * - Project root resolution (up-search for .git / package.json)
8
+ * - Project ID computation (sha256 of realpath)
9
+ * - Registry/lock file path management
10
+ * - IPC path (Unix Domain Socket / Windows Named Pipe)
11
+ * - ensureDaemonRunning() - atomic daemon startup with lock
12
+ */
13
+
14
+ interface DaemonRegistry {
15
+ projectId: string;
16
+ projectRoot: string;
17
+ ipcPath: string;
18
+ token: string;
19
+ pid: number;
20
+ startedAt: string;
21
+ version: string;
22
+ }
23
+ interface EnsureDaemonOptions {
24
+ projectRoot?: string;
25
+ timeoutMs?: number;
26
+ /** For testing: override cache directory */
27
+ cacheDir?: string;
28
+ }
29
+ /**
30
+ * Resolve the project root by searching upward for .git or package.json.
31
+ * Falls back to the starting directory if nothing is found.
32
+ */
33
+ declare function resolveProjectRoot(startDir?: string): string;
34
+ /**
35
+ * Compute a stable project ID from the project root path.
36
+ * Uses sha256 of the realpath, truncated to 16 characters.
37
+ */
38
+ declare function computeProjectId(projectRoot: string): string;
39
+ /**
40
+ * Get the cache directory for mock-mcp files.
41
+ * Priority: override > MOCK_MCP_CACHE_DIR env > XDG_CACHE_HOME > LOCALAPPDATA (Windows) > ~/.cache
42
+ */
43
+ declare function getCacheDir(override?: string): string;
44
+ interface DaemonPaths {
45
+ base: string;
46
+ registryPath: string;
47
+ lockPath: string;
48
+ ipcPath: string;
49
+ }
50
+ /**
51
+ * Get all relevant paths for a given project ID.
52
+ */
53
+ declare function getPaths(projectId: string, cacheDir?: string): DaemonPaths;
54
+ /**
55
+ * Read the daemon registry file.
56
+ */
57
+ declare function readRegistry(registryPath: string): Promise<DaemonRegistry | null>;
58
+ /**
59
+ * Write the daemon registry file with restricted permissions.
60
+ */
61
+ declare function writeRegistry(registryPath: string, registry: DaemonRegistry): Promise<void>;
62
+ /**
63
+ * Check if the daemon is healthy by making an HTTP request to /health.
64
+ */
65
+ declare function healthCheck(ipcPath: string, timeoutMs?: number): Promise<boolean>;
66
+ /**
67
+ * Try to acquire an exclusive lock file.
68
+ * Returns the file handle if successful, null otherwise.
69
+ */
70
+ declare function tryAcquireLock(lockPath: string): Promise<fssync.promises.FileHandle | null>;
71
+ /**
72
+ * Release the lock file.
73
+ */
74
+ declare function releaseLock(lockPath: string, fh: fssync.promises.FileHandle): Promise<void>;
75
+ /**
76
+ * Generate a random token for daemon authentication.
77
+ */
78
+ declare function randomToken(): string;
79
+ /**
80
+ * Get the path to the daemon entry point.
81
+ *
82
+ * This function handles multiple scenarios:
83
+ * 1. Direct execution from source (development)
84
+ * 2. Execution from dist (production)
85
+ * 3. When re-bundled by another tool (Vitest, esbuild, webpack)
86
+ *
87
+ * We use require.resolve to reliably find the mock-mcp package location,
88
+ * which works even when the code is re-bundled by another tool.
89
+ */
90
+ declare function getDaemonEntryPath(): string;
91
+ /**
92
+ * Ensure the daemon is running for the given project.
93
+ *
94
+ * This function:
95
+ * 1. Checks if a daemon is already running (via registry + health check)
96
+ * 2. If not, acquires a lock and spawns a new daemon
97
+ * 3. Waits for the daemon to become healthy
98
+ * 4. Returns the registry information
99
+ *
100
+ * Thread-safe: multiple processes calling this concurrently will only start one daemon.
101
+ */
102
+ declare function ensureDaemonRunning(opts?: EnsureDaemonOptions): Promise<DaemonRegistry>;
103
+ declare function sleep(ms: number): Promise<void>;
104
+ /**
105
+ * Entry in the global active daemons index.
106
+ */
107
+ interface ActiveDaemonEntry {
108
+ projectId: string;
109
+ projectRoot: string;
110
+ ipcPath: string;
111
+ registryPath: string;
112
+ pid: number;
113
+ startedAt: string;
114
+ version: string;
115
+ }
116
+ /**
117
+ * Global index of all active daemons.
118
+ */
119
+ interface ActiveDaemonsIndex {
120
+ daemons: ActiveDaemonEntry[];
121
+ updatedAt: string;
122
+ }
123
+ /**
124
+ * Get the path to the global active daemons index file.
125
+ */
126
+ declare function getGlobalIndexPath(cacheDir?: string): string;
127
+ /**
128
+ * Read the global active daemons index.
129
+ */
130
+ declare function readGlobalIndex(cacheDir?: string): Promise<ActiveDaemonsIndex>;
131
+ /**
132
+ * Write the global active daemons index.
133
+ */
134
+ declare function writeGlobalIndex(index: ActiveDaemonsIndex, cacheDir?: string): Promise<void>;
135
+ /**
136
+ * Register a daemon in the global index.
137
+ * Called when a daemon starts.
138
+ */
139
+ declare function registerDaemonGlobally(entry: ActiveDaemonEntry, cacheDir?: string): Promise<void>;
140
+ /**
141
+ * Unregister a daemon from the global index.
142
+ * Called when a daemon stops.
143
+ */
144
+ declare function unregisterDaemonGlobally(projectId: string, cacheDir?: string): Promise<void>;
145
+ /**
146
+ * Clean up stale entries from the global index.
147
+ * Removes entries where the daemon is no longer running.
148
+ */
149
+ declare function cleanupGlobalIndex(cacheDir?: string): Promise<void>;
150
+ /**
151
+ * Discover all active daemons.
152
+ * Returns list of daemons with their connection info.
153
+ */
154
+ declare function discoverAllDaemons(cacheDir?: string): Promise<{
155
+ registry: DaemonRegistry;
156
+ healthy: boolean;
157
+ }[]>;
158
+
159
+ /**
160
+ * Core types for mock-mcp.
161
+ */
162
+ /**
163
+ * Shape of a mock request emitted by the test process.
164
+ */
165
+ interface MockRequestDescriptor {
166
+ requestId: string;
167
+ endpoint: string;
168
+ method: string;
169
+ body?: unknown;
170
+ headers?: Record<string, string>;
171
+ metadata?: Record<string, unknown>;
172
+ }
173
+ /**
174
+ * Shape of the mock data that needs to be returned for a request.
175
+ */
176
+ interface MockResponseDescriptor {
177
+ requestId: string;
178
+ data: unknown;
179
+ status?: number;
180
+ headers?: Record<string, string>;
181
+ delayMs?: number;
182
+ }
183
+ /**
184
+ * Resolved mock with typed data.
185
+ */
186
+ interface ResolvedMock<T = unknown> extends Omit<MockResponseDescriptor, "data"> {
187
+ data: T;
188
+ }
189
+
190
+ export { type ActiveDaemonEntry as A, type DaemonRegistry as D, type EnsureDaemonOptions as E, type MockResponseDescriptor as M, type ResolvedMock as R, type MockRequestDescriptor as a, getPaths as b, computeProjectId as c, readRegistry as d, releaseLock as e, randomToken as f, getCacheDir as g, healthCheck as h, getDaemonEntryPath as i, ensureDaemonRunning as j, getGlobalIndexPath as k, readGlobalIndex as l, writeGlobalIndex as m, registerDaemonGlobally as n, cleanupGlobalIndex as o, discoverAllDaemons as p, type DaemonPaths as q, resolveProjectRoot as r, sleep as s, tryAcquireLock as t, unregisterDaemonGlobally as u, type ActiveDaemonsIndex as v, writeRegistry as w };
package/package.json CHANGED
@@ -1,12 +1,46 @@
1
1
  {
2
2
  "name": "mock-mcp",
3
- "version": "0.3.1",
3
+ "version": "0.5.1",
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",
@@ -55,7 +94,9 @@
55
94
  "@vitest/coverage-v8": "^3.2.4",
56
95
  "commitizen": "^4.3.1",
57
96
  "eslint": "^9.31.0",
97
+ "fetch-mock": "^12.6.0",
58
98
  "node-fetch": "^3.3.2",
99
+ "rimraf": "^6.1.2",
59
100
  "tsup": "^8.5.0",
60
101
  "tsx": "^4.20.3",
61
102
  "typescript": "^5.8.3",
@@ -70,4 +111,4 @@
70
111
  "path": "cz-conventional-changelog"
71
112
  }
72
113
  }
73
- }
114
+ }
@@ -1,111 +0,0 @@
1
- import { type ResolvedMock } from "../types.js";
2
- type Logger = Pick<Console, "log" | "warn" | "error"> & {
3
- debug?: (...args: unknown[]) => void;
4
- };
5
- export interface BatchMockCollectorOptions {
6
- /**
7
- * TCP port exposed by {@link TestMockMCPServer}.
8
- *
9
- * @default 3002
10
- */
11
- port?: number;
12
- /**
13
- * Timeout for individual mock requests in milliseconds.
14
- *
15
- * @default 60000
16
- */
17
- timeout?: number;
18
- /**
19
- * Delay (in milliseconds) that determines how long the collector waits before
20
- * flushing the current batch. Setting this to 0 mirrors the "flush on the next
21
- * macrotask" approach described in the technical design document.
22
- *
23
- * @default 0
24
- */
25
- batchDebounceMs?: number;
26
- /**
27
- * Maximum number of requests that may be included in a single batch payload.
28
- * Requests that exceed this limit will be split into multiple batches.
29
- *
30
- * @default 50
31
- */
32
- maxBatchSize?: number;
33
- /**
34
- * Optional custom logger. Defaults to `console`.
35
- */
36
- logger?: Logger;
37
- /**
38
- * Interval for WebSocket heartbeats in milliseconds. Set to 0 to disable.
39
- *
40
- * @default 15000
41
- */
42
- heartbeatIntervalMs?: number;
43
- /**
44
- * Automatically attempt to reconnect when the WebSocket closes unexpectedly.
45
- *
46
- * @default true
47
- */
48
- enableReconnect?: boolean;
49
- }
50
- export interface RequestMockOptions {
51
- body?: unknown;
52
- headers?: Record<string, string>;
53
- metadata?: Record<string, unknown>;
54
- }
55
- /**
56
- * Collects HTTP requests issued during a single macrotask and forwards them to
57
- * the MCP server as a batch for AI-assisted mock generation.
58
- */
59
- export declare class BatchMockCollector {
60
- private ws;
61
- private readonly pendingRequests;
62
- private readonly queuedRequestIds;
63
- private readonly timeout;
64
- private readonly batchDebounceMs;
65
- private readonly maxBatchSize;
66
- private readonly logger;
67
- private readonly heartbeatIntervalMs;
68
- private readonly enableReconnect;
69
- private readonly port;
70
- private batchTimer;
71
- private heartbeatTimer;
72
- private reconnectTimer;
73
- private requestIdCounter;
74
- private closed;
75
- private readyResolve?;
76
- private readyReject?;
77
- private readyPromise;
78
- constructor(options?: BatchMockCollectorOptions);
79
- /**
80
- * Ensures the underlying WebSocket connection is ready for use.
81
- */
82
- waitUntilReady(): Promise<void>;
83
- /**
84
- * Request mock data for a specific endpoint/method pair.
85
- */
86
- requestMock<T = unknown>(endpoint: string, method: string, options?: RequestMockOptions): Promise<ResolvedMock<T>>;
87
- /**
88
- * Wait for all requests that are currently pending to settle. Requests
89
- * created after this method is called are not included.
90
- */
91
- waitForPendingRequests(): Promise<void>;
92
- /**
93
- * Close the underlying connection and fail all pending requests.
94
- */
95
- close(code?: number): Promise<void>;
96
- private setupWebSocket;
97
- private createWebSocket;
98
- private resetReadyPromise;
99
- private startHeartbeat;
100
- private stopHeartbeat;
101
- private scheduleReconnect;
102
- private handleMessage;
103
- private resolveRequest;
104
- private enqueueRequest;
105
- private flushQueue;
106
- private sendBatch;
107
- private buildResolvedMock;
108
- private rejectRequest;
109
- private failAllPending;
110
- }
111
- export {};
@@ -1,308 +0,0 @@
1
- import WebSocket from "ws";
2
- import { BATCH_MOCK_REQUEST, BATCH_MOCK_RESPONSE, } from "../types.js";
3
- import { isEnabled } from "./util.js";
4
- const DEFAULT_TIMEOUT = 60_000;
5
- const DEFAULT_BATCH_DEBOUNCE_MS = 0;
6
- const DEFAULT_MAX_BATCH_SIZE = 50;
7
- const DEFAULT_PORT = 3002;
8
- const DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000;
9
- /**
10
- * Collects HTTP requests issued during a single macrotask and forwards them to
11
- * the MCP server as a batch for AI-assisted mock generation.
12
- */
13
- export class BatchMockCollector {
14
- ws;
15
- pendingRequests = new Map();
16
- queuedRequestIds = new Set();
17
- timeout;
18
- batchDebounceMs;
19
- maxBatchSize;
20
- logger;
21
- heartbeatIntervalMs;
22
- enableReconnect;
23
- port;
24
- batchTimer = null;
25
- heartbeatTimer = null;
26
- reconnectTimer = null;
27
- requestIdCounter = 0;
28
- closed = false;
29
- readyResolve;
30
- readyReject;
31
- readyPromise = Promise.resolve();
32
- constructor(options = {}) {
33
- this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
34
- this.batchDebounceMs = options.batchDebounceMs ?? DEFAULT_BATCH_DEBOUNCE_MS;
35
- this.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
36
- this.logger = options.logger ?? console;
37
- this.heartbeatIntervalMs =
38
- options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
39
- this.enableReconnect = options.enableReconnect ?? true;
40
- this.port = options.port ?? DEFAULT_PORT;
41
- this.resetReadyPromise();
42
- this.ws = this.createWebSocket();
43
- this.setupWebSocket();
44
- }
45
- /**
46
- * Ensures the underlying WebSocket connection is ready for use.
47
- */
48
- async waitUntilReady() {
49
- return this.readyPromise;
50
- }
51
- /**
52
- * Request mock data for a specific endpoint/method pair.
53
- */
54
- async requestMock(endpoint, method, options = {}) {
55
- if (this.closed) {
56
- throw new Error("BatchMockCollector has been closed");
57
- }
58
- await this.waitUntilReady();
59
- const requestId = `req-${++this.requestIdCounter}`;
60
- const request = {
61
- requestId,
62
- endpoint,
63
- method,
64
- body: options.body,
65
- headers: options.headers,
66
- metadata: options.metadata,
67
- };
68
- let settleCompletion;
69
- const completion = new Promise((resolve) => {
70
- settleCompletion = resolve;
71
- });
72
- return new Promise((resolve, reject) => {
73
- const timeoutId = setTimeout(() => {
74
- this.rejectRequest(requestId, new Error(`Mock request timed out after ${this.timeout}ms: ${method} ${endpoint}`));
75
- }, this.timeout);
76
- this.pendingRequests.set(requestId, {
77
- request,
78
- resolve: (mock) => {
79
- settleCompletion({ status: "fulfilled", value: undefined });
80
- resolve(this.buildResolvedMock(mock));
81
- },
82
- reject: (error) => {
83
- settleCompletion({ status: "rejected", reason: error });
84
- reject(error);
85
- },
86
- timeoutId,
87
- completion,
88
- });
89
- this.enqueueRequest(requestId);
90
- });
91
- }
92
- /**
93
- * Wait for all requests that are currently pending to settle. Requests
94
- * created after this method is called are not included.
95
- */
96
- async waitForPendingRequests() {
97
- if (!isEnabled()) {
98
- return;
99
- }
100
- const pendingCompletions = Array.from(this.pendingRequests.values()).map((pending) => pending.completion);
101
- const results = await Promise.all(pendingCompletions);
102
- const rejected = results.find((result) => result.status === "rejected");
103
- if (rejected) {
104
- throw rejected.reason;
105
- }
106
- }
107
- /**
108
- * Close the underlying connection and fail all pending requests.
109
- */
110
- async close(code) {
111
- if (this.closed) {
112
- return;
113
- }
114
- this.closed = true;
115
- if (this.batchTimer) {
116
- clearTimeout(this.batchTimer);
117
- this.batchTimer = null;
118
- }
119
- if (this.heartbeatTimer) {
120
- clearInterval(this.heartbeatTimer);
121
- this.heartbeatTimer = null;
122
- }
123
- if (this.reconnectTimer) {
124
- clearTimeout(this.reconnectTimer);
125
- this.reconnectTimer = null;
126
- }
127
- this.queuedRequestIds.clear();
128
- const closePromise = new Promise((resolve) => {
129
- this.ws.once("close", () => resolve());
130
- });
131
- this.ws.close(code);
132
- this.failAllPending(new Error("BatchMockCollector has been closed"));
133
- await closePromise;
134
- }
135
- setupWebSocket() {
136
- this.ws.on("open", () => {
137
- this.logger.log("🔌 Connected to mock MCP WebSocket endpoint");
138
- this.readyResolve?.();
139
- this.startHeartbeat();
140
- });
141
- this.ws.on("message", (data) => this.handleMessage(data));
142
- this.ws.on("error", (error) => {
143
- this.logger.error("❌ WebSocket error:", error);
144
- this.readyReject?.(error instanceof Error ? error : new Error(String(error)));
145
- this.failAllPending(error instanceof Error ? error : new Error(String(error)));
146
- });
147
- this.ws.on("close", () => {
148
- this.logger.warn("🔌 WebSocket connection closed");
149
- this.stopHeartbeat();
150
- this.failAllPending(new Error("WebSocket connection closed"));
151
- if (!this.closed && this.enableReconnect) {
152
- this.scheduleReconnect();
153
- }
154
- });
155
- }
156
- createWebSocket() {
157
- const wsUrl = `ws://localhost:${this.port}`;
158
- return new WebSocket(wsUrl);
159
- }
160
- resetReadyPromise() {
161
- this.readyPromise = new Promise((resolve, reject) => {
162
- this.readyResolve = resolve;
163
- this.readyReject = reject;
164
- });
165
- }
166
- startHeartbeat() {
167
- if (this.heartbeatIntervalMs <= 0 || this.heartbeatTimer) {
168
- return;
169
- }
170
- let lastPong = Date.now();
171
- this.ws.on("pong", () => {
172
- lastPong = Date.now();
173
- });
174
- this.heartbeatTimer = setInterval(() => {
175
- if (this.ws.readyState !== WebSocket.OPEN) {
176
- return;
177
- }
178
- const now = Date.now();
179
- if (now - lastPong > this.heartbeatIntervalMs * 2) {
180
- this.logger.warn("Heartbeat missed; closing socket to trigger reconnect...");
181
- this.ws.close();
182
- return;
183
- }
184
- this.ws.ping();
185
- }, this.heartbeatIntervalMs);
186
- this.heartbeatTimer.unref?.();
187
- }
188
- stopHeartbeat() {
189
- if (this.heartbeatTimer) {
190
- clearInterval(this.heartbeatTimer);
191
- this.heartbeatTimer = null;
192
- }
193
- }
194
- scheduleReconnect() {
195
- if (this.reconnectTimer || this.closed) {
196
- return;
197
- }
198
- this.reconnectTimer = setTimeout(() => {
199
- this.reconnectTimer = null;
200
- this.logger.warn("🔄 Reconnecting to mock MCP WebSocket endpoint...");
201
- this.stopHeartbeat();
202
- this.resetReadyPromise();
203
- this.ws = this.createWebSocket();
204
- this.setupWebSocket();
205
- }, 1_000);
206
- this.reconnectTimer.unref?.();
207
- }
208
- handleMessage(data) {
209
- let parsed;
210
- try {
211
- parsed = JSON.parse(data.toString());
212
- }
213
- catch (error) {
214
- this.logger.error("Failed to parse server message:", error);
215
- return;
216
- }
217
- if (parsed.type !== BATCH_MOCK_RESPONSE) {
218
- this.logger.warn("Received unsupported message type", parsed.type);
219
- return;
220
- }
221
- this.logger.debug?.(`📦 Received mock data for ${parsed.mocks.length} requests (batch ${parsed.batchId})`);
222
- for (const mock of parsed.mocks) {
223
- this.resolveRequest(mock);
224
- }
225
- }
226
- resolveRequest(mock) {
227
- const pending = this.pendingRequests.get(mock.requestId);
228
- if (!pending) {
229
- this.logger.warn(`Received mock for unknown request: ${mock.requestId}`);
230
- return;
231
- }
232
- clearTimeout(pending.timeoutId);
233
- this.pendingRequests.delete(mock.requestId);
234
- const resolve = () => pending.resolve(mock);
235
- if (mock.delayMs && mock.delayMs > 0) {
236
- setTimeout(resolve, mock.delayMs);
237
- }
238
- else {
239
- resolve();
240
- }
241
- }
242
- enqueueRequest(requestId) {
243
- this.queuedRequestIds.add(requestId);
244
- if (this.batchTimer) {
245
- return;
246
- }
247
- this.batchTimer = setTimeout(() => {
248
- this.batchTimer = null;
249
- this.flushQueue();
250
- }, this.batchDebounceMs);
251
- }
252
- flushQueue() {
253
- const queuedIds = Array.from(this.queuedRequestIds);
254
- this.queuedRequestIds.clear();
255
- if (queuedIds.length === 0) {
256
- return;
257
- }
258
- for (let i = 0; i < queuedIds.length; i += this.maxBatchSize) {
259
- const chunkIds = queuedIds.slice(i, i + this.maxBatchSize);
260
- const requests = [];
261
- for (const id of chunkIds) {
262
- const pending = this.pendingRequests.get(id);
263
- if (pending) {
264
- requests.push(pending.request);
265
- }
266
- }
267
- if (requests.length > 0) {
268
- this.sendBatch(requests);
269
- }
270
- }
271
- }
272
- sendBatch(requests) {
273
- if (this.ws.readyState !== WebSocket.OPEN) {
274
- const error = new Error("WebSocket is not open");
275
- requests.forEach((request) => this.rejectRequest(request.requestId, error));
276
- return;
277
- }
278
- const payload = {
279
- type: BATCH_MOCK_REQUEST,
280
- requests,
281
- };
282
- this.logger.debug?.(`📤 Sending batch with ${requests.length} request(s) to MCP server`);
283
- this.ws.send(JSON.stringify(payload));
284
- }
285
- buildResolvedMock(mock) {
286
- return {
287
- requestId: mock.requestId,
288
- data: mock.data,
289
- status: mock.status,
290
- headers: mock.headers,
291
- delayMs: mock.delayMs,
292
- };
293
- }
294
- rejectRequest(requestId, error) {
295
- const pending = this.pendingRequests.get(requestId);
296
- if (!pending) {
297
- return;
298
- }
299
- clearTimeout(pending.timeoutId);
300
- this.pendingRequests.delete(requestId);
301
- pending.reject(error);
302
- }
303
- failAllPending(error) {
304
- for (const requestId of Array.from(this.pendingRequests.keys())) {
305
- this.rejectRequest(requestId, error);
306
- }
307
- }
308
- }
@@ -1 +0,0 @@
1
- export declare const isEnabled: () => boolean;
@@ -1,3 +0,0 @@
1
- export const isEnabled = () => {
2
- return process.env.MOCK_MCP !== undefined && process.env.MOCK_MCP !== "0";
3
- };