mcpspec 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MCPSpec Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js ADDED
@@ -0,0 +1,1109 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command11 } from "commander";
5
+
6
+ // src/commands/test.ts
7
+ import { Command } from "commander";
8
+ import { readFileSync, writeFileSync, watch } from "fs";
9
+ import { resolve } from "path";
10
+ import { EXIT_CODES } from "@mcpspec/shared";
11
+ import {
12
+ loadYamlSafely,
13
+ TestRunner,
14
+ ConsoleReporter,
15
+ JsonReporter,
16
+ JunitReporter,
17
+ HtmlReporter,
18
+ TapReporter,
19
+ MCPSpecError,
20
+ formatError
21
+ } from "@mcpspec/core";
22
+ import { collectionSchema } from "@mcpspec/shared";
23
+ function coerceCollection(raw) {
24
+ if (!raw || typeof raw !== "object") {
25
+ throw new MCPSpecError("COLLECTION_PARSE_ERROR", "Collection file is empty or invalid", {});
26
+ }
27
+ const obj = raw;
28
+ const coerced = { ...obj };
29
+ if (Array.isArray(obj["tests"])) {
30
+ coerced["tests"] = obj["tests"].map((test) => {
31
+ const t = { ...test };
32
+ if (t["expectError"] === "true") t["expectError"] = true;
33
+ if (t["expectError"] === "false") t["expectError"] = false;
34
+ if (typeof t["timeout"] === "string") t["timeout"] = parseInt(t["timeout"], 10);
35
+ if (typeof t["retries"] === "string") t["retries"] = parseInt(t["retries"], 10);
36
+ if (Array.isArray(t["assertions"])) {
37
+ t["assertions"] = t["assertions"].map((a) => {
38
+ const assertion = { ...a };
39
+ if (typeof assertion["maxMs"] === "string")
40
+ assertion["maxMs"] = parseInt(assertion["maxMs"], 10);
41
+ if (typeof assertion["value"] === "string" && assertion["type"] === "length")
42
+ assertion["value"] = parseInt(assertion["value"], 10);
43
+ return assertion;
44
+ });
45
+ }
46
+ if (Array.isArray(t["expect"])) {
47
+ t["expect"] = t["expect"].map((e) => {
48
+ return e;
49
+ });
50
+ }
51
+ return t;
52
+ });
53
+ }
54
+ return coerced;
55
+ }
56
+ var testCommand = new Command("test").description("Run tests from a collection file").argument("[collection]", "Path to collection YAML file", "mcpspec.yaml").option("--env <environment>", "Environment to use").option("--reporter <type>", "Reporter type: console, json, junit, html, tap", "console").option("--output <path>", "Output file path for results").option("--ci", "CI mode (no colors, structured output)", false).option("--tag <tags...>", "Filter tests by tag").option("--parallel <n>", "Number of parallel test executions").option("--baseline <name>", "Compare results against named baseline").option("--watch", "Re-run tests on file changes", false).action(async (collectionPath, options) => {
57
+ if (options.watch) {
58
+ await runWatch(collectionPath, options);
59
+ } else {
60
+ const exitCode = await runOnce(collectionPath, options);
61
+ process.exit(exitCode);
62
+ }
63
+ });
64
+ async function runOnce(collectionPath, options) {
65
+ try {
66
+ const fullPath = resolve(collectionPath);
67
+ let content;
68
+ try {
69
+ content = readFileSync(fullPath, "utf-8");
70
+ } catch {
71
+ const formatted = formatError(
72
+ new MCPSpecError("COLLECTION_PARSE_ERROR", `Cannot read file: ${fullPath}`, {
73
+ filePath: fullPath
74
+ })
75
+ );
76
+ console.error(`
77
+ ${formatted.title}: ${formatted.description}`);
78
+ formatted.suggestions.forEach((s) => console.error(` - ${s}`));
79
+ return EXIT_CODES.CONFIG_ERROR;
80
+ }
81
+ const raw = loadYamlSafely(content);
82
+ const collection = coerceCollection(raw);
83
+ try {
84
+ collectionSchema.parse(collection);
85
+ } catch (err) {
86
+ const message = err instanceof Error ? err.message : String(err);
87
+ const formatted = formatError(
88
+ new MCPSpecError("COLLECTION_VALIDATION_ERROR", message, {
89
+ details: message,
90
+ filePath: fullPath
91
+ })
92
+ );
93
+ console.error(`
94
+ ${formatted.title}: ${formatted.description}`);
95
+ formatted.suggestions.forEach((s) => console.error(` - ${s}`));
96
+ return EXIT_CODES.CONFIG_ERROR;
97
+ }
98
+ const reporter = createReporter(options);
99
+ const runner = new TestRunner();
100
+ const parallelism = options.parallel ? parseInt(options.parallel, 10) : 1;
101
+ const result = await runner.run(collection, {
102
+ environment: options.env,
103
+ reporter,
104
+ parallelism,
105
+ tags: options.tag
106
+ });
107
+ if (options.output) {
108
+ const output = getReporterOutput(reporter, options.reporter);
109
+ if (output) {
110
+ writeFileSync(resolve(options.output), output, "utf-8");
111
+ }
112
+ }
113
+ if (options.baseline) {
114
+ const { BaselineStore: BaselineStore3, ResultDiffer: ResultDiffer2 } = await import("@mcpspec/core");
115
+ const store = new BaselineStore3();
116
+ const baseline = store.load(options.baseline);
117
+ if (baseline) {
118
+ const differ = new ResultDiffer2();
119
+ const diff = differ.diff(baseline, result, options.baseline);
120
+ console.log(`
121
+ Baseline comparison against "${options.baseline}":`);
122
+ if (diff.summary.regressions > 0) {
123
+ console.log(` Regressions: ${diff.summary.regressions}`);
124
+ for (const r of diff.regressions) {
125
+ console.log(` - ${r.testName}: ${r.before?.status} -> ${r.after?.status}`);
126
+ }
127
+ }
128
+ if (diff.summary.fixes > 0) {
129
+ console.log(` Fixes: ${diff.summary.fixes}`);
130
+ }
131
+ if (diff.summary.newTests > 0) {
132
+ console.log(` New tests: ${diff.summary.newTests}`);
133
+ }
134
+ if (diff.summary.removedTests > 0) {
135
+ console.log(` Removed tests: ${diff.summary.removedTests}`);
136
+ }
137
+ if (diff.summary.regressions === 0 && diff.summary.fixes === 0) {
138
+ console.log(" No changes detected.");
139
+ }
140
+ console.log("");
141
+ } else {
142
+ console.log(`
143
+ Baseline "${options.baseline}" not found. Run \`mcpspec baseline save ${options.baseline}\` first.
144
+ `);
145
+ }
146
+ }
147
+ await runner.cleanup();
148
+ if (result.summary.failed > 0 || result.summary.errors > 0) {
149
+ return EXIT_CODES.TEST_FAILURE;
150
+ }
151
+ return EXIT_CODES.SUCCESS;
152
+ } catch (err) {
153
+ const formatted = formatError(err);
154
+ console.error(`
155
+ ${formatted.title}: ${formatted.description}`);
156
+ formatted.suggestions.forEach((s) => console.error(` - ${s}`));
157
+ return formatted.exitCode;
158
+ }
159
+ }
160
+ function createReporter(options) {
161
+ switch (options.reporter) {
162
+ case "json":
163
+ return new JsonReporter(options.output);
164
+ case "junit":
165
+ return new JunitReporter(options.output);
166
+ case "html":
167
+ return new HtmlReporter(options.output);
168
+ case "tap":
169
+ return new TapReporter();
170
+ default:
171
+ return new ConsoleReporter({ ci: options.ci });
172
+ }
173
+ }
174
+ function getReporterOutput(reporter, type) {
175
+ if (type === "json") return reporter.getOutput();
176
+ if (type === "junit") return reporter.getOutput();
177
+ if (type === "html") return reporter.getOutput();
178
+ return void 0;
179
+ }
180
+ async function runWatch(collectionPath, options) {
181
+ const fullPath = resolve(collectionPath);
182
+ let running = false;
183
+ let debounceTimer = null;
184
+ const run = async () => {
185
+ if (running) return;
186
+ running = true;
187
+ console.log("\n--- Running tests ---\n");
188
+ await runOnce(collectionPath, options);
189
+ running = false;
190
+ };
191
+ await run();
192
+ console.log(`
193
+ Watching ${fullPath} for changes... (Ctrl+C to stop)
194
+ `);
195
+ watch(fullPath, () => {
196
+ if (debounceTimer) clearTimeout(debounceTimer);
197
+ debounceTimer = setTimeout(() => {
198
+ run();
199
+ }, 300);
200
+ });
201
+ await new Promise(() => {
202
+ });
203
+ }
204
+
205
+ // src/commands/inspect.ts
206
+ import { Command as Command2 } from "commander";
207
+ import { createInterface } from "readline";
208
+ import { EXIT_CODES as EXIT_CODES2 } from "@mcpspec/shared";
209
+ import { MCPClient, formatError as formatError2 } from "@mcpspec/core";
210
+ var COLORS = {
211
+ reset: "\x1B[0m",
212
+ green: "\x1B[32m",
213
+ red: "\x1B[31m",
214
+ yellow: "\x1B[33m",
215
+ gray: "\x1B[90m",
216
+ bold: "\x1B[1m",
217
+ cyan: "\x1B[36m"
218
+ };
219
+ var inspectCommand = new Command2("inspect").description("Interactive inspection of an MCP server").argument("<server>", 'Server command (e.g., "npx @modelcontextprotocol/server-filesystem /tmp")').action(async (serverCommand) => {
220
+ let client = null;
221
+ try {
222
+ client = new MCPClient({ serverConfig: serverCommand });
223
+ console.log(`${COLORS.cyan}Connecting to: ${COLORS.reset}${serverCommand}`);
224
+ await client.connect();
225
+ const info = client.getServerInfo();
226
+ if (info) {
227
+ console.log(
228
+ `${COLORS.green}Connected to ${info.name ?? "unknown"} v${info.version ?? "?"}${COLORS.reset}`
229
+ );
230
+ } else {
231
+ console.log(`${COLORS.green}Connected${COLORS.reset}`);
232
+ }
233
+ console.log(`
234
+ Type ${COLORS.bold}.help${COLORS.reset} for available commands
235
+ `);
236
+ const rl = createInterface({
237
+ input: process.stdin,
238
+ output: process.stdout,
239
+ prompt: `${COLORS.cyan}mcpspec>${COLORS.reset} `
240
+ });
241
+ rl.prompt();
242
+ rl.on("line", async (line) => {
243
+ const trimmed = line.trim();
244
+ if (!trimmed) {
245
+ rl.prompt();
246
+ return;
247
+ }
248
+ try {
249
+ if (trimmed === ".exit" || trimmed === ".quit") {
250
+ await client?.disconnect();
251
+ rl.close();
252
+ process.exit(EXIT_CODES2.SUCCESS);
253
+ return;
254
+ }
255
+ if (trimmed === ".help") {
256
+ console.log(`
257
+ ${COLORS.bold}Available commands:${COLORS.reset}
258
+ .tools List all available tools
259
+ .resources List all available resources
260
+ .call <tool> <json> Call a tool with JSON arguments
261
+ .schema <tool> Show input schema for a tool
262
+ .info Show server info
263
+ .exit Disconnect and exit
264
+ `);
265
+ rl.prompt();
266
+ return;
267
+ }
268
+ if (trimmed === ".tools") {
269
+ const tools = await client.listTools();
270
+ if (tools.length === 0) {
271
+ console.log(`${COLORS.gray}No tools available${COLORS.reset}`);
272
+ } else {
273
+ console.log(`
274
+ ${COLORS.bold}Tools (${tools.length}):${COLORS.reset}`);
275
+ for (const tool of tools) {
276
+ console.log(` ${COLORS.green}${tool.name}${COLORS.reset}`);
277
+ if (tool.description) {
278
+ console.log(` ${COLORS.gray}${tool.description}${COLORS.reset}`);
279
+ }
280
+ }
281
+ console.log("");
282
+ }
283
+ rl.prompt();
284
+ return;
285
+ }
286
+ if (trimmed === ".resources") {
287
+ const resources = await client.listResources();
288
+ if (resources.length === 0) {
289
+ console.log(`${COLORS.gray}No resources available${COLORS.reset}`);
290
+ } else {
291
+ console.log(`
292
+ ${COLORS.bold}Resources (${resources.length}):${COLORS.reset}`);
293
+ for (const resource of resources) {
294
+ console.log(` ${COLORS.green}${resource.uri}${COLORS.reset}`);
295
+ if (resource.name) {
296
+ console.log(` ${COLORS.gray}${resource.name}${COLORS.reset}`);
297
+ }
298
+ }
299
+ console.log("");
300
+ }
301
+ rl.prompt();
302
+ return;
303
+ }
304
+ if (trimmed === ".info") {
305
+ const serverInfo = client.getServerInfo();
306
+ console.log(JSON.stringify(serverInfo, null, 2));
307
+ rl.prompt();
308
+ return;
309
+ }
310
+ if (trimmed.startsWith(".schema ")) {
311
+ const toolName = trimmed.slice(8).trim();
312
+ const tools = await client.listTools();
313
+ const tool = tools.find((t) => t.name === toolName);
314
+ if (!tool) {
315
+ console.log(`${COLORS.red}Tool "${toolName}" not found${COLORS.reset}`);
316
+ console.log(
317
+ `${COLORS.gray}Available: ${tools.map((t) => t.name).join(", ")}${COLORS.reset}`
318
+ );
319
+ } else {
320
+ console.log(JSON.stringify(tool.inputSchema, null, 2));
321
+ }
322
+ rl.prompt();
323
+ return;
324
+ }
325
+ if (trimmed.startsWith(".call ")) {
326
+ const rest = trimmed.slice(6).trim();
327
+ const spaceIdx = rest.indexOf(" ");
328
+ let toolName;
329
+ let args = {};
330
+ if (spaceIdx === -1) {
331
+ toolName = rest;
332
+ } else {
333
+ toolName = rest.slice(0, spaceIdx);
334
+ const jsonStr = rest.slice(spaceIdx + 1).trim();
335
+ try {
336
+ args = JSON.parse(jsonStr);
337
+ } catch {
338
+ console.log(`${COLORS.red}Invalid JSON: ${jsonStr}${COLORS.reset}`);
339
+ rl.prompt();
340
+ return;
341
+ }
342
+ }
343
+ console.log(`${COLORS.gray}Calling ${toolName}...${COLORS.reset}`);
344
+ const result = await client.callTool(toolName, args);
345
+ console.log(JSON.stringify(result, null, 2));
346
+ rl.prompt();
347
+ return;
348
+ }
349
+ console.log(`${COLORS.yellow}Unknown command. Type .help for available commands.${COLORS.reset}`);
350
+ } catch (err) {
351
+ const formatted = formatError2(err);
352
+ console.log(`${COLORS.red}${formatted.title}: ${formatted.description}${COLORS.reset}`);
353
+ }
354
+ rl.prompt();
355
+ });
356
+ rl.on("close", async () => {
357
+ await client?.disconnect();
358
+ process.exit(EXIT_CODES2.SUCCESS);
359
+ });
360
+ } catch (err) {
361
+ const formatted = formatError2(err);
362
+ console.error(`
363
+ ${formatted.title}: ${formatted.description}`);
364
+ formatted.suggestions.forEach((s) => console.error(` - ${s}`));
365
+ await client?.disconnect();
366
+ process.exit(formatted.exitCode);
367
+ }
368
+ });
369
+
370
+ // src/commands/init.ts
371
+ import { Command as Command3 } from "commander";
372
+ import { mkdirSync, writeFileSync as writeFileSync2, existsSync } from "fs";
373
+ import { resolve as resolve2, join } from "path";
374
+ import { EXIT_CODES as EXIT_CODES3 } from "@mcpspec/shared";
375
+ var TEMPLATES = {
376
+ minimal: `name: My MCP Tests
377
+ server: npx my-mcp-server
378
+
379
+ tests:
380
+ - name: Basic test
381
+ call: my_tool
382
+ with:
383
+ param: value
384
+ expect:
385
+ - exists: $.content
386
+ `,
387
+ standard: `schemaVersion: "1.0"
388
+ name: My MCP Tests
389
+ description: Test collection for my MCP server
390
+
391
+ server:
392
+ transport: stdio
393
+ command: npx
394
+ args:
395
+ - my-mcp-server
396
+
397
+ tests:
398
+ - name: List tools are available
399
+ call: my_tool
400
+ with:
401
+ param: value
402
+ assertions:
403
+ - type: exists
404
+ path: $.content
405
+ - type: latency
406
+ maxMs: 5000
407
+
408
+ - name: Handle invalid input
409
+ call: my_tool
410
+ with:
411
+ invalid_param: true
412
+ expectError: true
413
+ `,
414
+ full: `schemaVersion: "1.0"
415
+ name: My MCP Tests
416
+ description: Comprehensive test collection
417
+
418
+ server:
419
+ name: my-server
420
+ transport: stdio
421
+ command: npx
422
+ args:
423
+ - my-mcp-server
424
+ env:
425
+ NODE_ENV: test
426
+
427
+ environments:
428
+ dev:
429
+ variables:
430
+ API_URL: http://localhost:3000
431
+ staging:
432
+ variables:
433
+ API_URL: https://staging.example.com
434
+
435
+ defaultEnvironment: dev
436
+
437
+ tests:
438
+ - id: test-basic
439
+ name: Basic tool call
440
+ tags:
441
+ - smoke
442
+ call: my_tool
443
+ with:
444
+ param: value
445
+ assertions:
446
+ - type: schema
447
+ - type: exists
448
+ path: $.content
449
+ - type: latency
450
+ maxMs: 5000
451
+
452
+ - id: test-error
453
+ name: Handle error gracefully
454
+ tags:
455
+ - error-handling
456
+ call: my_tool
457
+ with:
458
+ invalid: true
459
+ expectError: true
460
+
461
+ - id: test-extract
462
+ name: Extract and reuse data
463
+ tags:
464
+ - integration
465
+ call: get_data
466
+ with:
467
+ id: "1"
468
+ assertions:
469
+ - type: exists
470
+ path: $.id
471
+ extract:
472
+ - name: itemId
473
+ path: $.id
474
+
475
+ - id: test-use-extracted
476
+ name: Use extracted variable
477
+ tags:
478
+ - integration
479
+ call: get_detail
480
+ with:
481
+ id: "{{itemId}}"
482
+ assertions:
483
+ - type: exists
484
+ path: $.detail
485
+ `
486
+ };
487
+ function generateFromWizard(result) {
488
+ const serverBlock = result.transport === "stdio" ? `server: ${result.command}` : `server:
489
+ transport: ${result.transport}
490
+ url: ${result.url}`;
491
+ const template = TEMPLATES[result.template];
492
+ const lines = template.split("\n");
493
+ const output = [];
494
+ let skipServer = false;
495
+ for (const line of lines) {
496
+ if (line.startsWith("name:")) {
497
+ output.push(`name: ${result.name}`);
498
+ } else if (line.startsWith("server:") || line.startsWith("server ")) {
499
+ output.push(serverBlock);
500
+ if (line === "server:") {
501
+ skipServer = true;
502
+ }
503
+ } else if (skipServer) {
504
+ if (line.startsWith(" ") && !line.startsWith(" -") && !line.match(/^\w/)) {
505
+ if (line.match(/^[a-zA-Z]/)) {
506
+ skipServer = false;
507
+ output.push(line);
508
+ }
509
+ continue;
510
+ } else {
511
+ skipServer = false;
512
+ output.push(line);
513
+ }
514
+ } else {
515
+ output.push(line);
516
+ }
517
+ }
518
+ return output.join("\n");
519
+ }
520
+ var initCommand = new Command3("init").description("Initialize a new mcpspec project").argument("[directory]", "Target directory", ".").option("--template <type>", "Template type: minimal, standard, full").action(async (directory, options) => {
521
+ const dir = resolve2(directory);
522
+ try {
523
+ if (!options.template && process.stdin.isTTY) {
524
+ const { runOnboardingWizard } = await import("./onboarding-5G6D2OYR.js");
525
+ const result = await runOnboardingWizard();
526
+ if (!existsSync(dir)) {
527
+ mkdirSync(dir, { recursive: true });
528
+ }
529
+ const collectionPath2 = join(dir, "mcpspec.yaml");
530
+ if (existsSync(collectionPath2)) {
531
+ console.error(`File already exists: ${collectionPath2}`);
532
+ process.exit(EXIT_CODES3.CONFIG_ERROR);
533
+ }
534
+ const content = generateFromWizard(result);
535
+ writeFileSync2(collectionPath2, content, "utf-8");
536
+ console.log(`
537
+ Created ${collectionPath2}`);
538
+ console.log(`
539
+ Edit the file to configure your tests, then run: mcpspec test`);
540
+ return;
541
+ }
542
+ const template = options.template ?? "standard";
543
+ if (!TEMPLATES[template]) {
544
+ console.error(`Unknown template: ${template}. Available: minimal, standard, full`);
545
+ process.exit(EXIT_CODES3.CONFIG_ERROR);
546
+ }
547
+ if (!existsSync(dir)) {
548
+ mkdirSync(dir, { recursive: true });
549
+ }
550
+ const collectionPath = join(dir, "mcpspec.yaml");
551
+ if (existsSync(collectionPath)) {
552
+ console.error(`File already exists: ${collectionPath}`);
553
+ process.exit(EXIT_CODES3.CONFIG_ERROR);
554
+ }
555
+ writeFileSync2(collectionPath, TEMPLATES[template], "utf-8");
556
+ console.log(`Created ${collectionPath}`);
557
+ console.log(`
558
+ Edit the file to configure your MCP server and tests.`);
559
+ console.log(`Then run: mcpspec test`);
560
+ } catch (err) {
561
+ const message = err instanceof Error ? err.message : String(err);
562
+ console.error(`Failed to initialize: ${message}`);
563
+ process.exit(EXIT_CODES3.ERROR);
564
+ }
565
+ });
566
+
567
+ // src/commands/compare.ts
568
+ import { Command as Command4 } from "commander";
569
+ import { EXIT_CODES as EXIT_CODES4 } from "@mcpspec/shared";
570
+ import { BaselineStore, ResultDiffer } from "@mcpspec/core";
571
+ var compareCommand = new Command4("compare").description("Compare test runs against a baseline").argument("[run1]", "First run (baseline name)").argument("[run2]", "Second run (baseline name)").option("--baseline <name>", "Compare latest run against named baseline").action((run1, run2, options) => {
572
+ const store = new BaselineStore();
573
+ const differ = new ResultDiffer();
574
+ if (run1 && run2) {
575
+ const baseline = store.load(run1);
576
+ const current = store.load(run2);
577
+ if (!baseline) {
578
+ console.error(`Baseline "${run1}" not found.`);
579
+ process.exit(EXIT_CODES4.CONFIG_ERROR);
580
+ }
581
+ if (!current) {
582
+ console.error(`Baseline "${run2}" not found.`);
583
+ process.exit(EXIT_CODES4.CONFIG_ERROR);
584
+ }
585
+ const diff = differ.diff(baseline, current, run1);
586
+ printDiff(diff);
587
+ process.exit(diff.summary.regressions > 0 ? EXIT_CODES4.TEST_FAILURE : EXIT_CODES4.SUCCESS);
588
+ }
589
+ if (options.baseline) {
590
+ const baselines = store.list();
591
+ if (baselines.length === 0) {
592
+ console.error("No baselines found. Run `mcpspec baseline save <name>` first.");
593
+ process.exit(EXIT_CODES4.CONFIG_ERROR);
594
+ }
595
+ const baseline = store.load(options.baseline);
596
+ if (!baseline) {
597
+ console.error(`Baseline "${options.baseline}" not found.`);
598
+ console.error(`Available baselines: ${baselines.join(", ")}`);
599
+ process.exit(EXIT_CODES4.CONFIG_ERROR);
600
+ }
601
+ const otherBaselines = baselines.filter((b) => b !== options.baseline);
602
+ if (otherBaselines.length === 0) {
603
+ console.error("Need at least two baselines to compare. Run tests and save another baseline.");
604
+ process.exit(EXIT_CODES4.CONFIG_ERROR);
605
+ }
606
+ const current = store.load(otherBaselines[otherBaselines.length - 1]);
607
+ if (!current) {
608
+ console.error("Could not load comparison baseline.");
609
+ process.exit(EXIT_CODES4.CONFIG_ERROR);
610
+ }
611
+ const diff = differ.diff(baseline, current, options.baseline);
612
+ printDiff(diff);
613
+ process.exit(diff.summary.regressions > 0 ? EXIT_CODES4.TEST_FAILURE : EXIT_CODES4.SUCCESS);
614
+ }
615
+ console.error("Usage: mcpspec compare <run1> <run2> or mcpspec compare --baseline <name>");
616
+ process.exit(EXIT_CODES4.CONFIG_ERROR);
617
+ });
618
+ function printDiff(diff) {
619
+ console.log(`
620
+ Comparison against baseline "${diff.baselineName}":`);
621
+ console.log(` Tests before: ${diff.summary.totalBefore}`);
622
+ console.log(` Tests after: ${diff.summary.totalAfter}`);
623
+ console.log("");
624
+ if (diff.regressions.length > 0) {
625
+ console.log(" Regressions:");
626
+ for (const r of diff.regressions) {
627
+ console.log(` \u2717 ${r.testName}: ${r.before?.status} -> ${r.after?.status}`);
628
+ }
629
+ }
630
+ if (diff.fixes.length > 0) {
631
+ console.log(" Fixes:");
632
+ for (const r of diff.fixes) {
633
+ console.log(` \u2713 ${r.testName}: ${r.before?.status} -> ${r.after?.status}`);
634
+ }
635
+ }
636
+ if (diff.newTests.length > 0) {
637
+ console.log(" New tests:");
638
+ for (const r of diff.newTests) {
639
+ console.log(` + ${r.testName}: ${r.after?.status}`);
640
+ }
641
+ }
642
+ if (diff.removedTests.length > 0) {
643
+ console.log(" Removed tests:");
644
+ for (const r of diff.removedTests) {
645
+ console.log(` - ${r.testName}`);
646
+ }
647
+ }
648
+ if (diff.regressions.length === 0 && diff.fixes.length === 0 && diff.newTests.length === 0 && diff.removedTests.length === 0) {
649
+ console.log(" No changes detected.");
650
+ }
651
+ console.log("");
652
+ }
653
+
654
+ // src/commands/baseline.ts
655
+ import { Command as Command5 } from "commander";
656
+ import { readFileSync as readFileSync2 } from "fs";
657
+ import { resolve as resolve3 } from "path";
658
+ import { EXIT_CODES as EXIT_CODES5 } from "@mcpspec/shared";
659
+ import { BaselineStore as BaselineStore2 } from "@mcpspec/core";
660
+ var baselineCommand = new Command5("baseline").description("Manage test baselines").addCommand(
661
+ new Command5("save").description("Save a test run result as a named baseline").argument("<name>", "Baseline name").argument("[results-file]", "Path to JSON results file").action((name, resultsFile) => {
662
+ const store = new BaselineStore2();
663
+ if (!resultsFile) {
664
+ console.error("Usage: mcpspec baseline save <name> <results-file.json>");
665
+ console.error(" Run tests with --reporter json --output results.json first.");
666
+ process.exit(EXIT_CODES5.CONFIG_ERROR);
667
+ }
668
+ try {
669
+ const fullPath = resolve3(resultsFile);
670
+ const content = readFileSync2(fullPath, "utf-8");
671
+ const raw = JSON.parse(content);
672
+ const result = {
673
+ ...raw,
674
+ startedAt: new Date(raw.startedAt),
675
+ completedAt: new Date(raw.completedAt)
676
+ };
677
+ const savedPath = store.save(name, result);
678
+ console.log(`Baseline "${name}" saved to ${savedPath}`);
679
+ } catch (err) {
680
+ const message = err instanceof Error ? err.message : String(err);
681
+ console.error(`Failed to save baseline: ${message}`);
682
+ process.exit(EXIT_CODES5.ERROR);
683
+ }
684
+ })
685
+ ).addCommand(
686
+ new Command5("list").description("List saved baselines").action(() => {
687
+ const store = new BaselineStore2();
688
+ const baselines = store.list();
689
+ if (baselines.length === 0) {
690
+ console.log("No baselines saved.");
691
+ console.log("Run `mcpspec baseline save <name> <results.json>` to create one.");
692
+ return;
693
+ }
694
+ console.log("Saved baselines:");
695
+ for (const name of baselines) {
696
+ console.log(` - ${name}`);
697
+ }
698
+ })
699
+ );
700
+
701
+ // src/commands/ui.ts
702
+ import { Command as Command6 } from "commander";
703
+ var uiCommand = new Command6("ui").description("Launch the MCPSpec web UI").option("-p, --port <port>", "Port to listen on", "6274").option("--host <host>", "Host to bind to", "127.0.0.1").option("--no-open", "Do not auto-open browser").action(async (opts) => {
704
+ const port = parseInt(opts.port, 10);
705
+ if (isNaN(port) || port < 1 || port > 65535) {
706
+ console.error("Invalid port number");
707
+ process.exit(1);
708
+ }
709
+ const { startServer, UI_DIST_PATH } = await import("@mcpspec/server");
710
+ const uiDistPath = UI_DIST_PATH;
711
+ const server = await startServer({
712
+ port,
713
+ host: opts.host,
714
+ uiDistPath
715
+ });
716
+ const url = `http://${server.host}:${server.port}`;
717
+ if (opts.open) {
718
+ try {
719
+ const { default: open } = await import("open");
720
+ await open(url);
721
+ } catch {
722
+ console.log(`Open ${url} in your browser`);
723
+ }
724
+ }
725
+ process.on("SIGINT", () => {
726
+ console.log("\nShutting down...");
727
+ server.close();
728
+ process.exit(0);
729
+ });
730
+ process.on("SIGTERM", () => {
731
+ server.close();
732
+ process.exit(0);
733
+ });
734
+ });
735
+
736
+ // src/commands/audit.ts
737
+ import { Command as Command7 } from "commander";
738
+ import { EXIT_CODES as EXIT_CODES6 } from "@mcpspec/shared";
739
+ import {
740
+ MCPClient as MCPClient2,
741
+ ScanConfig,
742
+ SecurityScanner,
743
+ formatError as formatError3
744
+ } from "@mcpspec/core";
745
+ var COLORS2 = {
746
+ reset: "\x1B[0m",
747
+ red: "\x1B[31m",
748
+ yellow: "\x1B[33m",
749
+ green: "\x1B[32m",
750
+ cyan: "\x1B[36m",
751
+ gray: "\x1B[90m",
752
+ bold: "\x1B[1m"
753
+ };
754
+ var SEVERITY_COLORS = {
755
+ critical: COLORS2.red + COLORS2.bold,
756
+ high: COLORS2.red,
757
+ medium: COLORS2.yellow,
758
+ low: COLORS2.cyan,
759
+ info: COLORS2.gray
760
+ };
761
+ var auditCommand = new Command7("audit").description("Run security audit on an MCP server").argument("<server>", "Server command or URL").option("--mode <mode>", "Scan mode: passive, active, aggressive", "passive").option("--acknowledge-risk", "Skip confirmation prompt for active/aggressive modes", false).option("--fail-on <severity>", "Fail with exit code 6 if findings at or above severity: info, low, medium, high, critical").option("--rules <rules...>", "Only run specific rules").action(async (serverCommand, options) => {
762
+ let client = null;
763
+ try {
764
+ const mode = options.mode;
765
+ const config = new ScanConfig({
766
+ mode,
767
+ acknowledgeRisk: options.acknowledgeRisk,
768
+ rules: options.rules
769
+ });
770
+ if (config.requiresConfirmation()) {
771
+ console.log(`
772
+ ${COLORS2.yellow}${COLORS2.bold} WARNING: Security Scan${COLORS2.reset}`);
773
+ console.log(`${COLORS2.yellow} Mode: ${mode}${COLORS2.reset}`);
774
+ console.log(`${COLORS2.yellow} This sends potentially harmful payloads to the server.${COLORS2.reset}`);
775
+ console.log(`${COLORS2.yellow} NEVER run against production systems!${COLORS2.reset}
776
+ `);
777
+ const { confirm } = await import("@inquirer/prompts");
778
+ const confirmed = await confirm({
779
+ message: "Is this a TEST environment? Continue with scan?",
780
+ default: false
781
+ });
782
+ if (!confirmed) {
783
+ console.log(" Scan cancelled.");
784
+ process.exit(EXIT_CODES6.SUCCESS);
785
+ }
786
+ }
787
+ console.log(`
788
+ ${COLORS2.cyan} Connecting to:${COLORS2.reset} ${serverCommand}`);
789
+ client = new MCPClient2({ serverConfig: serverCommand });
790
+ await client.connect();
791
+ const info = client.getServerInfo();
792
+ console.log(`${COLORS2.green} Connected to ${info?.name ?? "unknown"} v${info?.version ?? "?"}${COLORS2.reset}`);
793
+ console.log(`${COLORS2.gray} Scan mode: ${mode} | Rules: ${config.rules.join(", ")}${COLORS2.reset}
794
+ `);
795
+ const scanner = new SecurityScanner();
796
+ const result = await scanner.scan(client, config, {
797
+ onRuleStart: (_ruleId, ruleName) => {
798
+ process.stdout.write(` ${COLORS2.gray}Running ${ruleName}...${COLORS2.reset}`);
799
+ },
800
+ onRuleComplete: (_ruleId, findingCount) => {
801
+ if (findingCount > 0) {
802
+ console.log(` ${COLORS2.yellow}${findingCount} finding(s)${COLORS2.reset}`);
803
+ } else {
804
+ console.log(` ${COLORS2.green}clean${COLORS2.reset}`);
805
+ }
806
+ }
807
+ });
808
+ console.log(`
809
+ ${COLORS2.bold} Security Scan Results${COLORS2.reset}`);
810
+ console.log(` ${"\u2500".repeat(50)}`);
811
+ console.log(` Server: ${result.serverName}`);
812
+ console.log(` Mode: ${result.mode}`);
813
+ console.log(` Findings: ${result.summary.totalFindings}`);
814
+ if (result.summary.totalFindings > 0) {
815
+ console.log(`
816
+ ${COLORS2.bold}By Severity:${COLORS2.reset}`);
817
+ for (const sev of ["critical", "high", "medium", "low", "info"]) {
818
+ const count = result.summary.bySeverity[sev];
819
+ if (count > 0) {
820
+ console.log(` ${SEVERITY_COLORS[sev]}${sev.toUpperCase()}${COLORS2.reset}: ${count}`);
821
+ }
822
+ }
823
+ console.log(`
824
+ ${COLORS2.bold}Findings:${COLORS2.reset}
825
+ `);
826
+ for (const finding of result.findings) {
827
+ const color = SEVERITY_COLORS[finding.severity];
828
+ console.log(` ${color}[${finding.severity.toUpperCase()}]${COLORS2.reset} ${finding.title}`);
829
+ console.log(` ${COLORS2.gray}${finding.description}${COLORS2.reset}`);
830
+ if (finding.evidence) {
831
+ console.log(` ${COLORS2.gray}Evidence: ${finding.evidence.slice(0, 100)}${COLORS2.reset}`);
832
+ }
833
+ if (finding.remediation) {
834
+ console.log(` ${COLORS2.cyan}Fix: ${finding.remediation}${COLORS2.reset}`);
835
+ }
836
+ console.log("");
837
+ }
838
+ } else {
839
+ console.log(`
840
+ ${COLORS2.green}No security findings detected.${COLORS2.reset}
841
+ `);
842
+ }
843
+ await client.disconnect();
844
+ if (options.failOn) {
845
+ const failOnSeverity = options.failOn;
846
+ const failConfig = new ScanConfig({ severityThreshold: failOnSeverity });
847
+ const matchingFindings = result.findings.filter((f) => failConfig.meetsThreshold(f.severity));
848
+ if (matchingFindings.length > 0) {
849
+ process.exit(EXIT_CODES6.SECURITY_FINDINGS);
850
+ }
851
+ }
852
+ process.exit(EXIT_CODES6.SUCCESS);
853
+ } catch (err) {
854
+ const formatted = formatError3(err);
855
+ console.error(`
856
+ ${formatted.title}: ${formatted.description}`);
857
+ formatted.suggestions.forEach((s) => console.error(` - ${s}`));
858
+ await client?.disconnect();
859
+ process.exit(formatted.exitCode);
860
+ }
861
+ });
862
+
863
+ // src/commands/bench.ts
864
+ import { Command as Command8 } from "commander";
865
+ import { EXIT_CODES as EXIT_CODES7 } from "@mcpspec/shared";
866
+ import {
867
+ MCPClient as MCPClient3,
868
+ BenchmarkRunner,
869
+ formatError as formatError4
870
+ } from "@mcpspec/core";
871
+ var COLORS3 = {
872
+ reset: "\x1B[0m",
873
+ green: "\x1B[32m",
874
+ cyan: "\x1B[36m",
875
+ gray: "\x1B[90m",
876
+ bold: "\x1B[1m",
877
+ yellow: "\x1B[33m"
878
+ };
879
+ var benchCommand = new Command8("bench").description("Run performance benchmark on an MCP server").argument("<server>", "Server command or URL").option("--iterations <n>", "Number of iterations", "100").option("--tool <name>", "Tool to benchmark (defaults to first available)").option("--args <json>", "JSON arguments for the tool call", "{}").option("--timeout <ms>", "Timeout per call in milliseconds", "30000").option("--warmup <n>", "Number of warmup iterations", "5").action(async (serverCommand, options) => {
880
+ let client = null;
881
+ try {
882
+ console.log(`
883
+ ${COLORS3.cyan} Connecting to:${COLORS3.reset} ${serverCommand}`);
884
+ client = new MCPClient3({ serverConfig: serverCommand });
885
+ await client.connect();
886
+ const info = client.getServerInfo();
887
+ console.log(`${COLORS3.green} Connected to ${info?.name ?? "unknown"} v${info?.version ?? "?"}${COLORS3.reset}
888
+ `);
889
+ const tools = await client.listTools();
890
+ if (tools.length === 0) {
891
+ console.error(" No tools available on this server.");
892
+ await client.disconnect();
893
+ process.exit(EXIT_CODES7.ERROR);
894
+ }
895
+ let toolName = options.tool;
896
+ if (!toolName) {
897
+ toolName = tools[0].name;
898
+ console.log(`${COLORS3.gray} No --tool specified, using: ${toolName}${COLORS3.reset}`);
899
+ } else {
900
+ const found = tools.find((t) => t.name === toolName);
901
+ if (!found) {
902
+ console.error(` Tool "${toolName}" not found. Available: ${tools.map((t) => t.name).join(", ")}`);
903
+ await client.disconnect();
904
+ process.exit(EXIT_CODES7.ERROR);
905
+ }
906
+ }
907
+ let args = {};
908
+ if (options.args) {
909
+ try {
910
+ args = JSON.parse(options.args);
911
+ } catch {
912
+ console.error(` Invalid JSON for --args: ${options.args}`);
913
+ await client.disconnect();
914
+ process.exit(EXIT_CODES7.CONFIG_ERROR);
915
+ }
916
+ }
917
+ const iterations = parseInt(options.iterations, 10);
918
+ const warmup = parseInt(options.warmup ?? "5", 10);
919
+ const timeout = parseInt(options.timeout ?? "30000", 10);
920
+ console.log(`${COLORS3.bold} Benchmarking: ${toolName}${COLORS3.reset}`);
921
+ console.log(` Iterations: ${iterations} | Warmup: ${warmup} | Timeout: ${timeout}ms
922
+ `);
923
+ const runner = new BenchmarkRunner();
924
+ const result = await runner.run(client, toolName, args, {
925
+ iterations,
926
+ warmupIterations: warmup,
927
+ concurrency: 1,
928
+ timeout
929
+ }, {
930
+ onWarmupStart: (n) => {
931
+ console.log(`${COLORS3.gray} Warming up (${n} iterations)...${COLORS3.reset}`);
932
+ },
933
+ onIterationComplete: (i, total) => {
934
+ if (i % Math.max(1, Math.floor(total / 10)) === 0 || i === total) {
935
+ process.stdout.write(`\r Progress: ${i}/${total}`);
936
+ }
937
+ },
938
+ onComplete: () => {
939
+ console.log("");
940
+ }
941
+ });
942
+ console.log(`
943
+ ${COLORS3.bold} Benchmark Results${COLORS3.reset}`);
944
+ console.log(` ${"\u2500".repeat(40)}`);
945
+ console.log(` Tool: ${result.toolName}`);
946
+ console.log(` Iterations: ${result.iterations}`);
947
+ console.log(` Errors: ${result.errors}`);
948
+ console.log("");
949
+ console.log(` ${COLORS3.bold}Latency Statistics:${COLORS3.reset}`);
950
+ console.log(` Min: ${result.stats.min.toFixed(2)}ms`);
951
+ console.log(` Max: ${result.stats.max.toFixed(2)}ms`);
952
+ console.log(` Mean: ${result.stats.mean.toFixed(2)}ms`);
953
+ console.log(` Median: ${result.stats.median.toFixed(2)}ms`);
954
+ console.log(` P95: ${result.stats.p95.toFixed(2)}ms`);
955
+ console.log(` P99: ${result.stats.p99.toFixed(2)}ms`);
956
+ console.log(` StdDev: ${result.stats.stddev.toFixed(2)}ms`);
957
+ const durationSec = (result.completedAt.getTime() - result.startedAt.getTime()) / 1e3;
958
+ const rps = result.iterations / durationSec;
959
+ console.log(`
960
+ Throughput: ${COLORS3.green}${rps.toFixed(1)} calls/sec${COLORS3.reset}`);
961
+ console.log("");
962
+ await client.disconnect();
963
+ process.exit(EXIT_CODES7.SUCCESS);
964
+ } catch (err) {
965
+ const formatted = formatError4(err);
966
+ console.error(`
967
+ ${formatted.title}: ${formatted.description}`);
968
+ formatted.suggestions.forEach((s) => console.error(` - ${s}`));
969
+ await client?.disconnect();
970
+ process.exit(formatted.exitCode);
971
+ }
972
+ });
973
+
974
+ // src/commands/docs.ts
975
+ import { Command as Command9 } from "commander";
976
+ import { EXIT_CODES as EXIT_CODES8 } from "@mcpspec/shared";
977
+ import { MCPClient as MCPClient4, DocGenerator, formatError as formatError5 } from "@mcpspec/core";
978
+ var COLORS4 = {
979
+ reset: "\x1B[0m",
980
+ green: "\x1B[32m",
981
+ cyan: "\x1B[36m",
982
+ bold: "\x1B[1m"
983
+ };
984
+ var docsCommand = new Command9("docs").description("Generate documentation from an MCP server").argument("<server>", "Server command or URL").option("--format <format>", "Output format: markdown, html", "markdown").option("--output <dir>", "Output directory").action(async (serverCommand, options) => {
985
+ let client = null;
986
+ try {
987
+ console.log(`
988
+ ${COLORS4.cyan} Connecting to:${COLORS4.reset} ${serverCommand}`);
989
+ client = new MCPClient4({ serverConfig: serverCommand });
990
+ await client.connect();
991
+ const info = client.getServerInfo();
992
+ console.log(`${COLORS4.green} Connected to ${info?.name ?? "unknown"} v${info?.version ?? "?"}${COLORS4.reset}
993
+ `);
994
+ const format = options.format === "html" ? "html" : "markdown";
995
+ const generator = new DocGenerator();
996
+ const content = await generator.generate(client, {
997
+ format,
998
+ outputDir: options.output
999
+ });
1000
+ if (options.output) {
1001
+ const filename = format === "html" ? "index.html" : "README.md";
1002
+ console.log(`${COLORS4.green} Documentation written to ${options.output}/${filename}${COLORS4.reset}
1003
+ `);
1004
+ } else {
1005
+ console.log(content);
1006
+ }
1007
+ await client.disconnect();
1008
+ process.exit(EXIT_CODES8.SUCCESS);
1009
+ } catch (err) {
1010
+ const formatted = formatError5(err);
1011
+ console.error(`
1012
+ ${formatted.title}: ${formatted.description}`);
1013
+ formatted.suggestions.forEach((s) => console.error(` - ${s}`));
1014
+ await client?.disconnect();
1015
+ process.exit(formatted.exitCode);
1016
+ }
1017
+ });
1018
+
1019
+ // src/commands/score.ts
1020
+ import { Command as Command10 } from "commander";
1021
+ import { writeFileSync as writeFileSync3 } from "fs";
1022
+ import { EXIT_CODES as EXIT_CODES9 } from "@mcpspec/shared";
1023
+ import { MCPClient as MCPClient5, MCPScoreCalculator, BadgeGenerator, formatError as formatError6 } from "@mcpspec/core";
1024
+ var COLORS5 = {
1025
+ reset: "\x1B[0m",
1026
+ green: "\x1B[32m",
1027
+ cyan: "\x1B[36m",
1028
+ yellow: "\x1B[33m",
1029
+ red: "\x1B[31m",
1030
+ bold: "\x1B[1m",
1031
+ gray: "\x1B[90m"
1032
+ };
1033
+ function scoreColor(score) {
1034
+ if (score >= 80) return COLORS5.green;
1035
+ if (score >= 60) return COLORS5.yellow;
1036
+ return COLORS5.red;
1037
+ }
1038
+ var scoreCommand = new Command10("score").description("Calculate MCP Score for a server").argument("<server>", "Server command or URL").option("--badge <path>", "Output badge SVG path").action(async (serverCommand, options) => {
1039
+ let client = null;
1040
+ try {
1041
+ console.log(`
1042
+ ${COLORS5.cyan} Connecting to:${COLORS5.reset} ${serverCommand}`);
1043
+ client = new MCPClient5({ serverConfig: serverCommand });
1044
+ await client.connect();
1045
+ const info = client.getServerInfo();
1046
+ console.log(`${COLORS5.green} Connected to ${info?.name ?? "unknown"} v${info?.version ?? "?"}${COLORS5.reset}
1047
+ `);
1048
+ const calculator = new MCPScoreCalculator();
1049
+ const score = await calculator.calculate(client, {
1050
+ onCategoryStart: (category) => {
1051
+ process.stdout.write(`${COLORS5.gray} Evaluating ${category}...${COLORS5.reset}`);
1052
+ },
1053
+ onCategoryComplete: (_category, categoryScore) => {
1054
+ const color = scoreColor(categoryScore);
1055
+ console.log(` ${color}${categoryScore}/100${COLORS5.reset}`);
1056
+ }
1057
+ });
1058
+ console.log(`
1059
+ ${COLORS5.bold} MCP Score${COLORS5.reset}`);
1060
+ console.log(` ${"\u2500".repeat(40)}`);
1061
+ const categories = [
1062
+ { name: "Documentation", score: score.categories.documentation },
1063
+ { name: "Schema Quality", score: score.categories.schemaQuality },
1064
+ { name: "Error Handling", score: score.categories.errorHandling },
1065
+ { name: "Performance", score: score.categories.performance },
1066
+ { name: "Security", score: score.categories.security }
1067
+ ];
1068
+ for (const cat of categories) {
1069
+ const color = scoreColor(cat.score);
1070
+ const bar = "\u2588".repeat(Math.round(cat.score / 5)) + "\u2591".repeat(20 - Math.round(cat.score / 5));
1071
+ console.log(` ${cat.name.padEnd(16)} ${bar} ${color}${cat.score}/100${COLORS5.reset}`);
1072
+ }
1073
+ const overallColor = scoreColor(score.overall);
1074
+ console.log(`
1075
+ ${COLORS5.bold}Overall: ${overallColor}${score.overall}/100${COLORS5.reset}
1076
+ `);
1077
+ if (options.badge) {
1078
+ const badgeGenerator = new BadgeGenerator();
1079
+ const svg = badgeGenerator.generate(score);
1080
+ writeFileSync3(options.badge, svg, "utf-8");
1081
+ console.log(`${COLORS5.green} Badge written to ${options.badge}${COLORS5.reset}
1082
+ `);
1083
+ }
1084
+ await client.disconnect();
1085
+ process.exit(EXIT_CODES9.SUCCESS);
1086
+ } catch (err) {
1087
+ const formatted = formatError6(err);
1088
+ console.error(`
1089
+ ${formatted.title}: ${formatted.description}`);
1090
+ formatted.suggestions.forEach((s) => console.error(` - ${s}`));
1091
+ await client?.disconnect();
1092
+ process.exit(formatted.exitCode);
1093
+ }
1094
+ });
1095
+
1096
+ // src/index.ts
1097
+ var program = new Command11();
1098
+ program.name("mcpspec").description("The definitive MCP server testing platform").version("1.0.0");
1099
+ program.addCommand(testCommand);
1100
+ program.addCommand(inspectCommand);
1101
+ program.addCommand(initCommand);
1102
+ program.addCommand(compareCommand);
1103
+ program.addCommand(baselineCommand);
1104
+ program.addCommand(uiCommand);
1105
+ program.addCommand(auditCommand);
1106
+ program.addCommand(benchCommand);
1107
+ program.addCommand(docsCommand);
1108
+ program.addCommand(scoreCommand);
1109
+ program.parse(process.argv);
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/wizard/onboarding.ts
4
+ import { input, select } from "@inquirer/prompts";
5
+ async function runOnboardingWizard() {
6
+ console.log("\n Welcome to MCPSpec! Let's set up your test collection.\n");
7
+ const name = await input({
8
+ message: "Collection name:",
9
+ default: "My MCP Tests"
10
+ });
11
+ const transport = await select({
12
+ message: "How does your MCP server communicate?",
13
+ choices: [
14
+ { name: "stdio (command-line process)", value: "stdio" },
15
+ { name: "SSE (Server-Sent Events)", value: "sse" },
16
+ { name: "Streamable HTTP", value: "streamable-http" }
17
+ ]
18
+ });
19
+ let command;
20
+ let url;
21
+ if (transport === "stdio") {
22
+ command = await input({
23
+ message: "Server command (e.g., npx @modelcontextprotocol/server-filesystem /tmp):",
24
+ default: "npx my-mcp-server"
25
+ });
26
+ } else {
27
+ url = await input({
28
+ message: "Server URL:",
29
+ default: "http://localhost:3000/mcp"
30
+ });
31
+ }
32
+ const template = await select({
33
+ message: "Template complexity:",
34
+ choices: [
35
+ { name: "Minimal - simple tests to get started", value: "minimal" },
36
+ { name: "Standard - tests with assertions (Recommended)", value: "standard" },
37
+ { name: "Full - environments, tags, extraction", value: "full" }
38
+ ]
39
+ });
40
+ return { name, transport, command, url, template };
41
+ }
42
+ export {
43
+ runOnboardingWizard
44
+ };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "mcpspec",
3
+ "version": "1.0.0",
4
+ "description": "The definitive MCP server testing platform",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "testing",
9
+ "cli"
10
+ ],
11
+ "type": "module",
12
+ "bin": {
13
+ "mcpspec": "./dist/index.js"
14
+ },
15
+ "main": "./dist/index.js",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/anthropics/mcpspec.git",
23
+ "directory": "packages/cli"
24
+ },
25
+ "engines": {
26
+ "node": ">=22.0.0"
27
+ },
28
+ "dependencies": {
29
+ "@inquirer/prompts": "^7.0.0",
30
+ "commander": "^12.1.0",
31
+ "open": "^10.1.0",
32
+ "@mcpspec/core": "1.0.0",
33
+ "@mcpspec/shared": "1.0.0",
34
+ "@mcpspec/server": "1.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "tsup": "^8.0.0",
38
+ "typescript": "^5.4.0",
39
+ "vitest": "^2.1.0"
40
+ },
41
+ "scripts": {
42
+ "build": "tsup",
43
+ "test": "vitest run --passWithNoTests",
44
+ "clean": "rm -rf dist .turbo"
45
+ }
46
+ }