@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.
Files changed (84) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +49 -0
  3. package/dist/ai/provider.d.ts +145 -0
  4. package/dist/ai/provider.d.ts.map +1 -0
  5. package/dist/ai/provider.js +428 -0
  6. package/dist/ai/provider.js.map +1 -0
  7. package/dist/cli/index.js +260 -151
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/client/MCPClient.d.ts +16 -2
  10. package/dist/client/MCPClient.d.ts.map +1 -1
  11. package/dist/client/MCPClient.js +77 -20
  12. package/dist/client/MCPClient.js.map +1 -1
  13. package/dist/generate-cases.d.ts +113 -0
  14. package/dist/generate-cases.d.ts.map +1 -0
  15. package/dist/generate-cases.js +565 -0
  16. package/dist/generate-cases.js.map +1 -0
  17. package/dist/generate-tests.d.ts +106 -0
  18. package/dist/generate-tests.d.ts.map +1 -0
  19. package/dist/generate-tests.js +441 -0
  20. package/dist/generate-tests.js.map +1 -0
  21. package/dist/generate-types.d.ts +86 -0
  22. package/dist/generate-types.d.ts.map +1 -0
  23. package/dist/generate-types.js +484 -0
  24. package/dist/generate-types.js.map +1 -0
  25. package/dist/index.d.ts +8 -4
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +5 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/matchers.d.ts +31 -21
  30. package/dist/matchers.d.ts.map +1 -1
  31. package/dist/matchers.js.map +1 -1
  32. package/dist/templates/full-stack/.github/workflows/test.yml +34 -0
  33. package/dist/templates/full-stack/README.md +73 -0
  34. package/dist/templates/full-stack/examples/mock-server.js +115 -0
  35. package/dist/templates/full-stack/jest.config.js +39 -0
  36. package/dist/templates/full-stack/package.json +31 -0
  37. package/dist/templates/full-stack/scripts/generate-tests.ts +32 -0
  38. package/dist/templates/full-stack/scripts/generate-types.ts +28 -0
  39. package/dist/templates/full-stack/src/server.ts +78 -0
  40. package/dist/templates/full-stack/tests/helpers.ts +29 -0
  41. package/dist/templates/full-stack/tests/parallel.test.ts +44 -0
  42. package/dist/templates/full-stack/tests/server.test.ts +36 -0
  43. package/dist/templates/full-stack/tests/tools.test.ts +61 -0
  44. package/dist/templates/full-stack/tsconfig.json +19 -0
  45. package/dist/templates/minimal-jest/README.md +42 -0
  46. package/dist/templates/minimal-jest/examples/mock-server.js +62 -0
  47. package/dist/templates/minimal-jest/jest.config.js +24 -0
  48. package/dist/templates/minimal-jest/package.json +24 -0
  49. package/dist/templates/minimal-jest/tests/server.test.ts +64 -0
  50. package/dist/templates/minimal-jest/tsconfig.json +19 -0
  51. package/dist/templates/standard-jest/.github/workflows/test.yml +32 -0
  52. package/dist/templates/standard-jest/README.md +44 -0
  53. package/dist/templates/standard-jest/examples/mock-server.js +122 -0
  54. package/dist/templates/standard-jest/jest.config.js +39 -0
  55. package/dist/templates/standard-jest/package.json +27 -0
  56. package/dist/templates/standard-jest/tests/helpers.ts +43 -0
  57. package/dist/templates/standard-jest/tests/prompts.test.ts +37 -0
  58. package/dist/templates/standard-jest/tests/resources.test.ts +37 -0
  59. package/dist/templates/standard-jest/tests/server.test.ts +30 -0
  60. package/dist/templates/standard-jest/tests/tools.test.ts +87 -0
  61. package/dist/templates/standard-jest/tsconfig.json +19 -0
  62. package/dist/utils/masking.d.ts.map +1 -1
  63. package/dist/utils/masking.js +0 -3
  64. package/dist/utils/masking.js.map +1 -1
  65. package/dist/utils/validation.d.ts.map +1 -1
  66. package/dist/utils/validation.js +9 -0
  67. package/dist/utils/validation.js.map +1 -1
  68. package/dist/utils/version.d.ts +13 -0
  69. package/dist/utils/version.d.ts.map +1 -0
  70. package/dist/utils/version.js +81 -0
  71. package/dist/utils/version.js.map +1 -0
  72. package/docs/advanced.md +424 -0
  73. package/docs/ai-generation.md +211 -0
  74. package/docs/api-reference.md +819 -0
  75. package/docs/cicd.md +289 -0
  76. package/docs/cli.md +225 -0
  77. package/docs/examples.md +300 -0
  78. package/docs/nodejs-compatibility.md +81 -0
  79. package/docs/releases.md +413 -0
  80. package/docs/starter-templates.md +110 -0
  81. package/docs/testing.md +443 -0
  82. package/docs/troubleshooting.md +139 -0
  83. package/jest.d.ts +43 -0
  84. package/package.json +11 -20
@@ -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.