mock-mcp 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -53
- package/dist/client/batch-mock-collector.d.ts +33 -4
- package/dist/client/batch-mock-collector.js +121 -14
- package/dist/client/connect.d.ts +9 -3
- package/dist/client/connect.js +18 -4
- package/dist/client/index.d.ts +1 -1
- package/dist/client/util.d.ts +1 -0
- package/dist/client/util.js +3 -0
- package/dist/connect.cjs +150 -19
- package/dist/connect.d.cts +34 -42
- package/dist/index.d.ts +3 -2
- package/dist/server/test-mock-mcp-server.js +31 -4
- package/dist/types.d.ts +3 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/mock-mcp)
|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
Mock MCP Server - AI-generated mock data. The project pairs a WebSocket batch bridge with MCP tooling so Cursor, Claude Desktop, or any compatible client can fulfill intercepted requests in real time.
|
|
7
|
+
Mock MCP Server - AI-generated mock data based on your **OpenAPI JSON Schema** definitions. The project pairs a WebSocket batch bridge with MCP tooling so Cursor, Claude Desktop, or any compatible client can fulfill intercepted requests in real time, ensuring strict contract compliance.
|
|
8
8
|
|
|
9
9
|
## Table of Contents
|
|
10
10
|
|
|
@@ -25,62 +25,67 @@ Mock MCP Server - AI-generated mock data. The project pairs a WebSocket batch br
|
|
|
25
25
|
|
|
26
26
|
1. **Install the package.** Add mock-mcp as a dev dependency inside your project.
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
```bash
|
|
29
|
+
npm install -D mock-mcp
|
|
30
|
+
# or
|
|
31
|
+
yarn add -D mock-mcp
|
|
32
|
+
# or
|
|
33
|
+
pnpm add -D mock-mcp
|
|
34
|
+
```
|
|
31
35
|
|
|
32
36
|
2. **Configure the Model Context Protocol server.** For example, Claude Desktop can launch the binary through npx:
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mock-mcp": {
|
|
41
|
+
"command": "npx",
|
|
42
|
+
"args": ["-y", "mock-mcp@latest"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
42
46
|
|
|
43
47
|
3. **Connect from your tests.** Use `connect` to retrieve a mock client and request data for intercepted calls.
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
49
|
+
```ts
|
|
50
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
51
|
+
import { connect } from "mock-mcp";
|
|
52
|
+
|
|
53
|
+
const userSchema = {
|
|
54
|
+
summary: "Fetch the current user",
|
|
55
|
+
response: {
|
|
56
|
+
type: "object",
|
|
57
|
+
required: ["id", "name"],
|
|
58
|
+
properties: {
|
|
59
|
+
id: { type: "number" },
|
|
60
|
+
name: { type: "string" },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
it("example", async () => {
|
|
66
|
+
const mockClient = await connect();
|
|
67
|
+
const metadata = {
|
|
68
|
+
schemaUrl: "https://example.com/openapi.json#/paths/~1user/get",
|
|
69
|
+
schema: userSchema,
|
|
70
|
+
instructions: "Respond with a single user described by the schema.",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
fetchMock.get("/user", async () => {
|
|
74
|
+
const response = await mockClient.requestMock("/user", "GET", { metadata }) // add mock via mock-mcp
|
|
75
|
+
return response.data
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const result = await fetch("/user");
|
|
79
|
+
const data = await result.json();
|
|
80
|
+
expect(data).toEqual({ id: 1, name: "Jane" });
|
|
81
|
+
}, 10 * 60 * 1000); // 10 minute timeout for AI interaction
|
|
82
|
+
```
|
|
78
83
|
|
|
79
84
|
4. **Run with MCP enabled.** Prompt your AI client to run the persistent test command and provide mocks through the tools.
|
|
80
85
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
```
|
|
87
|
+
Please run the persistent test: `MOCK_MCP=true npm test test/example.test.tsx` and mock fetch data with mock-mcp
|
|
88
|
+
```
|
|
84
89
|
|
|
85
90
|
## Why Mock MCP
|
|
86
91
|
|
|
@@ -116,15 +121,20 @@ Traditional: Write Test → Create Fixtures → Run Test → Maintain Fixtures
|
|
|
116
121
|
↑ ↓
|
|
117
122
|
└──────── Pain Loop ───────┘
|
|
118
123
|
|
|
119
|
-
Mock MCP: Write Test → AI Generates Data → Run Test → Solidify Code
|
|
120
|
-
↑
|
|
121
|
-
|
|
124
|
+
Mock MCP: Write Test → AI Generates Data (Schema-Compliant) → Run Test → Solidify Code
|
|
125
|
+
↑ ↓
|
|
126
|
+
└───────────── Evolution ────────────┘
|
|
122
127
|
```
|
|
123
128
|
|
|
129
|
+
### Schema-Driven Accuracy
|
|
130
|
+
|
|
131
|
+
Unlike "hallucinated" mocks, Mock MCP uses your actual **OpenAPI JSON Schema** definitions to ground the AI. This ensures that generated data not only looks real but strictly adheres to your API contracts, catching integration issues early.
|
|
132
|
+
|
|
124
133
|
## What Mock MCP Does
|
|
125
134
|
|
|
126
135
|
Mock MCP pairs a WebSocket batch bridge with MCP tooling to move intercepted requests from tests to AI helpers and back again.
|
|
127
136
|
|
|
137
|
+
- **Schema-aware generation** uses your provided metadata (OpenAPI JSON Schema) to ensure mocks match production behavior.
|
|
128
138
|
- **Batch-aware test client** collects every network interception inside a single macrotask and waits for the full response set.
|
|
129
139
|
- **MCP tooling** exposes `get_pending_batches` and `provide_batch_mock_data` so AI agents understand the waiting requests and push data back.
|
|
130
140
|
- **WebSocket bridge** connects the test runner to the MCP server while hiding transport details from both sides.
|
|
@@ -174,7 +184,7 @@ const mockClient = await connect({
|
|
|
174
184
|
|
|
175
185
|
await page.route("**/api/users", async (route) => {
|
|
176
186
|
const url = new URL(route.request().url());
|
|
177
|
-
const data = await mockClient.requestMock(
|
|
187
|
+
const { data } = await mockClient.requestMock(
|
|
178
188
|
url.pathname,
|
|
179
189
|
route.request().method()
|
|
180
190
|
);
|
|
@@ -189,9 +199,17 @@ await page.route("**/api/users", async (route) => {
|
|
|
189
199
|
|
|
190
200
|
Batch behaviour stays automatic: additional `requestMock` calls issued in the same macrotask are grouped, forwarded, and resolved together.
|
|
191
201
|
|
|
202
|
+
Need to pause the test until everything in-flight resolves? Call `waitForPendingRequests` to block on the current set of pending requests (anything started after the call is not included):
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// After routing a few requests
|
|
206
|
+
await mockClient.waitForPendingRequests();
|
|
207
|
+
// Safe to assert on the results produced by the mocked responses
|
|
208
|
+
```
|
|
209
|
+
|
|
192
210
|
## Describe Requests with Metadata
|
|
193
211
|
|
|
194
|
-
`requestMock` accepts an optional third argument (`RequestMockOptions`) that is forwarded without modification to the MCP server. The most important field in that object is `metadata`, which lets the test process describe each request with the exact OpenAPI
|
|
212
|
+
`requestMock` accepts an optional third argument (`RequestMockOptions`) that is forwarded without modification to the MCP server. The most important field in that object is `metadata`, which lets the test process describe each request with the exact OpenAPI JSON Schema fragment, sample payloads, or test context that the AI client needs to build a response.
|
|
195
213
|
|
|
196
214
|
When an MCP client calls `get_pending_batches`, every `requests[].metadata` entry from the test run is included in the response. That is the channel the LLM uses to understand the requested endpoint before supplying data through `provide_batch_mock_data`. Metadata is also persisted when batch logging is enabled, so you can audit what was sent to the model.
|
|
197
215
|
|
|
@@ -261,6 +279,7 @@ The library exports primitives so you can embed the workflow inside bespoke runn
|
|
|
261
279
|
|
|
262
280
|
- `TestMockMCPServer` starts and stops the WebSocket plus MCP tooling bridge programmatically.
|
|
263
281
|
- `BatchMockCollector` provides a low-level batching client used directly inside test environments.
|
|
282
|
+
- `BatchMockCollector.waitForPendingRequests()` waits for the currently pending mock requests to settle (resolves when all finish, rejects if any fail).
|
|
264
283
|
- `connect(options)` instantiates `BatchMockCollector` and waits for the WebSocket connection to open.
|
|
265
284
|
|
|
266
285
|
Each class accepts logger overrides, timeout tweaks, and other ergonomics surfaced in the technical design.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ResolvedMock } from "../types.js";
|
|
1
2
|
type Logger = Pick<Console, "log" | "warn" | "error"> & {
|
|
2
3
|
debug?: (...args: unknown[]) => void;
|
|
3
4
|
};
|
|
@@ -5,7 +6,7 @@ export interface BatchMockCollectorOptions {
|
|
|
5
6
|
/**
|
|
6
7
|
* TCP port exposed by {@link TestMockMCPServer}.
|
|
7
8
|
*
|
|
8
|
-
* @default
|
|
9
|
+
* @default 3002
|
|
9
10
|
*/
|
|
10
11
|
port?: number;
|
|
11
12
|
/**
|
|
@@ -33,6 +34,18 @@ export interface BatchMockCollectorOptions {
|
|
|
33
34
|
* Optional custom logger. Defaults to `console`.
|
|
34
35
|
*/
|
|
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;
|
|
36
49
|
}
|
|
37
50
|
export interface RequestMockOptions {
|
|
38
51
|
body?: unknown;
|
|
@@ -44,19 +57,24 @@ export interface RequestMockOptions {
|
|
|
44
57
|
* the MCP server as a batch for AI-assisted mock generation.
|
|
45
58
|
*/
|
|
46
59
|
export declare class BatchMockCollector {
|
|
47
|
-
private
|
|
60
|
+
private ws;
|
|
48
61
|
private readonly pendingRequests;
|
|
49
62
|
private readonly queuedRequestIds;
|
|
50
63
|
private readonly timeout;
|
|
51
64
|
private readonly batchDebounceMs;
|
|
52
65
|
private readonly maxBatchSize;
|
|
53
66
|
private readonly logger;
|
|
67
|
+
private readonly heartbeatIntervalMs;
|
|
68
|
+
private readonly enableReconnect;
|
|
69
|
+
private readonly port;
|
|
54
70
|
private batchTimer;
|
|
71
|
+
private heartbeatTimer;
|
|
72
|
+
private reconnectTimer;
|
|
55
73
|
private requestIdCounter;
|
|
56
74
|
private closed;
|
|
57
75
|
private readyResolve?;
|
|
58
76
|
private readyReject?;
|
|
59
|
-
private
|
|
77
|
+
private readyPromise;
|
|
60
78
|
constructor(options?: BatchMockCollectorOptions);
|
|
61
79
|
/**
|
|
62
80
|
* Ensures the underlying WebSocket connection is ready for use.
|
|
@@ -65,17 +83,28 @@ export declare class BatchMockCollector {
|
|
|
65
83
|
/**
|
|
66
84
|
* Request mock data for a specific endpoint/method pair.
|
|
67
85
|
*/
|
|
68
|
-
requestMock<T = unknown>(endpoint: string, method: string, options?: RequestMockOptions): Promise<T
|
|
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>;
|
|
69
92
|
/**
|
|
70
93
|
* Close the underlying connection and fail all pending requests.
|
|
71
94
|
*/
|
|
72
95
|
close(code?: number): Promise<void>;
|
|
73
96
|
private setupWebSocket;
|
|
97
|
+
private createWebSocket;
|
|
98
|
+
private resetReadyPromise;
|
|
99
|
+
private startHeartbeat;
|
|
100
|
+
private stopHeartbeat;
|
|
101
|
+
private scheduleReconnect;
|
|
74
102
|
private handleMessage;
|
|
75
103
|
private resolveRequest;
|
|
76
104
|
private enqueueRequest;
|
|
77
105
|
private flushQueue;
|
|
78
106
|
private sendBatch;
|
|
107
|
+
private buildResolvedMock;
|
|
79
108
|
private rejectRequest;
|
|
80
109
|
private failAllPending;
|
|
81
110
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
2
|
import { BATCH_MOCK_REQUEST, BATCH_MOCK_RESPONSE, } from "../types.js";
|
|
3
|
+
import { isEnabled } from "./util.js";
|
|
3
4
|
const DEFAULT_TIMEOUT = 60_000;
|
|
4
5
|
const DEFAULT_BATCH_DEBOUNCE_MS = 0;
|
|
5
6
|
const DEFAULT_MAX_BATCH_SIZE = 50;
|
|
6
|
-
const DEFAULT_PORT =
|
|
7
|
+
const DEFAULT_PORT = 3002;
|
|
8
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
7
9
|
/**
|
|
8
10
|
* Collects HTTP requests issued during a single macrotask and forwards them to
|
|
9
11
|
* the MCP server as a batch for AI-assisted mock generation.
|
|
@@ -16,24 +18,28 @@ export class BatchMockCollector {
|
|
|
16
18
|
batchDebounceMs;
|
|
17
19
|
maxBatchSize;
|
|
18
20
|
logger;
|
|
21
|
+
heartbeatIntervalMs;
|
|
22
|
+
enableReconnect;
|
|
23
|
+
port;
|
|
19
24
|
batchTimer = null;
|
|
25
|
+
heartbeatTimer = null;
|
|
26
|
+
reconnectTimer = null;
|
|
20
27
|
requestIdCounter = 0;
|
|
21
28
|
closed = false;
|
|
22
29
|
readyResolve;
|
|
23
30
|
readyReject;
|
|
24
|
-
readyPromise;
|
|
31
|
+
readyPromise = Promise.resolve();
|
|
25
32
|
constructor(options = {}) {
|
|
26
33
|
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
27
34
|
this.batchDebounceMs = options.batchDebounceMs ?? DEFAULT_BATCH_DEBOUNCE_MS;
|
|
28
35
|
this.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
|
|
29
36
|
this.logger = options.logger ?? console;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.ws = new WebSocket(wsUrl);
|
|
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();
|
|
37
43
|
this.setupWebSocket();
|
|
38
44
|
}
|
|
39
45
|
/**
|
|
@@ -59,24 +65,45 @@ export class BatchMockCollector {
|
|
|
59
65
|
headers: options.headers,
|
|
60
66
|
metadata: options.metadata,
|
|
61
67
|
};
|
|
68
|
+
let settleCompletion;
|
|
69
|
+
const completion = new Promise((resolve) => {
|
|
70
|
+
settleCompletion = resolve;
|
|
71
|
+
});
|
|
62
72
|
return new Promise((resolve, reject) => {
|
|
63
73
|
const timeoutId = setTimeout(() => {
|
|
64
|
-
this.
|
|
65
|
-
reject(new Error(`Mock request timed out after ${this.timeout}ms: ${method} ${endpoint}`));
|
|
74
|
+
this.rejectRequest(requestId, new Error(`Mock request timed out after ${this.timeout}ms: ${method} ${endpoint}`));
|
|
66
75
|
}, this.timeout);
|
|
67
76
|
this.pendingRequests.set(requestId, {
|
|
68
77
|
request,
|
|
69
|
-
resolve: (
|
|
70
|
-
|
|
78
|
+
resolve: (mock) => {
|
|
79
|
+
settleCompletion({ status: "fulfilled", value: undefined });
|
|
80
|
+
resolve(this.buildResolvedMock(mock));
|
|
71
81
|
},
|
|
72
82
|
reject: (error) => {
|
|
83
|
+
settleCompletion({ status: "rejected", reason: error });
|
|
73
84
|
reject(error);
|
|
74
85
|
},
|
|
75
86
|
timeoutId,
|
|
87
|
+
completion,
|
|
76
88
|
});
|
|
77
89
|
this.enqueueRequest(requestId);
|
|
78
90
|
});
|
|
79
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
|
+
}
|
|
80
107
|
/**
|
|
81
108
|
* Close the underlying connection and fail all pending requests.
|
|
82
109
|
*/
|
|
@@ -89,6 +116,14 @@ export class BatchMockCollector {
|
|
|
89
116
|
clearTimeout(this.batchTimer);
|
|
90
117
|
this.batchTimer = null;
|
|
91
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
|
+
}
|
|
92
127
|
this.queuedRequestIds.clear();
|
|
93
128
|
const closePromise = new Promise((resolve) => {
|
|
94
129
|
this.ws.once("close", () => resolve());
|
|
@@ -101,6 +136,7 @@ export class BatchMockCollector {
|
|
|
101
136
|
this.ws.on("open", () => {
|
|
102
137
|
this.logger.log("🔌 Connected to mock MCP WebSocket endpoint");
|
|
103
138
|
this.readyResolve?.();
|
|
139
|
+
this.startHeartbeat();
|
|
104
140
|
});
|
|
105
141
|
this.ws.on("message", (data) => this.handleMessage(data));
|
|
106
142
|
this.ws.on("error", (error) => {
|
|
@@ -110,9 +146,65 @@ export class BatchMockCollector {
|
|
|
110
146
|
});
|
|
111
147
|
this.ws.on("close", () => {
|
|
112
148
|
this.logger.warn("🔌 WebSocket connection closed");
|
|
149
|
+
this.stopHeartbeat();
|
|
113
150
|
this.failAllPending(new Error("WebSocket connection closed"));
|
|
151
|
+
if (!this.closed && this.enableReconnect) {
|
|
152
|
+
this.scheduleReconnect();
|
|
153
|
+
}
|
|
114
154
|
});
|
|
115
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
|
+
}
|
|
116
208
|
handleMessage(data) {
|
|
117
209
|
let parsed;
|
|
118
210
|
try {
|
|
@@ -139,7 +231,13 @@ export class BatchMockCollector {
|
|
|
139
231
|
}
|
|
140
232
|
clearTimeout(pending.timeoutId);
|
|
141
233
|
this.pendingRequests.delete(mock.requestId);
|
|
142
|
-
pending.resolve(mock
|
|
234
|
+
const resolve = () => pending.resolve(mock);
|
|
235
|
+
if (mock.delayMs && mock.delayMs > 0) {
|
|
236
|
+
setTimeout(resolve, mock.delayMs);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
resolve();
|
|
240
|
+
}
|
|
143
241
|
}
|
|
144
242
|
enqueueRequest(requestId) {
|
|
145
243
|
this.queuedRequestIds.add(requestId);
|
|
@@ -184,6 +282,15 @@ export class BatchMockCollector {
|
|
|
184
282
|
this.logger.debug?.(`📤 Sending batch with ${requests.length} request(s) to MCP server`);
|
|
185
283
|
this.ws.send(JSON.stringify(payload));
|
|
186
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
|
+
}
|
|
187
294
|
rejectRequest(requestId, error) {
|
|
188
295
|
const pending = this.pendingRequests.get(requestId);
|
|
189
296
|
if (!pending) {
|
package/dist/client/connect.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
1
|
+
import type { BatchMockCollectorOptions, RequestMockOptions } from "./batch-mock-collector.js";
|
|
2
|
+
import type { ResolvedMock } from "../types.js";
|
|
3
3
|
export type ConnectOptions = number | BatchMockCollectorOptions | undefined;
|
|
4
|
+
export interface MockClient {
|
|
5
|
+
waitUntilReady(): Promise<void>;
|
|
6
|
+
requestMock<T = unknown>(endpoint: string, method: string, options?: RequestMockOptions): Promise<ResolvedMock<T>>;
|
|
7
|
+
waitForPendingRequests(): Promise<void>;
|
|
8
|
+
close(code?: number): Promise<void>;
|
|
9
|
+
}
|
|
4
10
|
/**
|
|
5
11
|
* Convenience helper that creates a {@link BatchMockCollector} and waits for the
|
|
6
12
|
* underlying WebSocket connection to become ready before resolving.
|
|
7
13
|
*/
|
|
8
|
-
export declare const connect: (options?: ConnectOptions) => Promise<
|
|
14
|
+
export declare const connect: (options?: ConnectOptions) => Promise<MockClient>;
|
package/dist/client/connect.js
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { BatchMockCollector } from "./batch-mock-collector.js";
|
|
2
|
+
import { isEnabled } from "./util.js";
|
|
3
|
+
class DisabledMockClient {
|
|
4
|
+
async waitUntilReady() {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
async requestMock() {
|
|
8
|
+
throw new Error("[mock-mcp] MOCK_MCP is not enabled. Set MOCK_MCP=1 to enable mock generation.");
|
|
9
|
+
}
|
|
10
|
+
async waitForPendingRequests() {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
async close() {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
2
17
|
/**
|
|
3
18
|
* Convenience helper that creates a {@link BatchMockCollector} and waits for the
|
|
4
19
|
* underlying WebSocket connection to become ready before resolving.
|
|
5
20
|
*/
|
|
6
21
|
export const connect = async (options) => {
|
|
7
|
-
const
|
|
8
|
-
if (!isEnabled) {
|
|
22
|
+
const resolvedOptions = typeof options === "number" ? { port: options } : options ?? {};
|
|
23
|
+
if (!isEnabled()) {
|
|
9
24
|
console.log("[mock-mcp] Skipping (set MOCK_MCP=1 to enable)");
|
|
10
|
-
return;
|
|
25
|
+
return new DisabledMockClient();
|
|
11
26
|
}
|
|
12
|
-
const resolvedOptions = typeof options === "number" ? { port: options } : options ?? {};
|
|
13
27
|
const collector = new BatchMockCollector(resolvedOptions);
|
|
14
28
|
await collector.waitUntilReady();
|
|
15
29
|
return collector;
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { BatchMockCollector, type BatchMockCollectorOptions, type RequestMockOptions, } from "./batch-mock-collector.js";
|
|
2
|
-
export { connect, type ConnectOptions } from "./connect.js";
|
|
2
|
+
export { connect, type ConnectOptions, type MockClient } from "./connect.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const isEnabled: () => boolean;
|
package/dist/connect.cjs
CHANGED
|
@@ -41,11 +41,17 @@ var import_ws = __toESM(require("ws"), 1);
|
|
|
41
41
|
var BATCH_MOCK_REQUEST = "BATCH_MOCK_REQUEST";
|
|
42
42
|
var BATCH_MOCK_RESPONSE = "BATCH_MOCK_RESPONSE";
|
|
43
43
|
|
|
44
|
+
// src/client/util.ts
|
|
45
|
+
var isEnabled = () => {
|
|
46
|
+
return process.env.MOCK_MCP !== void 0 && process.env.MOCK_MCP !== "0";
|
|
47
|
+
};
|
|
48
|
+
|
|
44
49
|
// src/client/batch-mock-collector.ts
|
|
45
50
|
var DEFAULT_TIMEOUT = 6e4;
|
|
46
51
|
var DEFAULT_BATCH_DEBOUNCE_MS = 0;
|
|
47
52
|
var DEFAULT_MAX_BATCH_SIZE = 50;
|
|
48
|
-
var DEFAULT_PORT =
|
|
53
|
+
var DEFAULT_PORT = 3002;
|
|
54
|
+
var DEFAULT_HEARTBEAT_INTERVAL_MS = 15e3;
|
|
49
55
|
var BatchMockCollector = class {
|
|
50
56
|
ws;
|
|
51
57
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
@@ -54,24 +60,27 @@ var BatchMockCollector = class {
|
|
|
54
60
|
batchDebounceMs;
|
|
55
61
|
maxBatchSize;
|
|
56
62
|
logger;
|
|
63
|
+
heartbeatIntervalMs;
|
|
64
|
+
enableReconnect;
|
|
65
|
+
port;
|
|
57
66
|
batchTimer = null;
|
|
67
|
+
heartbeatTimer = null;
|
|
68
|
+
reconnectTimer = null;
|
|
58
69
|
requestIdCounter = 0;
|
|
59
70
|
closed = false;
|
|
60
71
|
readyResolve;
|
|
61
72
|
readyReject;
|
|
62
|
-
readyPromise;
|
|
73
|
+
readyPromise = Promise.resolve();
|
|
63
74
|
constructor(options = {}) {
|
|
64
75
|
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
65
76
|
this.batchDebounceMs = options.batchDebounceMs ?? DEFAULT_BATCH_DEBOUNCE_MS;
|
|
66
77
|
this.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
|
|
67
78
|
this.logger = options.logger ?? console;
|
|
68
|
-
|
|
69
|
-
this.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const wsUrl = `ws://localhost:${port}`;
|
|
74
|
-
this.ws = new import_ws.default(wsUrl);
|
|
79
|
+
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
80
|
+
this.enableReconnect = options.enableReconnect ?? true;
|
|
81
|
+
this.port = options.port ?? DEFAULT_PORT;
|
|
82
|
+
this.resetReadyPromise();
|
|
83
|
+
this.ws = this.createWebSocket();
|
|
75
84
|
this.setupWebSocket();
|
|
76
85
|
}
|
|
77
86
|
/**
|
|
@@ -97,10 +106,14 @@ var BatchMockCollector = class {
|
|
|
97
106
|
headers: options.headers,
|
|
98
107
|
metadata: options.metadata
|
|
99
108
|
};
|
|
109
|
+
let settleCompletion;
|
|
110
|
+
const completion = new Promise((resolve) => {
|
|
111
|
+
settleCompletion = resolve;
|
|
112
|
+
});
|
|
100
113
|
return new Promise((resolve, reject) => {
|
|
101
114
|
const timeoutId = setTimeout(() => {
|
|
102
|
-
this.
|
|
103
|
-
|
|
115
|
+
this.rejectRequest(
|
|
116
|
+
requestId,
|
|
104
117
|
new Error(
|
|
105
118
|
`Mock request timed out after ${this.timeout}ms: ${method} ${endpoint}`
|
|
106
119
|
)
|
|
@@ -108,17 +121,39 @@ var BatchMockCollector = class {
|
|
|
108
121
|
}, this.timeout);
|
|
109
122
|
this.pendingRequests.set(requestId, {
|
|
110
123
|
request,
|
|
111
|
-
resolve: (
|
|
112
|
-
|
|
124
|
+
resolve: (mock) => {
|
|
125
|
+
settleCompletion({ status: "fulfilled", value: void 0 });
|
|
126
|
+
resolve(this.buildResolvedMock(mock));
|
|
113
127
|
},
|
|
114
128
|
reject: (error) => {
|
|
129
|
+
settleCompletion({ status: "rejected", reason: error });
|
|
115
130
|
reject(error);
|
|
116
131
|
},
|
|
117
|
-
timeoutId
|
|
132
|
+
timeoutId,
|
|
133
|
+
completion
|
|
118
134
|
});
|
|
119
135
|
this.enqueueRequest(requestId);
|
|
120
136
|
});
|
|
121
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Wait for all requests that are currently pending to settle. Requests
|
|
140
|
+
* created after this method is called are not included.
|
|
141
|
+
*/
|
|
142
|
+
async waitForPendingRequests() {
|
|
143
|
+
if (!isEnabled()) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const pendingCompletions = Array.from(this.pendingRequests.values()).map(
|
|
147
|
+
(pending) => pending.completion
|
|
148
|
+
);
|
|
149
|
+
const results = await Promise.all(pendingCompletions);
|
|
150
|
+
const rejected = results.find(
|
|
151
|
+
(result) => result.status === "rejected"
|
|
152
|
+
);
|
|
153
|
+
if (rejected) {
|
|
154
|
+
throw rejected.reason;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
122
157
|
/**
|
|
123
158
|
* Close the underlying connection and fail all pending requests.
|
|
124
159
|
*/
|
|
@@ -131,6 +166,14 @@ var BatchMockCollector = class {
|
|
|
131
166
|
clearTimeout(this.batchTimer);
|
|
132
167
|
this.batchTimer = null;
|
|
133
168
|
}
|
|
169
|
+
if (this.heartbeatTimer) {
|
|
170
|
+
clearInterval(this.heartbeatTimer);
|
|
171
|
+
this.heartbeatTimer = null;
|
|
172
|
+
}
|
|
173
|
+
if (this.reconnectTimer) {
|
|
174
|
+
clearTimeout(this.reconnectTimer);
|
|
175
|
+
this.reconnectTimer = null;
|
|
176
|
+
}
|
|
134
177
|
this.queuedRequestIds.clear();
|
|
135
178
|
const closePromise = new Promise((resolve) => {
|
|
136
179
|
this.ws.once("close", () => resolve());
|
|
@@ -143,6 +186,7 @@ var BatchMockCollector = class {
|
|
|
143
186
|
this.ws.on("open", () => {
|
|
144
187
|
this.logger.log("\u{1F50C} Connected to mock MCP WebSocket endpoint");
|
|
145
188
|
this.readyResolve?.();
|
|
189
|
+
this.startHeartbeat();
|
|
146
190
|
});
|
|
147
191
|
this.ws.on("message", (data) => this.handleMessage(data));
|
|
148
192
|
this.ws.on("error", (error) => {
|
|
@@ -156,8 +200,66 @@ var BatchMockCollector = class {
|
|
|
156
200
|
});
|
|
157
201
|
this.ws.on("close", () => {
|
|
158
202
|
this.logger.warn("\u{1F50C} WebSocket connection closed");
|
|
203
|
+
this.stopHeartbeat();
|
|
159
204
|
this.failAllPending(new Error("WebSocket connection closed"));
|
|
205
|
+
if (!this.closed && this.enableReconnect) {
|
|
206
|
+
this.scheduleReconnect();
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
createWebSocket() {
|
|
211
|
+
const wsUrl = `ws://localhost:${this.port}`;
|
|
212
|
+
return new import_ws.default(wsUrl);
|
|
213
|
+
}
|
|
214
|
+
resetReadyPromise() {
|
|
215
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
216
|
+
this.readyResolve = resolve;
|
|
217
|
+
this.readyReject = reject;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
startHeartbeat() {
|
|
221
|
+
if (this.heartbeatIntervalMs <= 0 || this.heartbeatTimer) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
let lastPong = Date.now();
|
|
225
|
+
this.ws.on("pong", () => {
|
|
226
|
+
lastPong = Date.now();
|
|
160
227
|
});
|
|
228
|
+
this.heartbeatTimer = setInterval(() => {
|
|
229
|
+
if (this.ws.readyState !== import_ws.default.OPEN) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const now = Date.now();
|
|
233
|
+
if (now - lastPong > this.heartbeatIntervalMs * 2) {
|
|
234
|
+
this.logger.warn(
|
|
235
|
+
"Heartbeat missed; closing socket to trigger reconnect..."
|
|
236
|
+
);
|
|
237
|
+
this.ws.close();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
this.ws.ping();
|
|
241
|
+
}, this.heartbeatIntervalMs);
|
|
242
|
+
this.heartbeatTimer.unref?.();
|
|
243
|
+
}
|
|
244
|
+
stopHeartbeat() {
|
|
245
|
+
if (this.heartbeatTimer) {
|
|
246
|
+
clearInterval(this.heartbeatTimer);
|
|
247
|
+
this.heartbeatTimer = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
scheduleReconnect() {
|
|
251
|
+
if (this.reconnectTimer || this.closed) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
this.reconnectTimer = setTimeout(() => {
|
|
255
|
+
this.reconnectTimer = null;
|
|
256
|
+
this.logger.warn("\u{1F504} Reconnecting to mock MCP WebSocket endpoint...");
|
|
257
|
+
this.stopHeartbeat();
|
|
258
|
+
this.resetReadyPromise();
|
|
259
|
+
this.ws = this.createWebSocket();
|
|
260
|
+
this.setupWebSocket();
|
|
261
|
+
}, 1e3);
|
|
262
|
+
this.reconnectTimer.unref?.();
|
|
161
263
|
}
|
|
162
264
|
handleMessage(data) {
|
|
163
265
|
let parsed;
|
|
@@ -186,7 +288,12 @@ var BatchMockCollector = class {
|
|
|
186
288
|
}
|
|
187
289
|
clearTimeout(pending.timeoutId);
|
|
188
290
|
this.pendingRequests.delete(mock.requestId);
|
|
189
|
-
pending.resolve(mock
|
|
291
|
+
const resolve = () => pending.resolve(mock);
|
|
292
|
+
if (mock.delayMs && mock.delayMs > 0) {
|
|
293
|
+
setTimeout(resolve, mock.delayMs);
|
|
294
|
+
} else {
|
|
295
|
+
resolve();
|
|
296
|
+
}
|
|
190
297
|
}
|
|
191
298
|
enqueueRequest(requestId) {
|
|
192
299
|
this.queuedRequestIds.add(requestId);
|
|
@@ -235,6 +342,15 @@ var BatchMockCollector = class {
|
|
|
235
342
|
);
|
|
236
343
|
this.ws.send(JSON.stringify(payload));
|
|
237
344
|
}
|
|
345
|
+
buildResolvedMock(mock) {
|
|
346
|
+
return {
|
|
347
|
+
requestId: mock.requestId,
|
|
348
|
+
data: mock.data,
|
|
349
|
+
status: mock.status,
|
|
350
|
+
headers: mock.headers,
|
|
351
|
+
delayMs: mock.delayMs
|
|
352
|
+
};
|
|
353
|
+
}
|
|
238
354
|
rejectRequest(requestId, error) {
|
|
239
355
|
const pending = this.pendingRequests.get(requestId);
|
|
240
356
|
if (!pending) {
|
|
@@ -252,13 +368,28 @@ var BatchMockCollector = class {
|
|
|
252
368
|
};
|
|
253
369
|
|
|
254
370
|
// src/client/connect.ts
|
|
255
|
-
var
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
371
|
+
var DisabledMockClient = class {
|
|
372
|
+
async waitUntilReady() {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
async requestMock() {
|
|
376
|
+
throw new Error(
|
|
377
|
+
"[mock-mcp] MOCK_MCP is not enabled. Set MOCK_MCP=1 to enable mock generation."
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
async waitForPendingRequests() {
|
|
259
381
|
return;
|
|
260
382
|
}
|
|
383
|
+
async close() {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
var connect = async (options) => {
|
|
261
388
|
const resolvedOptions = typeof options === "number" ? { port: options } : options ?? {};
|
|
389
|
+
if (!isEnabled()) {
|
|
390
|
+
console.log("[mock-mcp] Skipping (set MOCK_MCP=1 to enable)");
|
|
391
|
+
return new DisabledMockClient();
|
|
392
|
+
}
|
|
262
393
|
const collector = new BatchMockCollector(resolvedOptions);
|
|
263
394
|
await collector.waitUntilReady();
|
|
264
395
|
return collector;
|
package/dist/connect.d.cts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape of the mock data that needs to be returned for a request.
|
|
3
|
+
*/
|
|
4
|
+
interface MockResponseDescriptor {
|
|
5
|
+
requestId: string;
|
|
6
|
+
data: unknown;
|
|
7
|
+
status?: number;
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
delayMs?: number;
|
|
10
|
+
}
|
|
11
|
+
interface ResolvedMock<T = unknown> extends Omit<MockResponseDescriptor, "data"> {
|
|
12
|
+
data: T;
|
|
13
|
+
}
|
|
14
|
+
|
|
1
15
|
type Logger = Pick<Console, "log" | "warn" | "error"> & {
|
|
2
16
|
debug?: (...args: unknown[]) => void;
|
|
3
17
|
};
|
|
@@ -5,7 +19,7 @@ interface BatchMockCollectorOptions {
|
|
|
5
19
|
/**
|
|
6
20
|
* TCP port exposed by {@link TestMockMCPServer}.
|
|
7
21
|
*
|
|
8
|
-
* @default
|
|
22
|
+
* @default 3002
|
|
9
23
|
*/
|
|
10
24
|
port?: number;
|
|
11
25
|
/**
|
|
@@ -33,58 +47,36 @@ interface BatchMockCollectorOptions {
|
|
|
33
47
|
* Optional custom logger. Defaults to `console`.
|
|
34
48
|
*/
|
|
35
49
|
logger?: Logger;
|
|
50
|
+
/**
|
|
51
|
+
* Interval for WebSocket heartbeats in milliseconds. Set to 0 to disable.
|
|
52
|
+
*
|
|
53
|
+
* @default 15000
|
|
54
|
+
*/
|
|
55
|
+
heartbeatIntervalMs?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Automatically attempt to reconnect when the WebSocket closes unexpectedly.
|
|
58
|
+
*
|
|
59
|
+
* @default true
|
|
60
|
+
*/
|
|
61
|
+
enableReconnect?: boolean;
|
|
36
62
|
}
|
|
37
63
|
interface RequestMockOptions {
|
|
38
64
|
body?: unknown;
|
|
39
65
|
headers?: Record<string, string>;
|
|
40
66
|
metadata?: Record<string, unknown>;
|
|
41
67
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
*/
|
|
68
|
+
|
|
69
|
+
type ConnectOptions = number | BatchMockCollectorOptions | undefined;
|
|
70
|
+
interface MockClient {
|
|
64
71
|
waitUntilReady(): Promise<void>;
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
requestMock<T = unknown>(endpoint: string, method: string, options?: RequestMockOptions): Promise<ResolvedMock<T>>;
|
|
73
|
+
waitForPendingRequests(): Promise<void>;
|
|
72
74
|
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
75
|
}
|
|
82
|
-
|
|
83
|
-
type ConnectOptions = number | BatchMockCollectorOptions | undefined;
|
|
84
76
|
/**
|
|
85
77
|
* Convenience helper that creates a {@link BatchMockCollector} and waits for the
|
|
86
78
|
* underlying WebSocket connection to become ready before resolving.
|
|
87
79
|
*/
|
|
88
|
-
declare const connect: (options?: ConnectOptions) => Promise<
|
|
80
|
+
declare const connect: (options?: ConnectOptions) => Promise<MockClient>;
|
|
89
81
|
|
|
90
|
-
export { type ConnectOptions, connect };
|
|
82
|
+
export { type ConnectOptions, type MockClient, connect };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { TestMockMCPServer, type TestMockMCPServerOptions } from "./server/test-mock-mcp-server.js";
|
|
3
3
|
import { BatchMockCollector, type BatchMockCollectorOptions, type RequestMockOptions } from "./client/batch-mock-collector.js";
|
|
4
|
-
import { connect, type ConnectOptions } from "./client/connect.js";
|
|
4
|
+
import { connect, type ConnectOptions, type MockClient } from "./client/connect.js";
|
|
5
|
+
import type { ResolvedMock } from "./types.js";
|
|
5
6
|
export { TestMockMCPServer };
|
|
6
7
|
export type { TestMockMCPServerOptions };
|
|
7
8
|
export { BatchMockCollector };
|
|
8
9
|
export type { BatchMockCollectorOptions, RequestMockOptions };
|
|
9
10
|
export { connect };
|
|
10
|
-
export type { ConnectOptions };
|
|
11
|
+
export type { ConnectOptions, MockClient, ResolvedMock };
|
|
@@ -109,9 +109,21 @@ export class TestMockMCPServer {
|
|
|
109
109
|
if (!batch) {
|
|
110
110
|
throw new Error(`Batch not found: ${batchId}`);
|
|
111
111
|
}
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
const expectedIds = new Set(batch.requests.map((request) => request.requestId));
|
|
113
|
+
const providedIds = new Set();
|
|
114
|
+
const unknownMock = mocks.find((mock) => !expectedIds.has(mock.requestId));
|
|
115
|
+
if (unknownMock) {
|
|
116
|
+
throw new Error(`Mock data references unknown requestId: ${unknownMock.requestId}`);
|
|
117
|
+
}
|
|
118
|
+
for (const mock of mocks) {
|
|
119
|
+
if (providedIds.has(mock.requestId)) {
|
|
120
|
+
throw new Error(`Duplicate mock data provided for requestId: ${mock.requestId}`);
|
|
121
|
+
}
|
|
122
|
+
providedIds.add(mock.requestId);
|
|
123
|
+
}
|
|
124
|
+
const missingIds = Array.from(expectedIds).filter((requestId) => !providedIds.has(requestId));
|
|
125
|
+
if (missingIds.length > 0) {
|
|
126
|
+
throw new Error(`Missing mock data for requestId(s): ${missingIds.join(", ")}`);
|
|
115
127
|
}
|
|
116
128
|
if (batch.ws.readyState !== WebSocket.OPEN) {
|
|
117
129
|
this.pendingBatches.delete(batchId);
|
|
@@ -204,7 +216,14 @@ export class TestMockMCPServer {
|
|
|
204
216
|
properties: {
|
|
205
217
|
requestId: { type: "string" },
|
|
206
218
|
data: {
|
|
207
|
-
|
|
219
|
+
anyOf: [
|
|
220
|
+
{ type: "object" },
|
|
221
|
+
{ type: "array" },
|
|
222
|
+
{ type: "string" },
|
|
223
|
+
{ type: "number" },
|
|
224
|
+
{ type: "boolean" },
|
|
225
|
+
{ type: "null" },
|
|
226
|
+
],
|
|
208
227
|
},
|
|
209
228
|
status: {
|
|
210
229
|
type: "number",
|
|
@@ -249,6 +268,14 @@ export class TestMockMCPServer {
|
|
|
249
268
|
this.logger.error("🔌 Test process connected");
|
|
250
269
|
this.clients.add(ws);
|
|
251
270
|
ws.on("message", (data) => this.handleClientMessage(ws, data));
|
|
271
|
+
ws.on("ping", () => {
|
|
272
|
+
try {
|
|
273
|
+
ws.pong();
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
this.logger.warn("Failed to respond to ping:", error);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
252
279
|
ws.on("close", () => {
|
|
253
280
|
this.logger.error("🔌 Test process disconnected");
|
|
254
281
|
this.clients.delete(ws);
|
package/dist/types.d.ts
CHANGED
|
@@ -21,6 +21,9 @@ export interface MockResponseDescriptor {
|
|
|
21
21
|
headers?: Record<string, string>;
|
|
22
22
|
delayMs?: number;
|
|
23
23
|
}
|
|
24
|
+
export interface ResolvedMock<T = unknown> extends Omit<MockResponseDescriptor, "data"> {
|
|
25
|
+
data: T;
|
|
26
|
+
}
|
|
24
27
|
export interface BatchMockRequestMessage {
|
|
25
28
|
type: typeof BATCH_MOCK_REQUEST;
|
|
26
29
|
requests: MockRequestDescriptor[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mock-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "An MCP server enabling LLMs to write integration tests through live test environment interaction",
|
|
5
5
|
"main": "./dist/connect.cjs",
|
|
6
6
|
"type": "module",
|
|
@@ -70,4 +70,4 @@
|
|
|
70
70
|
"path": "cz-conventional-changelog"
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
}
|
|
73
|
+
}
|