@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,443 @@
1
+ # Testing
2
+
3
+ Guide to writing and running tests with MCP Tester.
4
+
5
+ ## Running Tests
6
+
7
+ ```bash
8
+ npm test # Run all tests
9
+ npm run test:watch # Watch mode (auto-rerun on changes)
10
+ npm run test:coverage # Run with coverage report
11
+ npm run lint # Lint code
12
+ ```
13
+
14
+ ## Test Structure
15
+
16
+ ```
17
+ src/__tests__/
18
+ ├── client.test.ts # Basic client operations
19
+ ├── resources-prompts.test.ts # Resources and prompts
20
+ ├── advanced.test.ts # Sampling, elicitation
21
+ ├── real-server.test.ts # Integration tests (stdio transport)
22
+ ├── everything-server.test.ts # Full integration (server-everything)
23
+ ├── cli.test.ts # CLI tool tests
24
+ └── fixtures/
25
+ └── mock-server.ts # In-memory mock MCP server
26
+ ```
27
+
28
+ ## Writing Tests
29
+
30
+ ### Jest
31
+
32
+ ```typescript
33
+ import { MCPClient } from '@slbdn/mcp-tester';
34
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
35
+
36
+ describe('My MCP Server', () => {
37
+ let client: MCPClient;
38
+
39
+ beforeEach(async () => {
40
+ client = new MCPClient({
41
+ name: 'test-client',
42
+ version: '1.0.0',
43
+ });
44
+ await client.start({
45
+ command: 'node',
46
+ args: ['./my-server.js'],
47
+ });
48
+ });
49
+
50
+ afterEach(async () => {
51
+ if (client.isConnected()) {
52
+ await client.stop();
53
+ }
54
+ });
55
+
56
+ it('should list tools', async () => {
57
+ const tools = await client.listTools();
58
+ expect(tools.length).toBeGreaterThan(0);
59
+ });
60
+ });
61
+ ```
62
+
63
+ ### Vitest
64
+
65
+ ```typescript
66
+ import { MCPClient, setupVitestMatchers } from '@slbdn/mcp-tester';
67
+ import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
68
+ // /// <reference types="@slbdn/mcp-tester/vitest" />
69
+
70
+ beforeAll(() => setupVitestMatchers());
71
+
72
+ describe('My MCP Server', () => {
73
+ let client: MCPClient;
74
+
75
+ beforeEach(async () => {
76
+ client = new MCPClient({ name: 'test-client', version: '1.0.0' });
77
+ await client.start({ command: 'node', args: ['./my-server.js'] });
78
+ });
79
+
80
+ afterEach(async () => {
81
+ if (client.isConnected()) await client.stop();
82
+ });
83
+
84
+ it('should list tools', async () => {
85
+ const tools = await client.listTools();
86
+ expect(tools).toHaveTool('echo');
87
+ });
88
+ });
89
+ ```
90
+
91
+ ### Custom Test Runner (assert module)
92
+
93
+ For any test runner — no framework dependency:
94
+
95
+ ```typescript
96
+ import { MCPClient, assert } from '@slbdn/mcp-tester';
97
+
98
+ const client = new MCPClient();
99
+ await client.start({ command: 'node', args: ['./server.js'] });
100
+
101
+ const result = await client.callTool({ name: 'echo', arguments: { message: 'hello' } });
102
+ assert.toolTextContains(result, 'hello');
103
+ assert.equal(tools.length, 4);
104
+ ```
105
+
106
+ See [Examples](./examples.md) for full assert usage.
107
+
108
+ ## Testing Patterns
109
+
110
+ **Always use async/await:**
111
+
112
+ ```typescript
113
+ const result = await client.listTools(); // ✓ Correct
114
+ const result = client.listTools(); // ✗ Returns Promise
115
+ ```
116
+
117
+ **Always clean up in afterEach:**
118
+
119
+ ```typescript
120
+ afterEach(async () => {
121
+ if (client.isConnected()) {
122
+ await client.stop();
123
+ }
124
+ });
125
+ ```
126
+
127
+ **Test errors with rejects.toThrow():**
128
+
129
+ ```typescript
130
+ it('should handle invalid tool', async () => {
131
+ await expect(
132
+ client.callTool({ name: 'non-existent', arguments: {} })
133
+ ).rejects.toThrow();
134
+ });
135
+ ```
136
+
137
+ **Test specific error types:**
138
+
139
+ ```typescript
140
+ import { MCPTimeoutError, MCPServerError } from '@slbdn/mcp-tester';
141
+
142
+ it('should throw timeout error', async () => {
143
+ try {
144
+ await client.callTool({ name: 'slow-tool', arguments: {}, timeout: 1 });
145
+ } catch (error) {
146
+ expect(error).toBeInstanceOf(MCPTimeoutError);
147
+ }
148
+ });
149
+ ```
150
+
151
+ ## Custom Matchers
152
+
153
+ ### Jest Setup
154
+
155
+ ```typescript
156
+ import { setupJestMatchers } from '@slbdn/mcp-tester';
157
+ beforeAll(() => setupJestMatchers());
158
+ ```
159
+
160
+ ### Vitest Setup
161
+
162
+ ```typescript
163
+ import { setupVitestMatchers } from '@slbdn/mcp-tester';
164
+ import { beforeAll } from 'vitest';
165
+ // /// <reference types="@slbdn/mcp-tester/vitest" />
166
+ beforeAll(() => setupVitestMatchers());
167
+ ```
168
+
169
+ ### All Matchers
170
+
171
+ | Matcher | Description |
172
+ |---------|-------------|
173
+ | **Collection** | |
174
+ | `toHaveTool(name)` | Assert a tool exists by name |
175
+ | `toHaveToolWithSchema(name)` | Assert a tool exists and has an input schema |
176
+ | `toHaveToolCount(n)` | Assert exact number of tools |
177
+ | `toHaveResource(uri)` | Assert a resource exists by URI |
178
+ | `toHaveResourceByName(name)` | Assert a resource exists by display name |
179
+ | `toHaveResourceCount(n)` | Assert exact number of resources |
180
+ | `toHavePrompt(name)` | Assert a prompt exists by name |
181
+ | `toHavePromptWithArgs(name)` | Assert a prompt exists and has arguments |
182
+ | `toHavePromptCount(n)` | Assert exact number of prompts |
183
+ | **Tool Results** | |
184
+ | `toReturnText(expected?)` | Tool text exact match (or just has text) |
185
+ | `toReturnTextContaining(sub)` | Tool text contains substring |
186
+ | `toReturnError()` | Tool result is an error |
187
+ | `toReturnOk()` | Tool result is successful |
188
+ | `toReturnJson(obj)` | Parse tool text as JSON, deep compare |
189
+ | `toReturnContentCount(n)` | Tool has N content items |
190
+ | `toReturnImage()` | Tool result contains an image |
191
+ | **Resource Results** | |
192
+ | `toReturnResourceText(expected?)` | Resource text exact match (or just has text) |
193
+ | `toReturnResourceTextContaining(sub)` | Resource text contains substring |
194
+ | **Prompt Results** | |
195
+ | `toReturnPromptTextContaining(sub)` | Prompt message text contains substring |
196
+ | `toReturnPromptMessageCount(n)` | Prompt has N messages |
197
+
198
+ All matchers support `.not` negation and work identically in Jest and Vitest.
199
+
200
+ ## Assertion Module (Framework-Agnostic)
201
+
202
+ For custom test runners or simple scripts — throws `AssertionError` on failure:
203
+
204
+ | Assertion | Description |
205
+ |-----------|-------------|
206
+ | **Value** | |
207
+ | `equal(a, b)` / `notEqual(a, b)` | Strict equality |
208
+ | `deepEqual(a, b)` | JSON deep equality |
209
+ | `ok(val)` / `notOk(val)` | Truthy / falsy |
210
+ | `throws(fn)` / `doesNotThrow(fn)` | Async error checks |
211
+ | **Numeric** | |
212
+ | `equalNum(a, b)` | Number equality |
213
+ | `greaterThan(a, b)` / `atLeast(a, b)` / `lessThan(a, b)` | Comparisons |
214
+ | `closeTo(a, b, eps?)` | Approximate equality |
215
+ | **String** | |
216
+ | `contains(str, sub)` / `notContains(str, sub)` | Substring checks |
217
+ | `matches(str, regex)` | Regex match |
218
+ | **Tool Results** | |
219
+ | `toolTextEquals(r, str)` | Tool text exact match |
220
+ | `toolTextContains(r, str)` | Tool text contains substring |
221
+ | `toolNumEquals(r, num)` | Parse tool text as number, compare |
222
+ | `toolNumCloseTo(r, num, eps?)` | Numeric approximate match |
223
+ | `toolJsonEquals(r, obj)` | Parse tool text as JSON, deep compare |
224
+ | `toolIsError(r)` / `toolIsOk(r)` | Error/success check |
225
+ | `toolHasContent(r, n?)` | Has N content items |
226
+ | `toolHasImage(r)` | Contains image content |
227
+ | **Resources** | |
228
+ | `resourceHasContent(r, n?)` | Has N resource contents |
229
+ | `resourceTextContains(r, str)` | Resource text contains |
230
+ | **Prompts** | |
231
+ | `promptHasMessages(r, n?)` | Has N messages |
232
+ | `promptTextContains(r, str)` | Prompt text contains |
233
+
234
+ ## Mock Server
235
+
236
+ Use the in-memory mock server for fast unit tests without a real server process:
237
+
238
+ ```typescript
239
+ import { MockMCPServer } from './fixtures/mock-server.js';
240
+
241
+ describe('Unit Test', () => {
242
+ let mockServer: MockMCPServer;
243
+
244
+ beforeEach(() => {
245
+ mockServer = new MockMCPServer();
246
+ });
247
+
248
+ it('should echo messages', async () => {
249
+ const result = await mockServer.handleToolCall('echo', { message: 'test' });
250
+ expect(result.content[0].text).toBe('Echo: test');
251
+ });
252
+ });
253
+ ```
254
+
255
+ ### Built-in Tools
256
+
257
+ | Tool | Description |
258
+ |------|-------------|
259
+ | `echo` | Echo back the input message |
260
+ | `add` | Add two numbers |
261
+ | `delay` | Delay for specified milliseconds |
262
+ | `error_tool` | Always throws an error |
263
+ | `counter` | Stateful counter: increment, get, reset |
264
+ | `items` | Stateful item list: add, list, remove, clear |
265
+ | `transform` | Transform text: upper, lower, reverse, length |
266
+
267
+ ### Configurable Behavior
268
+
269
+ ```typescript
270
+ // Simulate slow responses
271
+ const server = new MockMCPServer({ defaultDelay: 100 });
272
+
273
+ // Simulate random failures (30% chance)
274
+ const server = new MockMCPServer({ failureRate: 0.3, failureMessage: 'Network error' });
275
+
276
+ // Enable input schema validation
277
+ const server = new MockMCPServer({ validateSchemas: true });
278
+
279
+ // Update config at runtime
280
+ server.setConfig({ failureRate: 1.0 }); // always fail
281
+ ```
282
+
283
+ ### Custom Handlers
284
+
285
+ ```typescript
286
+ // Custom tool handler
287
+ server.registerToolHandler('echo', (args) => [
288
+ { type: 'text', text: `Custom: ${args.message}` },
289
+ ]);
290
+
291
+ // Custom resource handler
292
+ server.registerResourceHandler('text://example', (uri) => [
293
+ { uri, mimeType: 'text/plain', text: 'Custom content' },
294
+ ]);
295
+
296
+ // Custom prompt handler
297
+ server.registerPromptHandler('greet', (args) => [
298
+ { role: 'assistant', content: { type: 'text', text: `Hi ${args.name}!` } },
299
+ ]);
300
+ ```
301
+
302
+ ### Stateful Testing
303
+
304
+ ```typescript
305
+ // Counter tool
306
+ await server.handleToolCall('counter', { action: 'increment', by: 5 });
307
+ const result = await server.handleToolCall('counter', { action: 'get' });
308
+ // result.content[0].text === 'Counter: 5'
309
+
310
+ // Items tool
311
+ await server.handleToolCall('items', { action: 'add', value: 'apple' });
312
+ await server.handleToolCall('items', { action: 'add', value: 'banana' });
313
+ const list = await server.handleToolCall('items', { action: 'list' });
314
+ // list.content[0].text === '["apple","banana"]'
315
+ ```
316
+
317
+ ### Call History & Assertions
318
+
319
+ ```typescript
320
+ await server.handleToolCall('echo', { message: 'a' });
321
+ await server.handleToolCall('echo', { message: 'b' });
322
+
323
+ server.getCallCount('echo'); // 2
324
+ server.getCallHistory(); // [{ tool: 'echo', args: {...}, timestamp: ... }, ...]
325
+ ```
326
+
327
+ ### Dynamic Registration
328
+
329
+ ```typescript
330
+ // Add new tools/resources/prompts at runtime
331
+ server.addTool({ name: 'myTool', description: '...', inputSchema: { ... } });
332
+ server.addResource({ uri: 'custom://data', name: 'Data', description: '...', mimeType: 'text/plain' });
333
+ server.addPrompt({ name: 'myPrompt', description: '...', arguments: [] });
334
+
335
+ // Remove them
336
+ server.removeTool('echo');
337
+ server.removeResource('text://example');
338
+ server.removePrompt('greet');
339
+
340
+ // Reset all stateful data
341
+ server.resetState();
342
+ ```
343
+ **Built-in mock resources:** `text://example`, `config://settings`
344
+ **Built-in mock prompts:** `greet`, `summarize`
345
+
346
+ ## Test Coverage
347
+
348
+ ```bash
349
+ npm run test:coverage
350
+ open coverage/lcov-report/index.html
351
+ ```
352
+
353
+ Coverage thresholds are configured in `jest.config.js` with per-file floors to catch regressions. Run `npm run test:coverage` to see the full breakdown by file.
354
+
355
+ | File | Stmts | Branch | Func | Lines |
356
+ |------|-------|--------|------|-------|
357
+ | Global | 67% | 61% | 60% | 67% |
358
+ | `assert.ts` | 96% | 87% | 100% | 96% |
359
+ | `matchers.ts` | 68% | 28% | 42% | 64% |
360
+ | `MCPClient.ts` | 68% | 54% | 75% | 68% |
361
+ | `logger.ts` | 84% | 82% | 94% | 83% |
362
+ | `masking.ts` | 100% | 88% | 100% | 100% |
363
+ | `errors.ts` | 100% | 100% | 100% | 100% |
364
+ | `env.ts` | 80% | 100% | 60% | 87% |
365
+
366
+ If coverage drops below these thresholds, the test command will exit with an error. PRs automatically receive a coverage comment via GitHub Actions (see [CI/CD Integration](./cicd.md)).
367
+
368
+ ## Test Reports
369
+
370
+ Every test run auto-generates an HTML report with collapsible test trees, timing, and failure details:
371
+
372
+ ```bash
373
+ npm test # generates reports/test-report.html
374
+ open reports/test-report.html # macOS
375
+ ```
376
+
377
+ The report is generated by `jest-html-reporters` (configured in `jest.config.js`). CI uploads reports as downloadable artifacts with 14-day retention.
378
+
379
+ ## Property-Based Testing
380
+
381
+ Property-based tests use [fast-check](https://github.com/dubzzz/fast-check) to generate random inputs for pure utility functions. 73 tests across 3 modules:
382
+
383
+ | Module | Tests | What's tested |
384
+ |--------|-------|---------------|
385
+ | `validation.ts` | 44 | Reject/accept all input types, error code invariants |
386
+ | `masking.ts` | 17 | Idempotency, completeness, secret detection, length bounds |
387
+ | `generate-tests.ts` | 12 | No-throw on any schema, JSON serializability, priority order |
388
+
389
+ Property tests run with 25 iterations per property (configured via `fc.configureGlobal({ numRuns: 25 })`). They're fast (~1s for all 73 tests) and integrated into the normal test suite — no separate command needed.
390
+
391
+ ## TypeScript Type Generation
392
+
393
+ Generate typed `.d.ts` declarations from your server's tool schemas for type-safe testing:
394
+
395
+ ```bash
396
+ npx @slbdn/mcp-tester generate-types node ./server.js -o server.d.ts
397
+ ```
398
+
399
+ Then use the types in your tests:
400
+
401
+ ```typescript
402
+ import type { ToolArgsMap, ToolName } from './server.d.ts';
403
+
404
+ it('should add numbers correctly', async () => {
405
+ const result = await client.callTool({
406
+ name: 'add',
407
+ arguments: { a: 3, b: 4 } as ToolArgsMap['add'], // ← type-checked!
408
+ });
409
+ expect(result).toReturnText('7');
410
+ });
411
+ ```
412
+
413
+ See [Advanced Usage](./advanced.md#typescript-type-generation) for full documentation.
414
+
415
+ ## Server Health Checks
416
+
417
+ Use health checks to ensure your server is alive during long test suites:
418
+
419
+ ```typescript
420
+ const health = await client.isHealthy();
421
+ expect(health.healthy).toBe(true);
422
+ expect(health.latencyMs).toBeLessThan(1000);
423
+ expect(health.pid).not.toBeNull();
424
+ ```
425
+
426
+ For long-running test suites, use periodic monitoring to fail fast when the server crashes:
427
+
428
+ ```typescript
429
+ describe('Long test suite', () => {
430
+ beforeAll(() => {
431
+ client.startHealthMonitor({
432
+ interval: 5000,
433
+ onUnhealthy: (status) => {
434
+ throw new Error(`Server died: ${status.message}`);
435
+ },
436
+ });
437
+ });
438
+
439
+ afterAll(() => client.stopHealthMonitor());
440
+ });
441
+ ```
442
+
443
+ See [Advanced Usage](./advanced.md#server-health-checks) for full documentation.
@@ -0,0 +1,139 @@
1
+ # Troubleshooting
2
+
3
+ ## Common Issues
4
+
5
+ ### "Client not started" Error
6
+
7
+ **Problem:** Calling methods before `start()`
8
+
9
+ ```typescript
10
+ // ✗ Wrong
11
+ const client = new MCPClient();
12
+ const tools = await client.listTools(); // Error!
13
+
14
+ // ✓ Correct
15
+ const client = new MCPClient();
16
+ await client.start({ command: 'node', args: ['./server.js'] });
17
+ const tools = await client.listTools(); // Works
18
+ ```
19
+
20
+ ### Timeout Errors
21
+
22
+ **Problem:** Requests timing out
23
+
24
+ ```typescript
25
+ // Increase global timeout
26
+ const client = new MCPClient({ timeout: 60000 });
27
+
28
+ // Or per-call timeout
29
+ await client.callTool({
30
+ name: 'slow-tool',
31
+ arguments: {},
32
+ timeout: 30000,
33
+ });
34
+ ```
35
+
36
+ ### Server Process Fails to Start
37
+
38
+ **Problem:** Server command not found or crashes immediately
39
+
40
+ ```bash
41
+ # Verify server exists and runs standalone
42
+ ls -la ./my-server.js
43
+ node ./my-server.js
44
+ ```
45
+
46
+ ### "Module not found" Errors
47
+
48
+ **Problem:** ESM import without `.js` extension
49
+
50
+ ```typescript
51
+ // ✗ Wrong
52
+ import { MCPClient } from './client/MCPClient';
53
+
54
+ // ✓ Correct
55
+ import { MCPClient } from './client/MCPClient.js';
56
+ ```
57
+
58
+ ### TypeScript Build Errors
59
+
60
+ ```bash
61
+ # Clean and rebuild
62
+ rm -rf dist/
63
+ npm run build
64
+ ```
65
+
66
+ ### Server Crashes During Tests
67
+
68
+ **Problem:** Server process dies mid-test, but `isConnected()` still returns `true`
69
+
70
+ ```typescript
71
+ // Use health checks to detect zombie processes
72
+ const health = await client.isHealthy();
73
+ if (!health.healthy) {
74
+ console.error(`Server died: ${health.message}`);
75
+ await client.stop();
76
+ await client.start({ command: 'node', args: ['./server.js'] });
77
+ }
78
+
79
+ // Or use periodic monitoring
80
+ client.startHealthMonitor({
81
+ interval: 3000,
82
+ onUnhealthy: () => { throw new Error('Server crashed during tests'); },
83
+ });
84
+ ```
85
+
86
+ ### Tests Fail in CI but Pass Locally
87
+
88
+ - Check Node.js version matches (>= 20)
89
+ - Verify server paths are correct
90
+ - Ensure environment variables are set
91
+ - Check `maxWorkers: 1` in Jest config (avoids SIGSEGV crashes)
92
+
93
+ ## Debug Mode
94
+
95
+ Enable verbose logging:
96
+
97
+ ```typescript
98
+ const client = new MCPClient({
99
+ logLevel: 'debug',
100
+ enableProtocolLogging: true,
101
+ });
102
+ ```
103
+
104
+ All log output goes to stderr.
105
+
106
+ ## Test Coverage
107
+
108
+ ```bash
109
+ npm run test:coverage
110
+ open coverage/lcov-report/index.html
111
+ ```
112
+
113
+ Coverage thresholds: **80%** for branches, functions, lines, and statements.
114
+
115
+ ## Known Limitations
116
+
117
+ - Supports **stdio**, **Streamable HTTP**, and **SSE** transports
118
+ - Designed for **Node.js** servers (other runtimes may need adjustments)
119
+ - Mock server has configurable delays, failures, stateful tools, and validation
120
+
121
+ ## Vitest
122
+
123
+ Custom matchers work with Vitest. Use `setupVitestMatchers()` and add the type declarations:
124
+
125
+ ```typescript
126
+ import { setupVitestMatchers } from '@slbdn/mcp-tester';
127
+ import { beforeAll } from 'vitest';
128
+ // /// <reference types="@slbdn/mcp-tester/vitest" />
129
+
130
+ beforeAll(() => setupVitestMatchers());
131
+ ```
132
+
133
+ If you get TypeScript errors with custom matchers, make sure your `tsconfig.json` includes the `vitest.d.ts` from the package:
134
+
135
+ ```json
136
+ {
137
+ "include": ["node_modules/@slbdn/mcp-tester/vitest.d.ts"]
138
+ }
139
+ ```
package/jest.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Jest type declarations for MCP Tester custom matchers.
3
+ *
4
+ * IMPORTANT: TypeScript does NOT propagate `declare module 'expect' { ... }`
5
+ * augmentations across `node_modules` boundaries. A `.d.ts` file inside a
6
+ * package cannot augment the consumer's `expect` type — the augmentation
7
+ * only takes effect when the declaring file is part of the consumer's own
8
+ * compilation (i.e. included by their tsconfig or referenced from a file
9
+ * the consumer owns).
10
+ *
11
+ * This is a known TypeScript limitation that also affects libraries like
12
+ * `jest-extended`. See https://github.com/microsoft/TypeScript/issues/15031
13
+ *
14
+ * Setup (pick one):
15
+ *
16
+ * 1. Copy this file into your project root (or a `tests/` folder) and
17
+ * include it in your `tsconfig.json`:
18
+ * "include": ["jest.d.ts", "tests"]
19
+ *
20
+ * 2. Inline the augmentation in your own `tests.d.ts` / `global.d.ts`:
21
+ * import type { MCPMatchers } from '@slbdn/mcp-tester/dist/matchers';
22
+ * declare module 'expect' {
23
+ * interface Matchers<R extends void | Promise<void>, T = unknown> extends MCPMatchers<R> {}
24
+ * }
25
+ *
26
+ * 3. Use Vitest instead (the `vitest` package's module structure accepts
27
+ * the augmentation directly; just install vitest and import
28
+ * `setupVitestMatchers`).
29
+ *
30
+ * Without one of these, the matchers still RUN at runtime (via
31
+ * `expect.extend`) but will not be type-checked by `tsc`.
32
+ *
33
+ * Then in your test setup:
34
+ * import { setupJestMatchers } from '@slbdn/mcp-tester';
35
+ * beforeAll(() => setupJestMatchers());
36
+ */
37
+
38
+ import type { MCPMatchers } from '@slbdn/mcp-tester/dist/matchers';
39
+
40
+ declare module 'expect' {
41
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars -- module augmentation requires an interface; T matches `expect`'s arity
42
+ interface Matchers<R extends void | Promise<void>, T = unknown> extends MCPMatchers<R> {}
43
+ }
package/package.json CHANGED
@@ -1,40 +1,30 @@
1
1
  {
2
2
  "name": "@slbdn/mcp-tester",
3
- "version": "1.4.1",
3
+ "version": "1.5.3",
4
4
  "description": "Minimal MCP client implementation for CI/CD testing of MCP servers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "type": "module",
8
8
  "sideEffects": false,
9
9
  "files": [
10
- "dist/client",
11
- "dist/utils",
12
- "dist/cli",
13
- "dist/index.js",
14
- "dist/index.d.ts",
15
- "dist/index.d.ts.map",
16
- "dist/index.js.map",
17
- "dist/assert.js",
18
- "dist/assert.d.ts",
19
- "dist/assert.d.ts.map",
20
- "dist/assert.js.map",
21
- "dist/matchers.js",
22
- "dist/matchers.d.ts",
23
- "dist/matchers.d.ts.map",
24
- "dist/matchers.js.map",
10
+ "dist",
11
+ "docs/*.md",
25
12
  "vitest.d.ts",
13
+ "jest.d.ts",
26
14
  "LICENSE",
27
15
  "README.md",
28
16
  "CHANGELOG.md"
29
17
  ],
30
18
  "scripts": {
31
- "build": "tsc",
19
+ "build": "tsc && node scripts/copy-templates.mjs",
20
+ "predev": "node scripts/copy-templates.mjs",
32
21
  "dev": "tsc --watch",
33
22
  "docs": "typedoc",
34
23
  "test": "jest",
35
24
  "test:watch": "jest --watch",
36
25
  "test:coverage": "jest --coverage",
37
- "benchmark": "npx tsx src/__tests__/benchmarks.ts 2>/dev/null",
26
+ "test:package": "node scripts/smoke-test-package.mjs",
27
+ "benchmark": "tsx src/__tests__/benchmarks.ts",
38
28
  "lint": "eslint src --ext .ts",
39
29
  "format": "prettier --write \"src/**/*.ts\"",
40
30
  "format:check": "prettier --check \"src/**/*.ts\"",
@@ -83,18 +73,19 @@
83
73
  },
84
74
  "devDependencies": {
85
75
  "@modelcontextprotocol/server-everything": "^2026.1.26",
86
- "@types/jest": "^29.5.0",
76
+ "@types/jest": "^30.0.0",
87
77
  "@types/node": "^20.19.41",
88
78
  "@typescript-eslint/eslint-plugin": "^8.60.0",
89
79
  "@typescript-eslint/parser": "^8.60.0",
90
80
  "eslint": "^8.56.0",
91
81
  "fast-check": "^4.8.0",
92
82
  "husky": "^9.1.7",
93
- "jest": "^29.7.0",
83
+ "jest": "^30.5.2",
94
84
  "jest-html-reporters": "^3.1.7",
95
85
  "lint-staged": "^16.4.0",
96
86
  "prettier": "^3.8.3",
97
87
  "ts-jest": "^29.4.11",
88
+ "tsx": "^4.23.15",
98
89
  "typedoc": "^0.28.19",
99
90
  "typedoc-plugin-markdown": "^4.11.0",
100
91
  "typescript": "^5.3.0"