@slbdn/mcp-tester 1.3.0 → 1.4.0

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 CHANGED
@@ -5,6 +5,27 @@ 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.0] - 2026-07-01
11
+
12
+ ### Added
13
+ - **HTTP and SSE transport support** — connect to remote MCP servers:
14
+ - `StreamableHttpServerConfig` (`transport: 'http'`) — modern MCP Streamable HTTP (POST + SSE GET)
15
+ - `SseServerConfig` (`transport: 'sse'`) — legacy SSE transport
16
+ - `ServerConfig` union type — `StdioServerConfig | StreamableHttpServerConfig | SseServerConfig`
17
+ - `getTransportType()` method — returns `'stdio'`, `'http'`, `'sse'`, or `null`
18
+ - URL auto-detection in CLI (passing a URL as first arg uses HTTP transport)
19
+ - CLI `--transport`, `--url`, `--headers` options on all commands
20
+ - 15 integration tests (Streamable HTTP + SSE) against server-everything
21
+ - 5 CLI HTTP transport tests
22
+ - 14 validation tests for HTTP/SSE configs
23
+
24
+ ### Changed
25
+ - `MCPServerConfig` is now a deprecated alias for `StdioServerConfig` (backward compatible)
26
+ - `start()` accepts the `ServerConfig` discriminated union instead of `MCPServerConfig`
27
+ - CLI commands now accept `[command]` (optional) instead of `<command>` to support HTTP/SSE
28
+
8
29
  ## [1.3.0] - 2026-07-01
9
30
 
10
31
  ### Added
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  [![npm downloads](https://img.shields.io/npm/dm/@slbdn/mcp-tester)](https://www.npmjs.com/package/@slbdn/mcp-tester)
9
9
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
- [![Test Status](https://img.shields.io/badge/tests-635%20passing-brightgreen)](https://github.com/islobodan/mcp-tester/actions/workflows/test.yml)
11
+ [![Test Status](https://img.shields.io/badge/tests-669%20passing-brightgreen)](https://github.com/islobodan/mcp-tester/actions/workflows/test.yml)
12
12
  [![Coverage](https://img.shields.io/badge/coverage-68%2F61%2F60%2F68-brightgreen)](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 (JSON-RPC) │
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 process → server responds → `MCPClient` returns typed results → your assertion validates them.
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 | 635 passing |
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
 
@@ -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
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 - use string type since commander passes strings
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
- // Test command - connects and verifies server is working
33
- program
34
- .command('test')
35
- .description('Test MCP server connection and list available capabilities')
36
- .argument('<command>', 'Command to run the MCP server (e.g., "node", "python")')
37
- .argument('[args...]', 'Arguments for the server command')
38
- .action(async (command, args, opts) => {
39
- const client = new MCPClient({
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(` Command: ${command} ${args.join(' ')}`);
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(' → Server failed to start. Check command and server process.');
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('<command>', 'Command to run the MCP server')
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 client = new MCPClient({
119
- name: 'mcp-tester-cli',
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({ command, args });
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('<command>', 'Command to run the MCP server')
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 client = new MCPClient({
163
- name: 'mcp-tester-cli',
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({ command, args: serverArgs });
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('<command>', 'Command to run the MCP server')
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 client = new MCPClient({
224
- name: 'mcp-tester-cli',
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({ command, args: serverArgs });
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('<command>', 'Command to run the MCP server')
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 client = new MCPClient({
267
- name: 'mcp-tester-cli',
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({ command, args: serverArgs });
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('<command>', 'Command to run the MCP server')
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
- const code = await generateTests({
332
- command,
333
- args: serverArgs,
334
- framework: framework,
335
- description: opts['description'],
336
- includeResources: opts['resources'] !== false,
337
- includePrompts: opts['prompts'] !== false,
338
- includeTools: opts['tools'] !== false,
339
- includeMatchers: opts['matchers'] !== false,
340
- timeout: parseInt(String(opts['timeout'] || '30000')),
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('<command>', 'Command to run the MCP server')
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 types = await generateTypes({
377
- command,
378
- args: serverArgs,
379
- moduleName: opts['moduleName'],
380
- includeResources: opts['resources'] !== false,
381
- includePrompts: opts['prompts'] !== false,
382
- timeout: parseInt(String(opts['timeout'] || '30000')),
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
- // Add custom help with examples using Commander's built-in help system
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
- mcp-tester read-resource text://example node ./server.js
411
- mcp-tester get-prompt greet node ./server.js --args '{"name":"World"}'
412
- mcp-tester lt node ./server.js # short alias for list-tools
413
- mcp-tester ct echo node ./server.js --params '{}' # short alias for call-tool
414
- mcp-tester generate node ./server.js -o server.test.ts # generate test file
415
- mcp-tester generate-types node ./server.js -o server.d.ts # generate TypeScript types
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);