@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.
- package/CHANGELOG.md +54 -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 +214 -14
- package/dist/cli/index.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 +52 -1
- package/dist/generate-tests.d.ts.map +1 -1
- package/dist/generate-tests.js +208 -81
- package/dist/generate-tests.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- 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/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 +5 -2
package/docs/testing.md
ADDED
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slbdn/mcp-tester",
|
|
3
|
-
"version": "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",
|
|
@@ -8,13 +8,16 @@
|
|
|
8
8
|
"sideEffects": false,
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
+
"docs/*.md",
|
|
11
12
|
"vitest.d.ts",
|
|
13
|
+
"jest.d.ts",
|
|
12
14
|
"LICENSE",
|
|
13
15
|
"README.md",
|
|
14
16
|
"CHANGELOG.md"
|
|
15
17
|
],
|
|
16
18
|
"scripts": {
|
|
17
|
-
"build": "tsc",
|
|
19
|
+
"build": "tsc && node scripts/copy-templates.mjs",
|
|
20
|
+
"predev": "node scripts/copy-templates.mjs",
|
|
18
21
|
"dev": "tsc --watch",
|
|
19
22
|
"docs": "typedoc",
|
|
20
23
|
"test": "jest",
|