@slbdn/mcp-tester 1.4.1 → 1.5.3
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/CHANGELOG.md +96 -0
- package/README.md +49 -0
- package/dist/ai/provider.d.ts +145 -0
- package/dist/ai/provider.d.ts.map +1 -0
- package/dist/ai/provider.js +428 -0
- package/dist/ai/provider.js.map +1 -0
- package/dist/cli/index.js +260 -151
- package/dist/cli/index.js.map +1 -1
- package/dist/client/MCPClient.d.ts +16 -2
- package/dist/client/MCPClient.d.ts.map +1 -1
- package/dist/client/MCPClient.js +77 -20
- package/dist/client/MCPClient.js.map +1 -1
- package/dist/generate-cases.d.ts +113 -0
- package/dist/generate-cases.d.ts.map +1 -0
- package/dist/generate-cases.js +565 -0
- package/dist/generate-cases.js.map +1 -0
- package/dist/generate-tests.d.ts +106 -0
- package/dist/generate-tests.d.ts.map +1 -0
- package/dist/generate-tests.js +441 -0
- package/dist/generate-tests.js.map +1 -0
- package/dist/generate-types.d.ts +86 -0
- package/dist/generate-types.d.ts.map +1 -0
- package/dist/generate-types.js +484 -0
- package/dist/generate-types.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/matchers.d.ts +31 -21
- package/dist/matchers.d.ts.map +1 -1
- package/dist/matchers.js.map +1 -1
- package/dist/templates/full-stack/.github/workflows/test.yml +34 -0
- package/dist/templates/full-stack/README.md +73 -0
- package/dist/templates/full-stack/examples/mock-server.js +115 -0
- package/dist/templates/full-stack/jest.config.js +39 -0
- package/dist/templates/full-stack/package.json +31 -0
- package/dist/templates/full-stack/scripts/generate-tests.ts +32 -0
- package/dist/templates/full-stack/scripts/generate-types.ts +28 -0
- package/dist/templates/full-stack/src/server.ts +78 -0
- package/dist/templates/full-stack/tests/helpers.ts +29 -0
- package/dist/templates/full-stack/tests/parallel.test.ts +44 -0
- package/dist/templates/full-stack/tests/server.test.ts +36 -0
- package/dist/templates/full-stack/tests/tools.test.ts +61 -0
- package/dist/templates/full-stack/tsconfig.json +19 -0
- package/dist/templates/minimal-jest/README.md +42 -0
- package/dist/templates/minimal-jest/examples/mock-server.js +62 -0
- package/dist/templates/minimal-jest/jest.config.js +24 -0
- package/dist/templates/minimal-jest/package.json +24 -0
- package/dist/templates/minimal-jest/tests/server.test.ts +64 -0
- package/dist/templates/minimal-jest/tsconfig.json +19 -0
- package/dist/templates/standard-jest/.github/workflows/test.yml +32 -0
- package/dist/templates/standard-jest/README.md +44 -0
- package/dist/templates/standard-jest/examples/mock-server.js +122 -0
- package/dist/templates/standard-jest/jest.config.js +39 -0
- package/dist/templates/standard-jest/package.json +27 -0
- package/dist/templates/standard-jest/tests/helpers.ts +43 -0
- package/dist/templates/standard-jest/tests/prompts.test.ts +37 -0
- package/dist/templates/standard-jest/tests/resources.test.ts +37 -0
- package/dist/templates/standard-jest/tests/server.test.ts +30 -0
- package/dist/templates/standard-jest/tests/tools.test.ts +87 -0
- package/dist/templates/standard-jest/tsconfig.json +19 -0
- package/dist/utils/masking.d.ts.map +1 -1
- package/dist/utils/masking.js +0 -3
- package/dist/utils/masking.js.map +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +9 -0
- package/dist/utils/validation.js.map +1 -1
- package/dist/utils/version.d.ts +13 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +81 -0
- package/dist/utils/version.js.map +1 -0
- package/docs/advanced.md +424 -0
- package/docs/ai-generation.md +211 -0
- package/docs/api-reference.md +819 -0
- package/docs/cicd.md +289 -0
- package/docs/cli.md +225 -0
- package/docs/examples.md +300 -0
- package/docs/nodejs-compatibility.md +81 -0
- package/docs/releases.md +413 -0
- package/docs/starter-templates.md +110 -0
- package/docs/testing.md +443 -0
- package/docs/troubleshooting.md +139 -0
- package/jest.d.ts +43 -0
- package/package.json +11 -20
package/docs/advanced.md
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# Advanced Usage
|
|
2
|
+
|
|
3
|
+
Advanced patterns and features of MCP Tester.
|
|
4
|
+
|
|
5
|
+
## Concurrent Operations
|
|
6
|
+
|
|
7
|
+
Run multiple tool calls in parallel:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
const results = await Promise.all([
|
|
11
|
+
client.callTool({ name: 'tool1', arguments: {} }),
|
|
12
|
+
client.callTool({ name: 'tool2', arguments: {} }),
|
|
13
|
+
client.callTool({ name: 'tool3', arguments: {} }),
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
results.forEach((result, index) => {
|
|
17
|
+
console.log(`Tool ${index + 1}:`, result.content[0]?.text);
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Timeout Handling
|
|
22
|
+
|
|
23
|
+
Customize timeouts at two levels:
|
|
24
|
+
|
|
25
|
+
**Global timeout** — applies to all requests:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
const client = new MCPClient({ timeout: 60000 });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Per-call timeout** — overrides the global default:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const result = await client.callTool({
|
|
35
|
+
name: 'slow-operation',
|
|
36
|
+
arguments: { duration: 5000 },
|
|
37
|
+
timeout: 10000,
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Retry Logic
|
|
42
|
+
|
|
43
|
+
Configure automatic retries with exponential backoff:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const client = new MCPClient({
|
|
47
|
+
retries: 3,
|
|
48
|
+
retryDelay: 1000, // base delay in ms
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Per-call override:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
await client.callTool({
|
|
56
|
+
name: 'flaky-tool',
|
|
57
|
+
arguments: {},
|
|
58
|
+
retries: 5,
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Retry behavior:
|
|
63
|
+
- Exponential backoff: `delay * backoffMultiplier^(attempt - 1)`
|
|
64
|
+
- Default multiplier: 2
|
|
65
|
+
- Max delay cap: 10,000ms
|
|
66
|
+
- Retries only on thrown errors (not on successful responses with error content)
|
|
67
|
+
|
|
68
|
+
## Environment Variables
|
|
69
|
+
|
|
70
|
+
Pass environment variables to the MCP server process:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
await client.start({
|
|
74
|
+
command: 'node',
|
|
75
|
+
args: ['./server.js'],
|
|
76
|
+
env: {
|
|
77
|
+
NODE_ENV: 'test',
|
|
78
|
+
API_ENDPOINT: process.env.API_ENDPOINT || 'http://localhost:3000',
|
|
79
|
+
LOG_LEVEL: 'debug',
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`undefined` values are automatically filtered out, so conditional env vars work cleanly:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
env: {
|
|
88
|
+
NODE_ENV: 'production',
|
|
89
|
+
API_KEY: process.env.API_KEY, // won't be passed if undefined
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Notification Handling
|
|
94
|
+
|
|
95
|
+
Monitor server-initiated notifications:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
client.setNotificationHandlers({
|
|
99
|
+
onLoggingMessage: (level, data) => {
|
|
100
|
+
console.log(`[${level}] ${data}`);
|
|
101
|
+
},
|
|
102
|
+
onResourceListChanged: async () => {
|
|
103
|
+
console.log('Resources changed, refreshing...');
|
|
104
|
+
const resources = await client.listResources();
|
|
105
|
+
console.log(`Now have ${resources.length} resources`);
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Elicitation Handling
|
|
111
|
+
|
|
112
|
+
Respond to server requests for user input:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
await client.setElicitationHandler(async (request) => {
|
|
116
|
+
console.log('Elicitation request:', request.params.mode);
|
|
117
|
+
|
|
118
|
+
if (request.params.mode === 'form') {
|
|
119
|
+
return {
|
|
120
|
+
action: 'accept',
|
|
121
|
+
content: {
|
|
122
|
+
username: 'test-user',
|
|
123
|
+
email: 'test@example.com',
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { action: 'decline' };
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Handler return values:
|
|
133
|
+
- `action: 'accept'` — provide `content` with the collected input
|
|
134
|
+
- `action: 'decline'` — user declined the request
|
|
135
|
+
- `action: 'cancel'` — user cancelled the operation
|
|
136
|
+
|
|
137
|
+
## Logging
|
|
138
|
+
|
|
139
|
+
Configure logging at construction or dynamically:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
const client = new MCPClient({
|
|
143
|
+
logLevel: 'debug', // 'debug' | 'info' | 'warn' | 'error' | 'none'
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Change at runtime
|
|
147
|
+
client.setLogLevel('warn');
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Enable protocol-level logging to see raw JSON messages:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
const client = new MCPClient({
|
|
154
|
+
enableProtocolLogging: true,
|
|
155
|
+
logLevel: 'debug',
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
All log output goes to stderr so it doesn't interfere with stdout.
|
|
160
|
+
|
|
161
|
+
## Mock Server for Unit Testing
|
|
162
|
+
|
|
163
|
+
Use the in-memory mock server for tests that don't need a real server process:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { MockMCPServer } from './fixtures/mock-server.js';
|
|
167
|
+
|
|
168
|
+
describe('My Feature', () => {
|
|
169
|
+
let mockServer: MockMCPServer;
|
|
170
|
+
|
|
171
|
+
beforeEach(() => {
|
|
172
|
+
mockServer = new MockMCPServer();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should handle tool call', async () => {
|
|
176
|
+
const result = await mockServer.handleToolCall('echo', { message: 'test' });
|
|
177
|
+
expect(result.content[0].text).toBe('Echo: test');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Built-in Tools
|
|
183
|
+
|
|
184
|
+
| Tool | Description |
|
|
185
|
+
|------|-------------|
|
|
186
|
+
| `echo` | Echo back the input message |
|
|
187
|
+
| `add` | Add two numbers |
|
|
188
|
+
| `delay` | Delay for specified milliseconds |
|
|
189
|
+
| `error_tool` | Always throws an error |
|
|
190
|
+
| `counter` | Stateful counter: increment, get, reset |
|
|
191
|
+
| `items` | Stateful item list: add, list, remove, clear |
|
|
192
|
+
| `transform` | Transform text: upper, lower, reverse, length |
|
|
193
|
+
|
|
194
|
+
### Built-in Resources: `text://example`, `config://settings`
|
|
195
|
+
### Built-in Prompts: `greet`, `summarize`
|
|
196
|
+
|
|
197
|
+
### Testing Retry Logic
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
const server = new MockMCPServer({ failureRate: 0.5 });
|
|
201
|
+
// 50% of tool calls will throw randomly
|
|
202
|
+
// Great for testing retry logic
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Testing with Delays
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
const server = new MockMCPServer({ defaultDelay: 100 });
|
|
209
|
+
// All tool calls take ≥100ms — test timeout handling
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Input Validation Testing
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
const server = new MockMCPServer({ validateSchemas: true });
|
|
216
|
+
await expect(
|
|
217
|
+
server.handleToolCall('echo', {})
|
|
218
|
+
).rejects.toThrow('Missing required field');
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Stateful Testing
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
await server.handleToolCall('counter', { action: 'increment', by: 5 });
|
|
225
|
+
await server.handleToolCall('counter', { action: 'increment', by: 3 });
|
|
226
|
+
const result = await server.handleToolCall('counter', { action: 'get' });
|
|
227
|
+
// result.content[0].text === 'Counter: 8'
|
|
228
|
+
|
|
229
|
+
server.resetState(); // clears counters, items, call history
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Custom Handlers
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
server.registerToolHandler('echo', (args) => [
|
|
236
|
+
{ type: 'text', text: `Override: ${args.message}` },
|
|
237
|
+
]);
|
|
238
|
+
server.registerResourceHandler('text://example', (uri) => [
|
|
239
|
+
{ uri, mimeType: 'text/plain', text: 'Custom content' },
|
|
240
|
+
]);
|
|
241
|
+
server.registerPromptHandler('greet', (args) => [
|
|
242
|
+
{ role: 'assistant', content: { type: 'text', text: `Hi ${args.name}!` } },
|
|
243
|
+
]);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Call History Assertions
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
await server.handleToolCall('echo', { message: 'test' });
|
|
250
|
+
server.getCallCount('echo'); // 1
|
|
251
|
+
server.getCallHistory()[0].args; // { message: 'test' }
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
See [Testing Guide](./testing.md#mock-server) for the full mock server API reference.
|
|
255
|
+
|
|
256
|
+
## TypeScript Type Generation
|
|
257
|
+
|
|
258
|
+
Generate typed `.d.ts` declarations from any MCP server's tool schemas:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
npx mcp-tester generate-types node ./server.js -o server.d.ts
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Or use the API:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import { generateTypes } from '@slbdn/mcp-tester';
|
|
268
|
+
import { writeFileSync } from 'fs';
|
|
269
|
+
|
|
270
|
+
const types = await generateTypes({
|
|
271
|
+
command: 'node',
|
|
272
|
+
args: ['./server.js'],
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
writeFileSync('server.d.ts', types);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Generated Output
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
/** Echo back the input */
|
|
282
|
+
export interface EchoArgs {
|
|
283
|
+
/** Message to echo */
|
|
284
|
+
message: string;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Add two numbers */
|
|
288
|
+
export interface AddArgs {
|
|
289
|
+
a: number;
|
|
290
|
+
b: number;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Union of all tool names */
|
|
294
|
+
export type ToolName = 'echo' | 'add' | 'delay' | 'error_tool';
|
|
295
|
+
|
|
296
|
+
/** Look up the argument type for a tool by name */
|
|
297
|
+
export interface ToolArgsMap {
|
|
298
|
+
'echo': EchoArgs;
|
|
299
|
+
'add': AddArgs;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Discriminated union for typed callTool() */
|
|
303
|
+
export type ToolCall =
|
|
304
|
+
| { name: 'echo'; arguments: EchoArgs }
|
|
305
|
+
| { name: 'add'; arguments: AddArgs };
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Using Generated Types
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import type { ToolArgsMap, ToolName } from './server.d.ts';
|
|
312
|
+
|
|
313
|
+
// Type-safe arguments
|
|
314
|
+
const result = await client.callTool({
|
|
315
|
+
name: 'add',
|
|
316
|
+
arguments: { a: 1, b: 2 } as ToolArgsMap['add'],
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Supported JSON Schema Features
|
|
321
|
+
|
|
322
|
+
| Feature | TypeScript Output |
|
|
323
|
+
|---------|------------------|
|
|
324
|
+
| `{ type: 'string' }` | `string` |
|
|
325
|
+
| `{ type: 'number' }` | `number` |
|
|
326
|
+
| `{ type: 'boolean' }` | `boolean` |
|
|
327
|
+
| `{ enum: ['a', 'b'] }` | `'a' \| 'b'` |
|
|
328
|
+
| `{ const: 'fixed' }` | `'fixed'` |
|
|
329
|
+
| `{ type: 'array', items: ... }` | `T[]` or `Array<T>` |
|
|
330
|
+
| `{ type: 'object', properties: ... }` | `{ prop: Type }` |
|
|
331
|
+
| `{ oneOf: [...] }` | Union type |
|
|
332
|
+
| `{ anyOf: [...] }` | Union type |
|
|
333
|
+
| `{ allOf: [...] }` | Intersection type |
|
|
334
|
+
| `{ $ref: '#/definitions/X' }` | `X` |
|
|
335
|
+
| Optional fields (not in `required`) | `field?: Type` |
|
|
336
|
+
| `description` | JSDoc comments |
|
|
337
|
+
|
|
338
|
+
## Server Health Checks
|
|
339
|
+
|
|
340
|
+
Detect zombie processes and monitor server health during long-running test suites.
|
|
341
|
+
|
|
342
|
+
### One-Time Check
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
const health = await client.isHealthy();
|
|
346
|
+
if (!health.healthy) {
|
|
347
|
+
console.error(`Server unhealthy: ${health.message}`);
|
|
348
|
+
console.error(`PID ${health.pid}, latency: ${health.latencyMs}ms`);
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Periodic Monitoring
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
client.startHealthMonitor({
|
|
356
|
+
interval: 3000,
|
|
357
|
+
onUnhealthy: (status) => {
|
|
358
|
+
console.error(`Server died: ${status.message}`);
|
|
359
|
+
// Optionally restart or fail the test
|
|
360
|
+
},
|
|
361
|
+
onRecovery: (status) => {
|
|
362
|
+
console.log(`Server recovered after crash`);
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Clean up
|
|
367
|
+
client.stopHealthMonitor(); // explicit
|
|
368
|
+
await client.stop(); // also stops the monitor
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Zombie Detection
|
|
372
|
+
|
|
373
|
+
Uses `process.kill(pid, 0)` to check if the server process is still alive. Works on macOS, Linux, and Windows. Detects crashed, killed, or orphaned processes that `isConnected()` cannot detect (the client object still exists but the process is gone).
|
|
374
|
+
|
|
375
|
+
### HealthStatus Fields
|
|
376
|
+
|
|
377
|
+
| Field | Type | Description |
|
|
378
|
+
|-------|------|-------------|
|
|
379
|
+
| `healthy` | `boolean` | Is the server responsive |
|
|
380
|
+
| `checkedAt` | `number` | Unix timestamp of the check |
|
|
381
|
+
| `latencyMs` | `number` | Round-trip time, or -1 if failed |
|
|
382
|
+
| `pid` | `number \| null` | Server process ID |
|
|
383
|
+
| `message` | `string` | Human-readable status |
|
|
384
|
+
|
|
385
|
+
## Performance Benchmarks
|
|
386
|
+
|
|
387
|
+
Run the benchmark suite to measure MCP Tester's performance characteristics:
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
npm run benchmark
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
This runs 19 benchmarks against the mock server (50 runs each, 3 warmup), measuring:
|
|
394
|
+
|
|
395
|
+
| Category | Benchmarks |
|
|
396
|
+
|----------|------------|
|
|
397
|
+
| **Connection** | Reconnect, disconnect |
|
|
398
|
+
| **Tool calls** | Echo, add, delay (10ms simulated) |
|
|
399
|
+
| **Sequential** | 5 and 10 tools in a row |
|
|
400
|
+
| **Parallel** | 5, 10, and 20 tools concurrently |
|
|
401
|
+
| **Payloads** | 10KB, 100KB, 1MB echo payloads |
|
|
402
|
+
| **Metadata** | List tools, resources, prompts; get prompt with args |
|
|
403
|
+
| **Lifecycle** | Full start → list → call → stop patterns |
|
|
404
|
+
|
|
405
|
+
### Reference Numbers (Node.js 24, Apple Silicon)
|
|
406
|
+
|
|
407
|
+
| Operation | Mean Latency | Throughput |
|
|
408
|
+
|----------|-------------|------------|
|
|
409
|
+
| Single tool call (echo) | **0.19 ms** | ~5,130 ops/s |
|
|
410
|
+
| Add tool (numbers) | **0.19 ms** | ~5,360 ops/s |
|
|
411
|
+
| 10 parallel tool calls | **0.55 ms total** | ~18,200 ops/s |
|
|
412
|
+
| 20 parallel tool calls | **0.86 ms total** | ~23,300 ops/s |
|
|
413
|
+
| List tools / resources / prompts | **0.15 ms** | ~6,700 ops/s |
|
|
414
|
+
| 1MB payload echo | **7.79 ms** | ~1,080 Mbps |
|
|
415
|
+
| Server reconnect | **165 ms** | ~6 ops/s |
|
|
416
|
+
| Full lifecycle (start+call+stop) | **167 ms** | ~6 ops/s |
|
|
417
|
+
|
|
418
|
+
### Key Takeaways
|
|
419
|
+
|
|
420
|
+
- **Parallelization wins** — 10 parallel calls take the same time as 1 (~0.5ms total), giving ~18x throughput vs sequential
|
|
421
|
+
- **Connection overhead dominates single-call cost** — once connected, tool calls are sub-millisecond
|
|
422
|
+
- **Payload size has minimal impact** — going from 10KB to 1MB adds only ~7ms of overhead
|
|
423
|
+
- **Metadata listings are cheap** — listing tools, resources, or prompts costs ~0.15ms
|
|
424
|
+
- **The stdio transport is fast** — most latency comes from process spawn, not the protocol itself
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# AI-Assisted Test Generation
|
|
2
|
+
|
|
3
|
+
`mcp-tester` can derive test cases for your MCP server's tools in three ways:
|
|
4
|
+
|
|
5
|
+
| Mode | Flag | What it does | Needs network? |
|
|
6
|
+
|------|------|--------------|:---:|
|
|
7
|
+
| `static` | *(default)* | One happy-path call per tool | No |
|
|
8
|
+
| `edge` | `--edge-cases` | Deterministic boundary/invalid cases from JSON Schema rules | No |
|
|
9
|
+
| `ai` | `--ai-generate` | Edge cases **plus** AI-suggested cases, validated against the real schemas | Yes (or a local model) |
|
|
10
|
+
|
|
11
|
+
**AI never writes test code.** The model only proposes *structured test-case
|
|
12
|
+
data* (tool, arguments, expectation, rationale). A deterministic renderer emits
|
|
13
|
+
the final TypeScript, and every AI-proposed case is validated against the
|
|
14
|
+
tool's real input schema before it reaches your test file — hallucinated
|
|
15
|
+
arguments are dropped, not compiled.
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
### Deterministic edge cases (offline)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx mcp-tester generate node ./server.js --edge-cases -o tests/generated.test.ts
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The rules engine inspects each tool's `inputSchema` and emits cases per rule:
|
|
26
|
+
|
|
27
|
+
| Rule | Case | Expectation |
|
|
28
|
+
|------|------|-------------|
|
|
29
|
+
| `rule:required` | omit each required property | error |
|
|
30
|
+
| `rule:enum` | value outside the enum | error |
|
|
31
|
+
| `rule:min-length` | string one shorter than `minLength` | error |
|
|
32
|
+
| `rule:max-length` | string one longer than `maxLength` | observe |
|
|
33
|
+
| `rule:bounds` | number one below `minimum` / above `maximum` | error |
|
|
34
|
+
| `rule:type` | wrong JSON type per property | error |
|
|
35
|
+
| `rule:min-items` | array shorter than `minItems` | error |
|
|
36
|
+
| `rule:additional-properties` | unknown key when `additionalProperties: false` | error |
|
|
37
|
+
|
|
38
|
+
Expectations:
|
|
39
|
+
|
|
40
|
+
- **`error`** — the case asserts the call fails (`expect(isToolError(result)).toBe(true)`)
|
|
41
|
+
- **`observe`** — the case calls the tool and logs what happened (no hard
|
|
42
|
+
assertion; for behavior that is legitimately server-defined)
|
|
43
|
+
- **`success`** — the case asserts the call succeeds
|
|
44
|
+
|
|
45
|
+
### AI-suggested cases
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
export MCP_TESTER_AI_API_KEY=sk-...
|
|
49
|
+
npx mcp-tester generate node ./server.js --ai-generate -o tests/generated.test.ts
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`--ai-generate` includes everything `--edge-cases` produces, then asks the
|
|
53
|
+
model for additional interesting cases (unusual-but-valid inputs, realistic
|
|
54
|
+
payloads, combinations the rules can't reason about). Each suggestion is
|
|
55
|
+
checked against the tool's schema:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
mcp-tester: AI suggested cases: 5, kept after schema validation: 4 (dropped 1)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Works with any OpenAI-compatible endpoint:
|
|
62
|
+
|
|
63
|
+
| Provider | Base URL | Key needed |
|
|
64
|
+
|----------|----------|:---:|
|
|
65
|
+
| OpenAI | `https://api.openai.com/v1` (default) | yes |
|
|
66
|
+
| Ollama | `http://localhost:11434/v1` | no |
|
|
67
|
+
| LM Studio | `http://localhost:1234/v1` | no |
|
|
68
|
+
| vLLM | `http://host:8000/v1` | optional |
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Ollama — no API key required for localhost
|
|
72
|
+
npx mcp-tester generate node ./server.js \
|
|
73
|
+
--ai-generate --ai-base-url http://localhost:11434/v1 --ai-model llama3
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Flags
|
|
77
|
+
|
|
78
|
+
| Flag | Meaning |
|
|
79
|
+
|------|---------|
|
|
80
|
+
| `--edge-cases` | Add deterministic schema-derived cases (offline) |
|
|
81
|
+
| `--ai-generate` | Edge cases + AI suggestions (implies `--edge-cases`) |
|
|
82
|
+
| `--ai-model <model>` | Model name (default `gpt-4o-mini` or `MCP_TESTER_AI_MODEL`) |
|
|
83
|
+
| `--ai-base-url <url>` | OpenAI-compatible base URL (default `MCP_TESTER_AI_BASE_URL`) |
|
|
84
|
+
| `--require-ai` | Fail instead of falling back when AI is unavailable |
|
|
85
|
+
| `--verify` | Dry-run every derived case against the live server first |
|
|
86
|
+
|
|
87
|
+
Passing both `--edge-cases` and `--ai-generate` is an error — `--ai-generate`
|
|
88
|
+
already includes the edge cases.
|
|
89
|
+
|
|
90
|
+
## Verifying derived cases against the real server
|
|
91
|
+
|
|
92
|
+
Derived expectations are *predictions*. `--verify` dry-runs every case against
|
|
93
|
+
the live server before writing the file:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx mcp-tester generate node ./server.js --edge-cases --verify -o tests/generated.test.ts
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- Cases whose prediction **holds** stay as normal assertions.
|
|
100
|
+
- Cases where the server **disagrees** (e.g. the rules predicted an error but
|
|
101
|
+
the server returned success) are emitted as `it.skip` blocks with a comment:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// rule:required — "message" is required; the server should reject its absence
|
|
105
|
+
// verify: expected error but server returned success — adjust manually
|
|
106
|
+
it.skip('rejects call without required "message"', async () => { ... });
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
That skip is a signal, not a failure: either the server is too permissive (fix
|
|
110
|
+
the server — many MCP servers forget to validate `missing required property`)
|
|
111
|
+
or the expectation was wrong (edit the test). Either way you've learned
|
|
112
|
+
something concrete about your server.
|
|
113
|
+
|
|
114
|
+
## Environment variables
|
|
115
|
+
|
|
116
|
+
| Variable | Purpose | Default |
|
|
117
|
+
|----------|---------|---------|
|
|
118
|
+
| `MCP_TESTER_AI_API_KEY` | API key for hosted endpoints | *(none)* |
|
|
119
|
+
| `MCP_TESTER_AI_BASE_URL` | OpenAI-compatible endpoint | `https://api.openai.com/v1` |
|
|
120
|
+
| `MCP_TESTER_AI_MODEL` | Model name | `gpt-4o-mini` |
|
|
121
|
+
|
|
122
|
+
A base URL pointing at `localhost`/`127.0.0.1`/`[::1]` is considered
|
|
123
|
+
configured **without** an API key (local model servers don't need auth).
|
|
124
|
+
|
|
125
|
+
Without configuration, `--ai-generate` prints a warning and falls back to the
|
|
126
|
+
deterministic edge cases. With `--require-ai`, it fails instead.
|
|
127
|
+
|
|
128
|
+
## Library API
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import {
|
|
132
|
+
generateTests,
|
|
133
|
+
generateTestsFromClient,
|
|
134
|
+
OpenAICompatProvider,
|
|
135
|
+
suggestEdgeCasesForTools,
|
|
136
|
+
} from '@slbdn/mcp-tester';
|
|
137
|
+
|
|
138
|
+
// Deterministic cases only
|
|
139
|
+
await generateTests({ command: 'node', args: ['./server.js'], mode: 'edge' });
|
|
140
|
+
|
|
141
|
+
// AI-assisted with an explicit provider (no env vars needed)
|
|
142
|
+
await generateTests({
|
|
143
|
+
command: 'node',
|
|
144
|
+
args: ['./server.js'],
|
|
145
|
+
mode: 'ai',
|
|
146
|
+
provider: new OpenAICompatProvider({
|
|
147
|
+
baseUrl: 'http://localhost:11434/v1',
|
|
148
|
+
model: 'llama3',
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Rules only, no file emission — inspect the cases yourself
|
|
153
|
+
const cases = suggestEdgeCasesForTools(tools);
|
|
154
|
+
// → [{ tool, title, args, expectation, rationale, source }, ...]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
A `GeneratedCase` is plain data — safe to log, diff, and version-control:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
interface GeneratedCase {
|
|
161
|
+
tool: string; // tool name
|
|
162
|
+
title: string; // human-readable test title
|
|
163
|
+
args: Record<string, unknown>; // arguments to send
|
|
164
|
+
expectation: 'success' | 'error' | 'observe';
|
|
165
|
+
rationale: string; // why this case exists
|
|
166
|
+
source: `rule:${string}` | `ai:${string}`; // provenance, e.g. rule:required
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Custom providers implement one method:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
interface AIProvider {
|
|
174
|
+
name: string;
|
|
175
|
+
suggestCases(analysis: ServerAnalysis): Promise<GeneratedCase[]>;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`OpenAICompatProvider` accepts `fetchImpl` for testing and custom HTTP
|
|
180
|
+
behavior, caches responses in `.mcp-tester-cache/` (set `cacheDir: null` to
|
|
181
|
+
disable), truncates long tool descriptions, and batches at most 40 tools per
|
|
182
|
+
request.
|
|
183
|
+
|
|
184
|
+
### Provider options reference
|
|
185
|
+
|
|
186
|
+
| Option | Default | Effect |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| `apiKey` | *(unset)* | Bearer token. Optional for local gateways (Ollama, LM Studio). |
|
|
189
|
+
| `baseUrl` | `https://api.openai.com/v1` | Chat-completions endpoint. |
|
|
190
|
+
| `model` | `gpt-4o-mini` | Model name. |
|
|
191
|
+
| `timeout` | `60000` | Per-request timeout in ms. |
|
|
192
|
+
| `maxTokens` | `4096` | Cap on completion tokens. |
|
|
193
|
+
| `cacheDir` | `.mcp-tester-cache` | Where to write cache files. `null` disables caching. |
|
|
194
|
+
| `cacheMaxAgeMs` | *(unset)* | When set, cache files older than this are ignored and re-fetched. Useful when iterating on a model. |
|
|
195
|
+
| `strictJson` | `true` | Sends `response_format: { type: 'json_object' }`. Set to `false` for gateways that reject it (Ollama, older vLLM). The provider auto-falls-back on HTTP 400 mentioning `response_format` regardless. |
|
|
196
|
+
| `fetchImpl` | `globalThis.fetch` | Injectable fetch for tests and hermetic CI. |
|
|
197
|
+
|
|
198
|
+
Cache key includes the model name, the `serverLabel`, and the tool set
|
|
199
|
+
(sorted by name) — so changing any of them invalidates the entry.
|
|
200
|
+
The cache file is JSON: `{ "model", "serverLabel", "cases": [...] }`.
|
|
201
|
+
|
|
202
|
+
## Safety and cost notes
|
|
203
|
+
|
|
204
|
+
- **No code from the model.** AI output is parsed as JSON case data and
|
|
205
|
+
validated; it can never become executable test logic.
|
|
206
|
+
- **Prompt-injection resistant.** Tool descriptions are untrusted input; at
|
|
207
|
+
worst they influence *which cases are proposed* — still schema-validated.
|
|
208
|
+
- **Caching.** Identical analyses hit the on-disk cache, so re-running
|
|
209
|
+
generation is free and deterministic.
|
|
210
|
+
- **Deterministic fallback.** No key, no quota, no network — you still get the
|
|
211
|
+
full rules engine.
|