@slbdn/mcp-tester 1.3.0 → 1.4.1
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 +39 -0
- package/README.md +24 -8
- package/dist/cli/index.js +295 -88
- package/dist/cli/index.js.map +1 -1
- package/dist/client/MCPClient.d.ts +102 -11
- package/dist/client/MCPClient.d.ts.map +1 -1
- package/dist/client/MCPClient.js +117 -31
- package/dist/client/MCPClient.js.map +1 -1
- package/dist/client/index.d.ts +3 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/masking.d.ts +2 -2
- package/dist/utils/masking.d.ts.map +1 -1
- package/dist/utils/masking.js +57 -3
- package/dist/utils/masking.js.map +1 -1
- package/dist/utils/validation.d.ts +11 -7
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +81 -4
- package/dist/utils/validation.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,45 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.4.1] - 2026-07-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`maskSecrets()` no longer throws on hostile objects** — objects with a non-function `toString` (e.g. `{ toString: false }`), non-function `valueOf`, non-function `Symbol.toPrimitive`, throwing coercion methods, or circular references previously crashed `String(input)` with `TypeError: Cannot convert object to primitive value`.
|
|
14
|
+
- Root cause: `String()` invokes `toString()`/`valueOf()`/`Symbol.toPrimitive`, but throws when those are present yet not callable.
|
|
15
|
+
- Since masking runs inside `ConsoleLogger.format()` on every log line, this could take down logging entirely.
|
|
16
|
+
- New internal `safeToString()` helper guards coercion with `typeof` checks and `try/catch`, falling back to `JSON.stringify` then `Object.prototype.toString.call()`.
|
|
17
|
+
- Found by property-based testing (`fc.anything()`); verified against 2000 randomized runs with 0 crashes.
|
|
18
|
+
- Added regression tests for hostile objects and circular references.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- **CI: bumped GitHub Actions to current versions** (fixes "Node 20 is being deprecated" warning):
|
|
22
|
+
- `actions/checkout` v3 → v5 (node16 → node24 runtime)
|
|
23
|
+
- `actions/setup-node` v3 → v5
|
|
24
|
+
- `codecov/codecov-action` v3/v4 → v5
|
|
25
|
+
- Replaced deprecated `actions/create-release@v1` with `gh release create`
|
|
26
|
+
- The deprecation warning was about the action runtime, not the test matrix.
|
|
27
|
+
|
|
28
|
+
## [1.4.0] - 2026-07-01
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **HTTP and SSE transport support** — connect to remote MCP servers:
|
|
32
|
+
- `StreamableHttpServerConfig` (`transport: 'http'`) — modern MCP Streamable HTTP (POST + SSE GET)
|
|
33
|
+
- `SseServerConfig` (`transport: 'sse'`) — legacy SSE transport
|
|
34
|
+
- `ServerConfig` union type — `StdioServerConfig | StreamableHttpServerConfig | SseServerConfig`
|
|
35
|
+
- `getTransportType()` method — returns `'stdio'`, `'http'`, `'sse'`, or `null`
|
|
36
|
+
- URL auto-detection in CLI (passing a URL as first arg uses HTTP transport)
|
|
37
|
+
- CLI `--transport`, `--url`, `--headers` options on all commands
|
|
38
|
+
- 15 integration tests (Streamable HTTP + SSE) against server-everything
|
|
39
|
+
- 5 CLI HTTP transport tests
|
|
40
|
+
- 14 validation tests for HTTP/SSE configs
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- `MCPServerConfig` is now a deprecated alias for `StdioServerConfig` (backward compatible)
|
|
44
|
+
- `start()` accepts the `ServerConfig` discriminated union instead of `MCPServerConfig`
|
|
45
|
+
- CLI commands now accept `[command]` (optional) instead of `<command>` to support HTTP/SSE
|
|
46
|
+
|
|
8
47
|
## [1.3.0] - 2026-07-01
|
|
9
48
|
|
|
10
49
|
### Added
|
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/@slbdn/mcp-tester)
|
|
8
8
|
[](https://www.npmjs.com/package/@slbdn/mcp-tester)
|
|
9
|
-
[](https://nodejs.org)
|
|
10
10
|
[](https://opensource.org/licenses/MIT)
|
|
11
|
-
[](https://github.com/islobodan/mcp-tester/actions/workflows/test.yml)
|
|
12
12
|
[](https://github.com/islobodan/mcp-tester/actions/workflows/test.yml) <!-- statements/branches/functions/lines -->
|
|
13
13
|
|
|
14
14
|
A production-ready MCP (Model Context Protocol) client for **automated testing** of MCP servers with Jest.
|
|
@@ -50,7 +50,7 @@ No more manual clicking through inspectors — write real tests, run them in CI,
|
|
|
50
50
|
│ │ │ MCPTimeoutError │ MCPConnectionError │ ... │ │ │
|
|
51
51
|
│ │ └──────────────────────┬───────────────────────────────┘ │ │
|
|
52
52
|
│ └─────────────────────────┼─────────────────────────────────────┘ │
|
|
53
|
-
│ │ stdio
|
|
53
|
+
│ │ stdio, HTTP, SSE │
|
|
54
54
|
│ ┌─────────────────────────▼─────────────────────────────────────┐ │
|
|
55
55
|
│ │ MCP Server (child process) │ │
|
|
56
56
|
│ │ ┌────────┐ ┌───────────┐ ┌──────────┐ ┌────────────┐ │ │
|
|
@@ -60,9 +60,9 @@ No more manual clicking through inspectors — write real tests, run them in CI,
|
|
|
60
60
|
└─────────────────────────────────────────────────────────────────────┘
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
**Data flow**: Your test calls `MCPClient` → which sends JSON-RPC over stdio → to the MCP server
|
|
63
|
+
**Data flow**: Your test calls `MCPClient` → which sends JSON-RPC over stdio/HTTP/SSE → to the MCP server → server responds → `MCPClient` returns typed results → your assertion validates them.
|
|
64
64
|
|
|
65
|
-
**Key design**: `MCPClient` manages the server lifecycle (spawn/kill) and protocol handling. Your tests never deal with the transport layer directly.
|
|
65
|
+
**Key design**: `MCPClient` manages the server lifecycle (spawn/kill for stdio, connect/disconnect for HTTP/SSE) and protocol handling. Your tests never deal with the transport layer directly.
|
|
66
66
|
|
|
67
67
|
## Why MCP Tester?
|
|
68
68
|
|
|
@@ -84,7 +84,7 @@ No more manual clicking through inspectors — write real tests, run them in CI,
|
|
|
84
84
|
| Branches | 61% |
|
|
85
85
|
| Functions | 60% |
|
|
86
86
|
| Lines | 68% |
|
|
87
|
-
| Tests |
|
|
87
|
+
| Tests | 669 passing |
|
|
88
88
|
|
|
89
89
|
Per-file coverage thresholds are set in `jest.config.js` to catch regressions while avoiding CI noise. Run `npm run test:coverage` to see the full breakdown by file. PRs automatically receive a coverage comment via GitHub Actions.
|
|
90
90
|
|
|
@@ -106,7 +106,7 @@ The report includes collapsible test trees, timing per test, and failure details
|
|
|
106
106
|
npm install @slbdn/mcp-tester --save-dev
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
Requires **Node.js >=
|
|
109
|
+
Requires **Node.js >= 20**. See [Node.js Compatibility](./docs/nodejs-compatibility.md) for details.
|
|
110
110
|
|
|
111
111
|
## Quick Start
|
|
112
112
|
|
|
@@ -136,6 +136,22 @@ const result = await client.callTool({
|
|
|
136
136
|
await client.stop();
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
### Connect via HTTP
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// Streamable HTTP transport (recommended for remote servers)
|
|
143
|
+
await client.start({
|
|
144
|
+
transport: 'http',
|
|
145
|
+
url: 'https://api.example.com/mcp',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Legacy SSE transport
|
|
149
|
+
await client.start({
|
|
150
|
+
transport: 'sse',
|
|
151
|
+
url: 'http://localhost:3000/sse',
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
139
155
|
### Test with Jest
|
|
140
156
|
|
|
141
157
|
```typescript
|
|
@@ -557,7 +573,7 @@ import { MCPClient } from './client/MCPClient.js';
|
|
|
557
573
|
### Tests Fail in CI
|
|
558
574
|
|
|
559
575
|
- Set `maxWorkers: 1` in Jest config (parallel workers can crash)
|
|
560
|
-
- Verify Node.js >=
|
|
576
|
+
- Verify Node.js >= 20
|
|
561
577
|
- Check that server paths are correct
|
|
562
578
|
|
|
563
579
|
### Debug Mode
|
package/dist/cli/index.js
CHANGED
|
@@ -22,33 +22,95 @@ function getPackageVersion() {
|
|
|
22
22
|
const program = new Command();
|
|
23
23
|
program
|
|
24
24
|
.name('mcp-tester')
|
|
25
|
-
.description('CLI tool for testing MCP servers')
|
|
25
|
+
.description('CLI tool for testing MCP servers (stdio, HTTP, and SSE transports)')
|
|
26
26
|
.version(getPackageVersion(), '-V, --version');
|
|
27
|
-
// Global options
|
|
27
|
+
// Global options (also added per-command via addTransportOptions below)
|
|
28
28
|
program
|
|
29
29
|
.option('-t, --timeout <ms>', 'Request timeout in milliseconds', '30000')
|
|
30
30
|
.option('-v, --verbose', 'Enable verbose output')
|
|
31
31
|
.option('-l, --log-level <level>', 'Log level (debug|info|warn|error)', 'info');
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Add transport options to a command. Commander requires options to be defined
|
|
34
|
+
* on the command itself for them to be recognized during argument parsing.
|
|
35
|
+
*/
|
|
36
|
+
function withTransport(cmd) {
|
|
37
|
+
return cmd
|
|
38
|
+
.option('-T, --transport <type>', 'Transport: stdio (default), http, or sse', undefined)
|
|
39
|
+
.option('--url <url>', 'Server URL for http/sse (e.g. http://localhost:3000/mcp)', undefined)
|
|
40
|
+
.option('--headers <json>', 'HTTP headers as JSON (e.g. \'{"Authorization":"Bearer x"}\')', undefined);
|
|
41
|
+
}
|
|
42
|
+
// ─── Helper: resolve server config from global opts + positional args ──────
|
|
43
|
+
/**
|
|
44
|
+
* Build a ServerConfig from the global options and positional command/args.
|
|
45
|
+
*
|
|
46
|
+
* - If `--url` is provided → use HTTP/SSE transport
|
|
47
|
+
* - If `--transport http|sse` without `--url` → treat first positional arg as URL
|
|
48
|
+
* - Otherwise → stdio with command + args
|
|
49
|
+
*/
|
|
50
|
+
function resolveServerConfig(opts, command, args) {
|
|
51
|
+
const transport = String(opts['transport'] || '');
|
|
52
|
+
const url = opts['url'] ? String(opts['url']) : undefined;
|
|
53
|
+
const headersRaw = opts['headers'] ? String(opts['headers']) : undefined;
|
|
54
|
+
let headers;
|
|
55
|
+
if (headersRaw) {
|
|
56
|
+
try {
|
|
57
|
+
headers = JSON.parse(headersRaw);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
console.error('❌ Invalid JSON for --headers');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// HTTP or SSE transport
|
|
65
|
+
if (transport === 'http' || transport === 'sse' || (url && transport)) {
|
|
66
|
+
const actualTransport = (transport || 'http');
|
|
67
|
+
const actualUrl = url || command;
|
|
68
|
+
if (!actualUrl) {
|
|
69
|
+
console.error(`❌ URL is required for ${actualTransport} transport. Use --url or pass as first argument.`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
return { transport: actualTransport, url: actualUrl, headers };
|
|
73
|
+
}
|
|
74
|
+
// Auto-detect: if command looks like a URL, use HTTP
|
|
75
|
+
if (command && (command.startsWith('http://') || command.startsWith('https://'))) {
|
|
76
|
+
return { transport: 'http', url: command, headers };
|
|
77
|
+
}
|
|
78
|
+
// Default: stdio
|
|
79
|
+
if (!command) {
|
|
80
|
+
console.error('❌ Command is required for stdio transport. Example: mcp-tester test node ./server.js');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
return { command, args: args || [] };
|
|
84
|
+
}
|
|
85
|
+
/** Human-readable description of the connection target for logging. */
|
|
86
|
+
function describeTarget(config) {
|
|
87
|
+
if ('url' in config) {
|
|
88
|
+
return `${config.transport}: ${config.url}`;
|
|
89
|
+
}
|
|
90
|
+
return `stdio: ${config.command} ${(config.args || []).join(' ')}`;
|
|
91
|
+
}
|
|
92
|
+
/** Create a client with global options. */
|
|
93
|
+
function createClient(opts) {
|
|
94
|
+
return new MCPClient({
|
|
40
95
|
name: 'mcp-tester-cli',
|
|
41
96
|
version: '1.0.0',
|
|
42
97
|
timeout: parseInt(String(opts['timeout'] || '30000')),
|
|
43
98
|
logLevel: (opts['verbose'] ? 'debug' : String(opts['logLevel'] || 'info')),
|
|
44
99
|
});
|
|
100
|
+
}
|
|
101
|
+
// ─── Commands ────────────────────────────────────────────────────────────
|
|
102
|
+
// Test command - connects and verifies server is working
|
|
103
|
+
withTransport(program
|
|
104
|
+
.command('test')
|
|
105
|
+
.description('Test MCP server connection and list available capabilities')
|
|
106
|
+
.argument('[command]', 'Command to run (stdio) or URL (http/sse)')
|
|
107
|
+
.argument('[args...]', 'Arguments for the server command')).action(async (command, args, opts) => {
|
|
108
|
+
const config = resolveServerConfig(opts, command, args);
|
|
109
|
+
const client = createClient(opts);
|
|
45
110
|
try {
|
|
46
111
|
console.log(`🔌 Connecting to MCP server...`);
|
|
47
|
-
console.log(`
|
|
48
|
-
await client.start(
|
|
49
|
-
command,
|
|
50
|
-
args,
|
|
51
|
-
});
|
|
112
|
+
console.log(` ${describeTarget(config)}`);
|
|
113
|
+
await client.start(config);
|
|
52
114
|
console.log(`✅ Connected successfully\n`);
|
|
53
115
|
// Test tools
|
|
54
116
|
console.log(`🛠️ Tools:`);
|
|
@@ -92,7 +154,7 @@ program
|
|
|
92
154
|
catch (error) {
|
|
93
155
|
console.error(`❌ Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
94
156
|
if (error instanceof MCPConnectionError) {
|
|
95
|
-
console.error(' →
|
|
157
|
+
console.error(' → Connection failed. Check the URL or command.');
|
|
96
158
|
}
|
|
97
159
|
else if (error instanceof MCPTimeoutError) {
|
|
98
160
|
console.error(` → Request timed out after ${opts['timeout']}ms`);
|
|
@@ -107,22 +169,18 @@ program
|
|
|
107
169
|
}
|
|
108
170
|
});
|
|
109
171
|
// List tools command
|
|
110
|
-
program
|
|
172
|
+
withTransport(program
|
|
111
173
|
.command('list-tools')
|
|
112
174
|
.alias('lt')
|
|
113
175
|
.description('List all available tools from the MCP server')
|
|
114
|
-
.argument('
|
|
115
|
-
.argument('[args...]', 'Arguments for the server command')
|
|
176
|
+
.argument('[command]', 'Command to run (stdio) or URL (http/sse)')
|
|
177
|
+
.argument('[args...]', 'Arguments for the server command'))
|
|
116
178
|
.option('--json', 'Output as JSON')
|
|
117
179
|
.action(async (command, args, opts) => {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
version: '1.0.0',
|
|
121
|
-
timeout: parseInt(String(opts['timeout'] || '30000')),
|
|
122
|
-
logLevel: opts['verbose'] ? 'debug' : 'info',
|
|
123
|
-
});
|
|
180
|
+
const config = resolveServerConfig(opts, command, args);
|
|
181
|
+
const client = createClient(opts);
|
|
124
182
|
try {
|
|
125
|
-
await client.start(
|
|
183
|
+
await client.start(config);
|
|
126
184
|
const tools = await client.listTools();
|
|
127
185
|
if (opts['json']) {
|
|
128
186
|
console.log(JSON.stringify({ tools }, null, 2));
|
|
@@ -149,22 +207,18 @@ program
|
|
|
149
207
|
}
|
|
150
208
|
});
|
|
151
209
|
// Call tool command
|
|
152
|
-
program
|
|
210
|
+
withTransport(program
|
|
153
211
|
.command('call-tool')
|
|
154
212
|
.alias('ct')
|
|
155
213
|
.description('Call a tool on the MCP server')
|
|
156
214
|
.argument('<tool-name>', 'Name of the tool to call')
|
|
157
|
-
.argument('
|
|
158
|
-
.argument('[args...]', 'Arguments for the server command')
|
|
215
|
+
.argument('[command]', 'Command to run (stdio) or URL (http/sse)')
|
|
216
|
+
.argument('[args...]', 'Arguments for the server command'))
|
|
159
217
|
.option('--params <json>', 'Tool parameters as JSON string')
|
|
160
218
|
.option('--json', 'Output as JSON')
|
|
161
219
|
.action(async (toolName, command, serverArgs, opts) => {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
version: '1.0.0',
|
|
165
|
-
timeout: parseInt(String(opts['timeout'] || '30000')),
|
|
166
|
-
logLevel: opts['verbose'] ? 'debug' : 'info',
|
|
167
|
-
});
|
|
220
|
+
const config = resolveServerConfig(opts, command, serverArgs);
|
|
221
|
+
const client = createClient(opts);
|
|
168
222
|
let toolArgs = {};
|
|
169
223
|
if (opts['params']) {
|
|
170
224
|
try {
|
|
@@ -176,7 +230,7 @@ program
|
|
|
176
230
|
}
|
|
177
231
|
}
|
|
178
232
|
try {
|
|
179
|
-
await client.start(
|
|
233
|
+
await client.start(config);
|
|
180
234
|
if (!opts['json']) {
|
|
181
235
|
console.log(`🔧 Calling tool: ${toolName}`);
|
|
182
236
|
if (Object.keys(toolArgs).length > 0) {
|
|
@@ -211,23 +265,19 @@ program
|
|
|
211
265
|
}
|
|
212
266
|
});
|
|
213
267
|
// Read resource command
|
|
214
|
-
program
|
|
268
|
+
withTransport(program
|
|
215
269
|
.command('read-resource')
|
|
216
270
|
.alias('rr')
|
|
217
271
|
.description('Read a resource from the MCP server')
|
|
218
272
|
.argument('<uri>', 'URI of the resource to read')
|
|
219
|
-
.argument('
|
|
220
|
-
.argument('[args...]', 'Arguments for the server command')
|
|
273
|
+
.argument('[command]', 'Command to run (stdio) or URL (http/sse)')
|
|
274
|
+
.argument('[args...]', 'Arguments for the server command'))
|
|
221
275
|
.option('--json', 'Output as JSON')
|
|
222
276
|
.action(async (uri, command, serverArgs, opts) => {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
version: '1.0.0',
|
|
226
|
-
timeout: parseInt(String(opts['timeout'] || '30000')),
|
|
227
|
-
logLevel: opts['verbose'] ? 'debug' : 'info',
|
|
228
|
-
});
|
|
277
|
+
const config = resolveServerConfig(opts, command, serverArgs);
|
|
278
|
+
const client = createClient(opts);
|
|
229
279
|
try {
|
|
230
|
-
await client.start(
|
|
280
|
+
await client.start(config);
|
|
231
281
|
if (!opts['json']) {
|
|
232
282
|
console.log(`📖 Reading resource: ${uri}`);
|
|
233
283
|
}
|
|
@@ -253,22 +303,18 @@ program
|
|
|
253
303
|
}
|
|
254
304
|
});
|
|
255
305
|
// Get prompt command
|
|
256
|
-
program
|
|
306
|
+
withTransport(program
|
|
257
307
|
.command('get-prompt')
|
|
258
308
|
.alias('gp')
|
|
259
309
|
.description('Get a prompt from the MCP server')
|
|
260
310
|
.argument('<name>', 'Name of the prompt')
|
|
261
|
-
.argument('
|
|
262
|
-
.argument('[args...]', 'Arguments for the server command')
|
|
311
|
+
.argument('[command]', 'Command to run (stdio) or URL (http/sse)')
|
|
312
|
+
.argument('[args...]', 'Arguments for the server command'))
|
|
263
313
|
.option('--args <json>', 'Prompt arguments as JSON string')
|
|
264
314
|
.option('--json', 'Output as JSON')
|
|
265
315
|
.action(async (promptName, command, serverArgs, opts) => {
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
version: '1.0.0',
|
|
269
|
-
timeout: parseInt(String(opts['timeout'] || '30000')),
|
|
270
|
-
logLevel: opts['verbose'] ? 'debug' : 'info',
|
|
271
|
-
});
|
|
316
|
+
const config = resolveServerConfig(opts, command, serverArgs);
|
|
317
|
+
const client = createClient(opts);
|
|
272
318
|
let promptArgs = {};
|
|
273
319
|
if (opts['args']) {
|
|
274
320
|
try {
|
|
@@ -280,7 +326,7 @@ program
|
|
|
280
326
|
}
|
|
281
327
|
}
|
|
282
328
|
try {
|
|
283
|
-
await client.start(
|
|
329
|
+
await client.start(config);
|
|
284
330
|
if (!opts['json']) {
|
|
285
331
|
console.log(`💬 Getting prompt: ${promptName}`);
|
|
286
332
|
}
|
|
@@ -307,12 +353,12 @@ program
|
|
|
307
353
|
}
|
|
308
354
|
});
|
|
309
355
|
// Generate test file command
|
|
310
|
-
program
|
|
356
|
+
withTransport(program
|
|
311
357
|
.command('generate')
|
|
312
358
|
.alias('gen')
|
|
313
359
|
.description('Generate test file from MCP server inspection')
|
|
314
|
-
.argument('
|
|
315
|
-
.argument('[args...]', 'Arguments for the server command')
|
|
360
|
+
.argument('[command]', 'Command to run (stdio) or URL (http/sse)')
|
|
361
|
+
.argument('[args...]', 'Arguments for the server command'))
|
|
316
362
|
.option('-o, --output <file>', 'Output file path (prints to stdout if omitted)')
|
|
317
363
|
.option('--framework <fw>', 'Test framework: jest or vitest', 'jest')
|
|
318
364
|
.option('--description <desc>', 'Description for the test suite')
|
|
@@ -328,17 +374,42 @@ program
|
|
|
328
374
|
}
|
|
329
375
|
try {
|
|
330
376
|
console.error('🔍 Inspecting MCP server...');
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
377
|
+
// generateTests supports stdio only (connects and inspects)
|
|
378
|
+
const config = resolveServerConfig(opts, command, serverArgs);
|
|
379
|
+
const timeout = parseInt(String(opts['timeout'] || '30000'));
|
|
380
|
+
let code;
|
|
381
|
+
if ('command' in config) {
|
|
382
|
+
code = await generateTests({
|
|
383
|
+
command: config.command,
|
|
384
|
+
args: config.args || [],
|
|
385
|
+
framework: framework,
|
|
386
|
+
description: opts['description'],
|
|
387
|
+
includeResources: opts['resources'] !== false,
|
|
388
|
+
includePrompts: opts['prompts'] !== false,
|
|
389
|
+
includeTools: opts['tools'] !== false,
|
|
390
|
+
includeMatchers: opts['matchers'] !== false,
|
|
391
|
+
timeout,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// HTTP/SSE — connect directly
|
|
396
|
+
const client = new MCPClient({ name: 'mcp-tester-gen', version: '1.0.0', timeout });
|
|
397
|
+
try {
|
|
398
|
+
await client.start(config);
|
|
399
|
+
// For HTTP/SSE, generate from connected client
|
|
400
|
+
code = await generateFromClient(client, {
|
|
401
|
+
framework: framework,
|
|
402
|
+
description: opts['description'],
|
|
403
|
+
includeResources: opts['resources'] !== false,
|
|
404
|
+
includePrompts: opts['prompts'] !== false,
|
|
405
|
+
includeTools: opts['tools'] !== false,
|
|
406
|
+
includeMatchers: opts['matchers'] !== false,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
await client.stop();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
342
413
|
if (opts['output']) {
|
|
343
414
|
const fs = await import('fs');
|
|
344
415
|
const path = await import('path');
|
|
@@ -360,12 +431,12 @@ program
|
|
|
360
431
|
}
|
|
361
432
|
});
|
|
362
433
|
// Generate TypeScript types command
|
|
363
|
-
program
|
|
434
|
+
withTransport(program
|
|
364
435
|
.command('generate-types')
|
|
365
436
|
.alias('gen-types')
|
|
366
437
|
.description('Generate TypeScript type declarations from MCP server tool schemas')
|
|
367
|
-
.argument('
|
|
368
|
-
.argument('[args...]', 'Arguments for the server command')
|
|
438
|
+
.argument('[command]', 'Command to run (stdio) or URL (http/sse)')
|
|
439
|
+
.argument('[args...]', 'Arguments for the server command'))
|
|
369
440
|
.option('-o, --output <file>', 'Output file path (prints to stdout if omitted)')
|
|
370
441
|
.option('--module-name <name>', 'Module name for import hints', '@slbdn/mcp-tester')
|
|
371
442
|
.option('--no-resources', 'Skip resource URI types')
|
|
@@ -373,14 +444,33 @@ program
|
|
|
373
444
|
.action(async (command, serverArgs, opts) => {
|
|
374
445
|
try {
|
|
375
446
|
console.error('🔍 Inspecting MCP server for type generation...');
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
447
|
+
const config = resolveServerConfig(opts, command, serverArgs);
|
|
448
|
+
const timeout = parseInt(String(opts['timeout'] || '30000'));
|
|
449
|
+
let types;
|
|
450
|
+
if ('command' in config) {
|
|
451
|
+
types = await generateTypes({
|
|
452
|
+
command: config.command,
|
|
453
|
+
args: config.args || [],
|
|
454
|
+
moduleName: opts['moduleName'],
|
|
455
|
+
includeResources: opts['resources'] !== false,
|
|
456
|
+
includePrompts: opts['prompts'] !== false,
|
|
457
|
+
timeout,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
const client = new MCPClient({ name: 'mcp-tester-gen', version: '1.0.0', timeout });
|
|
462
|
+
try {
|
|
463
|
+
await client.start(config);
|
|
464
|
+
types = await generateTypesFromClient(client, {
|
|
465
|
+
moduleName: opts['moduleName'],
|
|
466
|
+
includeResources: opts['resources'] !== false,
|
|
467
|
+
includePrompts: opts['prompts'] !== false,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
await client.stop();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
384
474
|
if (opts['output']) {
|
|
385
475
|
const fs = await import('fs');
|
|
386
476
|
const path = await import('path');
|
|
@@ -401,18 +491,135 @@ program
|
|
|
401
491
|
process.exit(1);
|
|
402
492
|
}
|
|
403
493
|
});
|
|
404
|
-
//
|
|
494
|
+
// ─── Helpers for HTTP/SSE codegen (inline inspection) ─────────────────────
|
|
495
|
+
async function generateFromClient(client, opts) {
|
|
496
|
+
const tools = opts.includeTools ? await client.listTools() : [];
|
|
497
|
+
const resources = opts.includeResources ? await client.listResources() : [];
|
|
498
|
+
const prompts = opts.includePrompts ? await client.listPrompts() : [];
|
|
499
|
+
// Build the test code inline (simplified version for HTTP/SSE)
|
|
500
|
+
const importLine = opts.framework === 'vitest' ? 'vitest' : '@jest/globals';
|
|
501
|
+
const matchersImport = opts.includeMatchers
|
|
502
|
+
? `import { setup${opts.framework === 'vitest' ? 'Vitest' : 'Jest'}Matchers } from '@slbdn/mcp-tester';\n`
|
|
503
|
+
: '';
|
|
504
|
+
let code = `import { describe, it, expect, beforeAll, afterAll } from '${importLine}';\n`;
|
|
505
|
+
code += `import { MCPClient } from '@slbdn/mcp-tester';\n`;
|
|
506
|
+
if (matchersImport)
|
|
507
|
+
code += matchersImport;
|
|
508
|
+
code += '\n';
|
|
509
|
+
const desc = opts.description || 'Generated MCP Server Tests';
|
|
510
|
+
code += `describe('${desc}', () => {\n`;
|
|
511
|
+
code += ` let client: MCPClient;\n\n`;
|
|
512
|
+
code += ` beforeAll(async () => {\n`;
|
|
513
|
+
if (matchersImport) {
|
|
514
|
+
code += ` setup${opts.framework === 'vitest' ? 'Vitest' : 'Jest'}Matchers();\n`;
|
|
515
|
+
}
|
|
516
|
+
code += ` client = new MCPClient({ timeout: 30000 });\n`;
|
|
517
|
+
code += ` // Update the URL/command for your server\n`;
|
|
518
|
+
code += ` await client.start({ command: 'node', args: ['./server.js'] });\n`;
|
|
519
|
+
code += ` });\n\n`;
|
|
520
|
+
code += ` afterAll(async () => {\n if (client.isConnected()) await client.stop();\n });\n\n`;
|
|
521
|
+
if (opts.includeTools) {
|
|
522
|
+
code += ` // ─── Tools (${tools.length}) ───\n`;
|
|
523
|
+
for (const tool of tools) {
|
|
524
|
+
code += ` it('should have tool: ${tool.name}', async () => {\n`;
|
|
525
|
+
code += ` const tools = await client.listTools();\n`;
|
|
526
|
+
code += ` expect(tools.map(t => t.name)).toContain('${tool.name}');\n`;
|
|
527
|
+
code += ` });\n\n`;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (opts.includeResources) {
|
|
531
|
+
code += ` // ─── Resources (${resources.length}) ───\n`;
|
|
532
|
+
code += ` it('should list resources', async () => {\n`;
|
|
533
|
+
code += ` const resources = await client.listResources();\n`;
|
|
534
|
+
code += ` expect(resources.length).toBeGreaterThan(0);\n`;
|
|
535
|
+
code += ` });\n\n`;
|
|
536
|
+
}
|
|
537
|
+
if (opts.includePrompts) {
|
|
538
|
+
code += ` // ─── Prompts (${prompts.length}) ───\n`;
|
|
539
|
+
code += ` it('should list prompts', async () => {\n`;
|
|
540
|
+
code += ` const prompts = await client.listPrompts();\n`;
|
|
541
|
+
code += ` expect(prompts.length).toBeGreaterThan(0);\n`;
|
|
542
|
+
code += ` });\n\n`;
|
|
543
|
+
}
|
|
544
|
+
code += `});\n`;
|
|
545
|
+
return code;
|
|
546
|
+
}
|
|
547
|
+
async function generateTypesFromClient(client, opts) {
|
|
548
|
+
const tools = await client.listTools();
|
|
549
|
+
const resources = opts.includeResources ? await client.listResources() : [];
|
|
550
|
+
const prompts = opts.includePrompts ? await client.listPrompts() : [];
|
|
551
|
+
const moduleName = opts.moduleName || '@slbdn/mcp-tester';
|
|
552
|
+
let code = `// Generated by mcp-tester generate-types\n`;
|
|
553
|
+
code += `// Source: MCP server inspection\n\n`;
|
|
554
|
+
code += `// Tool argument types\n`;
|
|
555
|
+
for (const tool of tools) {
|
|
556
|
+
code += `export interface ${toPascalCase(tool.name)}Args {\n`;
|
|
557
|
+
const props = tool.inputSchema?.properties || {};
|
|
558
|
+
const required = tool.inputSchema?.required || [];
|
|
559
|
+
for (const [key, schema] of Object.entries(props)) {
|
|
560
|
+
const s = schema;
|
|
561
|
+
const tsType = jsonSchemaToTs(s);
|
|
562
|
+
const optional = !required.includes(key) ? '?' : '';
|
|
563
|
+
code += ` ${key}${optional}: ${tsType};\n`;
|
|
564
|
+
}
|
|
565
|
+
code += `}\n\n`;
|
|
566
|
+
}
|
|
567
|
+
code += `export type ToolName = ${tools.map((t) => `'${t.name}'`).join(' | ') || 'string'};\n\n`;
|
|
568
|
+
if (resources.length > 0) {
|
|
569
|
+
code += `export type ResourceUri = ${resources.map((r) => `'${r.uri}'`).join(' | ')};\n\n`;
|
|
570
|
+
}
|
|
571
|
+
if (prompts.length > 0) {
|
|
572
|
+
code += `export type PromptName = ${prompts.map((p) => `'${p.name}'`).join(' | ')};\n\n`;
|
|
573
|
+
}
|
|
574
|
+
code += `// Import the client\n`;
|
|
575
|
+
code += `import { MCPClient, type ToolCall } from '${moduleName}';\n`;
|
|
576
|
+
return code;
|
|
577
|
+
}
|
|
578
|
+
function toPascalCase(s) {
|
|
579
|
+
return s.replace(/(^|[-_])(.)/g, (_, __, c) => c.toUpperCase());
|
|
580
|
+
}
|
|
581
|
+
function jsonSchemaToTs(schema) {
|
|
582
|
+
const type = schema.type;
|
|
583
|
+
switch (type) {
|
|
584
|
+
case 'string':
|
|
585
|
+
return 'string';
|
|
586
|
+
case 'number':
|
|
587
|
+
case 'integer':
|
|
588
|
+
return 'number';
|
|
589
|
+
case 'boolean':
|
|
590
|
+
return 'boolean';
|
|
591
|
+
case 'array':
|
|
592
|
+
return 'unknown[]';
|
|
593
|
+
case 'object':
|
|
594
|
+
return 'Record<string, unknown>';
|
|
595
|
+
default:
|
|
596
|
+
return 'unknown';
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// ─── Help ────────────────────────────────────────────────────────────────
|
|
405
600
|
program.addHelpText('after', `
|
|
406
601
|
Examples:
|
|
602
|
+
# Stdio transport (default)
|
|
407
603
|
mcp-tester test node ./server.js
|
|
408
604
|
mcp-tester list-tools node ./server.js --json
|
|
409
605
|
mcp-tester call-tool echo node ./server.js --params '{"message":"Hello"}'
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
mcp-tester
|
|
413
|
-
mcp-tester
|
|
414
|
-
mcp-tester
|
|
415
|
-
|
|
606
|
+
|
|
607
|
+
# HTTP transport
|
|
608
|
+
mcp-tester test --transport http --url http://localhost:3000/mcp
|
|
609
|
+
mcp-tester list-tools http://localhost:3000/mcp --json
|
|
610
|
+
mcp-tester call-tool echo http://localhost:3000/mcp --params '{"message":"Hi"}'
|
|
611
|
+
|
|
612
|
+
# SSE transport
|
|
613
|
+
mcp-tester test --transport sse --url http://localhost:3000/sse
|
|
614
|
+
|
|
615
|
+
# With custom headers
|
|
616
|
+
mcp-tester test --transport http --url https://api.example.com/mcp --headers '{"Authorization":"Bearer token"}'
|
|
617
|
+
|
|
618
|
+
# Generate tests and types (stdio)
|
|
619
|
+
mcp-tester generate node ./server.js -o server.test.ts
|
|
620
|
+
mcp-tester generate-types node ./server.js -o server.d.ts
|
|
621
|
+
|
|
622
|
+
Short aliases: lt (list-tools), ct (call-tool), rr (read-resource), gp (get-prompt), gen (generate)
|
|
416
623
|
`);
|
|
417
624
|
// Show help when invoked with no arguments
|
|
418
625
|
const args = process.argv.slice(2);
|