@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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource tests.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, it } from '@jest/globals';
|
|
6
|
+
import type { MCPClient } from '@slbdn/mcp-tester';
|
|
7
|
+
import { contentText, createConnectedClient, promptText, resourceText } from './helpers.js';
|
|
8
|
+
|
|
9
|
+
describe('Resources', () => {
|
|
10
|
+
let client: MCPClient;
|
|
11
|
+
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
client = await createConnectedClient('resources-test');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
if (client?.isConnected()) {
|
|
18
|
+
await client.stop();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('lists available resources', async () => {
|
|
23
|
+
const resources = await client.listResources();
|
|
24
|
+
expect(resources.length).toBeGreaterThan(0);
|
|
25
|
+
expect(resources.map((r) => r.uri)).toContain('config://version');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('reads the version resource', async () => {
|
|
29
|
+
const result = await client.readResource('config://version');
|
|
30
|
+
expect(result.contents.length).toBe(1);
|
|
31
|
+
expect(resourceText(result)).toBe('1.0.0');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('rejects unknown resources', async () => {
|
|
35
|
+
await expect(client.readResource('config://does-not-exist')).rejects.toThrow();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server lifecycle tests.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, it } from '@jest/globals';
|
|
6
|
+
import type { MCPClient } from '@slbdn/mcp-tester';
|
|
7
|
+
import { createConnectedClient } from './helpers.js';
|
|
8
|
+
|
|
9
|
+
describe('Server lifecycle', () => {
|
|
10
|
+
let client: MCPClient;
|
|
11
|
+
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
client = await createConnectedClient('lifecycle-test');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
if (client?.isConnected()) {
|
|
18
|
+
await client.stop();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('connects to the mock server', () => {
|
|
23
|
+
expect(client.isConnected()).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('reports a healthy server', async () => {
|
|
27
|
+
const health = await client.isHealthy();
|
|
28
|
+
expect(health.healthy).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool tests — covers listing, schema validation, and calling.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, it } from '@jest/globals';
|
|
6
|
+
import type { MCPClient } from '@slbdn/mcp-tester';
|
|
7
|
+
import { contentText, createConnectedClient } from './helpers.js';
|
|
8
|
+
|
|
9
|
+
describe('Tools', () => {
|
|
10
|
+
let client: MCPClient;
|
|
11
|
+
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
client = await createConnectedClient('tools-test');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
if (client?.isConnected()) {
|
|
18
|
+
await client.stop();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('lists all expected tools', async () => {
|
|
23
|
+
const tools = await client.listTools();
|
|
24
|
+
const names = tools.map((t) => t.name);
|
|
25
|
+
expect(names).toEqual(expect.arrayContaining(['echo', 'add', 'slow']));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('validates tool input schemas', async () => {
|
|
29
|
+
const tools = await client.listTools();
|
|
30
|
+
for (const tool of tools) {
|
|
31
|
+
expect(tool.inputSchema).toBeDefined();
|
|
32
|
+
expect(tool.inputSchema.type).toBe('object');
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('echo', () => {
|
|
37
|
+
it('returns the message', async () => {
|
|
38
|
+
const result = await client.callTool({
|
|
39
|
+
name: 'echo',
|
|
40
|
+
arguments: { message: 'hello' },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(contentText(result)).toBe('hello');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('add', () => {
|
|
48
|
+
it('adds two positive integers', async () => {
|
|
49
|
+
const result = await client.callTool({
|
|
50
|
+
name: 'add',
|
|
51
|
+
arguments: { a: 2, b: 3 },
|
|
52
|
+
});
|
|
53
|
+
expect(contentText(result)).toBe('5');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('adds negative numbers', async () => {
|
|
57
|
+
const result = await client.callTool({
|
|
58
|
+
name: 'add',
|
|
59
|
+
arguments: { a: -5, b: 10 },
|
|
60
|
+
});
|
|
61
|
+
expect(contentText(result)).toBe('5');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('slow', () => {
|
|
66
|
+
it('respects the requested delay', async () => {
|
|
67
|
+
const start = Date.now();
|
|
68
|
+
const result = await client.callTool({
|
|
69
|
+
name: 'slow',
|
|
70
|
+
arguments: { ms: 50 },
|
|
71
|
+
});
|
|
72
|
+
const elapsed = Date.now() - start;
|
|
73
|
+
expect(contentText(result)).toBe('waited 50ms');
|
|
74
|
+
expect(elapsed).toBeGreaterThanOrEqual(45);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('times out when requested', async () => {
|
|
78
|
+
await expect(
|
|
79
|
+
client.callTool({
|
|
80
|
+
name: 'slow',
|
|
81
|
+
arguments: { ms: 5000 },
|
|
82
|
+
timeout: 200,
|
|
83
|
+
})
|
|
84
|
+
).rejects.toThrow(/timed out/i);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"rootDir": "./",
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"allowSyntheticDefaultImports": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["tests/**/*.ts", "fixtures/**/*.ts", "examples/**/*.js"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|
package/docs/advanced.md
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# Advanced Usage
|
|
2
|
+
|
|
3
|
+
Advanced patterns and features of MCP Tester.
|
|
4
|
+
|
|
5
|
+
## Concurrent Operations
|
|
6
|
+
|
|
7
|
+
Run multiple tool calls in parallel:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
const results = await Promise.all([
|
|
11
|
+
client.callTool({ name: 'tool1', arguments: {} }),
|
|
12
|
+
client.callTool({ name: 'tool2', arguments: {} }),
|
|
13
|
+
client.callTool({ name: 'tool3', arguments: {} }),
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
results.forEach((result, index) => {
|
|
17
|
+
console.log(`Tool ${index + 1}:`, result.content[0]?.text);
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Timeout Handling
|
|
22
|
+
|
|
23
|
+
Customize timeouts at two levels:
|
|
24
|
+
|
|
25
|
+
**Global timeout** — applies to all requests:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
const client = new MCPClient({ timeout: 60000 });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Per-call timeout** — overrides the global default:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const result = await client.callTool({
|
|
35
|
+
name: 'slow-operation',
|
|
36
|
+
arguments: { duration: 5000 },
|
|
37
|
+
timeout: 10000,
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Retry Logic
|
|
42
|
+
|
|
43
|
+
Configure automatic retries with exponential backoff:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const client = new MCPClient({
|
|
47
|
+
retries: 3,
|
|
48
|
+
retryDelay: 1000, // base delay in ms
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Per-call override:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
await client.callTool({
|
|
56
|
+
name: 'flaky-tool',
|
|
57
|
+
arguments: {},
|
|
58
|
+
retries: 5,
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Retry behavior:
|
|
63
|
+
- Exponential backoff: `delay * backoffMultiplier^(attempt - 1)`
|
|
64
|
+
- Default multiplier: 2
|
|
65
|
+
- Max delay cap: 10,000ms
|
|
66
|
+
- Retries only on thrown errors (not on successful responses with error content)
|
|
67
|
+
|
|
68
|
+
## Environment Variables
|
|
69
|
+
|
|
70
|
+
Pass environment variables to the MCP server process:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
await client.start({
|
|
74
|
+
command: 'node',
|
|
75
|
+
args: ['./server.js'],
|
|
76
|
+
env: {
|
|
77
|
+
NODE_ENV: 'test',
|
|
78
|
+
API_ENDPOINT: process.env.API_ENDPOINT || 'http://localhost:3000',
|
|
79
|
+
LOG_LEVEL: 'debug',
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`undefined` values are automatically filtered out, so conditional env vars work cleanly:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
env: {
|
|
88
|
+
NODE_ENV: 'production',
|
|
89
|
+
API_KEY: process.env.API_KEY, // won't be passed if undefined
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Notification Handling
|
|
94
|
+
|
|
95
|
+
Monitor server-initiated notifications:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
client.setNotificationHandlers({
|
|
99
|
+
onLoggingMessage: (level, data) => {
|
|
100
|
+
console.log(`[${level}] ${data}`);
|
|
101
|
+
},
|
|
102
|
+
onResourceListChanged: async () => {
|
|
103
|
+
console.log('Resources changed, refreshing...');
|
|
104
|
+
const resources = await client.listResources();
|
|
105
|
+
console.log(`Now have ${resources.length} resources`);
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Elicitation Handling
|
|
111
|
+
|
|
112
|
+
Respond to server requests for user input:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
await client.setElicitationHandler(async (request) => {
|
|
116
|
+
console.log('Elicitation request:', request.params.mode);
|
|
117
|
+
|
|
118
|
+
if (request.params.mode === 'form') {
|
|
119
|
+
return {
|
|
120
|
+
action: 'accept',
|
|
121
|
+
content: {
|
|
122
|
+
username: 'test-user',
|
|
123
|
+
email: 'test@example.com',
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { action: 'decline' };
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Handler return values:
|
|
133
|
+
- `action: 'accept'` — provide `content` with the collected input
|
|
134
|
+
- `action: 'decline'` — user declined the request
|
|
135
|
+
- `action: 'cancel'` — user cancelled the operation
|
|
136
|
+
|
|
137
|
+
## Logging
|
|
138
|
+
|
|
139
|
+
Configure logging at construction or dynamically:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
const client = new MCPClient({
|
|
143
|
+
logLevel: 'debug', // 'debug' | 'info' | 'warn' | 'error' | 'none'
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Change at runtime
|
|
147
|
+
client.setLogLevel('warn');
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Enable protocol-level logging to see raw JSON messages:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
const client = new MCPClient({
|
|
154
|
+
enableProtocolLogging: true,
|
|
155
|
+
logLevel: 'debug',
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
All log output goes to stderr so it doesn't interfere with stdout.
|
|
160
|
+
|
|
161
|
+
## Mock Server for Unit Testing
|
|
162
|
+
|
|
163
|
+
Use the in-memory mock server for tests that don't need a real server process:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { MockMCPServer } from './fixtures/mock-server.js';
|
|
167
|
+
|
|
168
|
+
describe('My Feature', () => {
|
|
169
|
+
let mockServer: MockMCPServer;
|
|
170
|
+
|
|
171
|
+
beforeEach(() => {
|
|
172
|
+
mockServer = new MockMCPServer();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should handle tool call', async () => {
|
|
176
|
+
const result = await mockServer.handleToolCall('echo', { message: 'test' });
|
|
177
|
+
expect(result.content[0].text).toBe('Echo: test');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Built-in Tools
|
|
183
|
+
|
|
184
|
+
| Tool | Description |
|
|
185
|
+
|------|-------------|
|
|
186
|
+
| `echo` | Echo back the input message |
|
|
187
|
+
| `add` | Add two numbers |
|
|
188
|
+
| `delay` | Delay for specified milliseconds |
|
|
189
|
+
| `error_tool` | Always throws an error |
|
|
190
|
+
| `counter` | Stateful counter: increment, get, reset |
|
|
191
|
+
| `items` | Stateful item list: add, list, remove, clear |
|
|
192
|
+
| `transform` | Transform text: upper, lower, reverse, length |
|
|
193
|
+
|
|
194
|
+
### Built-in Resources: `text://example`, `config://settings`
|
|
195
|
+
### Built-in Prompts: `greet`, `summarize`
|
|
196
|
+
|
|
197
|
+
### Testing Retry Logic
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
const server = new MockMCPServer({ failureRate: 0.5 });
|
|
201
|
+
// 50% of tool calls will throw randomly
|
|
202
|
+
// Great for testing retry logic
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Testing with Delays
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
const server = new MockMCPServer({ defaultDelay: 100 });
|
|
209
|
+
// All tool calls take ≥100ms — test timeout handling
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Input Validation Testing
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
const server = new MockMCPServer({ validateSchemas: true });
|
|
216
|
+
await expect(
|
|
217
|
+
server.handleToolCall('echo', {})
|
|
218
|
+
).rejects.toThrow('Missing required field');
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Stateful Testing
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
await server.handleToolCall('counter', { action: 'increment', by: 5 });
|
|
225
|
+
await server.handleToolCall('counter', { action: 'increment', by: 3 });
|
|
226
|
+
const result = await server.handleToolCall('counter', { action: 'get' });
|
|
227
|
+
// result.content[0].text === 'Counter: 8'
|
|
228
|
+
|
|
229
|
+
server.resetState(); // clears counters, items, call history
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Custom Handlers
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
server.registerToolHandler('echo', (args) => [
|
|
236
|
+
{ type: 'text', text: `Override: ${args.message}` },
|
|
237
|
+
]);
|
|
238
|
+
server.registerResourceHandler('text://example', (uri) => [
|
|
239
|
+
{ uri, mimeType: 'text/plain', text: 'Custom content' },
|
|
240
|
+
]);
|
|
241
|
+
server.registerPromptHandler('greet', (args) => [
|
|
242
|
+
{ role: 'assistant', content: { type: 'text', text: `Hi ${args.name}!` } },
|
|
243
|
+
]);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Call History Assertions
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
await server.handleToolCall('echo', { message: 'test' });
|
|
250
|
+
server.getCallCount('echo'); // 1
|
|
251
|
+
server.getCallHistory()[0].args; // { message: 'test' }
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
See [Testing Guide](./testing.md#mock-server) for the full mock server API reference.
|
|
255
|
+
|
|
256
|
+
## TypeScript Type Generation
|
|
257
|
+
|
|
258
|
+
Generate typed `.d.ts` declarations from any MCP server's tool schemas:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
npx mcp-tester generate-types node ./server.js -o server.d.ts
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Or use the API:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import { generateTypes } from '@slbdn/mcp-tester';
|
|
268
|
+
import { writeFileSync } from 'fs';
|
|
269
|
+
|
|
270
|
+
const types = await generateTypes({
|
|
271
|
+
command: 'node',
|
|
272
|
+
args: ['./server.js'],
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
writeFileSync('server.d.ts', types);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Generated Output
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
/** Echo back the input */
|
|
282
|
+
export interface EchoArgs {
|
|
283
|
+
/** Message to echo */
|
|
284
|
+
message: string;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Add two numbers */
|
|
288
|
+
export interface AddArgs {
|
|
289
|
+
a: number;
|
|
290
|
+
b: number;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Union of all tool names */
|
|
294
|
+
export type ToolName = 'echo' | 'add' | 'delay' | 'error_tool';
|
|
295
|
+
|
|
296
|
+
/** Look up the argument type for a tool by name */
|
|
297
|
+
export interface ToolArgsMap {
|
|
298
|
+
'echo': EchoArgs;
|
|
299
|
+
'add': AddArgs;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Discriminated union for typed callTool() */
|
|
303
|
+
export type ToolCall =
|
|
304
|
+
| { name: 'echo'; arguments: EchoArgs }
|
|
305
|
+
| { name: 'add'; arguments: AddArgs };
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Using Generated Types
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import type { ToolArgsMap, ToolName } from './server.d.ts';
|
|
312
|
+
|
|
313
|
+
// Type-safe arguments
|
|
314
|
+
const result = await client.callTool({
|
|
315
|
+
name: 'add',
|
|
316
|
+
arguments: { a: 1, b: 2 } as ToolArgsMap['add'],
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Supported JSON Schema Features
|
|
321
|
+
|
|
322
|
+
| Feature | TypeScript Output |
|
|
323
|
+
|---------|------------------|
|
|
324
|
+
| `{ type: 'string' }` | `string` |
|
|
325
|
+
| `{ type: 'number' }` | `number` |
|
|
326
|
+
| `{ type: 'boolean' }` | `boolean` |
|
|
327
|
+
| `{ enum: ['a', 'b'] }` | `'a' \| 'b'` |
|
|
328
|
+
| `{ const: 'fixed' }` | `'fixed'` |
|
|
329
|
+
| `{ type: 'array', items: ... }` | `T[]` or `Array<T>` |
|
|
330
|
+
| `{ type: 'object', properties: ... }` | `{ prop: Type }` |
|
|
331
|
+
| `{ oneOf: [...] }` | Union type |
|
|
332
|
+
| `{ anyOf: [...] }` | Union type |
|
|
333
|
+
| `{ allOf: [...] }` | Intersection type |
|
|
334
|
+
| `{ $ref: '#/definitions/X' }` | `X` |
|
|
335
|
+
| Optional fields (not in `required`) | `field?: Type` |
|
|
336
|
+
| `description` | JSDoc comments |
|
|
337
|
+
|
|
338
|
+
## Server Health Checks
|
|
339
|
+
|
|
340
|
+
Detect zombie processes and monitor server health during long-running test suites.
|
|
341
|
+
|
|
342
|
+
### One-Time Check
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
const health = await client.isHealthy();
|
|
346
|
+
if (!health.healthy) {
|
|
347
|
+
console.error(`Server unhealthy: ${health.message}`);
|
|
348
|
+
console.error(`PID ${health.pid}, latency: ${health.latencyMs}ms`);
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Periodic Monitoring
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
client.startHealthMonitor({
|
|
356
|
+
interval: 3000,
|
|
357
|
+
onUnhealthy: (status) => {
|
|
358
|
+
console.error(`Server died: ${status.message}`);
|
|
359
|
+
// Optionally restart or fail the test
|
|
360
|
+
},
|
|
361
|
+
onRecovery: (status) => {
|
|
362
|
+
console.log(`Server recovered after crash`);
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Clean up
|
|
367
|
+
client.stopHealthMonitor(); // explicit
|
|
368
|
+
await client.stop(); // also stops the monitor
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Zombie Detection
|
|
372
|
+
|
|
373
|
+
Uses `process.kill(pid, 0)` to check if the server process is still alive. Works on macOS, Linux, and Windows. Detects crashed, killed, or orphaned processes that `isConnected()` cannot detect (the client object still exists but the process is gone).
|
|
374
|
+
|
|
375
|
+
### HealthStatus Fields
|
|
376
|
+
|
|
377
|
+
| Field | Type | Description |
|
|
378
|
+
|-------|------|-------------|
|
|
379
|
+
| `healthy` | `boolean` | Is the server responsive |
|
|
380
|
+
| `checkedAt` | `number` | Unix timestamp of the check |
|
|
381
|
+
| `latencyMs` | `number` | Round-trip time, or -1 if failed |
|
|
382
|
+
| `pid` | `number \| null` | Server process ID |
|
|
383
|
+
| `message` | `string` | Human-readable status |
|
|
384
|
+
|
|
385
|
+
## Performance Benchmarks
|
|
386
|
+
|
|
387
|
+
Run the benchmark suite to measure MCP Tester's performance characteristics:
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
npm run benchmark
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
This runs 19 benchmarks against the mock server (50 runs each, 3 warmup), measuring:
|
|
394
|
+
|
|
395
|
+
| Category | Benchmarks |
|
|
396
|
+
|----------|------------|
|
|
397
|
+
| **Connection** | Reconnect, disconnect |
|
|
398
|
+
| **Tool calls** | Echo, add, delay (10ms simulated) |
|
|
399
|
+
| **Sequential** | 5 and 10 tools in a row |
|
|
400
|
+
| **Parallel** | 5, 10, and 20 tools concurrently |
|
|
401
|
+
| **Payloads** | 10KB, 100KB, 1MB echo payloads |
|
|
402
|
+
| **Metadata** | List tools, resources, prompts; get prompt with args |
|
|
403
|
+
| **Lifecycle** | Full start → list → call → stop patterns |
|
|
404
|
+
|
|
405
|
+
### Reference Numbers (Node.js 24, Apple Silicon)
|
|
406
|
+
|
|
407
|
+
| Operation | Mean Latency | Throughput |
|
|
408
|
+
|----------|-------------|------------|
|
|
409
|
+
| Single tool call (echo) | **0.19 ms** | ~5,130 ops/s |
|
|
410
|
+
| Add tool (numbers) | **0.19 ms** | ~5,360 ops/s |
|
|
411
|
+
| 10 parallel tool calls | **0.55 ms total** | ~18,200 ops/s |
|
|
412
|
+
| 20 parallel tool calls | **0.86 ms total** | ~23,300 ops/s |
|
|
413
|
+
| List tools / resources / prompts | **0.15 ms** | ~6,700 ops/s |
|
|
414
|
+
| 1MB payload echo | **7.79 ms** | ~1,080 Mbps |
|
|
415
|
+
| Server reconnect | **165 ms** | ~6 ops/s |
|
|
416
|
+
| Full lifecycle (start+call+stop) | **167 ms** | ~6 ops/s |
|
|
417
|
+
|
|
418
|
+
### Key Takeaways
|
|
419
|
+
|
|
420
|
+
- **Parallelization wins** — 10 parallel calls take the same time as 1 (~0.5ms total), giving ~18x throughput vs sequential
|
|
421
|
+
- **Connection overhead dominates single-call cost** — once connected, tool calls are sub-millisecond
|
|
422
|
+
- **Payload size has minimal impact** — going from 10KB to 1MB adds only ~7ms of overhead
|
|
423
|
+
- **Metadata listings are cheap** — listing tools, resources, or prompts costs ~0.15ms
|
|
424
|
+
- **The stdio transport is fast** — most latency comes from process spawn, not the protocol itself
|