mock-mcp 0.0.1 → 0.2.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.
@@ -0,0 +1,16 @@
1
+ import { BatchMockCollector } from "./batch-mock-collector.js";
2
+ /**
3
+ * Convenience helper that creates a {@link BatchMockCollector} and waits for the
4
+ * underlying WebSocket connection to become ready before resolving.
5
+ */
6
+ export const connect = async (options) => {
7
+ const isEnabled = process.env.MOCK_MCP !== undefined && process.env.MOCK_MCP !== "0";
8
+ if (!isEnabled) {
9
+ console.log("[mock-mcp] Skipping (set MOCK_MCP=1 to enable)");
10
+ return;
11
+ }
12
+ const resolvedOptions = typeof options === "number" ? { port: options } : options ?? {};
13
+ const collector = new BatchMockCollector(resolvedOptions);
14
+ await collector.waitUntilReady();
15
+ return collector;
16
+ };
@@ -0,0 +1,2 @@
1
+ export { BatchMockCollector, type BatchMockCollectorOptions, type RequestMockOptions, } from "./batch-mock-collector.js";
2
+ export { connect, type ConnectOptions } from "./connect.js";
@@ -0,0 +1,2 @@
1
+ export { BatchMockCollector, } from "./batch-mock-collector.js";
2
+ export { connect } from "./connect.js";
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/client/connect.ts
31
+ var connect_exports = {};
32
+ __export(connect_exports, {
33
+ connect: () => connect
34
+ });
35
+ module.exports = __toCommonJS(connect_exports);
36
+
37
+ // src/client/batch-mock-collector.ts
38
+ var import_ws = __toESM(require("ws"), 1);
39
+
40
+ // src/types.ts
41
+ var BATCH_MOCK_REQUEST = "BATCH_MOCK_REQUEST";
42
+ var BATCH_MOCK_RESPONSE = "BATCH_MOCK_RESPONSE";
43
+
44
+ // src/client/batch-mock-collector.ts
45
+ var DEFAULT_TIMEOUT = 6e4;
46
+ var DEFAULT_BATCH_DEBOUNCE_MS = 0;
47
+ var DEFAULT_MAX_BATCH_SIZE = 50;
48
+ var DEFAULT_PORT = 8080;
49
+ var BatchMockCollector = class {
50
+ ws;
51
+ pendingRequests = /* @__PURE__ */ new Map();
52
+ queuedRequestIds = /* @__PURE__ */ new Set();
53
+ timeout;
54
+ batchDebounceMs;
55
+ maxBatchSize;
56
+ logger;
57
+ batchTimer = null;
58
+ requestIdCounter = 0;
59
+ closed = false;
60
+ readyResolve;
61
+ readyReject;
62
+ readyPromise;
63
+ constructor(options = {}) {
64
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
65
+ this.batchDebounceMs = options.batchDebounceMs ?? DEFAULT_BATCH_DEBOUNCE_MS;
66
+ this.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
67
+ this.logger = options.logger ?? console;
68
+ const port = options.port ?? DEFAULT_PORT;
69
+ this.readyPromise = new Promise((resolve, reject) => {
70
+ this.readyResolve = resolve;
71
+ this.readyReject = reject;
72
+ });
73
+ const wsUrl = `ws://localhost:${port}`;
74
+ this.ws = new import_ws.default(wsUrl);
75
+ this.setupWebSocket();
76
+ }
77
+ /**
78
+ * Ensures the underlying WebSocket connection is ready for use.
79
+ */
80
+ async waitUntilReady() {
81
+ return this.readyPromise;
82
+ }
83
+ /**
84
+ * Request mock data for a specific endpoint/method pair.
85
+ */
86
+ async requestMock(endpoint, method, options = {}) {
87
+ if (this.closed) {
88
+ throw new Error("BatchMockCollector has been closed");
89
+ }
90
+ await this.waitUntilReady();
91
+ const requestId = `req-${++this.requestIdCounter}`;
92
+ const request = {
93
+ requestId,
94
+ endpoint,
95
+ method,
96
+ body: options.body,
97
+ headers: options.headers,
98
+ metadata: options.metadata
99
+ };
100
+ return new Promise((resolve, reject) => {
101
+ const timeoutId = setTimeout(() => {
102
+ this.pendingRequests.delete(requestId);
103
+ reject(
104
+ new Error(
105
+ `Mock request timed out after ${this.timeout}ms: ${method} ${endpoint}`
106
+ )
107
+ );
108
+ }, this.timeout);
109
+ this.pendingRequests.set(requestId, {
110
+ request,
111
+ resolve: (data) => {
112
+ resolve(data);
113
+ },
114
+ reject: (error) => {
115
+ reject(error);
116
+ },
117
+ timeoutId
118
+ });
119
+ this.enqueueRequest(requestId);
120
+ });
121
+ }
122
+ /**
123
+ * Close the underlying connection and fail all pending requests.
124
+ */
125
+ async close(code) {
126
+ if (this.closed) {
127
+ return;
128
+ }
129
+ this.closed = true;
130
+ if (this.batchTimer) {
131
+ clearTimeout(this.batchTimer);
132
+ this.batchTimer = null;
133
+ }
134
+ this.queuedRequestIds.clear();
135
+ const closePromise = new Promise((resolve) => {
136
+ this.ws.once("close", () => resolve());
137
+ });
138
+ this.ws.close(code);
139
+ this.failAllPending(new Error("BatchMockCollector has been closed"));
140
+ await closePromise;
141
+ }
142
+ setupWebSocket() {
143
+ this.ws.on("open", () => {
144
+ this.logger.log("\u{1F50C} Connected to mock MCP WebSocket endpoint");
145
+ this.readyResolve?.();
146
+ });
147
+ this.ws.on("message", (data) => this.handleMessage(data));
148
+ this.ws.on("error", (error) => {
149
+ this.logger.error("\u274C WebSocket error:", error);
150
+ this.readyReject?.(
151
+ error instanceof Error ? error : new Error(String(error))
152
+ );
153
+ this.failAllPending(
154
+ error instanceof Error ? error : new Error(String(error))
155
+ );
156
+ });
157
+ this.ws.on("close", () => {
158
+ this.logger.warn("\u{1F50C} WebSocket connection closed");
159
+ this.failAllPending(new Error("WebSocket connection closed"));
160
+ });
161
+ }
162
+ handleMessage(data) {
163
+ let parsed;
164
+ try {
165
+ parsed = JSON.parse(data.toString());
166
+ } catch (error) {
167
+ this.logger.error("Failed to parse server message:", error);
168
+ return;
169
+ }
170
+ if (parsed.type !== BATCH_MOCK_RESPONSE) {
171
+ this.logger.warn("Received unsupported message type", parsed.type);
172
+ return;
173
+ }
174
+ this.logger.debug?.(
175
+ `\u{1F4E6} Received mock data for ${parsed.mocks.length} requests (batch ${parsed.batchId})`
176
+ );
177
+ for (const mock of parsed.mocks) {
178
+ this.resolveRequest(mock);
179
+ }
180
+ }
181
+ resolveRequest(mock) {
182
+ const pending = this.pendingRequests.get(mock.requestId);
183
+ if (!pending) {
184
+ this.logger.warn(`Received mock for unknown request: ${mock.requestId}`);
185
+ return;
186
+ }
187
+ clearTimeout(pending.timeoutId);
188
+ this.pendingRequests.delete(mock.requestId);
189
+ pending.resolve(mock.data);
190
+ }
191
+ enqueueRequest(requestId) {
192
+ this.queuedRequestIds.add(requestId);
193
+ if (this.batchTimer) {
194
+ return;
195
+ }
196
+ this.batchTimer = setTimeout(() => {
197
+ this.batchTimer = null;
198
+ this.flushQueue();
199
+ }, this.batchDebounceMs);
200
+ }
201
+ flushQueue() {
202
+ const queuedIds = Array.from(this.queuedRequestIds);
203
+ this.queuedRequestIds.clear();
204
+ if (queuedIds.length === 0) {
205
+ return;
206
+ }
207
+ for (let i = 0; i < queuedIds.length; i += this.maxBatchSize) {
208
+ const chunkIds = queuedIds.slice(i, i + this.maxBatchSize);
209
+ const requests = [];
210
+ for (const id of chunkIds) {
211
+ const pending = this.pendingRequests.get(id);
212
+ if (pending) {
213
+ requests.push(pending.request);
214
+ }
215
+ }
216
+ if (requests.length > 0) {
217
+ this.sendBatch(requests);
218
+ }
219
+ }
220
+ }
221
+ sendBatch(requests) {
222
+ if (this.ws.readyState !== import_ws.default.OPEN) {
223
+ const error = new Error("WebSocket is not open");
224
+ requests.forEach(
225
+ (request) => this.rejectRequest(request.requestId, error)
226
+ );
227
+ return;
228
+ }
229
+ const payload = {
230
+ type: BATCH_MOCK_REQUEST,
231
+ requests
232
+ };
233
+ this.logger.debug?.(
234
+ `\u{1F4E4} Sending batch with ${requests.length} request(s) to MCP server`
235
+ );
236
+ this.ws.send(JSON.stringify(payload));
237
+ }
238
+ rejectRequest(requestId, error) {
239
+ const pending = this.pendingRequests.get(requestId);
240
+ if (!pending) {
241
+ return;
242
+ }
243
+ clearTimeout(pending.timeoutId);
244
+ this.pendingRequests.delete(requestId);
245
+ pending.reject(error);
246
+ }
247
+ failAllPending(error) {
248
+ for (const requestId of Array.from(this.pendingRequests.keys())) {
249
+ this.rejectRequest(requestId, error);
250
+ }
251
+ }
252
+ };
253
+
254
+ // src/client/connect.ts
255
+ var connect = async (options) => {
256
+ const isEnabled = process.env.MOCK_MCP !== void 0 && process.env.MOCK_MCP !== "0";
257
+ if (!isEnabled) {
258
+ console.log("[mock-mcp] Skipping (set MOCK_MCP=1 to enable)");
259
+ return;
260
+ }
261
+ const resolvedOptions = typeof options === "number" ? { port: options } : options ?? {};
262
+ const collector = new BatchMockCollector(resolvedOptions);
263
+ await collector.waitUntilReady();
264
+ return collector;
265
+ };
266
+ // Annotate the CommonJS export names for ESM import in node:
267
+ 0 && (module.exports = {
268
+ connect
269
+ });
@@ -0,0 +1,90 @@
1
+ type Logger = Pick<Console, "log" | "warn" | "error"> & {
2
+ debug?: (...args: unknown[]) => void;
3
+ };
4
+ interface BatchMockCollectorOptions {
5
+ /**
6
+ * TCP port exposed by {@link TestMockMCPServer}.
7
+ *
8
+ * @default 8080
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
+ 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
+ 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
+ * Close the underlying connection and fail all pending requests.
71
+ */
72
+ close(code?: number): Promise<void>;
73
+ private setupWebSocket;
74
+ private handleMessage;
75
+ private resolveRequest;
76
+ private enqueueRequest;
77
+ private flushQueue;
78
+ private sendBatch;
79
+ private rejectRequest;
80
+ private failAllPending;
81
+ }
82
+
83
+ type ConnectOptions = number | BatchMockCollectorOptions | undefined;
84
+ /**
85
+ * Convenience helper that creates a {@link BatchMockCollector} and waits for the
86
+ * underlying WebSocket connection to become ready before resolving.
87
+ */
88
+ declare const connect: (options?: ConnectOptions) => Promise<BatchMockCollector | void>;
89
+
90
+ export { type ConnectOptions, connect };
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { TestMockMCPServer, type TestMockMCPServerOptions } from "./server/test-mock-mcp-server.js";
3
+ import { BatchMockCollector, type BatchMockCollectorOptions, type RequestMockOptions } from "./client/batch-mock-collector.js";
4
+ import { connect, type ConnectOptions } from "./client/connect.js";
5
+ export { TestMockMCPServer };
6
+ export type { TestMockMCPServerOptions };
7
+ export { BatchMockCollector };
8
+ export type { BatchMockCollectorOptions, RequestMockOptions };
9
+ export { connect };
10
+ export type { ConnectOptions };
package/dist/index.js ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ import { pathToFileURL } from "node:url";
3
+ import process from "node:process";
4
+ import { TestMockMCPServer, } from "./server/test-mock-mcp-server.js";
5
+ import { BatchMockCollector, } from "./client/batch-mock-collector.js";
6
+ import { connect } from "./client/connect.js";
7
+ const DEFAULT_PORT = 3002;
8
+ async function runCli() {
9
+ const cliArgs = process.argv.slice(2);
10
+ let port = Number.parseInt(process.env.MCP_SERVER_PORT ?? "", 10);
11
+ let enableMcpTransport = true;
12
+ if (Number.isNaN(port)) {
13
+ port = DEFAULT_PORT;
14
+ }
15
+ for (let i = 0; i < cliArgs.length; i += 1) {
16
+ const arg = cliArgs[i];
17
+ if ((arg === "--port" || arg === "-p") && cliArgs[i + 1]) {
18
+ port = Number.parseInt(cliArgs[i + 1], 10);
19
+ i += 1;
20
+ }
21
+ else if (arg === "--no-stdio") {
22
+ enableMcpTransport = false;
23
+ }
24
+ }
25
+ const server = new TestMockMCPServer({
26
+ port,
27
+ enableMcpTransport,
28
+ });
29
+ await server.start();
30
+ console.log(`🎯 Test Mock MCP server ready on ws://localhost:${server.port ?? port}`);
31
+ const shutdown = async () => {
32
+ console.log("👋 Shutting down Test Mock MCP server...");
33
+ await server.stop();
34
+ process.exit(0);
35
+ };
36
+ process.on("SIGINT", shutdown);
37
+ process.on("SIGTERM", shutdown);
38
+ }
39
+ const isCliExecution = (() => {
40
+ if (typeof process === "undefined" || !process.argv?.[1]) {
41
+ return false;
42
+ }
43
+ return import.meta.url === pathToFileURL(process.argv[1]).href;
44
+ })();
45
+ if (isCliExecution) {
46
+ runCli().catch((error) => {
47
+ console.error("Failed to start Test Mock MCP server:", error);
48
+ process.exitCode = 1;
49
+ });
50
+ }
51
+ export { TestMockMCPServer };
52
+ export { BatchMockCollector };
53
+ export { connect };
@@ -0,0 +1 @@
1
+ export { TestMockMCPServer, type TestMockMCPServerOptions, } from "./test-mock-mcp-server.js";
@@ -0,0 +1 @@
1
+ export { TestMockMCPServer, } from "./test-mock-mcp-server.js";
@@ -0,0 +1,73 @@
1
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
2
+ import { type CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ import { type PendingBatchSummary, type ProvideBatchMockDataArgs } from "../types.js";
4
+ type Logger = Pick<Console, "log" | "warn" | "error"> & {
5
+ debug?: (...args: unknown[]) => void;
6
+ };
7
+ export interface TestMockMCPServerOptions {
8
+ port?: number;
9
+ logger?: Logger;
10
+ batchTtlMs?: number;
11
+ sweepIntervalMs?: number;
12
+ enableMcpTransport?: boolean;
13
+ transportFactory?: () => Transport;
14
+ serverName?: string;
15
+ serverVersion?: string;
16
+ mockLogOptions?: MockLogOptions;
17
+ }
18
+ export interface MockLogOptions {
19
+ enabled?: boolean;
20
+ directory?: string;
21
+ }
22
+ /**
23
+ * Bridges the integration-test process and the MCP client, making it possible
24
+ * to generate realistic mock data on demand.
25
+ */
26
+ export declare class TestMockMCPServer {
27
+ private readonly logger;
28
+ private readonly options;
29
+ private wss?;
30
+ private cleanupTimer?;
31
+ private mcpServer?;
32
+ private transport?;
33
+ private started;
34
+ private actualPort?;
35
+ private readonly pendingBatches;
36
+ private readonly clients;
37
+ private batchCounter;
38
+ constructor(options?: TestMockMCPServerOptions);
39
+ /**
40
+ * Start both the WebSocket server (for the test runner) and the MCP server
41
+ * (for the AI client).
42
+ */
43
+ start(): Promise<void>;
44
+ /**
45
+ * Shut down all transports and clear pending batches.
46
+ */
47
+ stop(): Promise<void>;
48
+ /**
49
+ * Expose the TCP port that the WebSocket server is listening on. Useful when
50
+ * `port=0` is supplied for ephemeral environments or tests.
51
+ */
52
+ get port(): number | undefined;
53
+ /**
54
+ * Return summaries of all batches that are awaiting AI-provided mock data.
55
+ */
56
+ getPendingBatches(): PendingBatchSummary[];
57
+ /**
58
+ * Send AI-generated mock data back to the corresponding test process.
59
+ */
60
+ provideMockData(args: ProvideBatchMockDataArgs): Promise<CallToolResult>;
61
+ private startWebSocketServer;
62
+ private startMcpServer;
63
+ private handleConnection;
64
+ private handleClientMessage;
65
+ private handleBatchRequest;
66
+ private dropBatchesForClient;
67
+ private sweepExpiredBatches;
68
+ private persistMockBatch;
69
+ private buildLogEntry;
70
+ private extractBatchContext;
71
+ private buildToolResponse;
72
+ }
73
+ export {};