@mcpspec/core 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/dist/index.js ADDED
@@ -0,0 +1,3305 @@
1
+ // src/errors/error-codes.ts
2
+ import { EXIT_CODES } from "@mcpspec/shared";
3
+ var ERROR_CODE_MAP = {
4
+ CONNECTION_TIMEOUT: EXIT_CODES.CONNECTION_ERROR,
5
+ CONNECTION_REFUSED: EXIT_CODES.CONNECTION_ERROR,
6
+ CONNECTION_LOST: EXIT_CODES.CONNECTION_ERROR,
7
+ PROCESS_SPAWN_FAILED: EXIT_CODES.ERROR,
8
+ PROCESS_CRASHED: EXIT_CODES.ERROR,
9
+ PROCESS_TIMEOUT: EXIT_CODES.TIMEOUT,
10
+ TOOL_NOT_FOUND: EXIT_CODES.ERROR,
11
+ TOOL_CALL_FAILED: EXIT_CODES.ERROR,
12
+ INVALID_RESPONSE: EXIT_CODES.ERROR,
13
+ SCHEMA_VALIDATION_FAILED: EXIT_CODES.VALIDATION_ERROR,
14
+ ASSERTION_FAILED: EXIT_CODES.TEST_FAILURE,
15
+ COLLECTION_PARSE_ERROR: EXIT_CODES.CONFIG_ERROR,
16
+ COLLECTION_VALIDATION_ERROR: EXIT_CODES.CONFIG_ERROR,
17
+ YAML_PARSE_ERROR: EXIT_CODES.CONFIG_ERROR,
18
+ YAML_TOO_LARGE: EXIT_CODES.CONFIG_ERROR,
19
+ YAML_TOO_DEEP: EXIT_CODES.CONFIG_ERROR,
20
+ TIMEOUT: EXIT_CODES.TIMEOUT,
21
+ RATE_LIMITED: EXIT_CODES.ERROR,
22
+ CONFIG_ERROR: EXIT_CODES.CONFIG_ERROR,
23
+ SECURITY_SCAN_ERROR: EXIT_CODES.SECURITY_FINDINGS,
24
+ NOT_IMPLEMENTED: EXIT_CODES.ERROR,
25
+ UNKNOWN_ERROR: EXIT_CODES.ERROR
26
+ };
27
+
28
+ // src/errors/mcpspec-error.ts
29
+ var MCPSpecError = class extends Error {
30
+ code;
31
+ exitCode;
32
+ context;
33
+ constructor(code, message, context = {}) {
34
+ super(message);
35
+ this.name = "MCPSpecError";
36
+ this.code = code;
37
+ this.exitCode = ERROR_CODE_MAP[code];
38
+ this.context = context;
39
+ }
40
+ };
41
+ var NotImplementedError = class extends MCPSpecError {
42
+ constructor(feature) {
43
+ super("NOT_IMPLEMENTED", `${feature} is not yet implemented. Coming in a future release.`, {
44
+ feature
45
+ });
46
+ this.name = "NotImplementedError";
47
+ }
48
+ };
49
+
50
+ // src/errors/error-messages.ts
51
+ var ERROR_TEMPLATES = {
52
+ CONNECTION_TIMEOUT: {
53
+ title: "Connection Timed Out",
54
+ description: "Could not connect to the MCP server within {{timeout}}ms.",
55
+ suggestions: [
56
+ "Verify the server is running",
57
+ "Check the command/URL is correct",
58
+ "Increase timeout with --timeout flag"
59
+ ],
60
+ docs: "https://mcpspec.dev/docs/troubleshooting#connection-timeout"
61
+ },
62
+ CONNECTION_REFUSED: {
63
+ title: "Connection Refused",
64
+ description: "The MCP server at {{target}} refused the connection.",
65
+ suggestions: [
66
+ "Verify the server is running and accepting connections",
67
+ "Check the port number is correct",
68
+ "Ensure no firewall is blocking the connection"
69
+ ],
70
+ docs: "https://mcpspec.dev/docs/troubleshooting#connection-refused"
71
+ },
72
+ PROCESS_SPAWN_FAILED: {
73
+ title: "Failed to Start Server",
74
+ description: "Could not spawn process: {{command}}",
75
+ suggestions: [
76
+ "Verify the command exists and is in PATH",
77
+ "Check that all required dependencies are installed",
78
+ "Try running the command directly in your terminal"
79
+ ]
80
+ },
81
+ PROCESS_CRASHED: {
82
+ title: "Server Process Crashed",
83
+ description: "The MCP server process exited unexpectedly with code {{exitCode}}.",
84
+ suggestions: [
85
+ "Check the server logs for errors",
86
+ "Ensure the server has the required environment variables",
87
+ "Try running the server command directly to see errors"
88
+ ]
89
+ },
90
+ TOOL_NOT_FOUND: {
91
+ title: "Tool Not Found",
92
+ description: 'The tool "{{toolName}}" does not exist on this server.',
93
+ suggestions: [
94
+ "Available tools: {{availableTools}}",
95
+ "Run `mcpspec inspect` to see all available tools",
96
+ "Check for typos in the tool name"
97
+ ]
98
+ },
99
+ COLLECTION_PARSE_ERROR: {
100
+ title: "Collection Parse Error",
101
+ description: "Failed to parse collection file: {{filePath}}",
102
+ suggestions: [
103
+ "Check YAML syntax is valid",
104
+ "Ensure the file is UTF-8 encoded",
105
+ "Validate against the collection schema"
106
+ ]
107
+ },
108
+ COLLECTION_VALIDATION_ERROR: {
109
+ title: "Collection Validation Error",
110
+ description: "Collection file has invalid structure: {{details}}",
111
+ suggestions: [
112
+ "Check required fields: name, server, tests",
113
+ "Ensure each test has a name and a tool/call",
114
+ "See collection docs for the correct format"
115
+ ],
116
+ docs: "https://mcpspec.dev/docs/collections"
117
+ },
118
+ YAML_TOO_LARGE: {
119
+ title: "YAML File Too Large",
120
+ description: "The YAML file exceeds the maximum size of {{maxSize}} bytes.",
121
+ suggestions: [
122
+ "Split large collections into multiple files",
123
+ "Remove unused tests or data"
124
+ ]
125
+ },
126
+ TIMEOUT: {
127
+ title: "Operation Timed Out",
128
+ description: "The operation did not complete within {{timeout}}ms.",
129
+ suggestions: [
130
+ "Increase the timeout in your collection or CLI flags",
131
+ "Check if the server is responding slowly",
132
+ "Reduce the complexity of the test"
133
+ ]
134
+ }
135
+ };
136
+
137
+ // src/errors/error-formatter.ts
138
+ function formatError(error) {
139
+ if (error instanceof MCPSpecError) {
140
+ const template = ERROR_TEMPLATES[error.code];
141
+ if (template) {
142
+ return {
143
+ title: interpolate(template.title, error.context),
144
+ description: interpolate(template.description, error.context),
145
+ suggestions: template.suggestions.map((s) => interpolate(s, error.context)),
146
+ docs: template.docs,
147
+ code: error.code,
148
+ exitCode: error.exitCode
149
+ };
150
+ }
151
+ return {
152
+ title: error.code,
153
+ description: error.message,
154
+ suggestions: [],
155
+ code: error.code,
156
+ exitCode: error.exitCode
157
+ };
158
+ }
159
+ if (error instanceof Error) {
160
+ return {
161
+ title: "Unexpected Error",
162
+ description: error.message,
163
+ suggestions: ["This may be a bug. Please report it at https://github.com/mcpspec/mcpspec/issues"],
164
+ code: "UNKNOWN_ERROR",
165
+ exitCode: 2
166
+ };
167
+ }
168
+ return {
169
+ title: "Unknown Error",
170
+ description: String(error),
171
+ suggestions: [],
172
+ code: "UNKNOWN_ERROR",
173
+ exitCode: 2
174
+ };
175
+ }
176
+ function interpolate(template, context) {
177
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
178
+ const value = context[key];
179
+ if (value === void 0) return `{{${key}}}`;
180
+ return String(value);
181
+ });
182
+ }
183
+
184
+ // src/utils/yaml-loader.ts
185
+ import yaml from "js-yaml";
186
+ var YAML_LIMITS = {
187
+ maxFileSize: 1024 * 1024,
188
+ // 1MB
189
+ maxNestingDepth: 10,
190
+ maxTests: 1e3
191
+ };
192
+ function loadYamlSafely(content) {
193
+ if (content.length > YAML_LIMITS.maxFileSize) {
194
+ throw new MCPSpecError("YAML_TOO_LARGE", `YAML content exceeds maximum size of ${YAML_LIMITS.maxFileSize} bytes`, {
195
+ maxSize: YAML_LIMITS.maxFileSize,
196
+ actualSize: content.length
197
+ });
198
+ }
199
+ let parsed;
200
+ try {
201
+ parsed = yaml.load(content, {
202
+ schema: yaml.FAILSAFE_SCHEMA
203
+ });
204
+ } catch (err) {
205
+ const message = err instanceof Error ? err.message : String(err);
206
+ throw new MCPSpecError("YAML_PARSE_ERROR", `Failed to parse YAML: ${message}`, {
207
+ parseError: message
208
+ });
209
+ }
210
+ validateNestingDepth(parsed, 0);
211
+ return parsed;
212
+ }
213
+ function validateNestingDepth(value, depth) {
214
+ if (depth > YAML_LIMITS.maxNestingDepth) {
215
+ throw new MCPSpecError("YAML_TOO_DEEP", `YAML nesting exceeds maximum depth of ${YAML_LIMITS.maxNestingDepth}`, {
216
+ maxDepth: YAML_LIMITS.maxNestingDepth
217
+ });
218
+ }
219
+ if (Array.isArray(value)) {
220
+ for (const item of value) {
221
+ validateNestingDepth(item, depth + 1);
222
+ }
223
+ } else if (value !== null && typeof value === "object") {
224
+ for (const val of Object.values(value)) {
225
+ validateNestingDepth(val, depth + 1);
226
+ }
227
+ }
228
+ }
229
+
230
+ // src/utils/secret-masker.ts
231
+ var SecretMasker = class _SecretMasker {
232
+ secrets = /* @__PURE__ */ new Set();
233
+ static REDACTED = "***REDACTED***";
234
+ static SECRET_PATTERNS = [
235
+ /api[_-]?key/i,
236
+ /password/i,
237
+ /secret/i,
238
+ /token/i,
239
+ /credential/i,
240
+ /auth/i,
241
+ /private[_-]?key/i
242
+ ];
243
+ static MIN_SECRET_LENGTH = 4;
244
+ register(secret) {
245
+ if (secret.length >= _SecretMasker.MIN_SECRET_LENGTH) {
246
+ this.secrets.add(secret);
247
+ }
248
+ }
249
+ registerFromEnv(env) {
250
+ for (const [key, value] of Object.entries(env)) {
251
+ if (_SecretMasker.SECRET_PATTERNS.some((p) => p.test(key)) && value.length >= _SecretMasker.MIN_SECRET_LENGTH) {
252
+ this.secrets.add(value);
253
+ }
254
+ }
255
+ }
256
+ mask(text) {
257
+ let masked = text;
258
+ for (const secret of this.secrets) {
259
+ masked = masked.replaceAll(secret, _SecretMasker.REDACTED);
260
+ }
261
+ return masked;
262
+ }
263
+ clear() {
264
+ this.secrets.clear();
265
+ }
266
+ get size() {
267
+ return this.secrets.size;
268
+ }
269
+ };
270
+
271
+ // src/utils/variable-resolver.ts
272
+ var VARIABLE_PATTERN = /\{\{(\w+(?:\.\w+)*)\}\}/g;
273
+ function resolveVariables(template, variables) {
274
+ return template.replace(VARIABLE_PATTERN, (match, path) => {
275
+ const value = getNestedValue(variables, path);
276
+ if (value === void 0) {
277
+ return match;
278
+ }
279
+ return String(value);
280
+ });
281
+ }
282
+ function resolveObjectVariables(obj, variables) {
283
+ if (typeof obj === "string") {
284
+ return resolveVariables(obj, variables);
285
+ }
286
+ if (Array.isArray(obj)) {
287
+ return obj.map((item) => resolveObjectVariables(item, variables));
288
+ }
289
+ if (obj !== null && typeof obj === "object") {
290
+ const result = {};
291
+ for (const [key, value] of Object.entries(obj)) {
292
+ result[key] = resolveObjectVariables(value, variables);
293
+ }
294
+ return result;
295
+ }
296
+ return obj;
297
+ }
298
+ function getNestedValue(obj, path) {
299
+ const parts = path.split(".");
300
+ let current = obj;
301
+ for (const part of parts) {
302
+ if (current === null || current === void 0 || typeof current !== "object") {
303
+ return void 0;
304
+ }
305
+ current = current[part];
306
+ }
307
+ return current;
308
+ }
309
+
310
+ // src/utils/jsonpath.ts
311
+ function queryJsonPath(data, path) {
312
+ if (!path.startsWith("$")) {
313
+ throw new Error(`Invalid JSONPath: must start with $. Got: ${path}`);
314
+ }
315
+ const remaining = path.slice(1);
316
+ if (remaining === "" || remaining === ".") {
317
+ return data;
318
+ }
319
+ const normalizedPath = remaining.startsWith(".") ? remaining.slice(1) : remaining;
320
+ const segments = parseSegments(normalizedPath);
321
+ let current = data;
322
+ for (const segment of segments) {
323
+ if (current === null || current === void 0) {
324
+ return void 0;
325
+ }
326
+ if (segment.type === "property") {
327
+ if (typeof current !== "object") {
328
+ return void 0;
329
+ }
330
+ current = current[segment.key];
331
+ } else if (segment.type === "index") {
332
+ if (!Array.isArray(current)) {
333
+ return void 0;
334
+ }
335
+ current = current[segment.index];
336
+ }
337
+ }
338
+ return current;
339
+ }
340
+ function parseSegments(path) {
341
+ const segments = [];
342
+ let i = 0;
343
+ while (i < path.length) {
344
+ if (path[i] === "[") {
345
+ const end = path.indexOf("]", i);
346
+ if (end === -1) {
347
+ throw new Error(`Invalid JSONPath: unclosed bracket at position ${i}`);
348
+ }
349
+ const indexStr = path.slice(i + 1, end);
350
+ const index = parseInt(indexStr, 10);
351
+ if (isNaN(index)) {
352
+ throw new Error(`Invalid JSONPath: non-numeric array index "${indexStr}"`);
353
+ }
354
+ segments.push({ type: "index", index });
355
+ i = end + 1;
356
+ if (i < path.length && path[i] === ".") {
357
+ i++;
358
+ }
359
+ } else {
360
+ let end = i;
361
+ while (end < path.length && path[end] !== "." && path[end] !== "[") {
362
+ end++;
363
+ }
364
+ const key = path.slice(i, end);
365
+ if (key.length > 0) {
366
+ segments.push({ type: "property", key });
367
+ }
368
+ i = end;
369
+ if (i < path.length && path[i] === ".") {
370
+ i++;
371
+ }
372
+ }
373
+ }
374
+ return segments;
375
+ }
376
+ function jsonPathExists(data, path) {
377
+ return queryJsonPath(data, path) !== void 0;
378
+ }
379
+
380
+ // src/utils/platform.ts
381
+ import { platform, arch, release, tmpdir, homedir } from "os";
382
+ import { join } from "path";
383
+ function getPlatformInfo() {
384
+ const os = platform();
385
+ return {
386
+ os,
387
+ arch: arch(),
388
+ release: release(),
389
+ nodeVersion: process.version,
390
+ isWindows: os === "win32",
391
+ isMacOS: os === "darwin",
392
+ isLinux: os === "linux",
393
+ tmpDir: tmpdir(),
394
+ homeDir: homedir(),
395
+ dataDir: getDataDir(os)
396
+ };
397
+ }
398
+ function getDataDir(os) {
399
+ if (os === "win32") {
400
+ return join(process.env["APPDATA"] ?? join(homedir(), "AppData", "Roaming"), "mcpspec");
401
+ }
402
+ if (os === "darwin") {
403
+ return join(homedir(), "Library", "Application Support", "mcpspec");
404
+ }
405
+ return join(process.env["XDG_DATA_HOME"] ?? join(homedir(), ".local", "share"), "mcpspec");
406
+ }
407
+
408
+ // src/process/process-manager.ts
409
+ import { execaCommand } from "execa";
410
+ import { randomUUID } from "crypto";
411
+
412
+ // src/process/process-registry.ts
413
+ var ProcessRegistry = class {
414
+ processes = /* @__PURE__ */ new Map();
415
+ register(process2) {
416
+ this.processes.set(process2.id, process2);
417
+ }
418
+ unregister(id) {
419
+ this.processes.delete(id);
420
+ }
421
+ get(id) {
422
+ return this.processes.get(id);
423
+ }
424
+ getAll() {
425
+ return Array.from(this.processes.values());
426
+ }
427
+ has(id) {
428
+ return this.processes.has(id);
429
+ }
430
+ get size() {
431
+ return this.processes.size;
432
+ }
433
+ clear() {
434
+ this.processes.clear();
435
+ }
436
+ };
437
+
438
+ // src/process/process-manager.ts
439
+ var ProcessManagerImpl = class {
440
+ registry;
441
+ defaultGracePeriod = 5e3;
442
+ constructor(registry) {
443
+ this.registry = registry ?? new ProcessRegistry();
444
+ }
445
+ async spawn(config) {
446
+ const id = randomUUID();
447
+ const fullCommand = [config.command, ...config.args].join(" ");
448
+ try {
449
+ const child = execaCommand(fullCommand, {
450
+ cwd: config.cwd,
451
+ env: { ...process.env, ...config.env },
452
+ stdin: "pipe",
453
+ stdout: "pipe",
454
+ stderr: "pipe",
455
+ buffer: false,
456
+ timeout: config.timeout
457
+ });
458
+ if (!child.pid || !child.stdin || !child.stdout || !child.stderr) {
459
+ throw new MCPSpecError("PROCESS_SPAWN_FAILED", `Failed to spawn process: ${fullCommand}`, {
460
+ command: fullCommand
461
+ });
462
+ }
463
+ const managed = {
464
+ id,
465
+ pid: child.pid,
466
+ command: config.command,
467
+ args: config.args,
468
+ startedAt: /* @__PURE__ */ new Date(),
469
+ stdin: child.stdin,
470
+ stdout: child.stdout,
471
+ stderr: child.stderr,
472
+ childProcess: child
473
+ };
474
+ this.registry.register(managed);
475
+ child.then(() => {
476
+ this.registry.unregister(id);
477
+ }).catch(() => {
478
+ this.registry.unregister(id);
479
+ });
480
+ return managed;
481
+ } catch (err) {
482
+ if (err instanceof MCPSpecError) throw err;
483
+ const message = err instanceof Error ? err.message : String(err);
484
+ throw new MCPSpecError("PROCESS_SPAWN_FAILED", `Failed to spawn process: ${message}`, {
485
+ command: fullCommand,
486
+ error: message
487
+ });
488
+ }
489
+ }
490
+ async shutdown(processId, gracePeriodMs) {
491
+ const managed = this.registry.get(processId);
492
+ if (!managed) return;
493
+ const grace = gracePeriodMs ?? this.defaultGracePeriod;
494
+ try {
495
+ managed.childProcess.kill("SIGTERM");
496
+ await Promise.race([
497
+ new Promise((resolve) => {
498
+ managed.childProcess.on("exit", () => resolve());
499
+ }),
500
+ new Promise(
501
+ (_, reject) => setTimeout(() => reject(new Error("Grace period exceeded")), grace)
502
+ )
503
+ ]);
504
+ } catch {
505
+ try {
506
+ managed.childProcess.kill("SIGKILL");
507
+ } catch {
508
+ }
509
+ } finally {
510
+ this.registry.unregister(processId);
511
+ }
512
+ }
513
+ async shutdownAll() {
514
+ const processes = this.registry.getAll();
515
+ await Promise.allSettled(processes.map((p) => this.shutdown(p.id)));
516
+ }
517
+ isAlive(processId) {
518
+ const managed = this.registry.get(processId);
519
+ if (!managed) return false;
520
+ try {
521
+ process.kill(managed.pid, 0);
522
+ return true;
523
+ } catch {
524
+ return false;
525
+ }
526
+ }
527
+ getRegistry() {
528
+ return this.registry;
529
+ }
530
+ };
531
+
532
+ // src/process/cleanup-handler.ts
533
+ var registered = false;
534
+ function registerCleanupHandlers(manager) {
535
+ if (registered) return;
536
+ registered = true;
537
+ const cleanup = async (signal) => {
538
+ process.stderr.write(`
539
+ Received ${signal}, cleaning up processes...
540
+ `);
541
+ await manager.shutdownAll();
542
+ process.exit(signal === "SIGINT" ? 130 : 0);
543
+ };
544
+ process.on("SIGINT", () => {
545
+ void cleanup("SIGINT");
546
+ });
547
+ process.on("SIGTERM", () => {
548
+ void cleanup("SIGTERM");
549
+ });
550
+ process.on("uncaughtException", (err) => {
551
+ process.stderr.write(`Uncaught exception: ${err.message}
552
+ `);
553
+ void manager.shutdownAll().finally(() => process.exit(1));
554
+ });
555
+ process.on("unhandledRejection", (reason) => {
556
+ const message = reason instanceof Error ? reason.message : String(reason);
557
+ process.stderr.write(`Unhandled rejection: ${message}
558
+ `);
559
+ });
560
+ process.on("exit", () => {
561
+ void manager.shutdownAll();
562
+ });
563
+ }
564
+
565
+ // src/client/mcp-client.ts
566
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
567
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
568
+
569
+ // src/client/connection-manager.ts
570
+ var VALID_TRANSITIONS = {
571
+ disconnected: ["connecting"],
572
+ connecting: ["connected", "error", "disconnected"],
573
+ connected: ["disconnecting", "reconnecting", "error"],
574
+ reconnecting: ["connected", "error", "disconnected"],
575
+ disconnecting: ["disconnected"],
576
+ error: ["connecting", "disconnected"]
577
+ };
578
+ var DEFAULT_CONNECTION_CONFIG = {
579
+ maxReconnectAttempts: 3,
580
+ reconnectBackoff: "exponential",
581
+ initialReconnectDelay: 1e3,
582
+ maxReconnectDelay: 3e4
583
+ };
584
+ var ConnectionManager = class {
585
+ state = "disconnected";
586
+ reconnectAttempts = 0;
587
+ config;
588
+ listeners = [];
589
+ constructor(config) {
590
+ this.config = { ...DEFAULT_CONNECTION_CONFIG, ...config };
591
+ }
592
+ getState() {
593
+ return this.state;
594
+ }
595
+ canTransition(to) {
596
+ const allowed = VALID_TRANSITIONS[this.state];
597
+ return allowed !== void 0 && allowed.includes(to);
598
+ }
599
+ transition(to) {
600
+ if (!this.canTransition(to)) {
601
+ throw new MCPSpecError(
602
+ "CONNECTION_LOST",
603
+ `Invalid state transition: ${this.state} -> ${to}`,
604
+ { from: this.state, to }
605
+ );
606
+ }
607
+ const from = this.state;
608
+ this.state = to;
609
+ if (to === "connected") {
610
+ this.reconnectAttempts = 0;
611
+ }
612
+ for (const listener of this.listeners) {
613
+ listener(from, to);
614
+ }
615
+ }
616
+ onTransition(listener) {
617
+ this.listeners.push(listener);
618
+ return () => {
619
+ this.listeners = this.listeners.filter((l) => l !== listener);
620
+ };
621
+ }
622
+ shouldReconnect() {
623
+ return this.reconnectAttempts < this.config.maxReconnectAttempts;
624
+ }
625
+ getReconnectDelay() {
626
+ const delay = this.config.initialReconnectDelay * Math.pow(2, this.reconnectAttempts);
627
+ this.reconnectAttempts++;
628
+ return Math.min(delay, this.config.maxReconnectDelay);
629
+ }
630
+ resetReconnectAttempts() {
631
+ this.reconnectAttempts = 0;
632
+ }
633
+ getConfig() {
634
+ return { ...this.config };
635
+ }
636
+ };
637
+
638
+ // src/rate-limiting/backoff.ts
639
+ var DEFAULT_BACKOFF = {
640
+ initial: 1e3,
641
+ multiplier: 2,
642
+ max: 3e4
643
+ };
644
+ function calculateBackoff(attempt, config = DEFAULT_BACKOFF) {
645
+ const delay = config.initial * Math.pow(config.multiplier, attempt);
646
+ return Math.min(delay, config.max);
647
+ }
648
+ function sleep(ms) {
649
+ return new Promise((resolve) => setTimeout(resolve, ms));
650
+ }
651
+
652
+ // src/client/logging-transport.ts
653
+ var LoggingTransport = class {
654
+ inner;
655
+ callback;
656
+ constructor(inner, callback) {
657
+ this.inner = inner;
658
+ this.callback = callback;
659
+ }
660
+ async start() {
661
+ return this.inner.start();
662
+ }
663
+ async send(message, options) {
664
+ this.callback("outgoing", message);
665
+ return this.inner.send(message, options);
666
+ }
667
+ async close() {
668
+ return this.inner.close();
669
+ }
670
+ get onclose() {
671
+ return this.inner.onclose;
672
+ }
673
+ set onclose(handler) {
674
+ this.inner.onclose = handler;
675
+ }
676
+ get onerror() {
677
+ return this.inner.onerror;
678
+ }
679
+ set onerror(handler) {
680
+ this.inner.onerror = handler;
681
+ }
682
+ get onmessage() {
683
+ return this.inner.onmessage;
684
+ }
685
+ set onmessage(handler) {
686
+ if (!handler) {
687
+ this.inner.onmessage = void 0;
688
+ return;
689
+ }
690
+ const cb = this.callback;
691
+ this.inner.onmessage = (message, extra) => {
692
+ cb("incoming", message);
693
+ handler(message, extra);
694
+ };
695
+ }
696
+ get sessionId() {
697
+ return this.inner.sessionId;
698
+ }
699
+ set sessionId(value) {
700
+ this.inner.sessionId = value;
701
+ }
702
+ get setProtocolVersion() {
703
+ return this.inner.setProtocolVersion;
704
+ }
705
+ set setProtocolVersion(handler) {
706
+ this.inner.setProtocolVersion = handler;
707
+ }
708
+ };
709
+
710
+ // src/client/mcp-client.ts
711
+ var MCPClient = class {
712
+ client = null;
713
+ transport = null;
714
+ connectionManager;
715
+ processManager;
716
+ serverConfig;
717
+ serverInfo;
718
+ onProtocolMessage;
719
+ constructor(options) {
720
+ this.connectionManager = new ConnectionManager();
721
+ this.processManager = options.processManager ?? new ProcessManagerImpl();
722
+ this.serverConfig = this.normalizeConfig(options.serverConfig);
723
+ this.onProtocolMessage = options.onProtocolMessage;
724
+ }
725
+ normalizeConfig(config) {
726
+ if (typeof config === "string") {
727
+ const parts = config.split(/\s+/);
728
+ const command = parts[0];
729
+ const args = parts.slice(1);
730
+ if (!command) {
731
+ throw new MCPSpecError("CONFIG_ERROR", "Empty server command", { config });
732
+ }
733
+ return {
734
+ transport: "stdio",
735
+ command,
736
+ args
737
+ };
738
+ }
739
+ return config;
740
+ }
741
+ async connect() {
742
+ if (this.connectionManager.getState() === "connected") return;
743
+ this.connectionManager.transition("connecting");
744
+ try {
745
+ let transport = await this.createTransport();
746
+ if (this.onProtocolMessage) {
747
+ transport = new LoggingTransport(transport, this.onProtocolMessage);
748
+ }
749
+ this.transport = transport;
750
+ this.client = new Client(
751
+ { name: "mcpspec", version: "0.2.0" },
752
+ { capabilities: {} }
753
+ );
754
+ await this.client.connect(this.transport);
755
+ this.serverInfo = this.client.getServerVersion();
756
+ this.connectionManager.transition("connected");
757
+ } catch (err) {
758
+ this.connectionManager.transition("error");
759
+ if (this.connectionManager.shouldReconnect()) {
760
+ return this.reconnect();
761
+ }
762
+ if (err instanceof MCPSpecError) throw err;
763
+ const message = err instanceof Error ? err.message : String(err);
764
+ throw new MCPSpecError("CONNECTION_TIMEOUT", `Failed to connect to MCP server: ${message}`, {
765
+ command: this.serverConfig.command,
766
+ url: this.serverConfig.url,
767
+ error: message
768
+ });
769
+ }
770
+ }
771
+ async reconnect() {
772
+ this.connectionManager.transition("connecting");
773
+ const delay = this.connectionManager.getReconnectDelay();
774
+ await sleep(delay);
775
+ return this.connect();
776
+ }
777
+ async createTransport() {
778
+ const transport = this.serverConfig.transport ?? "stdio";
779
+ switch (transport) {
780
+ case "stdio": {
781
+ const { command, args } = this.serverConfig;
782
+ if (!command) {
783
+ throw new MCPSpecError("CONFIG_ERROR", "Server command is required for stdio transport", {
784
+ config: this.serverConfig
785
+ });
786
+ }
787
+ return new StdioClientTransport({
788
+ command,
789
+ args: args ?? [],
790
+ env: this.serverConfig.env
791
+ });
792
+ }
793
+ case "sse": {
794
+ const { url } = this.serverConfig;
795
+ if (!url) {
796
+ throw new MCPSpecError("CONFIG_ERROR", "Server URL is required for SSE transport", {
797
+ config: this.serverConfig
798
+ });
799
+ }
800
+ const { createSSETransport } = await import("./sse-NXEF5JDZ.js");
801
+ return createSSETransport(url);
802
+ }
803
+ case "streamable-http": {
804
+ const { url } = this.serverConfig;
805
+ if (!url) {
806
+ throw new MCPSpecError("CONFIG_ERROR", "Server URL is required for streamable-http transport", {
807
+ config: this.serverConfig
808
+ });
809
+ }
810
+ const { createStreamableHTTPTransport } = await import("./http-XUWKDMSR.js");
811
+ return createStreamableHTTPTransport(url);
812
+ }
813
+ default:
814
+ throw new MCPSpecError("CONFIG_ERROR", `Unknown transport type: ${transport}`, {
815
+ transport
816
+ });
817
+ }
818
+ }
819
+ async disconnect() {
820
+ if (this.connectionManager.getState() === "disconnected") return;
821
+ if (this.connectionManager.canTransition("disconnecting")) {
822
+ this.connectionManager.transition("disconnecting");
823
+ }
824
+ try {
825
+ if (this.transport) {
826
+ await this.transport.close();
827
+ }
828
+ } catch {
829
+ } finally {
830
+ this.client = null;
831
+ this.transport = null;
832
+ if (this.connectionManager.canTransition("disconnected")) {
833
+ this.connectionManager.transition("disconnected");
834
+ }
835
+ }
836
+ }
837
+ isConnected() {
838
+ return this.connectionManager.getState() === "connected";
839
+ }
840
+ async listTools() {
841
+ this.ensureConnected();
842
+ try {
843
+ const result = await this.client.listTools();
844
+ return (result.tools ?? []).map((t) => ({
845
+ name: t.name,
846
+ description: t.description,
847
+ inputSchema: t.inputSchema
848
+ }));
849
+ } catch (err) {
850
+ const message = err instanceof Error ? err.message : String(err);
851
+ throw new MCPSpecError("TOOL_CALL_FAILED", `Failed to list tools: ${message}`, {
852
+ error: message
853
+ });
854
+ }
855
+ }
856
+ async listResources() {
857
+ this.ensureConnected();
858
+ try {
859
+ const result = await this.client.listResources();
860
+ return (result.resources ?? []).map((r) => ({
861
+ uri: r.uri,
862
+ name: r.name,
863
+ description: r.description,
864
+ mimeType: r.mimeType
865
+ }));
866
+ } catch (err) {
867
+ const message = err instanceof Error ? err.message : String(err);
868
+ throw new MCPSpecError("TOOL_CALL_FAILED", `Failed to list resources: ${message}`, {
869
+ error: message
870
+ });
871
+ }
872
+ }
873
+ async callTool(name, args) {
874
+ this.ensureConnected();
875
+ try {
876
+ const result = await this.client.callTool({ name, arguments: args });
877
+ return {
878
+ content: result.content,
879
+ isError: result.isError === true ? true : void 0
880
+ };
881
+ } catch (err) {
882
+ const message = err instanceof Error ? err.message : String(err);
883
+ throw new MCPSpecError("TOOL_CALL_FAILED", `Tool call "${name}" failed: ${message}`, {
884
+ toolName: name,
885
+ error: message
886
+ });
887
+ }
888
+ }
889
+ async readResource(uri) {
890
+ this.ensureConnected();
891
+ try {
892
+ const result = await this.client.readResource({ uri });
893
+ return { contents: result.contents };
894
+ } catch (err) {
895
+ const message = err instanceof Error ? err.message : String(err);
896
+ throw new MCPSpecError("TOOL_CALL_FAILED", `Failed to read resource "${uri}": ${message}`, {
897
+ uri,
898
+ error: message
899
+ });
900
+ }
901
+ }
902
+ getServerInfo() {
903
+ return this.serverInfo;
904
+ }
905
+ getConnectionState() {
906
+ return this.connectionManager.getState();
907
+ }
908
+ ensureConnected() {
909
+ if (!this.isConnected() || !this.client) {
910
+ throw new MCPSpecError("CONNECTION_LOST", "Not connected to MCP server. Call connect() first.", {});
911
+ }
912
+ }
913
+ };
914
+
915
+ // src/testing/test-executor.ts
916
+ import { DEFAULT_TIMEOUTS } from "@mcpspec/shared";
917
+
918
+ // src/testing/assertions/schema-assertion.ts
919
+ function assertSchema(response, inputSchema) {
920
+ if (response === void 0 || response === null) {
921
+ return {
922
+ type: "schema",
923
+ passed: false,
924
+ message: "Response is null or undefined",
925
+ actual: typeof response
926
+ };
927
+ }
928
+ if (typeof response !== "object") {
929
+ return {
930
+ type: "schema",
931
+ passed: false,
932
+ message: `Response is not an object or array, got ${typeof response}`,
933
+ actual: typeof response
934
+ };
935
+ }
936
+ if (inputSchema && typeof inputSchema["properties"] === "object" && inputSchema["properties"] !== null) {
937
+ const properties = inputSchema["properties"];
938
+ const required = Array.isArray(inputSchema["required"]) ? inputSchema["required"] : [];
939
+ const missingFields = [];
940
+ for (const field of required) {
941
+ if (!jsonPathExists(response, `$.${field}`)) {
942
+ missingFields.push(field);
943
+ }
944
+ }
945
+ if (missingFields.length > 0) {
946
+ return {
947
+ type: "schema",
948
+ passed: false,
949
+ message: `Missing required fields: ${missingFields.join(", ")}`,
950
+ expected: Object.keys(properties),
951
+ actual: Object.keys(response)
952
+ };
953
+ }
954
+ }
955
+ return {
956
+ type: "schema",
957
+ passed: true,
958
+ message: "Response has valid structure",
959
+ actual: typeof response
960
+ };
961
+ }
962
+
963
+ // src/testing/assertions/equals-assertion.ts
964
+ function assertEqual(response, path, expected) {
965
+ const actual = queryJsonPath(response, path);
966
+ const passed = JSON.stringify(actual) === JSON.stringify(expected);
967
+ return {
968
+ type: "equals",
969
+ passed,
970
+ message: passed ? `${path} equals expected value` : `${path} expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
971
+ expected,
972
+ actual
973
+ };
974
+ }
975
+
976
+ // src/testing/assertions/contains-assertion.ts
977
+ function assertContains(response, path, value) {
978
+ const actual = queryJsonPath(response, path);
979
+ let passed = false;
980
+ if (Array.isArray(actual)) {
981
+ passed = actual.some((item) => JSON.stringify(item) === JSON.stringify(value));
982
+ } else if (typeof actual === "string" && typeof value === "string") {
983
+ passed = actual.includes(value);
984
+ }
985
+ return {
986
+ type: "contains",
987
+ passed,
988
+ message: passed ? `${path} contains ${JSON.stringify(value)}` : `${path} does not contain ${JSON.stringify(value)}`,
989
+ expected: value,
990
+ actual
991
+ };
992
+ }
993
+
994
+ // src/testing/assertions/exists-assertion.ts
995
+ function assertExists(response, path) {
996
+ const passed = jsonPathExists(response, path);
997
+ return {
998
+ type: "exists",
999
+ passed,
1000
+ message: passed ? `${path} exists` : `${path} does not exist`
1001
+ };
1002
+ }
1003
+
1004
+ // src/testing/assertions/regex-assertion.ts
1005
+ function assertMatches(response, path, pattern) {
1006
+ const actual = queryJsonPath(response, path);
1007
+ const actualStr = typeof actual === "string" ? actual : JSON.stringify(actual);
1008
+ let passed = false;
1009
+ try {
1010
+ const regex = new RegExp(pattern);
1011
+ passed = regex.test(actualStr ?? "");
1012
+ } catch {
1013
+ return {
1014
+ type: "matches",
1015
+ passed: false,
1016
+ message: `Invalid regex pattern: ${pattern}`,
1017
+ expected: pattern,
1018
+ actual: actualStr
1019
+ };
1020
+ }
1021
+ return {
1022
+ type: "matches",
1023
+ passed,
1024
+ message: passed ? `${path} matches pattern /${pattern}/` : `${path} does not match pattern /${pattern}/`,
1025
+ expected: pattern,
1026
+ actual: actualStr
1027
+ };
1028
+ }
1029
+
1030
+ // src/testing/assertions/type-assertion.ts
1031
+ function assertType(response, path, expected) {
1032
+ const value = queryJsonPath(response, path);
1033
+ let actualType;
1034
+ if (value === null) {
1035
+ actualType = "null";
1036
+ } else if (Array.isArray(value)) {
1037
+ actualType = "array";
1038
+ } else {
1039
+ actualType = typeof value;
1040
+ }
1041
+ if (value === void 0) {
1042
+ return {
1043
+ type: "type",
1044
+ passed: false,
1045
+ message: `Path "${path}" does not exist`,
1046
+ expected,
1047
+ actual: "undefined"
1048
+ };
1049
+ }
1050
+ const passed = actualType === expected;
1051
+ return {
1052
+ type: "type",
1053
+ passed,
1054
+ message: passed ? `Value at "${path}" is type "${expected}"` : `Expected type "${expected}" at "${path}", got "${actualType}"`,
1055
+ expected,
1056
+ actual: actualType
1057
+ };
1058
+ }
1059
+
1060
+ // src/testing/assertions/expression-assertion.ts
1061
+ import { Parser } from "expr-eval";
1062
+ function assertExpression(response, expr) {
1063
+ try {
1064
+ const parser = new Parser();
1065
+ const result = parser.evaluate(expr, { response });
1066
+ const passed = Boolean(result);
1067
+ return {
1068
+ type: "expression",
1069
+ passed,
1070
+ message: passed ? `Expression "${expr}" evaluated to true` : `Expression "${expr}" evaluated to false`,
1071
+ expected: true,
1072
+ actual: result
1073
+ };
1074
+ } catch (err) {
1075
+ const message = err instanceof Error ? err.message : String(err);
1076
+ return {
1077
+ type: "expression",
1078
+ passed: false,
1079
+ message: `Expression evaluation error: ${message}`,
1080
+ expected: true,
1081
+ actual: message
1082
+ };
1083
+ }
1084
+ }
1085
+
1086
+ // src/testing/assertions/binary-assertion.ts
1087
+ function assertBinary(response, expected) {
1088
+ let actual;
1089
+ if (response && typeof response === "object") {
1090
+ const obj = response;
1091
+ if (typeof obj["mimeType"] === "string") {
1092
+ actual = obj["mimeType"];
1093
+ } else if (Array.isArray(obj["content"])) {
1094
+ for (const item of obj["content"]) {
1095
+ if (item && typeof item === "object" && typeof item["mimeType"] === "string") {
1096
+ actual = item["mimeType"];
1097
+ break;
1098
+ }
1099
+ }
1100
+ }
1101
+ }
1102
+ if (actual === void 0) {
1103
+ return {
1104
+ type: "mimeType",
1105
+ passed: false,
1106
+ message: "No mimeType found in response",
1107
+ expected,
1108
+ actual: "undefined"
1109
+ };
1110
+ }
1111
+ const passed = actual === expected;
1112
+ return {
1113
+ type: "mimeType",
1114
+ passed,
1115
+ message: passed ? `MIME type matches "${expected}"` : `Expected MIME type "${expected}", got "${actual}"`,
1116
+ expected,
1117
+ actual
1118
+ };
1119
+ }
1120
+
1121
+ // src/testing/test-executor.ts
1122
+ var TestExecutor = class {
1123
+ variables = {};
1124
+ rateLimiter;
1125
+ constructor(initialVariables, rateLimiter) {
1126
+ if (initialVariables) {
1127
+ this.variables = { ...initialVariables };
1128
+ }
1129
+ this.rateLimiter = rateLimiter;
1130
+ }
1131
+ async execute(test, client) {
1132
+ const timeout = test.timeout ?? DEFAULT_TIMEOUTS.test;
1133
+ const retries = test.retries ?? 0;
1134
+ let lastError;
1135
+ for (let attempt = 0; attempt <= retries; attempt++) {
1136
+ if (attempt > 0) {
1137
+ const delay = calculateBackoff(attempt - 1);
1138
+ await sleep(delay);
1139
+ }
1140
+ try {
1141
+ return await this.executeWithTimeout(test, client, timeout);
1142
+ } catch (err) {
1143
+ lastError = err instanceof Error ? err : new Error(String(err));
1144
+ if (attempt < retries) {
1145
+ continue;
1146
+ }
1147
+ }
1148
+ }
1149
+ return {
1150
+ testId: test.id ?? test.name,
1151
+ testName: test.name,
1152
+ status: "error",
1153
+ duration: 0,
1154
+ assertions: [],
1155
+ error: lastError?.message ?? "Unknown error"
1156
+ };
1157
+ }
1158
+ executeWithTimeout(test, client, timeoutMs) {
1159
+ return Promise.race([
1160
+ this.executeInternal(test, client),
1161
+ new Promise(
1162
+ (_, reject) => setTimeout(() => reject(new MCPSpecError("TIMEOUT", `Test "${test.name}" timed out after ${timeoutMs}ms`, {
1163
+ testName: test.name,
1164
+ timeout: timeoutMs
1165
+ })), timeoutMs)
1166
+ )
1167
+ ]);
1168
+ }
1169
+ async executeInternal(test, client) {
1170
+ const startTime = Date.now();
1171
+ const testId = test.id ?? test.name;
1172
+ const assertionResults = [];
1173
+ try {
1174
+ const toolName = test.call ?? test.tool;
1175
+ if (!toolName) {
1176
+ throw new MCPSpecError("CONFIG_ERROR", `Test "${test.name}" has no tool/call defined`, {
1177
+ testName: test.name
1178
+ });
1179
+ }
1180
+ const input = test.with ?? test.input ?? {};
1181
+ const resolvedInput = resolveObjectVariables(input, this.variables);
1182
+ let result;
1183
+ try {
1184
+ const callFn = () => client.callTool(toolName, resolvedInput);
1185
+ result = this.rateLimiter ? await this.rateLimiter.schedule(callFn) : await callFn();
1186
+ } catch (err) {
1187
+ if (test.expectError) {
1188
+ return {
1189
+ testId,
1190
+ testName: test.name,
1191
+ status: "passed",
1192
+ duration: Date.now() - startTime,
1193
+ assertions: [
1194
+ {
1195
+ type: "schema",
1196
+ passed: true,
1197
+ message: "Expected error occurred"
1198
+ }
1199
+ ]
1200
+ };
1201
+ }
1202
+ throw err;
1203
+ }
1204
+ if (test.expectError) {
1205
+ if (result.isError) {
1206
+ return {
1207
+ testId,
1208
+ testName: test.name,
1209
+ status: "passed",
1210
+ duration: Date.now() - startTime,
1211
+ assertions: [
1212
+ {
1213
+ type: "schema",
1214
+ passed: true,
1215
+ message: "Expected error response received"
1216
+ }
1217
+ ]
1218
+ };
1219
+ }
1220
+ assertionResults.push({
1221
+ type: "schema",
1222
+ passed: false,
1223
+ message: "Expected error but got success"
1224
+ });
1225
+ return {
1226
+ testId,
1227
+ testName: test.name,
1228
+ status: "failed",
1229
+ duration: Date.now() - startTime,
1230
+ assertions: assertionResults
1231
+ };
1232
+ }
1233
+ const response = this.buildResponse(result);
1234
+ if (test.assertions) {
1235
+ for (const assertion of test.assertions) {
1236
+ assertionResults.push(this.runAssertion(assertion, response, Date.now() - startTime));
1237
+ }
1238
+ }
1239
+ if (test.expect) {
1240
+ for (const expectation of test.expect) {
1241
+ assertionResults.push(this.runSimpleExpectation(expectation, response));
1242
+ }
1243
+ }
1244
+ if (!test.assertions && !test.expect) {
1245
+ assertionResults.push(assertSchema(response));
1246
+ }
1247
+ const extractedVariables = {};
1248
+ if (test.extract) {
1249
+ for (const extraction of test.extract) {
1250
+ const value = queryJsonPath(response, extraction.path);
1251
+ extractedVariables[extraction.name] = value;
1252
+ this.variables[extraction.name] = value;
1253
+ }
1254
+ }
1255
+ const allPassed = assertionResults.every((r) => r.passed);
1256
+ return {
1257
+ testId,
1258
+ testName: test.name,
1259
+ status: allPassed ? "passed" : "failed",
1260
+ duration: Date.now() - startTime,
1261
+ assertions: assertionResults,
1262
+ extractedVariables: Object.keys(extractedVariables).length > 0 ? extractedVariables : void 0
1263
+ };
1264
+ } catch (err) {
1265
+ const message = err instanceof Error ? err.message : String(err);
1266
+ return {
1267
+ testId,
1268
+ testName: test.name,
1269
+ status: "error",
1270
+ duration: Date.now() - startTime,
1271
+ assertions: assertionResults,
1272
+ error: message
1273
+ };
1274
+ }
1275
+ }
1276
+ buildResponse(result) {
1277
+ const contents = result.content;
1278
+ if (!Array.isArray(contents) || contents.length === 0) {
1279
+ return {};
1280
+ }
1281
+ if (contents.length === 1) {
1282
+ const item = contents[0];
1283
+ if (item["type"] === "text" && typeof item["text"] === "string") {
1284
+ try {
1285
+ return JSON.parse(item["text"]);
1286
+ } catch {
1287
+ return { content: item["text"], text: item["text"] };
1288
+ }
1289
+ }
1290
+ return item;
1291
+ }
1292
+ return { content: contents };
1293
+ }
1294
+ runAssertion(assertion, response, durationMs) {
1295
+ switch (assertion.type) {
1296
+ case "schema":
1297
+ return assertSchema(response);
1298
+ case "equals":
1299
+ return assertEqual(response, assertion.path ?? "$", assertion.value);
1300
+ case "contains":
1301
+ return assertContains(response, assertion.path ?? "$", assertion.value);
1302
+ case "exists":
1303
+ return assertExists(response, assertion.path ?? "$");
1304
+ case "matches":
1305
+ return assertMatches(response, assertion.path ?? "$", assertion.pattern ?? "");
1306
+ case "type":
1307
+ return assertType(response, assertion.path ?? "$", assertion.expected ?? "object");
1308
+ case "expression":
1309
+ return assertExpression(response, assertion.expr ?? "");
1310
+ case "mimeType":
1311
+ return assertBinary(response, assertion.expected ?? "");
1312
+ case "length": {
1313
+ const value = queryJsonPath(response, assertion.path ?? "$");
1314
+ const len = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : -1;
1315
+ if (len === -1) {
1316
+ return {
1317
+ type: "length",
1318
+ passed: false,
1319
+ message: `Value at "${assertion.path}" is not an array or string`,
1320
+ actual: typeof value
1321
+ };
1322
+ }
1323
+ const op = assertion.operator ?? "eq";
1324
+ const target = typeof assertion.value === "number" ? assertion.value : Number(assertion.value);
1325
+ let passed = false;
1326
+ switch (op) {
1327
+ case "eq":
1328
+ passed = len === target;
1329
+ break;
1330
+ case "gt":
1331
+ passed = len > target;
1332
+ break;
1333
+ case "gte":
1334
+ passed = len >= target;
1335
+ break;
1336
+ case "lt":
1337
+ passed = len < target;
1338
+ break;
1339
+ case "lte":
1340
+ passed = len <= target;
1341
+ break;
1342
+ default:
1343
+ passed = len === target;
1344
+ }
1345
+ return {
1346
+ type: "length",
1347
+ passed,
1348
+ message: passed ? `Length ${len} satisfies ${op} ${target}` : `Length ${len} does not satisfy ${op} ${target}`,
1349
+ expected: target,
1350
+ actual: len
1351
+ };
1352
+ }
1353
+ case "latency":
1354
+ return {
1355
+ type: "latency",
1356
+ passed: durationMs <= (assertion.maxMs ?? 1e3),
1357
+ message: durationMs <= (assertion.maxMs ?? 1e3) ? `Response time ${durationMs}ms within ${assertion.maxMs ?? 1e3}ms limit` : `Response time ${durationMs}ms exceeds ${assertion.maxMs ?? 1e3}ms limit`,
1358
+ expected: assertion.maxMs,
1359
+ actual: durationMs
1360
+ };
1361
+ default:
1362
+ return {
1363
+ type: assertion.type,
1364
+ passed: false,
1365
+ message: `Assertion type "${assertion.type}" not yet implemented`
1366
+ };
1367
+ }
1368
+ }
1369
+ runSimpleExpectation(expectation, response) {
1370
+ if ("exists" in expectation) {
1371
+ return assertExists(response, expectation.exists);
1372
+ }
1373
+ if ("equals" in expectation) {
1374
+ const [path, value] = expectation.equals;
1375
+ return assertEqual(response, path, value);
1376
+ }
1377
+ if ("contains" in expectation) {
1378
+ const [path, value] = expectation.contains;
1379
+ return assertContains(response, path, value);
1380
+ }
1381
+ if ("matches" in expectation) {
1382
+ const [path, pattern] = expectation.matches;
1383
+ return assertMatches(response, path, pattern);
1384
+ }
1385
+ return {
1386
+ type: "schema",
1387
+ passed: false,
1388
+ message: "Unknown expectation type"
1389
+ };
1390
+ }
1391
+ getVariables() {
1392
+ return { ...this.variables };
1393
+ }
1394
+ };
1395
+
1396
+ // src/testing/test-scheduler.ts
1397
+ function normalizeTags(tags) {
1398
+ return tags.map((t) => t.startsWith("@") ? t.slice(1) : t);
1399
+ }
1400
+ function matchesTags(test, filterTags) {
1401
+ if (filterTags.length === 0) return true;
1402
+ if (!test.tags || test.tags.length === 0) return false;
1403
+ const normalized = normalizeTags(test.tags);
1404
+ const normalizedFilter = normalizeTags(filterTags);
1405
+ return normalizedFilter.some((ft) => normalized.includes(ft));
1406
+ }
1407
+ var TestScheduler = class {
1408
+ async schedule(tests, client, options) {
1409
+ const { parallelism, tags, reporter, rateLimiter, initialVariables } = options;
1410
+ const filteredTests = tags && tags.length > 0 ? tests.filter((t) => matchesTags(t, tags)) : tests;
1411
+ const skippedTests = tags && tags.length > 0 ? tests.filter((t) => !matchesTags(t, tags)) : [];
1412
+ const skippedResults = skippedTests.map((t) => ({
1413
+ testId: t.id ?? t.name,
1414
+ testName: t.name,
1415
+ status: "skipped",
1416
+ duration: 0,
1417
+ assertions: []
1418
+ }));
1419
+ if (filteredTests.length === 0) {
1420
+ return skippedResults;
1421
+ }
1422
+ if (parallelism <= 1) {
1423
+ const executor2 = new TestExecutor(initialVariables, rateLimiter);
1424
+ const results2 = [];
1425
+ for (const test of filteredTests) {
1426
+ reporter?.onTestStart(test.name);
1427
+ const result = await executor2.execute(test, client);
1428
+ results2.push(result);
1429
+ reporter?.onTestComplete(result);
1430
+ }
1431
+ return [...results2, ...skippedResults];
1432
+ }
1433
+ const executor = new TestExecutor(initialVariables, rateLimiter);
1434
+ let running = 0;
1435
+ const results = new Array(filteredTests.length);
1436
+ const waitQueue = [];
1437
+ function acquire() {
1438
+ if (running < parallelism) {
1439
+ running++;
1440
+ return Promise.resolve();
1441
+ }
1442
+ return new Promise((resolve) => {
1443
+ waitQueue.push(resolve);
1444
+ });
1445
+ }
1446
+ function release2() {
1447
+ running--;
1448
+ const next = waitQueue.shift();
1449
+ if (next) {
1450
+ running++;
1451
+ next();
1452
+ }
1453
+ }
1454
+ const tasks = filteredTests.map((test, i) => {
1455
+ return (async () => {
1456
+ await acquire();
1457
+ try {
1458
+ reporter?.onTestStart(test.name);
1459
+ const result = await executor.execute(test, client);
1460
+ results[i] = result;
1461
+ reporter?.onTestComplete(result);
1462
+ } finally {
1463
+ release2();
1464
+ }
1465
+ })();
1466
+ });
1467
+ await Promise.allSettled(tasks);
1468
+ for (let i = 0; i < results.length; i++) {
1469
+ if (!results[i]) {
1470
+ results[i] = {
1471
+ testId: filteredTests[i].id ?? filteredTests[i].name,
1472
+ testName: filteredTests[i].name,
1473
+ status: "error",
1474
+ duration: 0,
1475
+ assertions: [],
1476
+ error: "Test execution failed unexpectedly"
1477
+ };
1478
+ }
1479
+ }
1480
+ return [...results, ...skippedResults];
1481
+ }
1482
+ };
1483
+
1484
+ // src/rate-limiting/rate-limiter.ts
1485
+ import Bottleneck from "bottleneck";
1486
+ import { DEFAULT_RATE_LIMIT } from "@mcpspec/shared";
1487
+ var RateLimiter = class {
1488
+ limiter;
1489
+ config;
1490
+ constructor(config) {
1491
+ this.config = { ...DEFAULT_RATE_LIMIT, ...config };
1492
+ this.limiter = new Bottleneck({
1493
+ maxConcurrent: this.config.maxConcurrent,
1494
+ minTime: Math.ceil(1e3 / this.config.maxCallsPerSecond)
1495
+ });
1496
+ }
1497
+ async schedule(fn) {
1498
+ return this.limiter.schedule(fn);
1499
+ }
1500
+ getConfig() {
1501
+ return { ...this.config };
1502
+ }
1503
+ async stop() {
1504
+ await this.limiter.stop();
1505
+ }
1506
+ };
1507
+
1508
+ // src/testing/test-runner.ts
1509
+ import { randomUUID as randomUUID2 } from "crypto";
1510
+ var TestRunner = class {
1511
+ processManager;
1512
+ constructor() {
1513
+ this.processManager = new ProcessManagerImpl();
1514
+ registerCleanupHandlers(this.processManager);
1515
+ }
1516
+ async run(collection, options) {
1517
+ const runId = randomUUID2();
1518
+ const startedAt = /* @__PURE__ */ new Date();
1519
+ const reporter = options?.reporter;
1520
+ const parallelism = options?.parallelism ?? 1;
1521
+ const tags = options?.tags;
1522
+ const secretMasker = new SecretMasker();
1523
+ const serverConfig = this.resolveServerConfig(collection.server);
1524
+ if (serverConfig.env) {
1525
+ secretMasker.registerFromEnv(serverConfig.env);
1526
+ }
1527
+ if (reporter && typeof reporter.setSecretMasker === "function") {
1528
+ reporter.setSecretMasker(secretMasker);
1529
+ }
1530
+ reporter?.onRunStart(collection.name, collection.tests.length);
1531
+ let envVariables = {};
1532
+ if (options?.environment && collection.environments) {
1533
+ const env = collection.environments[options.environment];
1534
+ if (!env) {
1535
+ throw new MCPSpecError("CONFIG_ERROR", `Environment "${options.environment}" not found`, {
1536
+ available: Object.keys(collection.environments)
1537
+ });
1538
+ }
1539
+ envVariables = env.variables;
1540
+ } else if (collection.defaultEnvironment && collection.environments) {
1541
+ const env = collection.environments[collection.defaultEnvironment];
1542
+ if (env) {
1543
+ envVariables = env.variables;
1544
+ }
1545
+ }
1546
+ const client = new MCPClient({
1547
+ serverConfig,
1548
+ processManager: this.processManager
1549
+ });
1550
+ let results;
1551
+ try {
1552
+ await client.connect();
1553
+ const rateLimiter = options?.rateLimitConfig ? new RateLimiter(options.rateLimitConfig) : void 0;
1554
+ const scheduler = new TestScheduler();
1555
+ results = await scheduler.schedule(collection.tests, client, {
1556
+ parallelism,
1557
+ tags,
1558
+ reporter,
1559
+ rateLimiter,
1560
+ initialVariables: envVariables
1561
+ });
1562
+ if (rateLimiter) {
1563
+ await rateLimiter.stop();
1564
+ }
1565
+ } finally {
1566
+ await client.disconnect();
1567
+ }
1568
+ const completedAt = /* @__PURE__ */ new Date();
1569
+ const summary = this.computeSummary(results, completedAt.getTime() - startedAt.getTime());
1570
+ const runResult = {
1571
+ id: runId,
1572
+ collectionName: collection.name,
1573
+ startedAt,
1574
+ completedAt,
1575
+ duration: completedAt.getTime() - startedAt.getTime(),
1576
+ results,
1577
+ summary
1578
+ };
1579
+ reporter?.onRunComplete(runResult);
1580
+ return runResult;
1581
+ }
1582
+ resolveServerConfig(server) {
1583
+ if (typeof server === "string") {
1584
+ const parts = server.split(/\s+/);
1585
+ const command = parts[0];
1586
+ if (!command) {
1587
+ throw new MCPSpecError("CONFIG_ERROR", "Empty server command", {});
1588
+ }
1589
+ return {
1590
+ transport: "stdio",
1591
+ command,
1592
+ args: parts.slice(1)
1593
+ };
1594
+ }
1595
+ return server;
1596
+ }
1597
+ computeSummary(results, duration) {
1598
+ return {
1599
+ total: results.length,
1600
+ passed: results.filter((r) => r.status === "passed").length,
1601
+ failed: results.filter((r) => r.status === "failed").length,
1602
+ skipped: results.filter((r) => r.status === "skipped").length,
1603
+ errors: results.filter((r) => r.status === "error").length,
1604
+ duration
1605
+ };
1606
+ }
1607
+ async cleanup() {
1608
+ await this.processManager.shutdownAll();
1609
+ }
1610
+ };
1611
+
1612
+ // src/testing/reporters/console-reporter.ts
1613
+ var COLORS = {
1614
+ reset: "\x1B[0m",
1615
+ green: "\x1B[32m",
1616
+ red: "\x1B[31m",
1617
+ yellow: "\x1B[33m",
1618
+ gray: "\x1B[90m",
1619
+ bold: "\x1B[1m",
1620
+ cyan: "\x1B[36m"
1621
+ };
1622
+ var ICONS = {
1623
+ pass: `${COLORS.green}\u2713${COLORS.reset}`,
1624
+ fail: `${COLORS.red}\u2717${COLORS.reset}`,
1625
+ error: `${COLORS.red}!${COLORS.reset}`,
1626
+ skip: `${COLORS.yellow}-${COLORS.reset}`
1627
+ };
1628
+ var ConsoleReporter = class {
1629
+ ci;
1630
+ constructor(options) {
1631
+ this.ci = options?.ci ?? false;
1632
+ }
1633
+ onRunStart(collectionName, testCount) {
1634
+ if (!this.ci) {
1635
+ console.log(
1636
+ `
1637
+ ${COLORS.bold}${COLORS.cyan}MCPSpec${COLORS.reset} running ${COLORS.bold}${collectionName}${COLORS.reset} (${testCount} tests)
1638
+ `
1639
+ );
1640
+ }
1641
+ }
1642
+ onTestStart(_testName) {
1643
+ }
1644
+ onTestComplete(result) {
1645
+ const icon = this.getIcon(result.status);
1646
+ const duration = `${COLORS.gray}(${result.duration}ms)${COLORS.reset}`;
1647
+ console.log(` ${icon} ${result.testName} ${duration}`);
1648
+ if (result.status === "failed") {
1649
+ for (const assertion of result.assertions) {
1650
+ if (!assertion.passed) {
1651
+ console.log(` ${COLORS.red}${assertion.message}${COLORS.reset}`);
1652
+ }
1653
+ }
1654
+ }
1655
+ if (result.status === "error" && result.error) {
1656
+ console.log(` ${COLORS.red}${result.error}${COLORS.reset}`);
1657
+ }
1658
+ }
1659
+ onRunComplete(result) {
1660
+ const { summary } = result;
1661
+ console.log("");
1662
+ const parts = [];
1663
+ if (summary.passed > 0) parts.push(`${COLORS.green}${summary.passed} passed${COLORS.reset}`);
1664
+ if (summary.failed > 0) parts.push(`${COLORS.red}${summary.failed} failed${COLORS.reset}`);
1665
+ if (summary.errors > 0) parts.push(`${COLORS.red}${summary.errors} errors${COLORS.reset}`);
1666
+ if (summary.skipped > 0) parts.push(`${COLORS.yellow}${summary.skipped} skipped${COLORS.reset}`);
1667
+ console.log(
1668
+ ` ${COLORS.bold}Tests:${COLORS.reset} ${parts.join(", ")} (${summary.total} total)`
1669
+ );
1670
+ console.log(
1671
+ ` ${COLORS.bold}Time:${COLORS.reset} ${(summary.duration / 1e3).toFixed(2)}s`
1672
+ );
1673
+ console.log("");
1674
+ }
1675
+ getIcon(status) {
1676
+ switch (status) {
1677
+ case "passed":
1678
+ return ICONS.pass;
1679
+ case "failed":
1680
+ return ICONS.fail;
1681
+ case "error":
1682
+ return ICONS.error;
1683
+ case "skipped":
1684
+ return ICONS.skip;
1685
+ default:
1686
+ return " ";
1687
+ }
1688
+ }
1689
+ };
1690
+
1691
+ // src/testing/reporters/json-reporter.ts
1692
+ var JsonReporter = class {
1693
+ constructor(outputPath) {
1694
+ this.outputPath = outputPath;
1695
+ }
1696
+ output;
1697
+ onRunStart(_collectionName, _testCount) {
1698
+ }
1699
+ onTestStart(_testName) {
1700
+ }
1701
+ onTestComplete(_result) {
1702
+ }
1703
+ onRunComplete(result) {
1704
+ const json = JSON.stringify(
1705
+ {
1706
+ id: result.id,
1707
+ collectionName: result.collectionName,
1708
+ startedAt: result.startedAt.toISOString(),
1709
+ completedAt: result.completedAt.toISOString(),
1710
+ duration: result.duration,
1711
+ summary: result.summary,
1712
+ results: result.results
1713
+ },
1714
+ null,
1715
+ 2
1716
+ );
1717
+ if (this.outputPath) {
1718
+ this.output = json;
1719
+ } else {
1720
+ console.log(json);
1721
+ }
1722
+ }
1723
+ getOutput() {
1724
+ return this.output;
1725
+ }
1726
+ };
1727
+
1728
+ // src/testing/reporters/junit-reporter.ts
1729
+ function escapeXml(str) {
1730
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1731
+ }
1732
+ var JunitReporter = class {
1733
+ constructor(outputPath) {
1734
+ this.outputPath = outputPath;
1735
+ }
1736
+ output;
1737
+ secretMasker;
1738
+ setSecretMasker(masker) {
1739
+ this.secretMasker = masker;
1740
+ }
1741
+ onRunStart(_collectionName, _testCount) {
1742
+ }
1743
+ onTestStart(_testName) {
1744
+ }
1745
+ onTestComplete(_result) {
1746
+ }
1747
+ onRunComplete(result) {
1748
+ const { summary, results, collectionName, duration } = result;
1749
+ const lines = [];
1750
+ lines.push('<?xml version="1.0" encoding="UTF-8"?>');
1751
+ lines.push(
1752
+ `<testsuites tests="${summary.total}" failures="${summary.failed}" errors="${summary.errors}" time="${(duration / 1e3).toFixed(3)}">`
1753
+ );
1754
+ lines.push(
1755
+ ` <testsuite name="${escapeXml(collectionName)}" tests="${summary.total}" failures="${summary.failed}" errors="${summary.errors}" skipped="${summary.skipped}" time="${(duration / 1e3).toFixed(3)}">`
1756
+ );
1757
+ for (const test of results) {
1758
+ const testTime = (test.duration / 1e3).toFixed(3);
1759
+ lines.push(
1760
+ ` <testcase name="${escapeXml(test.testName)}" classname="${escapeXml(collectionName)}" time="${testTime}">`
1761
+ );
1762
+ if (test.status === "failed") {
1763
+ const failedAssertions = test.assertions.filter((a) => !a.passed);
1764
+ const message = failedAssertions.map((a) => a.message).join("; ");
1765
+ lines.push(
1766
+ ` <failure message="${escapeXml(this.mask(message))}">${escapeXml(this.mask(message))}</failure>`
1767
+ );
1768
+ }
1769
+ if (test.status === "error") {
1770
+ const errorMessage = test.error ?? "Unknown error";
1771
+ lines.push(
1772
+ ` <error message="${escapeXml(this.mask(errorMessage))}">${escapeXml(this.mask(errorMessage))}</error>`
1773
+ );
1774
+ }
1775
+ if (test.status === "skipped") {
1776
+ lines.push(" <skipped/>");
1777
+ }
1778
+ lines.push(" </testcase>");
1779
+ }
1780
+ lines.push(" </testsuite>");
1781
+ lines.push("</testsuites>");
1782
+ const xml = lines.join("\n");
1783
+ if (this.outputPath) {
1784
+ this.output = xml;
1785
+ } else {
1786
+ console.log(xml);
1787
+ }
1788
+ }
1789
+ getOutput() {
1790
+ return this.output;
1791
+ }
1792
+ mask(text) {
1793
+ return this.secretMasker ? this.secretMasker.mask(text) : text;
1794
+ }
1795
+ };
1796
+
1797
+ // src/testing/reporters/html-reporter.ts
1798
+ import Handlebars from "handlebars";
1799
+ var HTML_TEMPLATE = `<!DOCTYPE html>
1800
+ <html lang="en">
1801
+ <head>
1802
+ <meta charset="UTF-8">
1803
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1804
+ <title>MCPSpec Test Report - {{collectionName}}</title>
1805
+ <style>
1806
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1807
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 24px; }
1808
+ .container { max-width: 960px; margin: 0 auto; }
1809
+ h1 { font-size: 24px; margin-bottom: 16px; }
1810
+ .summary { display: flex; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; }
1811
+ .card { background: #fff; border-radius: 8px; padding: 16px 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
1812
+ .card .label { font-size: 12px; color: #888; text-transform: uppercase; }
1813
+ .card .value { font-size: 28px; font-weight: 700; }
1814
+ .passed .value { color: #22c55e; }
1815
+ .failed .value { color: #ef4444; }
1816
+ .errors .value { color: #f97316; }
1817
+ .duration .value { color: #3b82f6; }
1818
+ table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
1819
+ th { background: #f9fafb; text-align: left; padding: 12px 16px; font-size: 12px; text-transform: uppercase; color: #888; border-bottom: 1px solid #e5e7eb; }
1820
+ td { padding: 12px 16px; border-bottom: 1px solid #f3f4f6; vertical-align: top; }
1821
+ tr:last-child td { border-bottom: none; }
1822
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; }
1823
+ .badge-passed { background: #dcfce7; color: #166534; }
1824
+ .badge-failed { background: #fee2e2; color: #991b1b; }
1825
+ .badge-error { background: #ffedd5; color: #9a3412; }
1826
+ .badge-skipped { background: #fef9c3; color: #854d0e; }
1827
+ .assertions { font-size: 13px; color: #666; margin-top: 4px; }
1828
+ .assertion-fail { color: #ef4444; }
1829
+ .footer { margin-top: 24px; font-size: 12px; color: #aaa; text-align: center; }
1830
+ </style>
1831
+ </head>
1832
+ <body>
1833
+ <div class="container">
1834
+ <h1>MCPSpec: {{collectionName}}</h1>
1835
+ <div class="summary">
1836
+ <div class="card passed"><div class="label">Passed</div><div class="value">{{summary.passed}}</div></div>
1837
+ <div class="card failed"><div class="label">Failed</div><div class="value">{{summary.failed}}</div></div>
1838
+ <div class="card errors"><div class="label">Errors</div><div class="value">{{summary.errors}}</div></div>
1839
+ <div class="card duration"><div class="label">Duration</div><div class="value">{{durationFormatted}}</div></div>
1840
+ </div>
1841
+ <table>
1842
+ <thead><tr><th>Test</th><th>Status</th><th>Duration</th><th>Details</th></tr></thead>
1843
+ <tbody>
1844
+ {{#each results}}
1845
+ <tr>
1846
+ <td>{{this.testName}}</td>
1847
+ <td><span class="badge badge-{{this.status}}">{{this.status}}</span></td>
1848
+ <td>{{this.duration}}ms</td>
1849
+ <td>
1850
+ {{#if this.error}}<div class="assertion-fail">{{this.error}}</div>{{/if}}
1851
+ {{#each this.assertions}}
1852
+ {{#unless this.passed}}<div class="assertion-fail">{{this.message}}</div>{{/unless}}
1853
+ {{/each}}
1854
+ {{#if this.allPassed}}<span style="color:#22c55e">All assertions passed</span>{{/if}}
1855
+ </td>
1856
+ </tr>
1857
+ {{/each}}
1858
+ </tbody>
1859
+ </table>
1860
+ <div class="footer">Generated by MCPSpec at {{timestamp}}</div>
1861
+ </div>
1862
+ </body>
1863
+ </html>`;
1864
+ var HtmlReporter = class {
1865
+ constructor(outputPath) {
1866
+ this.outputPath = outputPath;
1867
+ }
1868
+ output;
1869
+ secretMasker;
1870
+ setSecretMasker(masker) {
1871
+ this.secretMasker = masker;
1872
+ }
1873
+ onRunStart(_collectionName, _testCount) {
1874
+ }
1875
+ onTestStart(_testName) {
1876
+ }
1877
+ onTestComplete(_result) {
1878
+ }
1879
+ onRunComplete(result) {
1880
+ const template = Handlebars.compile(HTML_TEMPLATE);
1881
+ const data = {
1882
+ collectionName: result.collectionName,
1883
+ summary: result.summary,
1884
+ durationFormatted: `${(result.duration / 1e3).toFixed(2)}s`,
1885
+ timestamp: result.completedAt.toISOString(),
1886
+ results: result.results.map((r) => ({
1887
+ testName: r.testName,
1888
+ status: r.status,
1889
+ duration: r.duration,
1890
+ error: r.error ? this.mask(r.error) : void 0,
1891
+ assertions: r.assertions.map((a) => ({
1892
+ passed: a.passed,
1893
+ message: this.mask(a.message)
1894
+ })),
1895
+ allPassed: r.assertions.every((a) => a.passed) && !r.error
1896
+ }))
1897
+ };
1898
+ const html = template(data);
1899
+ if (this.outputPath) {
1900
+ this.output = html;
1901
+ } else {
1902
+ console.log(html);
1903
+ }
1904
+ }
1905
+ getOutput() {
1906
+ return this.output;
1907
+ }
1908
+ mask(text) {
1909
+ return this.secretMasker ? this.secretMasker.mask(text) : text;
1910
+ }
1911
+ };
1912
+
1913
+ // src/testing/reporters/tap-reporter.ts
1914
+ var TapReporter = class {
1915
+ testIndex = 0;
1916
+ secretMasker;
1917
+ setSecretMasker(masker) {
1918
+ this.secretMasker = masker;
1919
+ }
1920
+ onRunStart(_collectionName, testCount) {
1921
+ console.log("TAP version 14");
1922
+ console.log(`1..${testCount}`);
1923
+ }
1924
+ onTestStart(_testName) {
1925
+ }
1926
+ onTestComplete(result) {
1927
+ this.testIndex++;
1928
+ const status = result.status === "passed" ? "ok" : "not ok";
1929
+ const directive = result.status === "skipped" ? " # SKIP" : "";
1930
+ console.log(`${status} ${this.testIndex} - ${result.testName}${directive}`);
1931
+ if (result.status === "failed") {
1932
+ console.log(" ---");
1933
+ console.log(" severity: fail");
1934
+ const failedAssertions = result.assertions.filter((a) => !a.passed);
1935
+ if (failedAssertions.length > 0) {
1936
+ console.log(" failures:");
1937
+ for (const a of failedAssertions) {
1938
+ console.log(` - message: "${this.mask(a.message)}"`);
1939
+ if (a.expected !== void 0) console.log(` expected: ${JSON.stringify(a.expected)}`);
1940
+ if (a.actual !== void 0) console.log(` actual: ${JSON.stringify(a.actual)}`);
1941
+ }
1942
+ }
1943
+ console.log(` duration_ms: ${result.duration}`);
1944
+ console.log(" ...");
1945
+ }
1946
+ if (result.status === "error") {
1947
+ console.log(" ---");
1948
+ console.log(" severity: error");
1949
+ console.log(` message: "${this.mask(result.error ?? "Unknown error")}"`);
1950
+ console.log(` duration_ms: ${result.duration}`);
1951
+ console.log(" ...");
1952
+ }
1953
+ }
1954
+ onRunComplete(_result) {
1955
+ }
1956
+ mask(text) {
1957
+ return this.secretMasker ? this.secretMasker.mask(text) : text;
1958
+ }
1959
+ };
1960
+
1961
+ // src/testing/comparison/baseline-store.ts
1962
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync } from "fs";
1963
+ import { join as join2 } from "path";
1964
+ var BaselineStore = class {
1965
+ basePath;
1966
+ constructor(basePath) {
1967
+ this.basePath = basePath ?? join2(getPlatformInfo().dataDir, "baselines");
1968
+ }
1969
+ save(name, result) {
1970
+ this.ensureDir();
1971
+ const filePath = this.getFilePath(name);
1972
+ const serialized = JSON.stringify(
1973
+ {
1974
+ id: result.id,
1975
+ collectionName: result.collectionName,
1976
+ startedAt: result.startedAt.toISOString(),
1977
+ completedAt: result.completedAt.toISOString(),
1978
+ duration: result.duration,
1979
+ results: result.results,
1980
+ summary: result.summary
1981
+ },
1982
+ null,
1983
+ 2
1984
+ );
1985
+ writeFileSync(filePath, serialized, "utf-8");
1986
+ return filePath;
1987
+ }
1988
+ load(name) {
1989
+ const filePath = this.getFilePath(name);
1990
+ if (!existsSync(filePath)) {
1991
+ return null;
1992
+ }
1993
+ const content = readFileSync(filePath, "utf-8");
1994
+ const raw = JSON.parse(content);
1995
+ return {
1996
+ ...raw,
1997
+ startedAt: new Date(raw.startedAt),
1998
+ completedAt: new Date(raw.completedAt)
1999
+ };
2000
+ }
2001
+ list() {
2002
+ this.ensureDir();
2003
+ return readdirSync(this.basePath).filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
2004
+ }
2005
+ getFilePath(name) {
2006
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
2007
+ return join2(this.basePath, `${safeName}.json`);
2008
+ }
2009
+ ensureDir() {
2010
+ if (!existsSync(this.basePath)) {
2011
+ mkdirSync(this.basePath, { recursive: true });
2012
+ }
2013
+ }
2014
+ };
2015
+
2016
+ // src/testing/comparison/result-differ.ts
2017
+ var ResultDiffer = class {
2018
+ diff(baseline, current, baselineName = "baseline") {
2019
+ const baselineMap = /* @__PURE__ */ new Map();
2020
+ for (const r of baseline.results) {
2021
+ baselineMap.set(r.testName, r);
2022
+ }
2023
+ const currentMap = /* @__PURE__ */ new Map();
2024
+ for (const r of current.results) {
2025
+ currentMap.set(r.testName, r);
2026
+ }
2027
+ const regressions = [];
2028
+ const fixes = [];
2029
+ const newTests = [];
2030
+ const removedTests = [];
2031
+ const unchanged = [];
2032
+ for (const [name, currentResult] of currentMap) {
2033
+ const baselineResult = baselineMap.get(name);
2034
+ if (!baselineResult) {
2035
+ newTests.push({ testName: name, type: "new", after: currentResult });
2036
+ continue;
2037
+ }
2038
+ const wasPassing = baselineResult.status === "passed";
2039
+ const isPassing = currentResult.status === "passed";
2040
+ if (wasPassing && !isPassing) {
2041
+ regressions.push({ testName: name, type: "regression", before: baselineResult, after: currentResult });
2042
+ } else if (!wasPassing && isPassing) {
2043
+ fixes.push({ testName: name, type: "fix", before: baselineResult, after: currentResult });
2044
+ } else {
2045
+ unchanged.push({ testName: name, type: "unchanged", before: baselineResult, after: currentResult });
2046
+ }
2047
+ }
2048
+ for (const [name, baselineResult] of baselineMap) {
2049
+ if (!currentMap.has(name)) {
2050
+ removedTests.push({ testName: name, type: "removed", before: baselineResult });
2051
+ }
2052
+ }
2053
+ return {
2054
+ baselineName,
2055
+ currentRunId: current.id,
2056
+ regressions,
2057
+ fixes,
2058
+ newTests,
2059
+ removedTests,
2060
+ unchanged,
2061
+ summary: {
2062
+ totalBefore: baseline.results.length,
2063
+ totalAfter: current.results.length,
2064
+ regressions: regressions.length,
2065
+ fixes: fixes.length,
2066
+ newTests: newTests.length,
2067
+ removedTests: removedTests.length
2068
+ }
2069
+ };
2070
+ }
2071
+ };
2072
+
2073
+ // src/security/scan-config.ts
2074
+ var SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
2075
+ var PASSIVE_RULES = [
2076
+ "path-traversal",
2077
+ "input-validation",
2078
+ "information-disclosure"
2079
+ ];
2080
+ var ACTIVE_RULES = [
2081
+ ...PASSIVE_RULES,
2082
+ "resource-exhaustion",
2083
+ "auth-bypass",
2084
+ "injection"
2085
+ ];
2086
+ var AGGRESSIVE_RULES = [...ACTIVE_RULES];
2087
+ var DEFAULT_TIMEOUT = 1e4;
2088
+ var DEFAULT_MAX_PROBES = 50;
2089
+ var ScanConfig = class {
2090
+ mode;
2091
+ rules;
2092
+ severityThreshold;
2093
+ acknowledgeRisk;
2094
+ timeout;
2095
+ maxProbesPerTool;
2096
+ constructor(config = {}) {
2097
+ this.mode = config.mode ?? "passive";
2098
+ this.severityThreshold = config.severityThreshold ?? "info";
2099
+ this.acknowledgeRisk = config.acknowledgeRisk ?? false;
2100
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
2101
+ this.maxProbesPerTool = config.maxProbesPerTool ?? DEFAULT_MAX_PROBES;
2102
+ const allRulesForMode = this.getRulesForMode(this.mode);
2103
+ if (config.rules && config.rules.length > 0) {
2104
+ this.rules = config.rules.filter((r) => allRulesForMode.includes(r));
2105
+ } else {
2106
+ this.rules = allRulesForMode;
2107
+ }
2108
+ }
2109
+ requiresConfirmation() {
2110
+ return this.mode !== "passive" && !this.acknowledgeRisk;
2111
+ }
2112
+ meetsThreshold(severity) {
2113
+ const thresholdIdx = SEVERITY_ORDER.indexOf(this.severityThreshold);
2114
+ const severityIdx = SEVERITY_ORDER.indexOf(severity);
2115
+ return severityIdx >= thresholdIdx;
2116
+ }
2117
+ getRulesForMode(mode) {
2118
+ switch (mode) {
2119
+ case "passive":
2120
+ return PASSIVE_RULES;
2121
+ case "active":
2122
+ return ACTIVE_RULES;
2123
+ case "aggressive":
2124
+ return AGGRESSIVE_RULES;
2125
+ }
2126
+ }
2127
+ };
2128
+
2129
+ // src/security/security-scanner.ts
2130
+ import { randomUUID as randomUUID9 } from "crypto";
2131
+
2132
+ // src/security/rules/path-traversal.ts
2133
+ import { randomUUID as randomUUID3 } from "crypto";
2134
+
2135
+ // src/security/payloads/platform-payloads.ts
2136
+ function getPlatformPayloads() {
2137
+ const info = getPlatformInfo();
2138
+ const all = getAllPayloads();
2139
+ return all.filter((p) => p.platforms.includes(info.os) || p.platforms.length === 0);
2140
+ }
2141
+ function getPayloadsForMode(mode) {
2142
+ const modeOrder = ["passive", "active", "aggressive"];
2143
+ const modeIdx = modeOrder.indexOf(mode);
2144
+ return getPlatformPayloads().filter((p) => {
2145
+ const payloadModeIdx = modeOrder.indexOf(p.minMode);
2146
+ return payloadModeIdx <= modeIdx;
2147
+ });
2148
+ }
2149
+ function getAllPayloads() {
2150
+ return [
2151
+ // Path traversal - active
2152
+ { category: "path-traversal", label: "unix-parent", value: "../../../etc/passwd", description: "Unix parent directory traversal", minMode: "active", platforms: ["linux", "darwin"] },
2153
+ { category: "path-traversal", label: "unix-absolute", value: "/etc/passwd", description: "Unix absolute path", minMode: "active", platforms: ["linux", "darwin"] },
2154
+ { category: "path-traversal", label: "unix-home", value: "~/.ssh/id_rsa", description: "Unix home directory access", minMode: "active", platforms: ["linux", "darwin"] },
2155
+ { category: "path-traversal", label: "win-parent", value: "..\\..\\..\\windows\\system32\\config\\sam", description: "Windows parent directory traversal", minMode: "active", platforms: ["win32"] },
2156
+ { category: "path-traversal", label: "win-absolute", value: "C:\\Windows\\System32\\config\\sam", description: "Windows absolute path", minMode: "active", platforms: ["win32"] },
2157
+ { category: "path-traversal", label: "encoded-traversal", value: "..%2F..%2F..%2Fetc%2Fpasswd", description: "URL-encoded traversal", minMode: "active", platforms: [] },
2158
+ { category: "path-traversal", label: "null-byte", value: "../../etc/passwd\0.txt", description: "Null byte injection in path", minMode: "active", platforms: [] },
2159
+ // Command injection - active
2160
+ { category: "command-injection", label: "semicolon", value: "; echo vulnerable", description: "Semicolon command injection", minMode: "active", platforms: ["linux", "darwin"] },
2161
+ { category: "command-injection", label: "pipe", value: "| echo vulnerable", description: "Pipe command injection", minMode: "active", platforms: [] },
2162
+ { category: "command-injection", label: "backtick", value: "`echo vulnerable`", description: "Backtick command injection", minMode: "active", platforms: ["linux", "darwin"] },
2163
+ { category: "command-injection", label: "dollar-paren", value: "$(echo vulnerable)", description: "Dollar-paren command injection", minMode: "active", platforms: ["linux", "darwin"] },
2164
+ { category: "command-injection", label: "and-chain", value: "&& echo vulnerable", description: "AND chain command injection", minMode: "active", platforms: [] },
2165
+ // SQL injection - active
2166
+ { category: "sql-injection", label: "single-quote", value: "' OR '1'='1", description: "Classic SQL injection", minMode: "active", platforms: [] },
2167
+ { category: "sql-injection", label: "union-select", value: "' UNION SELECT * FROM users --", description: "UNION SELECT injection", minMode: "active", platforms: [] },
2168
+ { category: "sql-injection", label: "drop-table", value: "'; DROP TABLE users; --", description: "DROP TABLE injection", minMode: "active", platforms: [] },
2169
+ { category: "sql-injection", label: "comment", value: "admin'--", description: "Comment-based SQL injection", minMode: "active", platforms: [] },
2170
+ // Template injection - active
2171
+ { category: "template-injection", label: "jinja", value: "{{7*7}}", description: "Jinja/Handlebars template injection", minMode: "active", platforms: [] },
2172
+ { category: "template-injection", label: "erb", value: "<%= 7*7 %>", description: "ERB template injection", minMode: "active", platforms: [] },
2173
+ { category: "template-injection", label: "expression", value: "${7*7}", description: "Expression language injection", minMode: "active", platforms: [] },
2174
+ // Resource exhaustion - aggressive only
2175
+ { category: "resource-exhaustion", label: "huge-string", value: "X".repeat(1e4), description: "Very long input string", minMode: "aggressive", platforms: [] },
2176
+ { category: "resource-exhaustion", label: "deep-nesting", value: JSON.stringify(createDeepObject(20)), description: "Deeply nested object", minMode: "aggressive", platforms: [] },
2177
+ { category: "resource-exhaustion", label: "many-keys", value: JSON.stringify(createManyKeys(100)), description: "Object with many keys", minMode: "aggressive", platforms: [] }
2178
+ ];
2179
+ }
2180
+ function createDeepObject(depth) {
2181
+ let obj = { value: "leaf" };
2182
+ for (let i = 0; i < depth; i++) {
2183
+ obj = { nested: obj };
2184
+ }
2185
+ return obj;
2186
+ }
2187
+ function createManyKeys(count) {
2188
+ const obj = {};
2189
+ for (let i = 0; i < count; i++) {
2190
+ obj[`key_${i}`] = `value_${i}`;
2191
+ }
2192
+ return obj;
2193
+ }
2194
+
2195
+ // src/security/rules/utils.ts
2196
+ async function callWithTimeout(client, toolName, args, timeout) {
2197
+ try {
2198
+ const result = await Promise.race([
2199
+ client.callTool(toolName, args),
2200
+ new Promise((resolve) => setTimeout(() => resolve(null), timeout))
2201
+ ]);
2202
+ return result;
2203
+ } catch {
2204
+ return null;
2205
+ }
2206
+ }
2207
+
2208
+ // src/security/rules/path-traversal.ts
2209
+ var PATH_PARAM_PATTERNS = /^(path|file|filename|filepath|dir|directory|folder|uri|url|location|src|dest|source|destination|target)$/i;
2210
+ var PathTraversalRule = class {
2211
+ id = "path-traversal";
2212
+ name = "Path Traversal";
2213
+ description = "Tests for directory traversal vulnerabilities in path-based parameters";
2214
+ async scan(client, tools, config) {
2215
+ const findings = [];
2216
+ const payloads = getPayloadsForMode(config.mode).filter((p) => p.category === "path-traversal");
2217
+ for (const tool of tools) {
2218
+ const pathParams = this.findPathParams(tool);
2219
+ if (pathParams.length === 0) continue;
2220
+ for (const param of pathParams) {
2221
+ const passiveResult = await callWithTimeout(
2222
+ client,
2223
+ tool.name,
2224
+ { [param]: "../test" },
2225
+ config.timeout
2226
+ );
2227
+ if (passiveResult && !passiveResult.isError) {
2228
+ findings.push({
2229
+ id: randomUUID3(),
2230
+ rule: this.id,
2231
+ severity: "medium",
2232
+ title: `Path traversal possible on ${tool.name}.${param}`,
2233
+ description: `The tool "${tool.name}" accepted a relative path "../test" on parameter "${param}" without rejection.`,
2234
+ evidence: JSON.stringify(passiveResult.content).slice(0, 200),
2235
+ remediation: 'Validate and sanitize path inputs. Reject paths containing ".." and resolve to absolute paths within an allowed directory.'
2236
+ });
2237
+ }
2238
+ for (const payload of payloads) {
2239
+ const result = await callWithTimeout(
2240
+ client,
2241
+ tool.name,
2242
+ { [param]: payload.value },
2243
+ config.timeout
2244
+ );
2245
+ if (!result) continue;
2246
+ const contentStr = JSON.stringify(result.content);
2247
+ const hasSensitiveContent = /root:|admin:|password|shadow|id_rsa|PRIVATE KEY|sam|SYSTEM/i.test(contentStr);
2248
+ const hasPathDisclosure = /\/etc\/|\/home\/|C:\\Users|C:\\Windows/i.test(contentStr);
2249
+ if (!result.isError && (hasSensitiveContent || hasPathDisclosure)) {
2250
+ findings.push({
2251
+ id: randomUUID3(),
2252
+ rule: this.id,
2253
+ severity: hasSensitiveContent ? "critical" : "high",
2254
+ title: `Path traversal: ${payload.label} on ${tool.name}.${param}`,
2255
+ description: `The tool "${tool.name}" returned sensitive content when given "${payload.label}" payload on parameter "${param}".`,
2256
+ evidence: contentStr.slice(0, 500),
2257
+ remediation: "Restrict file access to a specific directory. Validate paths against an allow-list. Use chroot or similar sandboxing."
2258
+ });
2259
+ }
2260
+ }
2261
+ }
2262
+ }
2263
+ return findings;
2264
+ }
2265
+ findPathParams(tool) {
2266
+ const schema = tool.inputSchema;
2267
+ if (!schema || typeof schema !== "object") return [];
2268
+ const properties = schema["properties"];
2269
+ if (!properties || typeof properties !== "object") return [];
2270
+ return Object.keys(properties).filter(
2271
+ (key) => PATH_PARAM_PATTERNS.test(key)
2272
+ );
2273
+ }
2274
+ };
2275
+
2276
+ // src/security/rules/input-validation.ts
2277
+ import { randomUUID as randomUUID4 } from "crypto";
2278
+
2279
+ // src/security/payloads/safe-payloads.ts
2280
+ function getSafePayloads() {
2281
+ return [
2282
+ // Empty values
2283
+ { category: "empty", label: "empty-string", value: "", description: "Empty string input" },
2284
+ { category: "empty", label: "null-value", value: null, description: "Null value input" },
2285
+ { category: "empty", label: "undefined-value", value: void 0, description: "Undefined value input" },
2286
+ // Boundary values
2287
+ { category: "boundary", label: "zero", value: 0, description: "Zero numeric input" },
2288
+ { category: "boundary", label: "negative", value: -1, description: "Negative numeric input" },
2289
+ { category: "boundary", label: "max-int", value: Number.MAX_SAFE_INTEGER, description: "Maximum safe integer" },
2290
+ { category: "boundary", label: "min-int", value: Number.MIN_SAFE_INTEGER, description: "Minimum safe integer" },
2291
+ { category: "boundary", label: "float", value: 1.5, description: "Float value where int expected" },
2292
+ // Long strings
2293
+ { category: "long-string", label: "long-256", value: "A".repeat(256), description: "256-char string" },
2294
+ { category: "long-string", label: "long-1024", value: "B".repeat(1024), description: "1024-char string" },
2295
+ // Special characters
2296
+ { category: "special-chars", label: "unicode", value: "\0\uFFFF", description: "Unicode control characters" },
2297
+ { category: "special-chars", label: "newlines", value: "line1\nline2\rline3", description: "Newline characters" },
2298
+ { category: "special-chars", label: "tabs", value: " ", description: "Tab characters" },
2299
+ { category: "special-chars", label: "quotes", value: "\"'`", description: "Quote characters" },
2300
+ { category: "special-chars", label: "backslash", value: "\\\\\\", description: "Backslash characters" },
2301
+ // Type confusion
2302
+ { category: "type-confusion", label: "string-number", value: "123", description: "Numeric string where number expected" },
2303
+ { category: "type-confusion", label: "string-boolean", value: "true", description: "Boolean string where boolean expected" },
2304
+ { category: "type-confusion", label: "array-value", value: [1, 2, 3], description: "Array where scalar expected" },
2305
+ { category: "type-confusion", label: "object-value", value: { key: "value" }, description: "Object where scalar expected" },
2306
+ { category: "type-confusion", label: "boolean-value", value: true, description: "Boolean where string expected" }
2307
+ ];
2308
+ }
2309
+
2310
+ // src/security/rules/input-validation.ts
2311
+ var InputValidationRule = class {
2312
+ id = "input-validation";
2313
+ name = "Input Validation";
2314
+ description = "Tests for missing or inadequate input validation";
2315
+ async scan(client, tools, config) {
2316
+ const findings = [];
2317
+ const payloads = getSafePayloads();
2318
+ for (const tool of tools) {
2319
+ const emptyResult = await callWithTimeout(client, tool.name, {}, config.timeout);
2320
+ if (emptyResult && !emptyResult.isError) {
2321
+ const required = this.getRequiredFields(tool);
2322
+ if (required.length > 0) {
2323
+ findings.push({
2324
+ id: randomUUID4(),
2325
+ rule: this.id,
2326
+ severity: "medium",
2327
+ title: `Missing required field validation on ${tool.name}`,
2328
+ description: `The tool "${tool.name}" accepted a call with no arguments despite having required fields: ${required.join(", ")}.`,
2329
+ evidence: JSON.stringify(emptyResult.content).slice(0, 200),
2330
+ remediation: "Validate that all required parameters are present before processing the request."
2331
+ });
2332
+ }
2333
+ }
2334
+ const properties = this.getProperties(tool);
2335
+ for (const [param, schema] of Object.entries(properties)) {
2336
+ const expectedType = schema["type"];
2337
+ if (!expectedType) continue;
2338
+ const wrongTypePayloads = payloads.filter((p) => p.category === "type-confusion");
2339
+ for (const payload of wrongTypePayloads) {
2340
+ const result = await callWithTimeout(
2341
+ client,
2342
+ tool.name,
2343
+ { [param]: payload.value },
2344
+ config.timeout
2345
+ );
2346
+ if (result && !result.isError) {
2347
+ const actualType = typeof payload.value;
2348
+ const isArray = Array.isArray(payload.value);
2349
+ const payloadType = isArray ? "array" : actualType;
2350
+ if (payloadType !== expectedType && expectedType !== "any") {
2351
+ findings.push({
2352
+ id: randomUUID4(),
2353
+ rule: this.id,
2354
+ severity: "low",
2355
+ title: `Type confusion accepted on ${tool.name}.${param}`,
2356
+ description: `The tool "${tool.name}" accepted ${payloadType} for parameter "${param}" which expects ${expectedType}.`,
2357
+ evidence: `Input: ${JSON.stringify(payload.value)}, Response: ${JSON.stringify(result.content).slice(0, 200)}`,
2358
+ remediation: "Validate parameter types match the declared schema before processing."
2359
+ });
2360
+ break;
2361
+ }
2362
+ }
2363
+ }
2364
+ }
2365
+ }
2366
+ return findings;
2367
+ }
2368
+ getRequiredFields(tool) {
2369
+ const schema = tool.inputSchema;
2370
+ if (!schema || typeof schema !== "object") return [];
2371
+ const required = schema["required"];
2372
+ if (!Array.isArray(required)) return [];
2373
+ return required;
2374
+ }
2375
+ getProperties(tool) {
2376
+ const schema = tool.inputSchema;
2377
+ if (!schema || typeof schema !== "object") return {};
2378
+ const properties = schema["properties"];
2379
+ if (!properties || typeof properties !== "object") return {};
2380
+ return properties;
2381
+ }
2382
+ };
2383
+
2384
+ // src/security/rules/resource-exhaustion.ts
2385
+ import { randomUUID as randomUUID5 } from "crypto";
2386
+ var LARGE_STRING = "X".repeat(1e4);
2387
+ var VERY_LARGE_STRING = "Y".repeat(1e5);
2388
+ var SLOW_THRESHOLD_MS = 5e3;
2389
+ var ResourceExhaustionRule = class {
2390
+ id = "resource-exhaustion";
2391
+ name = "Resource Exhaustion";
2392
+ description = "Tests for resource exhaustion vulnerabilities (DoS potential)";
2393
+ async scan(client, tools, config) {
2394
+ const findings = [];
2395
+ for (const tool of tools) {
2396
+ const params = this.getStringParams(tool);
2397
+ const firstParam = params[0];
2398
+ if (!firstParam) continue;
2399
+ const param = firstParam;
2400
+ const largeStr = config.mode === "aggressive" ? VERY_LARGE_STRING : LARGE_STRING;
2401
+ const start = Date.now();
2402
+ const result = await callWithTimeout(client, tool.name, { [param]: largeStr }, config.timeout);
2403
+ const elapsed = Date.now() - start;
2404
+ if (result === null) {
2405
+ findings.push({
2406
+ id: randomUUID5(),
2407
+ rule: this.id,
2408
+ severity: "high",
2409
+ title: `Timeout with large input on ${tool.name}`,
2410
+ description: `The tool "${tool.name}" timed out when given a ${largeStr.length}-character string for parameter "${param}". This could indicate a resource exhaustion vulnerability.`,
2411
+ remediation: "Implement input size limits. Add timeouts to processing. Validate input length before processing."
2412
+ });
2413
+ } else if (elapsed > SLOW_THRESHOLD_MS) {
2414
+ findings.push({
2415
+ id: randomUUID5(),
2416
+ rule: this.id,
2417
+ severity: "medium",
2418
+ title: `Slow response with large input on ${tool.name}`,
2419
+ description: `The tool "${tool.name}" took ${elapsed}ms to process a ${largeStr.length}-character string for parameter "${param}".`,
2420
+ evidence: `Response time: ${elapsed}ms`,
2421
+ remediation: "Implement input size limits and processing timeouts to prevent slow responses."
2422
+ });
2423
+ }
2424
+ if (config.mode === "aggressive") {
2425
+ const deepObj = this.createDeepObject(50);
2426
+ const deepStart = Date.now();
2427
+ const deepResult = await callWithTimeout(client, tool.name, { [param]: deepObj }, config.timeout);
2428
+ const deepElapsed = Date.now() - deepStart;
2429
+ if (deepResult === null) {
2430
+ findings.push({
2431
+ id: randomUUID5(),
2432
+ rule: this.id,
2433
+ severity: "high",
2434
+ title: `Timeout with deeply nested input on ${tool.name}`,
2435
+ description: `The tool "${tool.name}" timed out when given a deeply nested object (50 levels) for parameter "${param}".`,
2436
+ remediation: "Implement nesting depth limits on JSON input parsing."
2437
+ });
2438
+ } else if (deepElapsed > SLOW_THRESHOLD_MS) {
2439
+ findings.push({
2440
+ id: randomUUID5(),
2441
+ rule: this.id,
2442
+ severity: "medium",
2443
+ title: `Slow response with nested input on ${tool.name}`,
2444
+ description: `The tool "${tool.name}" took ${deepElapsed}ms to process a deeply nested object for parameter "${param}".`,
2445
+ evidence: `Response time: ${deepElapsed}ms`,
2446
+ remediation: "Implement nesting depth limits on JSON input parsing."
2447
+ });
2448
+ }
2449
+ }
2450
+ }
2451
+ return findings;
2452
+ }
2453
+ getStringParams(tool) {
2454
+ const schema = tool.inputSchema;
2455
+ if (!schema || typeof schema !== "object") return [];
2456
+ const properties = schema["properties"];
2457
+ if (!properties || typeof properties !== "object") return [];
2458
+ return Object.entries(properties).filter(([, v]) => v["type"] === "string").map(([k]) => k);
2459
+ }
2460
+ createDeepObject(depth) {
2461
+ let obj = { value: "leaf" };
2462
+ for (let i = 0; i < depth; i++) {
2463
+ obj = { nested: obj };
2464
+ }
2465
+ return obj;
2466
+ }
2467
+ };
2468
+
2469
+ // src/security/rules/auth-bypass.ts
2470
+ import { randomUUID as randomUUID6 } from "crypto";
2471
+ var ADMIN_PATTERNS = /^(admin|delete|remove|drop|create|update|write|modify|set|config|configure|manage|grant|revoke|reset|destroy|purge|execute|exec|run|deploy|install|uninstall)_?/i;
2472
+ var AuthBypassRule = class {
2473
+ id = "auth-bypass";
2474
+ name = "Auth Bypass";
2475
+ description = "Tests for unrestricted access to administrative or privileged tools";
2476
+ async scan(client, tools, config) {
2477
+ const findings = [];
2478
+ const adminTools = tools.filter((t) => ADMIN_PATTERNS.test(t.name));
2479
+ for (const tool of adminTools) {
2480
+ const result = await callWithTimeout(client, tool.name, {}, config.timeout);
2481
+ if (result && !result.isError) {
2482
+ findings.push({
2483
+ id: randomUUID6(),
2484
+ rule: this.id,
2485
+ severity: "high",
2486
+ title: `Unrestricted access to ${tool.name}`,
2487
+ description: `The administrative tool "${tool.name}" was callable with empty arguments without any authentication or authorization check.`,
2488
+ evidence: JSON.stringify(result.content).slice(0, 200),
2489
+ remediation: "Implement authentication and authorization checks for administrative tools. Require valid credentials or tokens."
2490
+ });
2491
+ }
2492
+ const required = this.getRequiredFields(tool);
2493
+ if (required.length > 0) {
2494
+ const minimalArgs = {};
2495
+ for (const field of required) {
2496
+ minimalArgs[field] = "test";
2497
+ }
2498
+ const minResult = await callWithTimeout(client, tool.name, minimalArgs, config.timeout);
2499
+ if (minResult && !minResult.isError) {
2500
+ findings.push({
2501
+ id: randomUUID6(),
2502
+ rule: this.id,
2503
+ severity: "high",
2504
+ title: `Admin tool ${tool.name} accessible without auth`,
2505
+ description: `The administrative tool "${tool.name}" accepted minimal arguments without authentication verification.`,
2506
+ evidence: JSON.stringify(minResult.content).slice(0, 200),
2507
+ remediation: "Implement proper authentication and authorization before allowing access to administrative operations."
2508
+ });
2509
+ }
2510
+ }
2511
+ }
2512
+ return findings;
2513
+ }
2514
+ getRequiredFields(tool) {
2515
+ const schema = tool.inputSchema;
2516
+ if (!schema || typeof schema !== "object") return [];
2517
+ const required = schema["required"];
2518
+ if (!Array.isArray(required)) return [];
2519
+ return required;
2520
+ }
2521
+ };
2522
+
2523
+ // src/security/rules/injection.ts
2524
+ import { randomUUID as randomUUID7 } from "crypto";
2525
+ var SQL_ERROR_PATTERNS = /sql|syntax error|sqlite|mysql|postgresql|ora-\d|unterminated|unexpected token/i;
2526
+ var INJECTION_ECHO_PATTERNS = /vulnerable|<script>|alert\(|onerror=/i;
2527
+ var InjectionRule = class {
2528
+ id = "injection";
2529
+ name = "Injection";
2530
+ description = "Tests for SQL injection, command injection, and template injection vulnerabilities";
2531
+ async scan(client, tools, config) {
2532
+ const findings = [];
2533
+ const categories = ["sql-injection", "command-injection", "template-injection"];
2534
+ const payloads = getPayloadsForMode(config.mode).filter((p) => categories.includes(p.category));
2535
+ for (const tool of tools) {
2536
+ const stringParams = this.getStringParams(tool);
2537
+ if (stringParams.length === 0) continue;
2538
+ for (const param of stringParams) {
2539
+ for (const payload of payloads) {
2540
+ const result = await callWithTimeout(
2541
+ client,
2542
+ tool.name,
2543
+ { [param]: payload.value },
2544
+ config.timeout
2545
+ );
2546
+ if (!result) continue;
2547
+ const contentStr = JSON.stringify(result.content);
2548
+ if (payload.category === "sql-injection" && SQL_ERROR_PATTERNS.test(contentStr)) {
2549
+ findings.push({
2550
+ id: randomUUID7(),
2551
+ rule: this.id,
2552
+ severity: "critical",
2553
+ title: `SQL injection on ${tool.name}.${param}`,
2554
+ description: `The tool "${tool.name}" returned SQL error messages when given SQL injection payload "${payload.label}" on parameter "${param}".`,
2555
+ evidence: contentStr.slice(0, 500),
2556
+ remediation: "Use parameterized queries or prepared statements. Never concatenate user input into SQL queries."
2557
+ });
2558
+ break;
2559
+ }
2560
+ if (payload.category === "command-injection" && INJECTION_ECHO_PATTERNS.test(contentStr)) {
2561
+ findings.push({
2562
+ id: randomUUID7(),
2563
+ rule: this.id,
2564
+ severity: "critical",
2565
+ title: `Command injection on ${tool.name}.${param}`,
2566
+ description: `The tool "${tool.name}" appears to execute injected commands via parameter "${param}" using payload "${payload.label}".`,
2567
+ evidence: contentStr.slice(0, 500),
2568
+ remediation: "Never pass user input directly to shell commands. Use parameterized APIs or allow-lists for commands."
2569
+ });
2570
+ break;
2571
+ }
2572
+ if (payload.category === "template-injection" && /49/.test(contentStr) && !result.isError) {
2573
+ findings.push({
2574
+ id: randomUUID7(),
2575
+ rule: this.id,
2576
+ severity: "high",
2577
+ title: `Template injection on ${tool.name}.${param}`,
2578
+ description: `The tool "${tool.name}" evaluated a template expression via parameter "${param}" using payload "${payload.label}".`,
2579
+ evidence: contentStr.slice(0, 500),
2580
+ remediation: "Sanitize user input before passing to template engines. Use sandboxed template rendering."
2581
+ });
2582
+ break;
2583
+ }
2584
+ }
2585
+ }
2586
+ }
2587
+ return findings;
2588
+ }
2589
+ getStringParams(tool) {
2590
+ const schema = tool.inputSchema;
2591
+ if (!schema || typeof schema !== "object") return [];
2592
+ const properties = schema["properties"];
2593
+ if (!properties || typeof properties !== "object") return [];
2594
+ return Object.entries(properties).filter(([, v]) => v["type"] === "string").map(([k]) => k);
2595
+ }
2596
+ };
2597
+
2598
+ // src/security/rules/information-disclosure.ts
2599
+ import { randomUUID as randomUUID8 } from "crypto";
2600
+ var STACK_TRACE_PATTERNS = /at\s+\w+\s+\(|Error:\s+|Traceback\s+\(|stack.*trace|\.js:\d+:\d+|\.ts:\d+:\d+|\.py", line \d+/i;
2601
+ var INTERNAL_PATH_PATTERNS = /\/home\/\w+|\/Users\/\w+|C:\\Users\\\w+|\/var\/|\/opt\/|\/srv\//;
2602
+ var CONFIG_PATTERNS = /DATABASE_URL|DB_PASSWORD|API_KEY|SECRET_KEY|PRIVATE_KEY|ACCESS_TOKEN|AWS_SECRET/i;
2603
+ var VERSION_PATTERNS = /node\/\d+\.\d+|express\/\d+|nginx\/\d+|apache\/\d+|python\/\d+/i;
2604
+ var InformationDisclosureRule = class {
2605
+ id = "information-disclosure";
2606
+ name = "Information Disclosure";
2607
+ description = "Tests for unintended information disclosure in error messages and responses";
2608
+ async scan(client, tools, config) {
2609
+ const findings = [];
2610
+ for (const tool of tools) {
2611
+ const errorTriggers = [
2612
+ {},
2613
+ { nonexistent_param: "test" },
2614
+ { [this.getFirstParam(tool) ?? "id"]: null },
2615
+ { [this.getFirstParam(tool) ?? "id"]: "" }
2616
+ ];
2617
+ for (const args of errorTriggers) {
2618
+ const result = await callWithTimeout(client, tool.name, args, config.timeout);
2619
+ if (!result) continue;
2620
+ const contentStr = JSON.stringify(result.content);
2621
+ if (STACK_TRACE_PATTERNS.test(contentStr)) {
2622
+ findings.push({
2623
+ id: randomUUID8(),
2624
+ rule: this.id,
2625
+ severity: "medium",
2626
+ title: `Stack trace disclosed by ${tool.name}`,
2627
+ description: `The tool "${tool.name}" exposed a stack trace in its error response, potentially revealing internal implementation details.`,
2628
+ evidence: contentStr.slice(0, 500),
2629
+ remediation: "Return generic error messages to clients. Log detailed errors server-side only."
2630
+ });
2631
+ break;
2632
+ }
2633
+ if (INTERNAL_PATH_PATTERNS.test(contentStr)) {
2634
+ findings.push({
2635
+ id: randomUUID8(),
2636
+ rule: this.id,
2637
+ severity: "low",
2638
+ title: `Internal path disclosed by ${tool.name}`,
2639
+ description: `The tool "${tool.name}" exposed internal file system paths in its response.`,
2640
+ evidence: contentStr.slice(0, 500),
2641
+ remediation: "Sanitize error messages to remove internal file paths before returning to clients."
2642
+ });
2643
+ break;
2644
+ }
2645
+ if (CONFIG_PATTERNS.test(contentStr)) {
2646
+ findings.push({
2647
+ id: randomUUID8(),
2648
+ rule: this.id,
2649
+ severity: "high",
2650
+ title: `Configuration data disclosed by ${tool.name}`,
2651
+ description: `The tool "${tool.name}" exposed configuration values or secrets in its response.`,
2652
+ evidence: contentStr.slice(0, 500),
2653
+ remediation: "Never include configuration values, secrets, or environment variables in error responses."
2654
+ });
2655
+ break;
2656
+ }
2657
+ if (VERSION_PATTERNS.test(contentStr)) {
2658
+ findings.push({
2659
+ id: randomUUID8(),
2660
+ rule: this.id,
2661
+ severity: "info",
2662
+ title: `Version information disclosed by ${tool.name}`,
2663
+ description: `The tool "${tool.name}" exposed server/runtime version information in its response.`,
2664
+ evidence: contentStr.slice(0, 500),
2665
+ remediation: "Remove version headers and version information from error responses."
2666
+ });
2667
+ break;
2668
+ }
2669
+ }
2670
+ }
2671
+ return findings;
2672
+ }
2673
+ getFirstParam(tool) {
2674
+ const schema = tool.inputSchema;
2675
+ if (!schema || typeof schema !== "object") return void 0;
2676
+ const properties = schema["properties"];
2677
+ if (!properties || typeof properties !== "object") return void 0;
2678
+ const keys = Object.keys(properties);
2679
+ return keys[0];
2680
+ }
2681
+ };
2682
+
2683
+ // src/security/security-scanner.ts
2684
+ var SEVERITY_ORDER2 = ["info", "low", "medium", "high", "critical"];
2685
+ var SecurityScanner = class {
2686
+ rules = /* @__PURE__ */ new Map();
2687
+ constructor() {
2688
+ this.registerBuiltinRules();
2689
+ }
2690
+ registerRule(rule) {
2691
+ this.rules.set(rule.id, rule);
2692
+ }
2693
+ async scan(client, config, progress) {
2694
+ const startedAt = /* @__PURE__ */ new Date();
2695
+ const findings = [];
2696
+ const tools = await client.listTools();
2697
+ for (const ruleId of config.rules) {
2698
+ const rule = this.rules.get(ruleId);
2699
+ if (!rule) continue;
2700
+ progress?.onRuleStart?.(rule.id, rule.name);
2701
+ try {
2702
+ const ruleFindings = await rule.scan(client, tools, config);
2703
+ for (const finding of ruleFindings) {
2704
+ findings.push(finding);
2705
+ progress?.onFinding?.(finding);
2706
+ }
2707
+ progress?.onRuleComplete?.(rule.id, ruleFindings.length);
2708
+ } catch (err) {
2709
+ const errorFinding = {
2710
+ id: randomUUID9(),
2711
+ rule: ruleId,
2712
+ severity: "info",
2713
+ title: `Rule "${ruleId}" failed to complete`,
2714
+ description: `The security rule "${ruleId}" encountered an error during scanning: ${err instanceof Error ? err.message : String(err)}`
2715
+ };
2716
+ findings.push(errorFinding);
2717
+ progress?.onFinding?.(errorFinding);
2718
+ progress?.onRuleComplete?.(ruleId, 0);
2719
+ }
2720
+ }
2721
+ findings.sort((a, b) => {
2722
+ return SEVERITY_ORDER2.indexOf(b.severity) - SEVERITY_ORDER2.indexOf(a.severity);
2723
+ });
2724
+ const completedAt = /* @__PURE__ */ new Date();
2725
+ const serverInfo = client.getServerInfo();
2726
+ return {
2727
+ id: randomUUID9(),
2728
+ serverName: serverInfo?.name ?? "unknown",
2729
+ mode: config.mode,
2730
+ startedAt,
2731
+ completedAt,
2732
+ findings,
2733
+ summary: this.buildSummary(findings)
2734
+ };
2735
+ }
2736
+ buildSummary(findings) {
2737
+ const bySeverity = {
2738
+ info: 0,
2739
+ low: 0,
2740
+ medium: 0,
2741
+ high: 0,
2742
+ critical: 0
2743
+ };
2744
+ const byRule = {};
2745
+ for (const finding of findings) {
2746
+ bySeverity[finding.severity]++;
2747
+ byRule[finding.rule] = (byRule[finding.rule] ?? 0) + 1;
2748
+ }
2749
+ return {
2750
+ totalFindings: findings.length,
2751
+ bySeverity,
2752
+ byRule
2753
+ };
2754
+ }
2755
+ registerBuiltinRules() {
2756
+ this.registerRule(new PathTraversalRule());
2757
+ this.registerRule(new InputValidationRule());
2758
+ this.registerRule(new ResourceExhaustionRule());
2759
+ this.registerRule(new AuthBypassRule());
2760
+ this.registerRule(new InjectionRule());
2761
+ this.registerRule(new InformationDisclosureRule());
2762
+ }
2763
+ };
2764
+
2765
+ // src/performance/profiler.ts
2766
+ import { performance as performance2 } from "perf_hooks";
2767
+ function computeStats(sortedDurations) {
2768
+ if (sortedDurations.length === 0) {
2769
+ return { min: 0, max: 0, mean: 0, median: 0, p95: 0, p99: 0, stddev: 0 };
2770
+ }
2771
+ const n = sortedDurations.length;
2772
+ const min = sortedDurations[0];
2773
+ const max = sortedDurations[n - 1];
2774
+ const sum = sortedDurations.reduce((a, b) => a + b, 0);
2775
+ const mean = sum / n;
2776
+ const median = n % 2 === 0 ? (sortedDurations[n / 2 - 1] + sortedDurations[n / 2]) / 2 : sortedDurations[Math.floor(n / 2)];
2777
+ const p95 = sortedDurations[Math.ceil(n * 0.95) - 1];
2778
+ const p99 = sortedDurations[Math.ceil(n * 0.99) - 1];
2779
+ const variance = sortedDurations.reduce((acc, val) => acc + (val - mean) ** 2, 0) / n;
2780
+ const stddev = Math.sqrt(variance);
2781
+ return { min, max, mean, median, p95, p99, stddev };
2782
+ }
2783
+ var Profiler = class {
2784
+ entries = [];
2785
+ async profileCall(client, toolName, args) {
2786
+ const startMs = performance2.now();
2787
+ let success = true;
2788
+ let error;
2789
+ try {
2790
+ const result = await client.callTool(toolName, args);
2791
+ if (result.isError) {
2792
+ success = false;
2793
+ error = JSON.stringify(result.content).slice(0, 200);
2794
+ }
2795
+ } catch (err) {
2796
+ success = false;
2797
+ error = err instanceof Error ? err.message : String(err);
2798
+ }
2799
+ const durationMs = performance2.now() - startMs;
2800
+ const entry = {
2801
+ toolName,
2802
+ startMs,
2803
+ durationMs,
2804
+ success,
2805
+ error
2806
+ };
2807
+ this.entries.push(entry);
2808
+ return entry;
2809
+ }
2810
+ getEntries() {
2811
+ return [...this.entries];
2812
+ }
2813
+ getStats(toolName) {
2814
+ const filtered = toolName ? this.entries.filter((e) => e.toolName === toolName && e.success) : this.entries.filter((e) => e.success);
2815
+ const durations = filtered.map((e) => e.durationMs).sort((a, b) => a - b);
2816
+ return computeStats(durations);
2817
+ }
2818
+ clear() {
2819
+ this.entries = [];
2820
+ }
2821
+ };
2822
+
2823
+ // src/performance/benchmark-runner.ts
2824
+ import { performance as performance3 } from "perf_hooks";
2825
+ var DEFAULT_CONFIG = {
2826
+ iterations: 100,
2827
+ warmupIterations: 5,
2828
+ concurrency: 1,
2829
+ timeout: 3e4
2830
+ };
2831
+ var BenchmarkRunner = class {
2832
+ async run(client, toolName, args, config = {}, progress) {
2833
+ const cfg = { ...DEFAULT_CONFIG, ...config };
2834
+ const startedAt = /* @__PURE__ */ new Date();
2835
+ if (cfg.warmupIterations > 0) {
2836
+ progress?.onWarmupStart?.(cfg.warmupIterations);
2837
+ for (let i = 0; i < cfg.warmupIterations; i++) {
2838
+ await this.runSingleIteration(client, toolName, args, cfg.timeout);
2839
+ }
2840
+ }
2841
+ const durations = [];
2842
+ let errors = 0;
2843
+ for (let i = 0; i < cfg.iterations; i++) {
2844
+ const result = await this.runSingleIteration(client, toolName, args, cfg.timeout);
2845
+ if (result.success) {
2846
+ durations.push(result.durationMs);
2847
+ } else {
2848
+ errors++;
2849
+ }
2850
+ progress?.onIterationComplete?.(i + 1, cfg.iterations, result.durationMs);
2851
+ }
2852
+ durations.sort((a, b) => a - b);
2853
+ const stats = computeStats(durations);
2854
+ const completedAt = /* @__PURE__ */ new Date();
2855
+ const benchResult = {
2856
+ toolName,
2857
+ iterations: cfg.iterations,
2858
+ stats,
2859
+ errors,
2860
+ startedAt,
2861
+ completedAt
2862
+ };
2863
+ progress?.onComplete?.(benchResult);
2864
+ return benchResult;
2865
+ }
2866
+ async runSingleIteration(client, toolName, args, timeout) {
2867
+ const start = performance3.now();
2868
+ try {
2869
+ const result = await Promise.race([
2870
+ client.callTool(toolName, args),
2871
+ new Promise((resolve) => setTimeout(() => resolve(null), timeout))
2872
+ ]);
2873
+ const durationMs = performance3.now() - start;
2874
+ if (result === null) {
2875
+ return { success: false, durationMs };
2876
+ }
2877
+ return { success: !result.isError, durationMs };
2878
+ } catch {
2879
+ const durationMs = performance3.now() - start;
2880
+ return { success: false, durationMs };
2881
+ }
2882
+ }
2883
+ };
2884
+
2885
+ // src/performance/waterfall-generator.ts
2886
+ var WaterfallGenerator = class {
2887
+ generate(entries) {
2888
+ if (entries.length === 0) return [];
2889
+ const minStart = Math.min(...entries.map((e) => e.startMs));
2890
+ return entries.map((entry) => ({
2891
+ label: `${entry.toolName}${entry.success ? "" : " (ERR)"}`,
2892
+ startMs: entry.startMs - minStart,
2893
+ durationMs: entry.durationMs
2894
+ }));
2895
+ }
2896
+ toAscii(entries, width = 60) {
2897
+ if (entries.length === 0) return "";
2898
+ const maxEnd = Math.max(...entries.map((e) => e.startMs + e.durationMs));
2899
+ if (maxEnd === 0) return "";
2900
+ const maxLabelLen = Math.max(...entries.map((e) => e.label.length));
2901
+ const barWidth = width - maxLabelLen - 12;
2902
+ const lines = [];
2903
+ for (const entry of entries) {
2904
+ const label = entry.label.padEnd(maxLabelLen);
2905
+ const startCol = Math.floor(entry.startMs / maxEnd * barWidth);
2906
+ const endCol = Math.max(startCol + 1, Math.floor((entry.startMs + entry.durationMs) / maxEnd * barWidth));
2907
+ const prefix = " ".repeat(startCol);
2908
+ const bar = "\u2588".repeat(endCol - startCol);
2909
+ const suffix = ` ${entry.durationMs.toFixed(1)}ms`;
2910
+ lines.push(`${label} |${prefix}${bar}${suffix}`);
2911
+ }
2912
+ return lines.join("\n");
2913
+ }
2914
+ };
2915
+
2916
+ // src/documentation/doc-generator.ts
2917
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
2918
+ import { join as join3 } from "path";
2919
+
2920
+ // src/documentation/markdown-generator.ts
2921
+ var MarkdownGenerator = class {
2922
+ generate(data) {
2923
+ const lines = [];
2924
+ const version = data.serverVersion ? ` v${data.serverVersion}` : "";
2925
+ lines.push(`# ${data.serverName}${version}`);
2926
+ lines.push("");
2927
+ if (data.tools.length > 0) {
2928
+ lines.push("## Tools");
2929
+ lines.push("");
2930
+ lines.push("| Name | Description |");
2931
+ lines.push("|------|-------------|");
2932
+ for (const tool of data.tools) {
2933
+ const desc = tool.description ?? "-";
2934
+ lines.push(`| ${tool.name} | ${desc} |`);
2935
+ }
2936
+ lines.push("");
2937
+ for (const tool of data.tools) {
2938
+ lines.push(`### ${tool.name}`);
2939
+ lines.push("");
2940
+ if (tool.description) {
2941
+ lines.push(tool.description);
2942
+ lines.push("");
2943
+ }
2944
+ if (tool.inputSchema) {
2945
+ lines.push("**Input Schema:**");
2946
+ lines.push("");
2947
+ lines.push("```json");
2948
+ lines.push(JSON.stringify(tool.inputSchema, null, 2));
2949
+ lines.push("```");
2950
+ lines.push("");
2951
+ }
2952
+ }
2953
+ } else {
2954
+ lines.push("## Tools");
2955
+ lines.push("");
2956
+ lines.push("No tools available.");
2957
+ lines.push("");
2958
+ }
2959
+ if (data.resources.length > 0) {
2960
+ lines.push("## Resources");
2961
+ lines.push("");
2962
+ lines.push("| URI | Name | Description | MIME Type |");
2963
+ lines.push("|-----|------|-------------|-----------|");
2964
+ for (const res of data.resources) {
2965
+ const name = res.name ?? "-";
2966
+ const desc = res.description ?? "-";
2967
+ const mime = res.mimeType ?? "-";
2968
+ lines.push(`| ${res.uri} | ${name} | ${desc} | ${mime} |`);
2969
+ }
2970
+ lines.push("");
2971
+ }
2972
+ lines.push("---");
2973
+ lines.push("");
2974
+ lines.push(`*Generated by MCPSpec on ${data.generatedAt.toISOString()}*`);
2975
+ lines.push("");
2976
+ return lines.join("\n");
2977
+ }
2978
+ };
2979
+
2980
+ // src/documentation/html-generator.ts
2981
+ import Handlebars2 from "handlebars";
2982
+ var HTML_TEMPLATE2 = `<!DOCTYPE html>
2983
+ <html lang="en">
2984
+ <head>
2985
+ <meta charset="UTF-8">
2986
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2987
+ <title>{{serverName}} Documentation</title>
2988
+ <style>
2989
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2990
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #1a1a2e; background: #f8f9fa; line-height: 1.6; }
2991
+ .container { max-width: 960px; margin: 0 auto; padding: 2rem; }
2992
+ h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #2D5A27; }
2993
+ h2 { font-size: 1.5rem; margin: 2rem 0 1rem; border-bottom: 2px solid #e0e0e0; padding-bottom: 0.5rem; }
2994
+ h3 { font-size: 1.15rem; margin: 1.5rem 0 0.5rem; }
2995
+ .version { color: #666; font-size: 1rem; font-weight: normal; }
2996
+ .tool-card { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 1rem 1.25rem; margin-bottom: 1rem; }
2997
+ .tool-card h3 { margin-top: 0; }
2998
+ .tool-card p { color: #555; margin-bottom: 0.75rem; }
2999
+ pre { background: #1e1e2e; color: #cdd6f4; padding: 1rem; border-radius: 6px; overflow-x: auto; font-size: 0.85rem; }
3000
+ code { font-family: 'SF Mono', 'Fira Code', monospace; }
3001
+ table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
3002
+ th, td { text-align: left; padding: 0.5rem 0.75rem; border: 1px solid #e0e0e0; }
3003
+ th { background: #f1f3f5; font-weight: 600; }
3004
+ .footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid #e0e0e0; color: #888; font-size: 0.85rem; }
3005
+ </style>
3006
+ </head>
3007
+ <body>
3008
+ <div class="container">
3009
+ <h1>{{serverName}} {{#if serverVersion}}<span class="version">v{{serverVersion}}</span>{{/if}}</h1>
3010
+
3011
+ <h2>Tools</h2>
3012
+ {{#if tools.length}}
3013
+ {{#each tools}}
3014
+ <div class="tool-card">
3015
+ <h3>{{this.name}}</h3>
3016
+ {{#if this.description}}<p>{{this.description}}</p>{{/if}}
3017
+ {{#if this.inputSchema}}
3018
+ <strong>Input Schema:</strong>
3019
+ <pre><code>{{jsonPretty this.inputSchema}}</code></pre>
3020
+ {{/if}}
3021
+ </div>
3022
+ {{/each}}
3023
+ {{else}}
3024
+ <p>No tools available.</p>
3025
+ {{/if}}
3026
+
3027
+ {{#if resources.length}}
3028
+ <h2>Resources</h2>
3029
+ <table>
3030
+ <thead>
3031
+ <tr><th>URI</th><th>Name</th><th>Description</th><th>MIME Type</th></tr>
3032
+ </thead>
3033
+ <tbody>
3034
+ {{#each resources}}
3035
+ <tr>
3036
+ <td>{{this.uri}}</td>
3037
+ <td>{{or this.name "-"}}</td>
3038
+ <td>{{or this.description "-"}}</td>
3039
+ <td>{{or this.mimeType "-"}}</td>
3040
+ </tr>
3041
+ {{/each}}
3042
+ </tbody>
3043
+ </table>
3044
+ {{/if}}
3045
+
3046
+ <div class="footer">
3047
+ Generated by MCPSpec on {{generatedAt}}
3048
+ </div>
3049
+ </div>
3050
+ </body>
3051
+ </html>`;
3052
+ var HtmlDocGenerator = class {
3053
+ template;
3054
+ constructor() {
3055
+ Handlebars2.registerHelper("jsonPretty", (obj) => {
3056
+ return new Handlebars2.SafeString(JSON.stringify(obj, null, 2));
3057
+ });
3058
+ Handlebars2.registerHelper("or", (a, b) => a || b);
3059
+ this.template = Handlebars2.compile(HTML_TEMPLATE2);
3060
+ }
3061
+ generate(data) {
3062
+ return this.template({
3063
+ ...data,
3064
+ generatedAt: data.generatedAt.toISOString()
3065
+ });
3066
+ }
3067
+ };
3068
+
3069
+ // src/documentation/doc-generator.ts
3070
+ var DocGenerator = class {
3071
+ async introspect(client) {
3072
+ const serverInfo = client.getServerInfo();
3073
+ const tools = await client.listTools();
3074
+ let resources = [];
3075
+ try {
3076
+ resources = await client.listResources();
3077
+ } catch {
3078
+ }
3079
+ return {
3080
+ serverName: serverInfo?.name ?? "Unknown Server",
3081
+ serverVersion: serverInfo?.version,
3082
+ tools,
3083
+ resources,
3084
+ generatedAt: /* @__PURE__ */ new Date()
3085
+ };
3086
+ }
3087
+ async generate(client, options) {
3088
+ const data = await this.introspect(client);
3089
+ let content;
3090
+ if (options.format === "html") {
3091
+ const generator = new HtmlDocGenerator();
3092
+ content = generator.generate(data);
3093
+ } else {
3094
+ const generator = new MarkdownGenerator();
3095
+ content = generator.generate(data);
3096
+ }
3097
+ if (options.outputDir) {
3098
+ mkdirSync2(options.outputDir, { recursive: true });
3099
+ const filename = options.format === "html" ? "index.html" : "README.md";
3100
+ writeFileSync2(join3(options.outputDir, filename), content, "utf-8");
3101
+ }
3102
+ return content;
3103
+ }
3104
+ };
3105
+
3106
+ // src/scoring/mcp-score.ts
3107
+ var MCPScoreCalculator = class {
3108
+ async calculate(client, progress) {
3109
+ const tools = await client.listTools();
3110
+ let resources = [];
3111
+ try {
3112
+ resources = await client.listResources();
3113
+ } catch {
3114
+ }
3115
+ progress?.onCategoryStart?.("documentation");
3116
+ const documentation = this.scoreDocumentation(tools, resources);
3117
+ progress?.onCategoryComplete?.("documentation", documentation);
3118
+ progress?.onCategoryStart?.("schemaQuality");
3119
+ const schemaQuality = this.scoreSchemaQuality(tools);
3120
+ progress?.onCategoryComplete?.("schemaQuality", schemaQuality);
3121
+ progress?.onCategoryStart?.("errorHandling");
3122
+ const errorHandling = await this.scoreErrorHandling(client, tools);
3123
+ progress?.onCategoryComplete?.("errorHandling", errorHandling);
3124
+ progress?.onCategoryStart?.("performance");
3125
+ const performance4 = await this.scorePerformance(client, tools);
3126
+ progress?.onCategoryComplete?.("performance", performance4);
3127
+ progress?.onCategoryStart?.("security");
3128
+ const security = await this.scoreSecurity(client);
3129
+ progress?.onCategoryComplete?.("security", security);
3130
+ const overall = Math.round(
3131
+ documentation * 0.25 + schemaQuality * 0.25 + errorHandling * 0.2 + performance4 * 0.15 + security * 0.15
3132
+ );
3133
+ return {
3134
+ overall,
3135
+ categories: {
3136
+ documentation,
3137
+ schemaQuality,
3138
+ errorHandling,
3139
+ performance: performance4,
3140
+ security
3141
+ }
3142
+ };
3143
+ }
3144
+ scoreDocumentation(tools, resources) {
3145
+ const items = [...tools, ...resources];
3146
+ if (items.length === 0) return 0;
3147
+ const withDescription = items.filter((item) => {
3148
+ const desc = "description" in item ? item.description : void 0;
3149
+ return desc && desc.trim().length > 0;
3150
+ }).length;
3151
+ return Math.round(withDescription / items.length * 100);
3152
+ }
3153
+ scoreSchemaQuality(tools) {
3154
+ if (tools.length === 0) return 0;
3155
+ let totalPoints = 0;
3156
+ for (const tool of tools) {
3157
+ const schema = tool.inputSchema;
3158
+ if (!schema) continue;
3159
+ let toolPoints = 0;
3160
+ if (schema.type) toolPoints += 1 / 3;
3161
+ if (schema.properties && typeof schema.properties === "object") toolPoints += 1 / 3;
3162
+ if (schema.required && Array.isArray(schema.required)) toolPoints += 1 / 3;
3163
+ totalPoints += toolPoints;
3164
+ }
3165
+ return Math.round(totalPoints / tools.length * 100);
3166
+ }
3167
+ async scoreErrorHandling(client, tools) {
3168
+ if (tools.length === 0) return 0;
3169
+ const testTools = tools.slice(0, 5);
3170
+ let totalScore = 0;
3171
+ for (const tool of testTools) {
3172
+ try {
3173
+ const result = await client.callTool(tool.name, {});
3174
+ if (result.isError) {
3175
+ totalScore += 100;
3176
+ } else {
3177
+ totalScore += 50;
3178
+ }
3179
+ } catch {
3180
+ totalScore += 0;
3181
+ }
3182
+ }
3183
+ return Math.round(totalScore / testTools.length);
3184
+ }
3185
+ async scorePerformance(client, tools) {
3186
+ if (tools.length === 0) return 20;
3187
+ const tool = tools[0];
3188
+ const latencies = [];
3189
+ for (let i = 0; i < 5; i++) {
3190
+ const start = performance.now();
3191
+ try {
3192
+ await client.callTool(tool.name, {});
3193
+ } catch {
3194
+ }
3195
+ latencies.push(performance.now() - start);
3196
+ }
3197
+ latencies.sort((a, b) => a - b);
3198
+ const median = latencies[Math.floor(latencies.length / 2)];
3199
+ if (median < 100) return 100;
3200
+ if (median < 500) return 80;
3201
+ if (median < 1e3) return 60;
3202
+ if (median < 5e3) return 40;
3203
+ return 20;
3204
+ }
3205
+ async scoreSecurity(client) {
3206
+ try {
3207
+ const scanner = new SecurityScanner();
3208
+ const config = new ScanConfig({ mode: "passive" });
3209
+ const result = await scanner.scan(client, config);
3210
+ const findingCount = result.summary.totalFindings;
3211
+ if (findingCount === 0) return 100;
3212
+ if (findingCount <= 2) return 70;
3213
+ if (findingCount <= 5) return 40;
3214
+ return 20;
3215
+ } catch {
3216
+ return 50;
3217
+ }
3218
+ }
3219
+ };
3220
+
3221
+ // src/scoring/badge-generator.ts
3222
+ var BadgeGenerator = class {
3223
+ generate(score) {
3224
+ const color = this.getColor(score.overall);
3225
+ const label = "MCP Score";
3226
+ const value = `${score.overall}/100`;
3227
+ const labelWidth = 70;
3228
+ const valueWidth = 50;
3229
+ const totalWidth = labelWidth + valueWidth;
3230
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="20" role="img" aria-label="${label}: ${value}">
3231
+ <title>${label}: ${value}</title>
3232
+ <linearGradient id="s" x2="0" y2="100%">
3233
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
3234
+ <stop offset="1" stop-opacity=".1"/>
3235
+ </linearGradient>
3236
+ <clipPath id="r">
3237
+ <rect width="${totalWidth}" height="20" rx="3" fill="#fff"/>
3238
+ </clipPath>
3239
+ <g clip-path="url(#r)">
3240
+ <rect width="${labelWidth}" height="20" fill="#555"/>
3241
+ <rect x="${labelWidth}" width="${valueWidth}" height="20" fill="${color}"/>
3242
+ <rect width="${totalWidth}" height="20" fill="url(#s)"/>
3243
+ </g>
3244
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
3245
+ <text x="${labelWidth / 2}" y="14">${label}</text>
3246
+ <text x="${labelWidth + valueWidth / 2}" y="14">${value}</text>
3247
+ </g>
3248
+ </svg>`;
3249
+ }
3250
+ getColor(score) {
3251
+ if (score >= 80) return "#4c1";
3252
+ if (score >= 60) return "#dfb317";
3253
+ return "#e05d44";
3254
+ }
3255
+ };
3256
+ export {
3257
+ AuthBypassRule,
3258
+ BadgeGenerator,
3259
+ BaselineStore,
3260
+ BenchmarkRunner,
3261
+ ConnectionManager,
3262
+ ConsoleReporter,
3263
+ DocGenerator,
3264
+ ERROR_CODE_MAP,
3265
+ ERROR_TEMPLATES,
3266
+ HtmlDocGenerator,
3267
+ HtmlReporter,
3268
+ InformationDisclosureRule,
3269
+ InjectionRule,
3270
+ InputValidationRule,
3271
+ JsonReporter,
3272
+ JunitReporter,
3273
+ LoggingTransport,
3274
+ MCPClient,
3275
+ MCPScoreCalculator,
3276
+ MCPSpecError,
3277
+ MarkdownGenerator,
3278
+ NotImplementedError,
3279
+ PathTraversalRule,
3280
+ ProcessManagerImpl,
3281
+ ProcessRegistry,
3282
+ Profiler,
3283
+ RateLimiter,
3284
+ ResourceExhaustionRule,
3285
+ ResultDiffer,
3286
+ ScanConfig,
3287
+ SecretMasker,
3288
+ SecurityScanner,
3289
+ TapReporter,
3290
+ TestExecutor,
3291
+ TestRunner,
3292
+ TestScheduler,
3293
+ WaterfallGenerator,
3294
+ YAML_LIMITS,
3295
+ computeStats,
3296
+ formatError,
3297
+ getPayloadsForMode,
3298
+ getPlatformInfo,
3299
+ getPlatformPayloads,
3300
+ getSafePayloads,
3301
+ loadYamlSafely,
3302
+ queryJsonPath,
3303
+ registerCleanupHandlers,
3304
+ resolveVariables
3305
+ };