@slbdn/mcp-tester 1.4.1 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +49 -0
- package/dist/ai/provider.d.ts +145 -0
- package/dist/ai/provider.d.ts.map +1 -0
- package/dist/ai/provider.js +428 -0
- package/dist/ai/provider.js.map +1 -0
- package/dist/cli/index.js +260 -151
- package/dist/cli/index.js.map +1 -1
- package/dist/client/MCPClient.d.ts +16 -2
- package/dist/client/MCPClient.d.ts.map +1 -1
- package/dist/client/MCPClient.js +77 -20
- package/dist/client/MCPClient.js.map +1 -1
- package/dist/generate-cases.d.ts +113 -0
- package/dist/generate-cases.d.ts.map +1 -0
- package/dist/generate-cases.js +565 -0
- package/dist/generate-cases.js.map +1 -0
- package/dist/generate-tests.d.ts +106 -0
- package/dist/generate-tests.d.ts.map +1 -0
- package/dist/generate-tests.js +441 -0
- package/dist/generate-tests.js.map +1 -0
- package/dist/generate-types.d.ts +86 -0
- package/dist/generate-types.d.ts.map +1 -0
- package/dist/generate-types.js +484 -0
- package/dist/generate-types.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/matchers.d.ts +31 -21
- package/dist/matchers.d.ts.map +1 -1
- package/dist/matchers.js.map +1 -1
- package/dist/templates/full-stack/.github/workflows/test.yml +34 -0
- package/dist/templates/full-stack/README.md +73 -0
- package/dist/templates/full-stack/examples/mock-server.js +115 -0
- package/dist/templates/full-stack/jest.config.js +39 -0
- package/dist/templates/full-stack/package.json +31 -0
- package/dist/templates/full-stack/scripts/generate-tests.ts +32 -0
- package/dist/templates/full-stack/scripts/generate-types.ts +28 -0
- package/dist/templates/full-stack/src/server.ts +78 -0
- package/dist/templates/full-stack/tests/helpers.ts +29 -0
- package/dist/templates/full-stack/tests/parallel.test.ts +44 -0
- package/dist/templates/full-stack/tests/server.test.ts +36 -0
- package/dist/templates/full-stack/tests/tools.test.ts +61 -0
- package/dist/templates/full-stack/tsconfig.json +19 -0
- package/dist/templates/minimal-jest/README.md +42 -0
- package/dist/templates/minimal-jest/examples/mock-server.js +62 -0
- package/dist/templates/minimal-jest/jest.config.js +24 -0
- package/dist/templates/minimal-jest/package.json +24 -0
- package/dist/templates/minimal-jest/tests/server.test.ts +64 -0
- package/dist/templates/minimal-jest/tsconfig.json +19 -0
- package/dist/templates/standard-jest/.github/workflows/test.yml +32 -0
- package/dist/templates/standard-jest/README.md +44 -0
- package/dist/templates/standard-jest/examples/mock-server.js +122 -0
- package/dist/templates/standard-jest/jest.config.js +39 -0
- package/dist/templates/standard-jest/package.json +27 -0
- package/dist/templates/standard-jest/tests/helpers.ts +43 -0
- package/dist/templates/standard-jest/tests/prompts.test.ts +37 -0
- package/dist/templates/standard-jest/tests/resources.test.ts +37 -0
- package/dist/templates/standard-jest/tests/server.test.ts +30 -0
- package/dist/templates/standard-jest/tests/tools.test.ts +87 -0
- package/dist/templates/standard-jest/tsconfig.json +19 -0
- package/dist/utils/masking.d.ts.map +1 -1
- package/dist/utils/masking.js +0 -3
- package/dist/utils/masking.js.map +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +9 -0
- package/dist/utils/validation.js.map +1 -1
- package/dist/utils/version.d.ts +13 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +81 -0
- package/dist/utils/version.js.map +1 -0
- package/docs/advanced.md +424 -0
- package/docs/ai-generation.md +211 -0
- package/docs/api-reference.md +819 -0
- package/docs/cicd.md +289 -0
- package/docs/cli.md +225 -0
- package/docs/examples.md +300 -0
- package/docs/nodejs-compatibility.md +81 -0
- package/docs/releases.md +413 -0
- package/docs/starter-templates.md +110 -0
- package/docs/testing.md +443 -0
- package/docs/troubleshooting.md +139 -0
- package/jest.d.ts +43 -0
- package/package.json +11 -20
|
@@ -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) |
|