@slbdn/mcp-tester 1.4.3 → 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 (66) hide show
  1. package/CHANGELOG.md +54 -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 +214 -14
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/generate-cases.d.ts +113 -0
  10. package/dist/generate-cases.d.ts.map +1 -0
  11. package/dist/generate-cases.js +565 -0
  12. package/dist/generate-cases.js.map +1 -0
  13. package/dist/generate-tests.d.ts +52 -1
  14. package/dist/generate-tests.d.ts.map +1 -1
  15. package/dist/generate-tests.js +208 -81
  16. package/dist/generate-tests.js.map +1 -1
  17. package/dist/index.d.ts +3 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/matchers.d.ts +31 -21
  22. package/dist/matchers.d.ts.map +1 -1
  23. package/dist/matchers.js.map +1 -1
  24. package/dist/templates/full-stack/.github/workflows/test.yml +34 -0
  25. package/dist/templates/full-stack/README.md +73 -0
  26. package/dist/templates/full-stack/examples/mock-server.js +115 -0
  27. package/dist/templates/full-stack/jest.config.js +39 -0
  28. package/dist/templates/full-stack/package.json +31 -0
  29. package/dist/templates/full-stack/scripts/generate-tests.ts +32 -0
  30. package/dist/templates/full-stack/scripts/generate-types.ts +28 -0
  31. package/dist/templates/full-stack/src/server.ts +78 -0
  32. package/dist/templates/full-stack/tests/helpers.ts +29 -0
  33. package/dist/templates/full-stack/tests/parallel.test.ts +44 -0
  34. package/dist/templates/full-stack/tests/server.test.ts +36 -0
  35. package/dist/templates/full-stack/tests/tools.test.ts +61 -0
  36. package/dist/templates/full-stack/tsconfig.json +19 -0
  37. package/dist/templates/minimal-jest/README.md +42 -0
  38. package/dist/templates/minimal-jest/examples/mock-server.js +62 -0
  39. package/dist/templates/minimal-jest/jest.config.js +24 -0
  40. package/dist/templates/minimal-jest/package.json +24 -0
  41. package/dist/templates/minimal-jest/tests/server.test.ts +64 -0
  42. package/dist/templates/minimal-jest/tsconfig.json +19 -0
  43. package/dist/templates/standard-jest/.github/workflows/test.yml +32 -0
  44. package/dist/templates/standard-jest/README.md +44 -0
  45. package/dist/templates/standard-jest/examples/mock-server.js +122 -0
  46. package/dist/templates/standard-jest/jest.config.js +39 -0
  47. package/dist/templates/standard-jest/package.json +27 -0
  48. package/dist/templates/standard-jest/tests/helpers.ts +43 -0
  49. package/dist/templates/standard-jest/tests/prompts.test.ts +37 -0
  50. package/dist/templates/standard-jest/tests/resources.test.ts +37 -0
  51. package/dist/templates/standard-jest/tests/server.test.ts +30 -0
  52. package/dist/templates/standard-jest/tests/tools.test.ts +87 -0
  53. package/dist/templates/standard-jest/tsconfig.json +19 -0
  54. package/docs/advanced.md +424 -0
  55. package/docs/ai-generation.md +211 -0
  56. package/docs/api-reference.md +819 -0
  57. package/docs/cicd.md +289 -0
  58. package/docs/cli.md +225 -0
  59. package/docs/examples.md +300 -0
  60. package/docs/nodejs-compatibility.md +81 -0
  61. package/docs/releases.md +413 -0
  62. package/docs/starter-templates.md +110 -0
  63. package/docs/testing.md +443 -0
  64. package/docs/troubleshooting.md +139 -0
  65. package/jest.d.ts +43 -0
  66. package/package.json +5 -2
@@ -0,0 +1,819 @@
1
+ # API Reference
2
+
3
+ Complete API documentation for `MCPClient` and related types.
4
+
5
+ ## MCPClient
6
+
7
+ The main class for interacting with MCP servers.
8
+
9
+ ### Constructor
10
+
11
+ ```typescript
12
+ new MCPClient(options?: MCPClientOptions)
13
+ ```
14
+
15
+ **Parameters:**
16
+
17
+ | Option | Type | Default | Description |
18
+ |--------|------|---------|-------------|
19
+ | `name` | `string` | `'mcp-test-client'` | Client identifier sent to the server |
20
+ | `version` | `string` | `'1.0.0'` | Client version sent to the server |
21
+ | `timeout` | `number` | `30000` | Default request timeout (ms) |
22
+ | `logLevel` | `LogLevel` | `'info'` | Log level: `debug`, `info`, `warn`, `error`, `none` |
23
+ | `enableProtocolLogging` | `boolean` | `false` | Log raw JSON protocol messages |
24
+ | `retries` | `number` | `0` | Number of retry attempts for failed requests |
25
+ | `retryDelay` | `number` | `1000` | Base delay (ms) between retries |
26
+ | `startupDelay` | `number` | `0` | Delay after server start (ms). Opt-in; `0` disables |
27
+
28
+ **Example:**
29
+
30
+ ```typescript
31
+ const client = new MCPClient({
32
+ name: 'production-test-client',
33
+ version: '2.1.0',
34
+ timeout: 60000,
35
+ logLevel: 'debug',
36
+ retries: 3,
37
+ retryDelay: 1000,
38
+ });
39
+ ```
40
+
41
+ ---
42
+
43
+ ### `start(config: ServerConfig): Promise<void>`
44
+
45
+ Start the client and connect to an MCP server. Supports three transport types:
46
+
47
+ #### Stdio (default)
48
+
49
+ Spawns a local server process and communicates via stdin/stdout.
50
+
51
+ | Param | Type | Required | Description |
52
+ |-------|------|----------|-------------|
53
+ | `config.transport` | `'stdio'` | | Omit for default (stdio) |
54
+ | `config.command` | `string` | ✅ | Command to execute (e.g., `'node'`) |
55
+ | `config.args` | `string[]` | | Arguments to pass to the command |
56
+ | `config.env` | `Record<string, string \| undefined>` | | Environment variables |
57
+ | `config.startupDelay` | `number` | | Per-connection startup delay (ms), overrides the client option |
58
+
59
+ ```typescript
60
+ await client.start({
61
+ command: 'node',
62
+ args: ['./server.js'],
63
+ });
64
+ ```
65
+
66
+ #### Streamable HTTP
67
+
68
+ Connects to a remote server via the MCP Streamable HTTP transport (POST + SSE GET).
69
+
70
+ | Param | Type | Required | Description |
71
+ |-------|------|----------|-------------|
72
+ | `config.transport` | `'http'` | ✅ | Must be `'http'` |
73
+ | `config.url` | `string` | ✅ | Server endpoint URL |
74
+ | `config.headers` | `Record<string, string>` | | Custom HTTP headers |
75
+ | `config.sessionId` | `string` | | Resume existing session |
76
+
77
+ ```typescript
78
+ await client.start({
79
+ transport: 'http',
80
+ url: 'https://api.example.com/mcp',
81
+ headers: { Authorization: 'Bearer token' },
82
+ });
83
+ ```
84
+
85
+ #### SSE (legacy)
86
+
87
+ Connects to a server using the deprecated SSE transport.
88
+
89
+ | Param | Type | Required | Description |
90
+ |-------|------|----------|-------------|
91
+ | `config.transport` | `'sse'` | ✅ | Must be `'sse'` |
92
+ | `config.url` | `string` | ✅ | SSE endpoint URL |
93
+ | `config.headers` | `Record<string, string>` | | Custom HTTP headers |
94
+
95
+ ```typescript
96
+ await client.start({
97
+ transport: 'sse',
98
+ url: 'http://localhost:3000/sse',
99
+ });
100
+ ```
101
+
102
+ **Throws:**
103
+ - `MCPAlreadyStartedError` — if the client is already connected
104
+ - `MCPConnectionError` — if the connection fails
105
+
106
+ ---
107
+
108
+ ### `stop(): Promise<void>`
109
+
110
+ Stop the client and disconnect. Safe to call multiple times.
111
+
112
+ ```typescript
113
+ await client.stop();
114
+ ```
115
+
116
+ ---
117
+
118
+ ### `isConnected(): boolean`
119
+
120
+ Check if the client is connected.
121
+
122
+ ```typescript
123
+ if (client.isConnected()) {
124
+ console.log('Client is active');
125
+ }
126
+ ```
127
+
128
+ ### `getTransportType(): TransportType | null`
129
+
130
+ Get the active transport type. Returns `'stdio'`, `'http'`, `'sse'`, or `null`.
131
+
132
+ ```typescript
133
+ const type = client.getTransportType();
134
+ if (type === 'http') {
135
+ console.log('Connected via HTTP');
136
+ }
137
+ ```
138
+
139
+ ---
140
+
141
+ ### `listTools(): Promise<Tool[]>`
142
+
143
+ List all available tools from the server.
144
+
145
+ **Returns:** Array of `Tool` objects, each with:
146
+ - `name` — unique identifier
147
+ - `description` — human-readable description
148
+ - `inputSchema` — JSON Schema for parameters
149
+
150
+ **Throws:** `MCPNotStartedError` if client not started, `MCPServerError` on failure
151
+
152
+ ```typescript
153
+ const tools = await client.listTools();
154
+ tools.forEach(tool => {
155
+ console.log(`- ${tool.name}: ${tool.description}`);
156
+ });
157
+ ```
158
+
159
+ ---
160
+
161
+ ### `callTool(options: ToolCallOptions): Promise<CallToolResult>`
162
+
163
+ Call a tool on the server.
164
+
165
+ **Parameters:**
166
+
167
+ | Param | Type | Required | Description |
168
+ |-------|------|----------|-------------|
169
+ | `options.name` | `string` | ✅ | Tool name |
170
+ | `options.arguments` | `Record<string, unknown>` | | Tool arguments |
171
+ | `options.timeout` | `number` | | Override default timeout (ms) |
172
+ | `options.retries` | `number` | | Override default retry count |
173
+
174
+ **Returns:** `CallToolResult` with a `content` array of text/image items
175
+
176
+ **Throws:** `MCPNotStartedError`, `MCPTimeoutError`, `MCPServerError`
177
+
178
+ ```typescript
179
+ const result = await client.callTool({
180
+ name: 'calculator-add',
181
+ arguments: { a: 5, b: 3 },
182
+ timeout: 10000,
183
+ });
184
+ console.log('Result:', result.content[0].text);
185
+ ```
186
+
187
+ ---
188
+
189
+ ### `listResources(): Promise<Resource[]>`
190
+
191
+ List all available resources.
192
+
193
+ **Returns:** Array of `Resource` objects with `uri`, `name`, `description`, `mimeType`
194
+
195
+ ```typescript
196
+ const resources = await client.listResources();
197
+ resources.forEach(r => console.log(`- ${r.uri}: ${r.mimeType}`));
198
+ ```
199
+
200
+ ---
201
+
202
+ ### `readResource(uri: string): Promise<ReadResourceResult>`
203
+
204
+ Read a resource by URI.
205
+
206
+ ```typescript
207
+ const result = await client.readResource('config://settings');
208
+ console.log('Settings:', result.contents[0].text);
209
+ ```
210
+
211
+ ---
212
+
213
+ ### `listPrompts(): Promise<Prompt[]>`
214
+
215
+ List all available prompts.
216
+
217
+ **Returns:** Array of `Prompt` objects with `name`, `description`, `arguments`
218
+
219
+ ```typescript
220
+ const prompts = await client.listPrompts();
221
+ prompts.forEach(p => console.log(`- ${p.name}: ${p.description}`));
222
+ ```
223
+
224
+ ---
225
+
226
+ ### `getPrompt(name: string, args?: Record<string, string>): Promise<GetPromptResult>`
227
+
228
+ Get a prompt template with optional argument values.
229
+
230
+ ```typescript
231
+ const result = await client.getPrompt('greet', { name: 'Alice' });
232
+ console.log('Prompt:', result.messages[0].content.text);
233
+ ```
234
+
235
+ ---
236
+
237
+ ### `requestSampling(request: CreateMessageRequestParams): Promise<CreateMessageResult>`
238
+
239
+ Request LLM sampling from the server.
240
+
241
+ ```typescript
242
+ const result = await client.requestSampling({
243
+ messages: [
244
+ {
245
+ role: 'user',
246
+ content: { type: 'text', text: 'Explain quantum computing' },
247
+ },
248
+ ],
249
+ maxTokens: 500,
250
+ });
251
+ console.log(result.content.text);
252
+ ```
253
+
254
+ ---
255
+
256
+ ### `setElicitationHandler(handler): Promise<void>`
257
+
258
+ Configure a handler for server elicitation requests (user input).
259
+
260
+ ```typescript
261
+ await client.setElicitationHandler(async (request) => {
262
+ if (request.params.mode === 'form') {
263
+ return {
264
+ action: 'accept',
265
+ content: { userInput: 'User provided data' },
266
+ };
267
+ }
268
+ return { action: 'decline' };
269
+ });
270
+ ```
271
+
272
+ **Handler return values:**
273
+ - `action`: `'accept' | 'decline' | 'cancel'`
274
+ - `content` (optional): collected input data
275
+
276
+ ---
277
+
278
+ ### `setNotificationHandlers(handlers: NotificationHandler): void`
279
+
280
+ Configure handlers for server-initiated notifications.
281
+
282
+ ```typescript
283
+ client.setNotificationHandlers({
284
+ onLoggingMessage: (level, data) => {
285
+ console.log(`[${level}] ${data}`);
286
+ },
287
+ onResourceListChanged: () => {
288
+ console.log('Resources updated, refreshing...');
289
+ },
290
+ });
291
+ ```
292
+
293
+ ---
294
+
295
+ ### `setLogLevel(level: LogLevel): void`
296
+
297
+ Change the log level at runtime.
298
+
299
+ ```typescript
300
+ client.setLogLevel('debug');
301
+ ```
302
+
303
+ ---
304
+
305
+ ## Secret Masking
306
+
307
+ All log output is automatically masked to prevent accidental secret leakage. API keys, tokens, passwords, and other sensitive values are replaced with `***` or `sk-ab...789`-style masks.
308
+
309
+ ```typescript
310
+ import { maskSecrets, maskValue, addSecretPattern } from '@slbdn/mcp-tester';
311
+
312
+ // Built-in patterns are applied automatically
313
+ maskSecrets('API key: sk-proj-abcdefghijklmnopqrstuvwxyz');
314
+ // → 'API key: sk-pr...xyz'
315
+
316
+ // Mask environment variables with sensitive keys
317
+ maskSecrets('PASSWORD=mysecretpassword123');
318
+ // → 'PASSWORD=***'
319
+ maskSecrets('OPENAI_API_KEY=sk-abc...');
320
+ // → 'OPENAI_API_KEY=***'
321
+
322
+ // Add custom patterns
323
+ addSecretPattern(/my-org-key-[a-zA-Z0-9]{20,}/g, 'MyOrg key');
324
+ ```
325
+
326
+ ### Built-in Patterns
327
+
328
+ | Pattern | Description |
329
+ |---------|-------------|
330
+ | `sk-proj-...`, `sk-ant-...` | OpenAI/Anthropic API keys |
331
+ | `sk-...` (20+ chars) | Generic API keys |
332
+ | `AKIA...` | AWS access keys |
333
+ | `Bearer ...` | Bearer tokens |
334
+ | `eyJ...` | JWT tokens |
335
+ | 40+ hex chars | Long hex tokens |
336
+ | `:password@host` | URL credentials |
337
+ | `password=...`, `token=...` | Key-value secrets |
338
+
339
+ ### Sensitive Environment Variables
340
+
341
+ Values for these keys are automatically masked: `API_KEY`, `SECRET`, `PASSWORD`, `TOKEN`, `DATABASE_URL`, `AWS_SECRET_ACCESS_KEY`, `OPENAI_API_KEY`, and 30+ more.
342
+
343
+ ### Exports
344
+
345
+ | Function | Description |
346
+ |----------|-------------|
347
+ | `maskSecrets(input)` | Mask all secrets in a string |
348
+ | `maskValue(value, chars?)` | Mask a single value (show first/last `chars`) |
349
+ | `addSecretPattern(regex, name)` | Add a custom pattern |
350
+ | `resetSecretPatterns()` | Reset to built-in defaults |
351
+ | `getSecretPatterns()` | Get active pattern list |
352
+ | `getSensitiveEnvKeys()` | Get set of sensitive env var names |
353
+
354
+ ### ConsoleLogger Integration
355
+
356
+ `ConsoleLogger` automatically masks all log output. To disable:
357
+
358
+ ```typescript
359
+ const client = new MCPClient({
360
+ logLevel: 'debug',
361
+ // maskSecrets is true by default
362
+ // maskSecrets: false, // disable masking (not recommended)
363
+ });
364
+ ```
365
+
366
+ ---
367
+
368
+ ## Error Classes
369
+
370
+ All errors extend `MCPClientError` and include a `code` property. Errors provide
371
+ contextual information and actionable suggestions for debugging.
372
+
373
+ | Error | Code | Properties | When |
374
+ |-------|------|------------|------|
375
+ | `MCPClientError` | `MCP_CLIENT_ERROR` | `code` | Base error class |
376
+ | `MCPTimeoutError` | `MCP_TIMEOUT_ERROR` | `code`, `.timeout`, `.operation`, `.suggestions` | Request exceeds timeout |
377
+ | `MCPConnectionError` | `MCP_CONNECTION_ERROR` | `code`, `.command`, `.suggestions` | Server fails to start or connect |
378
+ | `MCPNotStartedError` | `MCP_NOT_STARTED` | `code`, `.method` | Method called before `start()` |
379
+ | `MCPAlreadyStartedError` | `MCP_ALREADY_STARTED` | `code` | `start()` called on running client |
380
+ | `MCPServerError` | `MCP_SERVER_ERROR` | `code`, `.operation`, `.serverCode` | Server returns an error response |
381
+
382
+ **Usage:**
383
+
384
+ ```typescript
385
+ import {
386
+ MCPClientError,
387
+ MCPTimeoutError,
388
+ MCPConnectionError,
389
+ MCPServerError,
390
+ } from '@slbdn/mcp-tester';
391
+
392
+ try {
393
+ await client.callTool({ name: 'tool', arguments: {} });
394
+ } catch (error) {
395
+ if (error instanceof MCPTimeoutError) {
396
+ console.error(`Timeout on ${error.operation} after ${error.timeout}ms`);
397
+ console.error('Suggestions:', error.suggestions);
398
+ } else if (error instanceof MCPConnectionError) {
399
+ console.error(`Connection failed, command: ${error.command}`);
400
+ console.error('Suggestions:', error.suggestions);
401
+ } else if (error instanceof MCPServerError) {
402
+ console.error(`Server error in ${error.operation}: ${error.message}`);
403
+ if (error.serverCode) console.error(`Server code: ${error.serverCode}`);
404
+ } else if (error instanceof MCPClientError) {
405
+ console.error('Client error:', error.message);
406
+ }
407
+ }
408
+ ```
409
+
410
+ ---
411
+
412
+ ## Assertion Module
413
+
414
+ Framework-agnostic assertions that throw `AssertionError` on failure. Works with any test runner — Jest, Vitest, Node.js `assert`, or custom harnesses.
415
+
416
+ ```typescript
417
+ import { assert, AssertionError } from '@slbdn/mcp-tester';
418
+
419
+ const result = await client.callTool({ name: 'echo', arguments: { message: 'hello' } });
420
+ assert.toolTextContains(result, 'hello');
421
+ assert.equal(tools.length, 4);
422
+ ```
423
+
424
+ ### Value Assertions
425
+
426
+ | Assertion | Description |
427
+ |-----------|------------|
428
+ | `equal(a, b)` | Strict equality (`===`) |
429
+ | `notEqual(a, b)` | Strict inequality (`!==`) |
430
+ | `deepEqual(a, b)` | JSON deep equality |
431
+ | `ok(val)` | Value is truthy |
432
+ | `notOk(val)` | Value is falsy |
433
+ | `throws(fn)` | Async function throws (returns the error) |
434
+ | `doesNotThrow(fn)` | Async function does not throw |
435
+
436
+ ### Numeric Assertions
437
+
438
+ | Assertion | Description |
439
+ |-----------|------------|
440
+ | `equalNum(a, b)` | Number equality |
441
+ | `greaterThan(a, b)` | `a > b` |
442
+ | `atLeast(a, b)` | `a >= b` |
443
+ | `lessThan(a, b)` | `a < b` |
444
+ | `closeTo(a, b, eps?)` | `|a - b| <= eps` (default epsilon 0.001) |
445
+
446
+ ### String Assertions
447
+
448
+ | Assertion | Description |
449
+ |-----------|------------|
450
+ | `contains(str, sub)` | String contains substring |
451
+ | `notContains(str, sub)` | String does not contain substring |
452
+ | `matches(str, regex)` | String matches regex |
453
+
454
+ ### Tool Result Assertions
455
+
456
+ | Assertion | Description |
457
+ |-----------|------------|
458
+ | `toolTextEquals(r, str)` | Tool text equals string exactly |
459
+ | `toolTextContains(r, str)` | Tool text contains substring |
460
+ | `toolNumEquals(r, num)` | Parse tool text as number, exact compare |
461
+ | `toolNumCloseTo(r, num, eps?)` | Parse tool text as number, approximate compare |
462
+ | `toolJsonEquals(r, obj)` | Parse tool text as JSON, deep compare |
463
+ | `toolIsError(r)` | Tool result `isError === true` or text contains "error" |
464
+ | `toolIsOk(r)` | Tool result is successful (not error) |
465
+ | `toolHasContent(r, n?)` | Tool result has at least `n` content items (default 1) |
466
+ | `toolHasImage(r)` | Tool result contains an image content item |
467
+
468
+ ### Resource Result Assertions
469
+
470
+ | Assertion | Description |
471
+ |-----------|------------|
472
+ | `resourceHasContent(r, n?)` | Resource result has at least `n` contents (default 1) |
473
+ | `resourceTextContains(r, str)` | First resource text contains substring |
474
+
475
+ ### Prompt Result Assertions
476
+
477
+ | Assertion | Description |
478
+ |-----------|------------|
479
+ | `promptHasMessages(r, n?)` | Prompt result has at least `n` messages (default 1) |
480
+ | `promptTextContains(r, str)` | First prompt message text contains substring |
481
+
482
+ ---
483
+
484
+ ## Custom Matchers (Jest & Vitest)
485
+
486
+ Register all matchers with one call:
487
+
488
+ ```typescript
489
+ // Jest
490
+ import { setupJestMatchers } from '@slbdn/mcp-tester';
491
+ beforeAll(() => setupJestMatchers());
492
+
493
+ // Vitest
494
+ import { setupVitestMatchers } from '@slbdn/mcp-tester';
495
+ import { beforeAll } from 'vitest';
496
+ /// <reference types="@slbdn/mcp-tester/vitest" />
497
+ beforeAll(() => setupVitestMatchers());
498
+ `` `
499
+
500
+ `setupCustomMatchers()` is available as a backward-compatible alias for `setupJestMatchers()`.
501
+
502
+ ### Collection Matchers
503
+
504
+ Apply to arrays returned by `listTools()`, `listResources()`, `listPrompts()`.
505
+
506
+ | Matcher | Description |
507
+ |---------|-------------|
508
+ | `toHaveTool(name)` | Assert a tool exists by name |
509
+ | `toHaveToolWithSchema(name)` | Assert a tool has an `inputSchema` |
510
+ | `toHaveToolCount(n)` | Assert exact number of tools |
511
+ | `toHaveResource(uri)` | Assert a resource exists by URI |
512
+ | `toHaveResourceByName(name)` | Assert a resource exists by display name |
513
+ | `toHaveResourceCount(n)` | Assert exact number of resources |
514
+ | `toHavePrompt(name)` | Assert a prompt exists by name |
515
+ | `toHavePromptWithArgs(name)` | Assert a prompt has defined arguments |
516
+ | `toHavePromptCount(n)` | Assert exact number of prompts |
517
+
518
+ ### Tool Result Matchers
519
+
520
+ Apply to `CallToolResult` returned by `callTool()`.
521
+
522
+ | Matcher | Description |
523
+ |---------|-------------|
524
+ | `toReturnText(expected?)` | Tool text equals string (or just has text if omitted) |
525
+ | `toReturnTextContaining(sub)` | Tool text contains substring |
526
+ | `toReturnError()` | Tool result is an error |
527
+ | `toReturnOk()` | Tool result is successful |
528
+ | `toReturnJson(obj)` | Parse tool text as JSON, deep compare |
529
+ | `toReturnContentCount(n)` | Tool has exactly `n` content items |
530
+ | `toReturnImage()` | Tool result contains an image |
531
+
532
+ ### Resource Result Matchers
533
+
534
+ Apply to `ReadResourceResult` returned by `readResource()`.
535
+
536
+ | Matcher | Description |
537
+ |---------|-------------|
538
+ | `toReturnResourceText(expected?)` | Resource text equals string (or just has text) |
539
+ | `toReturnResourceTextContaining(sub)` | Resource text contains substring |
540
+
541
+ ### Prompt Result Matchers
542
+
543
+ Apply to `GetPromptResult` returned by `getPrompt()`.
544
+
545
+ | Matcher | Description |
546
+ |---------|-------------|
547
+ | `toReturnPromptTextContaining(sub)` | First prompt message text contains substring |
548
+ | `toReturnPromptMessageCount(n)` | Prompt has exactly `n` messages |
549
+
550
+ All matchers support `.not` negation and work identically in Jest and Vitest.
551
+
552
+ ---
553
+
554
+ ## Types
555
+
556
+ ### MCPServerConfig
557
+
558
+ ```typescript
559
+ interface MCPServerConfig {
560
+ command: string;
561
+ args?: string[];
562
+ env?: Record<string, string | undefined>;
563
+ startupDelay?: number;
564
+ }
565
+ ```
566
+
567
+ ### MCPClientOptions
568
+
569
+ ```typescript
570
+ interface MCPClientOptions {
571
+ name?: string;
572
+ version?: string;
573
+ timeout?: number;
574
+ logLevel?: LogLevel;
575
+ enableProtocolLogging?: boolean;
576
+ retries?: number;
577
+ retryDelay?: number;
578
+ startupDelay?: number;
579
+ }
580
+ ```
581
+
582
+ ### ToolCallOptions
583
+
584
+ ```typescript
585
+ interface ToolCallOptions {
586
+ name: string;
587
+ arguments?: Record<string, unknown>;
588
+ timeout?: number;
589
+ retries?: number;
590
+ }
591
+ ```
592
+
593
+ ### NotificationHandler
594
+
595
+ ```typescript
596
+ interface NotificationHandler {
597
+ onLoggingMessage?: (level: string, data: string) => void;
598
+ onResourceListChanged?: () => void;
599
+ }
600
+ ```
601
+
602
+ ---
603
+
604
+ ## Server Health Checks
605
+
606
+ Detect zombie processes and monitor server health.
607
+
608
+ ```typescript
609
+ import { MCPClient } from '@slbdn/mcp-tester';
610
+
611
+ const client = new MCPClient();
612
+ await client.start({ command: 'node', args: ['./server.js'] });
613
+
614
+ // One-time health check
615
+ const health = await client.isHealthy();
616
+ if (!health.healthy) {
617
+ console.error(`Server unhealthy: ${health.message}`);
618
+ }
619
+ console.log(`PID: ${health.pid}, latency: ${health.latencyMs}ms`);
620
+ ```
621
+
622
+ ### Periodic Monitoring
623
+
624
+ ```typescript
625
+ client.startHealthMonitor({
626
+ interval: 3000, // check every 3 seconds
627
+ onUnhealthy: (status) => console.error('Server down:', status.message),
628
+ onRecovery: (status) => console.log('Server recovered!'),
629
+ onCheck: (status) => console.log(`Health: ${status.healthy} (${status.latencyMs}ms)`),
630
+ });
631
+
632
+ // Stop monitoring
633
+ client.stopHealthMonitor();
634
+ // Also stopped automatically by client.stop()
635
+ ```
636
+
637
+ ### Methods
638
+
639
+ | Method | Returns | Description |
640
+ |--------|---------|-------------|
641
+ | `isHealthy()` | `Promise<HealthStatus>` | Check server health and responsiveness |
642
+ | `getLastHealthStatus()` | `HealthStatus \| null` | Get last check result (no new request) |
643
+ | `getServerPid()` | `number \| null` | Get server process PID |
644
+ | `startHealthMonitor(opts)` | `void` | Start periodic health monitoring |
645
+ | `stopHealthMonitor()` | `void` | Stop periodic monitoring |
646
+
647
+ ### HealthStatus
648
+
649
+ ```typescript
650
+ interface HealthStatus {
651
+ healthy: boolean; // Is the server responsive?
652
+ checkedAt: number; // Timestamp of the check
653
+ latencyMs: number; // Round-trip latency, or -1 if failed
654
+ pid: number | null; // Server process PID
655
+ message: string; // Human-readable status
656
+ }
657
+ ```
658
+
659
+ ### HealthMonitorOptions
660
+
661
+ ```typescript
662
+ interface HealthMonitorOptions {
663
+ interval?: number; // Check interval in ms (default 5000)
664
+ onUnhealthy?: (status: HealthStatus) => void; // Called when server goes down
665
+ onRecovery?: (status: HealthStatus) => void; // Called when server recovers
666
+ onCheck?: (status: HealthStatus) => void; // Called on every check
667
+ }
668
+ ```
669
+
670
+ ### Zombie Process Detection
671
+
672
+ Health checks detect dead server processes using `process.kill(pid, 0)` (signal 0 — existence check only). If the PID is no longer alive, `isHealthy()` returns `{ healthy: false, message: 'Server process (PID 12345) is no longer running' }`.
673
+
674
+ ---
675
+
676
+ ## Test Generation (Edge Cases & AI)
677
+
678
+ Beyond `generateTests` / `generateTestsFromClient`, the generator can derive
679
+ boundary and invalid-input test cases from each tool's JSON Schema — either
680
+ deterministically (`mode: 'edge'`) or augmented by an LLM (`mode: 'ai'`).
681
+ See [AI-Assisted Test Generation](./ai-generation.md) for the CLI workflow.
682
+
683
+ ### `GeneratedCase`
684
+
685
+ ```typescript
686
+ interface GeneratedCase {
687
+ tool: string; // tool name
688
+ title: string; // human-readable test title
689
+ args: Record<string, unknown>; // arguments to send
690
+ expectation: 'success' | 'error' | 'observe';
691
+ rationale: string; // why this case exists
692
+ source: `rule:${string}` | `ai:${string}`; // provenance, e.g. 'rule:required'
693
+ }
694
+ ```
695
+
696
+ ### `suggestEdgeCasesForTools(tools)`
697
+
698
+ Derive deterministic edge cases from tool schemas (offline, no AI):
699
+
700
+ ```typescript
701
+ import { suggestEdgeCasesForTools } from '@slbdn/mcp-tester';
702
+
703
+ const cases = suggestEdgeCasesForTools(tools);
704
+ // Rules: required, enum, min-length, max-length, bounds, type,
705
+ // min-items, additional-properties
706
+ ```
707
+
708
+ ### `validateArgsAgainstSchema(args, schema)`
709
+
710
+ Shallow schema validation used as the AI gate. Returns `null` when acceptable,
711
+ otherwise a human-readable reason:
712
+
713
+ ```typescript
714
+ import { validateArgsAgainstSchema } from '@slbdn/mcp-tester';
715
+
716
+ validateArgsAgainstSchema({}, echoTool.inputSchema);
717
+ // → 'missing required property "message"'
718
+ ```
719
+
720
+ ### `mergeAndValidateCases(groups, schemas)`
721
+
722
+ Merge case groups (rules + AI), dedupe, drop hallucinated cases:
723
+
724
+ ```typescript
725
+ import { mergeAndValidateCases, suggestEdgeCasesForTools } from '@slbdn/mcp-tester';
726
+
727
+ const { cases, rejected } = mergeAndValidateCases(
728
+ [suggestEdgeCasesForTools(tools), aiCases],
729
+ new Map(tools.map((t) => [t.name, t]))
730
+ );
731
+ // rejected: Array<{ c: GeneratedCase; reason: string }>
732
+ ```
733
+
734
+ Error-expectation cases skip schema validation by design (they intentionally
735
+ violate the schema).
736
+
737
+ ### `OpenAICompatProvider`
738
+
739
+ AI case suggestions via any OpenAI-compatible `/chat/completions` endpoint:
740
+
741
+ ```typescript
742
+ import { OpenAICompatProvider } from '@slbdn/mcp-tester';
743
+
744
+ const provider = new OpenAICompatProvider({
745
+ baseUrl: 'http://localhost:11434/v1', // default: https://api.openai.com/v1
746
+ model: 'llama3', // default: gpt-4o-mini
747
+ apiKey: undefined, // optional (localhost needs none)
748
+ timeout: 60_000, // request timeout (ms)
749
+ cacheDir: '.mcp-tester-cache', // null disables caching
750
+ // fetchImpl: myFetch, // injectable for tests
751
+ });
752
+
753
+ await provider.suggestCases({ serverLabel: 'my-server', tools });
754
+ // → GeneratedCase[] (source: 'ai:<model>')
755
+ ```
756
+
757
+ `createProviderFromEnv()` builds a provider from `MCP_TESTER_AI_API_KEY`,
758
+ `MCP_TESTER_AI_BASE_URL`, and `MCP_TESTER_AI_MODEL`; a localhost base URL
759
+ counts as configured without a key.
760
+
761
+ ### `generateTests` generation options
762
+
763
+ | Option | Type | Description |
764
+ |--------|------|-------------|
765
+ | `mode` | `'edge' \| 'ai'` | `undefined` = static (happy path only) |
766
+ | `provider` | `AIProvider` | Explicit provider (skips env lookup) |
767
+ | `ai` | `{ requireAi?: boolean }` | `requireAi` fails instead of falling back |
768
+ | `verify` | `boolean` | Dry-run cases against the live server; mismatches become `it.skip` |
769
+
770
+ ---
771
+
772
+ Generate TypeScript type declarations from an MCP server's tool schemas.
773
+
774
+ ```typescript
775
+ import { generateTypes } from '@slbdn/mcp-tester';
776
+
777
+ const types = await generateTypes({
778
+ command: 'node',
779
+ args: ['./server.js'],
780
+ });
781
+ ```
782
+
783
+ ### Parameters: `GenerateTypesOptions`
784
+
785
+ | Option | Type | Default | Description |
786
+ |--------|------|---------|-------------|
787
+ | `command` | `string` | **required** | Command to run the MCP server |
788
+ | `args` | `string[]` | `[]` | Arguments for the server command |
789
+ | `timeout` | `number` | `30000` | Connection timeout (ms) |
790
+ | `includeResources` | `boolean` | `true` | Include resource URI types |
791
+ | `includePrompts` | `boolean` | `true` | Include prompt argument types |
792
+ | `moduleName` | `string` | `'@slbdn/mcp-tester'` | Module name for import hints |
793
+
794
+ **Returns:** `Promise<string>` — the generated `.d.ts` file content
795
+
796
+ ### Generated Types
797
+
798
+ | Type | Description |
799
+ |------|-------------|
800
+ | `{ToolName}Args` | Typed interface per tool's input schema |
801
+ | `ToolName` | Union of all tool names |
802
+ | `ToolArgsMap` | Lookup map: `ToolArgsMap['add']` → `AddArgs` |
803
+ | `ToolCall` | Discriminated union for typed `callTool()` |
804
+ | `ResourceUri` | Union of all resource URIs |
805
+ | `{PromptName}Args` | Typed interface per prompt's arguments |
806
+ | `PromptName` | Union of all prompt names |
807
+ | `PromptArgsMap` | Lookup map for prompt arguments |
808
+ | `PromptCall` | Discriminated union for typed `getPrompt()` |
809
+ | `ServerCapabilities` | Overview interface with typed arrays |
810
+
811
+ ### Schema Conversion Functions
812
+
813
+ These exported functions handle JSON Schema → TypeScript conversion:
814
+
815
+ | Function | Description |
816
+ |----------|-------------|
817
+ | `schemaToType(schema, indent?)` | Convert a JSON Schema to a TypeScript type string |
818
+ | `toTypeName(name)` | Sanitize a tool/prompt name into a valid TypeScript identifier |
819
+ | `escapePropertyName(name)` | Escape a property name for TypeScript (quote if needed) |