@bryan-thompson/inspector-assessment-cli 1.43.2 → 1.43.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,131 +3,561 @@
3
3
  * Full Assessment Runner CLI
4
4
  *
5
5
  * Runs comprehensive MCP server assessment using AssessmentOrchestrator
6
- * with all 17 assessor modules and optional Claude Code integration.
6
+ * with all 11 assessor modules and optional Claude Code integration.
7
7
  *
8
8
  * Usage:
9
9
  * mcp-assess-full --server <server-name> [--claude-enabled] [--full]
10
10
  * mcp-assess-full my-server --source ./my-server --output ./results.json
11
11
  */
12
- import { ScopedListenerConfig } from "./lib/event-config.js";
13
- // Import from extracted modules
14
- import { parseArgs } from "./lib/cli-parser.js";
15
- import { runFullAssessment, runSingleModule } from "./lib/assessment-runner.js";
16
- import { saveResults, saveTieredResults, saveSummaryOnly, displaySummary, saveSingleModuleResults, displaySingleModuleSummary, } from "./lib/result-output.js";
17
- import { handleComparison, displayComparisonSummary, } from "./lib/comparison-handler.js";
18
- import { shouldAutoTier, formatTokenEstimate, } from "../../client/lib/lib/assessment/summarizer/index.js";
19
- // ============================================================================
20
- // Main Entry Point
21
- // ============================================================================
12
+ import * as fs from "fs";
13
+ import * as path from "path";
14
+ import * as os from "os";
15
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
16
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
17
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
18
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
19
+ // Import from local client lib (will use package exports when published)
20
+ import { AssessmentOrchestrator, } from "../../client/lib/services/assessment/AssessmentOrchestrator.js";
21
+ import { DEFAULT_ASSESSMENT_CONFIG, } from "../../client/lib/lib/assessmentTypes.js";
22
+ import { FULL_CLAUDE_CODE_CONFIG } from "../../client/lib/services/assessment/lib/claudeCodeBridge.js";
22
23
  /**
23
- * Main execution
24
+ * Load server configuration from Claude Code's MCP settings
24
25
  */
25
- async function main() {
26
- // Use scoped listener configuration instead of global modification
27
- // See GitHub Issue #33 for rationale
28
- const listenerConfig = new ScopedListenerConfig(50);
29
- try {
30
- const options = parseArgs();
31
- if (options.helpRequested ||
32
- options.versionRequested ||
33
- options.listModules) {
34
- return;
26
+ function loadServerConfig(serverName, configPath) {
27
+ const possiblePaths = [
28
+ configPath,
29
+ path.join(os.homedir(), ".config", "mcp", "servers", `${serverName}.json`),
30
+ path.join(os.homedir(), ".config", "claude", "claude_desktop_config.json"),
31
+ ].filter(Boolean);
32
+ for (const tryPath of possiblePaths) {
33
+ if (!fs.existsSync(tryPath))
34
+ continue;
35
+ const config = JSON.parse(fs.readFileSync(tryPath, "utf-8"));
36
+ if (config.mcpServers && config.mcpServers[serverName]) {
37
+ const serverConfig = config.mcpServers[serverName];
38
+ return {
39
+ transport: "stdio",
40
+ command: serverConfig.command,
41
+ args: serverConfig.args || [],
42
+ env: serverConfig.env || {},
43
+ };
35
44
  }
36
- // Apply scoped listener configuration for assessment
37
- listenerConfig.apply();
38
- // Single module mode - bypass orchestrator for lightweight execution (Issue #184)
39
- if (options.singleModule) {
40
- const result = await runSingleModule(options.singleModule, options);
41
- if (!options.jsonOnly) {
42
- displaySingleModuleSummary(result);
45
+ if (config.url ||
46
+ config.transport === "http" ||
47
+ config.transport === "sse") {
48
+ if (!config.url) {
49
+ throw new Error(`Invalid server config: transport is '${config.transport}' but 'url' is missing`);
43
50
  }
44
- const outputPath = saveSingleModuleResults(options.serverName, options.singleModule, result, options);
45
- if (options.jsonOnly) {
46
- console.log(outputPath);
51
+ return {
52
+ transport: config.transport || "http",
53
+ url: config.url,
54
+ };
55
+ }
56
+ if (config.command) {
57
+ return {
58
+ transport: "stdio",
59
+ command: config.command,
60
+ args: config.args || [],
61
+ env: config.env || {},
62
+ };
63
+ }
64
+ }
65
+ throw new Error(`Server config not found for: ${serverName}\nTried: ${possiblePaths.join(", ")}`);
66
+ }
67
+ /**
68
+ * Load optional files from source code path
69
+ */
70
+ function loadSourceFiles(sourcePath) {
71
+ const result = {};
72
+ const readmePaths = ["README.md", "readme.md", "Readme.md"];
73
+ for (const readmePath of readmePaths) {
74
+ const fullPath = path.join(sourcePath, readmePath);
75
+ if (fs.existsSync(fullPath)) {
76
+ result.readmeContent = fs.readFileSync(fullPath, "utf-8");
77
+ break;
78
+ }
79
+ }
80
+ const packagePath = path.join(sourcePath, "package.json");
81
+ if (fs.existsSync(packagePath)) {
82
+ result.packageJson = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
83
+ }
84
+ const manifestPath = path.join(sourcePath, "manifest.json");
85
+ if (fs.existsSync(manifestPath)) {
86
+ result.manifestRaw = fs.readFileSync(manifestPath, "utf-8");
87
+ try {
88
+ result.manifestJson = JSON.parse(result.manifestRaw);
89
+ }
90
+ catch {
91
+ console.warn("[Assessment] Failed to parse manifest.json");
92
+ }
93
+ }
94
+ result.sourceCodeFiles = new Map();
95
+ const sourceExtensions = [".ts", ".js", ".py", ".go", ".rs"];
96
+ const loadSourceDir = (dir, prefix = "") => {
97
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
98
+ for (const entry of entries) {
99
+ if (entry.name.startsWith(".") || entry.name === "node_modules")
100
+ continue;
101
+ const fullPath = path.join(dir, entry.name);
102
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
103
+ if (entry.isDirectory()) {
104
+ loadSourceDir(fullPath, relativePath);
47
105
  }
48
- else {
49
- console.log(`\n📄 Results saved to: ${outputPath}\n`);
106
+ else if (sourceExtensions.some((ext) => entry.name.endsWith(ext))) {
107
+ try {
108
+ const content = fs.readFileSync(fullPath, "utf-8");
109
+ if (content.length < 100000) {
110
+ result.sourceCodeFiles.set(relativePath, content);
111
+ }
112
+ }
113
+ catch {
114
+ // Skip unreadable files
115
+ }
50
116
  }
51
- const exitCode = result.status === "FAIL" ? 1 : 0;
52
- setTimeout(() => process.exit(exitCode), 10);
53
- return;
54
117
  }
55
- const results = await runFullAssessment(options);
56
- // Pre-flight mode handles its own output and exit
57
- if (options.preflightOnly) {
58
- return;
118
+ };
119
+ try {
120
+ loadSourceDir(sourcePath);
121
+ }
122
+ catch (e) {
123
+ console.warn("[Assessment] Could not load source files:", e);
124
+ }
125
+ return result;
126
+ }
127
+ /**
128
+ * Connect to MCP server via configured transport
129
+ */
130
+ async function connectToServer(config) {
131
+ let transport;
132
+ switch (config.transport) {
133
+ case "http":
134
+ if (!config.url)
135
+ throw new Error("URL required for HTTP transport");
136
+ transport = new StreamableHTTPClientTransport(new URL(config.url));
137
+ break;
138
+ case "sse":
139
+ if (!config.url)
140
+ throw new Error("URL required for SSE transport");
141
+ transport = new SSEClientTransport(new URL(config.url));
142
+ break;
143
+ case "stdio":
144
+ default:
145
+ if (!config.command)
146
+ throw new Error("Command required for stdio transport");
147
+ transport = new StdioClientTransport({
148
+ command: config.command,
149
+ args: config.args,
150
+ env: {
151
+ ...Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined)),
152
+ ...config.env,
153
+ },
154
+ stderr: "pipe",
155
+ });
156
+ break;
157
+ }
158
+ const client = new Client({
159
+ name: "mcp-assess-full",
160
+ version: "1.0.0",
161
+ }, {
162
+ capabilities: {},
163
+ });
164
+ await client.connect(transport);
165
+ return client;
166
+ }
167
+ /**
168
+ * Create callTool wrapper for assessment context
169
+ */
170
+ function createCallToolWrapper(client) {
171
+ return async (name, params) => {
172
+ try {
173
+ const response = await client.callTool({
174
+ name,
175
+ arguments: params,
176
+ });
177
+ return {
178
+ content: response.content,
179
+ isError: response.isError || false,
180
+ structuredContent: response
181
+ .structuredContent,
182
+ };
59
183
  }
60
- // Handle comparison mode
61
- const comparison = handleComparison(results, options);
62
- // If comparison was requested but returned null, baseline file was not found
63
- if (options.comparePath && !comparison) {
64
- setTimeout(() => process.exit(1), 10);
65
- return;
184
+ catch (error) {
185
+ return {
186
+ content: [
187
+ {
188
+ type: "text",
189
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
190
+ },
191
+ ],
192
+ isError: true,
193
+ };
66
194
  }
67
- if (comparison?.diffOutputPath) {
68
- // Diff-only mode: output path and exit
69
- console.log(comparison.diffOutputPath);
70
- setTimeout(() => process.exit(comparison.exitCode), 10);
71
- return;
195
+ };
196
+ }
197
+ /**
198
+ * Build assessment configuration
199
+ */
200
+ function buildConfig(options) {
201
+ const config = {
202
+ ...DEFAULT_ASSESSMENT_CONFIG,
203
+ enableExtendedAssessment: options.fullAssessment !== false,
204
+ parallelTesting: true,
205
+ testTimeout: 30000,
206
+ };
207
+ if (options.auditMode) {
208
+ // Audit mode: only HIGH-value modules for automated MCP auditing
209
+ config.assessmentCategories = {
210
+ functionality: true,
211
+ security: true,
212
+ documentation: false,
213
+ errorHandling: true,
214
+ usability: false,
215
+ mcpSpecCompliance: true,
216
+ aupCompliance: false,
217
+ toolAnnotations: true,
218
+ prohibitedLibraries: false,
219
+ manifestValidation: false,
220
+ portability: false,
221
+ };
222
+ }
223
+ else if (options.fullAssessment !== false) {
224
+ config.assessmentCategories = {
225
+ functionality: true,
226
+ security: true,
227
+ documentation: true,
228
+ errorHandling: true,
229
+ usability: true,
230
+ mcpSpecCompliance: true,
231
+ aupCompliance: true,
232
+ toolAnnotations: true,
233
+ prohibitedLibraries: true,
234
+ manifestValidation: true,
235
+ portability: true,
236
+ };
237
+ }
238
+ if (options.claudeEnabled) {
239
+ config.claudeCode = {
240
+ enabled: true,
241
+ timeout: FULL_CLAUDE_CODE_CONFIG.timeout || 60000,
242
+ maxRetries: FULL_CLAUDE_CODE_CONFIG.maxRetries || 2,
243
+ features: {
244
+ intelligentTestGeneration: true,
245
+ aupSemanticAnalysis: true,
246
+ annotationInference: true,
247
+ documentationQuality: true,
248
+ },
249
+ };
250
+ }
251
+ return config;
252
+ }
253
+ /**
254
+ * Run full assessment
255
+ */
256
+ async function runFullAssessment(options) {
257
+ if (!options.jsonOnly) {
258
+ console.log(`\n🔍 Starting full assessment for: ${options.serverName}`);
259
+ }
260
+ const serverConfig = loadServerConfig(options.serverName, options.serverConfigPath);
261
+ if (!options.jsonOnly) {
262
+ console.log("✅ Server config loaded");
263
+ }
264
+ const client = await connectToServer(serverConfig);
265
+ if (!options.jsonOnly) {
266
+ console.log("✅ Connected to MCP server");
267
+ }
268
+ const response = await client.listTools();
269
+ const tools = response.tools || [];
270
+ if (!options.jsonOnly) {
271
+ console.log(`🔧 Found ${tools.length} tool${tools.length !== 1 ? "s" : ""}`);
272
+ }
273
+ const config = buildConfig(options);
274
+ const orchestrator = new AssessmentOrchestrator(config);
275
+ if (!options.jsonOnly) {
276
+ if (orchestrator.isClaudeEnabled()) {
277
+ console.log("🤖 Claude Code integration enabled");
72
278
  }
73
- // Display comparison summary if in comparison mode (not diff-only)
74
- if (comparison && !options.jsonOnly) {
75
- displayComparisonSummary(comparison.diff);
279
+ else if (options.claudeEnabled) {
280
+ console.log("⚠️ Claude Code requested but not available");
76
281
  }
77
- // Display results summary
282
+ }
283
+ let sourceFiles = {};
284
+ if (options.sourceCodePath && fs.existsSync(options.sourceCodePath)) {
285
+ sourceFiles = loadSourceFiles(options.sourceCodePath);
78
286
  if (!options.jsonOnly) {
79
- displaySummary(results);
287
+ console.log(`📁 Loaded source files from: ${options.sourceCodePath}`);
80
288
  }
81
- // Determine output format (Issue #136: Tiered output strategy)
82
- let effectiveFormat = options.outputFormat || "full";
83
- // Auto-tier if requested and results exceed threshold
84
- if (effectiveFormat === "full" &&
85
- options.autoTier &&
86
- shouldAutoTier(results)) {
87
- effectiveFormat = "tiered";
88
- if (!options.jsonOnly) {
89
- const estimate = formatTokenEstimate(Math.ceil(JSON.stringify(results).length / 4));
90
- console.log(`\n📊 Auto-tiering enabled: ${estimate.tokens} tokens (${estimate.recommendation})`);
91
- }
289
+ }
290
+ const context = {
291
+ serverName: options.serverName,
292
+ tools,
293
+ callTool: createCallToolWrapper(client),
294
+ config,
295
+ sourceCodePath: options.sourceCodePath,
296
+ transportType: serverConfig.transport || "stdio",
297
+ ...sourceFiles,
298
+ };
299
+ if (!options.jsonOnly) {
300
+ console.log(`\n🏃 Running assessment with ${Object.keys(config.assessmentCategories || {}).length} modules...`);
301
+ console.log("");
302
+ }
303
+ const results = await orchestrator.runFullAssessment(context);
304
+ await client.close();
305
+ return results;
306
+ }
307
+ /**
308
+ * Save results to JSON file
309
+ */
310
+ function saveResults(serverName, results, outputPath, transportType) {
311
+ const defaultPath = `/tmp/inspector-full-assessment-${serverName}.json`;
312
+ const finalPath = outputPath || defaultPath;
313
+ // Build audit summary for automated consumption
314
+ const securityResult = results.security;
315
+ const functionalityResult = results.functionality;
316
+ const mcpResult = results.mcpSpecCompliance;
317
+ const errorResult = results.errorHandling;
318
+ const auditSummary = {
319
+ highConfidenceVulnerabilities: securityResult?.auditAnalysis?.highConfidenceVulnerabilities || [],
320
+ needsReview: securityResult?.auditAnalysis?.needsReview || [],
321
+ falsePositiveLikelihood: securityResult?.auditAnalysis?.falsePositiveLikelihood || {},
322
+ functionalTools: functionalityResult?.workingTools || 0,
323
+ totalTools: functionalityResult?.totalTools || 0,
324
+ mcpComplianceScore: errorResult?.metrics?.mcpComplianceScore || 0,
325
+ transportType: transportType || "unknown",
326
+ recommendedAction: results.overallStatus === "PASS"
327
+ ? "APPROVE"
328
+ : results.overallStatus === "FAIL"
329
+ ? "REJECT"
330
+ : "REVIEW",
331
+ };
332
+ const output = {
333
+ timestamp: new Date().toISOString(),
334
+ assessmentType: "full",
335
+ auditSummary,
336
+ ...results,
337
+ };
338
+ fs.writeFileSync(finalPath, JSON.stringify(output, null, 2));
339
+ return finalPath;
340
+ }
341
+ /**
342
+ * Display summary
343
+ */
344
+ function displaySummary(results) {
345
+ const { overallStatus, summary, totalTestsRun, executionTime, functionality, security, aupCompliance, toolAnnotations, portability, documentation, errorHandling, mcpSpecCompliance, prohibitedLibraries, manifestValidation, } = results;
346
+ console.log("\n" + "=".repeat(70));
347
+ console.log("FULL ASSESSMENT RESULTS");
348
+ console.log("=".repeat(70));
349
+ console.log(`Server: ${results.serverName}`);
350
+ console.log(`Overall Status: ${overallStatus}`);
351
+ console.log(`Total Tests Run: ${totalTestsRun}`);
352
+ console.log(`Execution Time: ${executionTime}ms`);
353
+ console.log("-".repeat(70));
354
+ console.log("\n📊 MODULE STATUS:");
355
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
356
+ const modules = [
357
+ ["Functionality", functionality],
358
+ ["Security", security],
359
+ ["Documentation", documentation],
360
+ ["Error Handling", errorHandling],
361
+ ["MCP Spec Compliance", mcpSpecCompliance],
362
+ ["AUP Compliance", aupCompliance],
363
+ ["Tool Annotations", toolAnnotations],
364
+ ["Prohibited Libraries", prohibitedLibraries],
365
+ ["Manifest Validation", manifestValidation],
366
+ ["Portability", portability],
367
+ ];
368
+ for (const [name, module] of modules) {
369
+ if (module) {
370
+ const icon = module.status === "PASS"
371
+ ? "✅"
372
+ : module.status === "FAIL"
373
+ ? "❌"
374
+ : "⚠️";
375
+ console.log(` ${icon} ${name}: ${module.status}`);
92
376
  }
93
- // Save results in appropriate format
94
- let outputPath;
95
- if (effectiveFormat === "tiered") {
96
- const tieredOutput = saveTieredResults(options.serverName, results, options);
97
- outputPath = tieredOutput.outputDir;
98
- if (options.jsonOnly) {
99
- console.log(outputPath);
100
- }
101
- else {
102
- console.log(`\n📁 Tiered output saved to: ${outputPath}/`);
103
- console.log(` 📋 Executive Summary: executive-summary.json`);
104
- console.log(` 📋 Tool Summaries: tool-summaries.json`);
105
- console.log(` 📋 Tool Details: tools/ (${tieredOutput.toolDetailRefs.length} files)`);
106
- console.log(` 📊 Total tokens: ~${tieredOutput.executiveSummary.estimatedTokens + tieredOutput.toolSummaries.estimatedTokens} (summaries only)\n`);
107
- }
377
+ }
378
+ console.log("\n📋 KEY FINDINGS:");
379
+ console.log(` ${summary}`);
380
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
381
+ const securityModule = security;
382
+ if (securityModule?.vulnerabilities?.length > 0) {
383
+ const vulns = securityModule.vulnerabilities;
384
+ console.log(`\n🔒 SECURITY VULNERABILITIES (${vulns.length}):`);
385
+ for (const vuln of vulns.slice(0, 5)) {
386
+ console.log(` • ${vuln}`);
108
387
  }
109
- else if (effectiveFormat === "summary-only") {
110
- outputPath = saveSummaryOnly(options.serverName, results, options);
111
- if (options.jsonOnly) {
112
- console.log(outputPath);
113
- }
114
- else {
115
- console.log(`\n📁 Summary output saved to: ${outputPath}/`);
116
- console.log(` 📋 Executive Summary: executive-summary.json`);
117
- console.log(` 📋 Tool Summaries: tool-summaries.json\n`);
118
- }
388
+ if (vulns.length > 5) {
389
+ console.log(` ... and ${vulns.length - 5} more`);
390
+ }
391
+ }
392
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
393
+ const aupModule = aupCompliance;
394
+ if (aupModule?.violations?.length > 0) {
395
+ const violations = aupModule.violations;
396
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
397
+ const critical = violations.filter((v) => v.severity === "CRITICAL");
398
+ console.log(`\n⚖️ AUP FINDINGS:`);
399
+ console.log(` Total flagged: ${violations.length}`);
400
+ if (critical.length > 0) {
401
+ console.log(` 🚨 CRITICAL violations: ${critical.length}`);
402
+ }
403
+ }
404
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
405
+ const annotationsModule = toolAnnotations;
406
+ if (annotationsModule) {
407
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
408
+ const funcModule = functionality;
409
+ console.log(`\n🏷️ TOOL ANNOTATIONS:`);
410
+ console.log(` Annotated: ${annotationsModule.annotatedCount || 0}/${funcModule?.workingTools || 0}`);
411
+ if (annotationsModule.missingAnnotationsCount > 0) {
412
+ console.log(` Missing: ${annotationsModule.missingAnnotationsCount}`);
413
+ }
414
+ if (annotationsModule.misalignedAnnotationsCount > 0) {
415
+ console.log(` ⚠️ Misalignments: ${annotationsModule.misalignedAnnotationsCount}`);
416
+ }
417
+ }
418
+ if (results.recommendations?.length > 0) {
419
+ console.log("\n💡 RECOMMENDATIONS:");
420
+ for (const rec of results.recommendations.slice(0, 5)) {
421
+ console.log(` • ${rec}`);
422
+ }
423
+ }
424
+ console.log("\n" + "=".repeat(70));
425
+ }
426
+ /**
427
+ * Parse command-line arguments
428
+ */
429
+ function parseArgs() {
430
+ const args = process.argv.slice(2);
431
+ const options = {};
432
+ for (let i = 0; i < args.length; i++) {
433
+ const arg = args[i];
434
+ if (!arg)
435
+ continue;
436
+ switch (arg) {
437
+ case "--server":
438
+ case "-s":
439
+ options.serverName = args[++i];
440
+ break;
441
+ case "--config":
442
+ case "-c":
443
+ options.serverConfigPath = args[++i];
444
+ break;
445
+ case "--output":
446
+ case "-o":
447
+ options.outputPath = args[++i];
448
+ break;
449
+ case "--source":
450
+ options.sourceCodePath = args[++i];
451
+ break;
452
+ case "--claude-enabled":
453
+ options.claudeEnabled = true;
454
+ break;
455
+ case "--full":
456
+ options.fullAssessment = true;
457
+ break;
458
+ case "--audit-mode":
459
+ options.auditMode = true;
460
+ break;
461
+ case "--verbose":
462
+ case "-v":
463
+ options.verbose = true;
464
+ break;
465
+ case "--json":
466
+ options.jsonOnly = true;
467
+ break;
468
+ case "--help":
469
+ case "-h":
470
+ printHelp();
471
+ options.helpRequested = true;
472
+ return options;
473
+ default:
474
+ if (!arg.startsWith("-")) {
475
+ if (!options.serverName) {
476
+ options.serverName = arg;
477
+ }
478
+ }
479
+ else {
480
+ console.error(`Unknown argument: ${arg}`);
481
+ printHelp();
482
+ setTimeout(() => process.exit(1), 10);
483
+ options.helpRequested = true;
484
+ return options;
485
+ }
486
+ }
487
+ }
488
+ if (!options.serverName) {
489
+ console.error("Error: --server is required");
490
+ printHelp();
491
+ setTimeout(() => process.exit(1), 10);
492
+ options.helpRequested = true;
493
+ return options;
494
+ }
495
+ return options;
496
+ }
497
+ /**
498
+ * Print help message
499
+ */
500
+ function printHelp() {
501
+ console.log(`
502
+ Usage: mcp-assess-full [options] [server-name]
503
+
504
+ Run comprehensive MCP server assessment with all 11 assessor modules.
505
+
506
+ Options:
507
+ --server, -s <name> Server name (required, or pass as first positional arg)
508
+ --config, -c <path> Path to server config JSON
509
+ --output, -o <path> Output JSON path (default: /tmp/inspector-full-assessment-<server>.json)
510
+ --source <path> Source code path for deep analysis (AUP, portability, etc.)
511
+ --claude-enabled Enable Claude Code integration for intelligent analysis
512
+ --full Enable all assessment modules (default)
513
+ --audit-mode Run only high-value modules for automated MCP auditing
514
+ (Functionality, Security, ErrorHandling, MCPSpecCompliance, ToolAnnotations)
515
+ Reduces false positives and includes audit summary in output
516
+ --json Output only JSON (no console summary)
517
+ --verbose, -v Enable verbose logging
518
+ --help, -h Show this help message
519
+
520
+ Assessment Modules (11 total):
521
+ • Functionality - Tests all tools work correctly
522
+ • Security - Prompt injection & vulnerability testing
523
+ • Documentation - README completeness checks
524
+ • Error Handling - Validates error responses
525
+ • Usability - Input validation & UX
526
+ • MCP Spec - Protocol compliance
527
+ • AUP Compliance - Acceptable Use Policy checks
528
+ • Tool Annotations - readOnlyHint/destructiveHint validation
529
+ • Prohibited Libs - Dependency security checks
530
+ • Manifest - MCPB manifest.json validation
531
+ • Portability - Cross-platform compatibility
532
+
533
+ Examples:
534
+ mcp-assess-full my-server
535
+ mcp-assess-full --server broken-mcp --claude-enabled
536
+ mcp-assess-full --server my-server --source ./my-server --output ./results.json
537
+ `);
538
+ }
539
+ /**
540
+ * Main execution
541
+ */
542
+ async function main() {
543
+ try {
544
+ const options = parseArgs();
545
+ if (options.helpRequested) {
546
+ return;
547
+ }
548
+ const results = await runFullAssessment(options);
549
+ if (!options.jsonOnly) {
550
+ displaySummary(results);
551
+ }
552
+ // Determine transport type for audit summary
553
+ const serverConfig = loadServerConfig(options.serverName, options.serverConfigPath);
554
+ const outputPath = saveResults(options.serverName, results, options.outputPath, serverConfig.transport || "stdio");
555
+ if (options.jsonOnly) {
556
+ console.log(outputPath);
119
557
  }
120
558
  else {
121
- // Default: full output
122
- outputPath = saveResults(options.serverName, results, options);
123
- if (options.jsonOnly) {
124
- console.log(outputPath);
125
- }
126
- else {
127
- console.log(`📄 Results saved to: ${outputPath}\n`);
128
- }
559
+ console.log(`📄 Results saved to: ${outputPath}\n`);
129
560
  }
130
- // Exit with appropriate code
131
561
  const exitCode = results.overallStatus === "FAIL" ? 1 : 0;
132
562
  setTimeout(() => process.exit(exitCode), 10);
133
563
  }
@@ -139,9 +569,5 @@ async function main() {
139
569
  }
140
570
  setTimeout(() => process.exit(1), 10);
141
571
  }
142
- finally {
143
- // Restore original listener configuration
144
- listenerConfig.restore();
145
- }
146
572
  }
147
573
  main();
@@ -11,73 +11,58 @@
11
11
  */
12
12
  import * as fs from "fs";
13
13
  import * as path from "path";
14
- import { execSync } from "child_process";
14
+ import * as os from "os";
15
15
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
16
16
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
17
17
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
18
18
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
19
- // Import shared server config loading (Issue #84 - Zod validation)
20
- import { loadServerConfig } from "./lib/assessment-runner/server-config.js";
21
- /**
22
- * Validate that a command is safe to execute
23
- * - Must be an absolute path or resolvable via PATH
24
- * - Must not contain shell metacharacters
25
- */
26
- function validateCommand(command) {
27
- // Check for shell metacharacters that could indicate injection
28
- const dangerousChars = /[;&|`$(){}[\]<>!\\]/;
29
- if (dangerousChars.test(command)) {
30
- throw new Error(`Invalid command: contains shell metacharacters: ${command}`);
31
- }
32
- // Verify the command exists and is executable
33
- try {
34
- // Use 'which' on Unix-like systems, 'where' on Windows
35
- const whichCmd = process.platform === "win32" ? "where" : "which";
36
- execSync(`${whichCmd} "${command}"`, { stdio: "pipe" });
37
- }
38
- catch {
39
- // Check if it's an absolute path that exists
40
- if (path.isAbsolute(command) && fs.existsSync(command)) {
41
- try {
42
- fs.accessSync(command, fs.constants.X_OK);
43
- return; // Command exists and is executable
44
- }
45
- catch {
46
- throw new Error(`Command not executable: ${command}`);
47
- }
48
- }
49
- throw new Error(`Command not found: ${command}`);
50
- }
51
- }
19
+ // Import from local client lib (will use package exports when published)
20
+ import { SecurityAssessor } from "../../client/lib/services/assessment/modules/SecurityAssessor.js";
21
+ import { DEFAULT_ASSESSMENT_CONFIG, } from "../../client/lib/lib/assessmentTypes.js";
52
22
  /**
53
- * Validate environment variables from config
54
- * - Keys must be valid env var names (alphanumeric + underscore)
55
- * - Values should not contain null bytes
23
+ * Load server configuration from Claude Code's MCP settings
56
24
  */
57
- function validateEnvVars(env) {
58
- if (!env)
59
- return {};
60
- const validatedEnv = {};
61
- const validKeyPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
62
- for (const [key, value] of Object.entries(env)) {
63
- // Validate key format
64
- if (!validKeyPattern.test(key)) {
65
- console.warn(`Skipping invalid environment variable name: ${key} (must match [a-zA-Z_][a-zA-Z0-9_]*)`);
25
+ function loadServerConfig(serverName, configPath) {
26
+ const possiblePaths = [
27
+ configPath,
28
+ path.join(os.homedir(), ".config", "mcp", "servers", `${serverName}.json`),
29
+ path.join(os.homedir(), ".config", "claude", "claude_desktop_config.json"),
30
+ ].filter(Boolean);
31
+ for (const tryPath of possiblePaths) {
32
+ if (!fs.existsSync(tryPath))
66
33
  continue;
34
+ const config = JSON.parse(fs.readFileSync(tryPath, "utf-8"));
35
+ if (config.mcpServers && config.mcpServers[serverName]) {
36
+ const serverConfig = config.mcpServers[serverName];
37
+ return {
38
+ transport: "stdio",
39
+ command: serverConfig.command,
40
+ args: serverConfig.args || [],
41
+ env: serverConfig.env || {},
42
+ };
67
43
  }
68
- // Check for null bytes in value (could truncate strings)
69
- if (typeof value === "string" && value.includes("\0")) {
70
- console.warn(`Skipping environment variable with null byte: ${key}`);
71
- continue;
44
+ if (config.url ||
45
+ config.transport === "http" ||
46
+ config.transport === "sse") {
47
+ if (!config.url) {
48
+ throw new Error(`Invalid server config: transport is '${config.transport}' but 'url' is missing`);
49
+ }
50
+ return {
51
+ transport: config.transport || "http",
52
+ url: config.url,
53
+ };
54
+ }
55
+ if (config.command) {
56
+ return {
57
+ transport: "stdio",
58
+ command: config.command,
59
+ args: config.args || [],
60
+ env: config.env || {},
61
+ };
72
62
  }
73
- validatedEnv[key] = String(value);
74
63
  }
75
- return validatedEnv;
64
+ throw new Error(`Server config not found for: ${serverName}\nTried: ${possiblePaths.join(", ")}`);
76
65
  }
77
- // Import from local client lib (will use package exports when published)
78
- import { SecurityAssessor } from "../../client/lib/services/assessment/modules/SecurityAssessor.js";
79
- import { DEFAULT_ASSESSMENT_CONFIG, } from "../../client/lib/lib/assessmentTypes.js";
80
- import { loadPerformanceConfig } from "../../client/lib/services/assessment/config/performanceConfig.js";
81
66
  /**
82
67
  * Connect to MCP server via configured transport
83
68
  */
@@ -98,16 +83,12 @@ async function connectToServer(config) {
98
83
  default:
99
84
  if (!config.command)
100
85
  throw new Error("Command required for stdio transport");
101
- // Validate command before execution to prevent injection attacks
102
- validateCommand(config.command);
103
- // Validate and sanitize environment variables from config
104
- const validatedEnv = validateEnvVars(config.env);
105
86
  transport = new StdioClientTransport({
106
87
  command: config.command,
107
88
  args: config.args,
108
89
  env: {
109
90
  ...Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined)),
110
- ...validatedEnv,
91
+ ...config.env,
111
92
  },
112
93
  stderr: "pipe",
113
94
  });
@@ -172,22 +153,6 @@ function createCallToolWrapper(client) {
172
153
  async function runSecurityAssessment(options) {
173
154
  console.log(`\n🔍 Connecting to MCP server: ${options.serverName}`);
174
155
  const serverConfig = loadServerConfig(options.serverName, options.serverConfigPath);
175
- // Load custom performance config if provided (Issue #37)
176
- // Note: Currently, modules use DEFAULT_PERFORMANCE_CONFIG directly.
177
- // This validates the config file but doesn't override runtime values yet.
178
- if (options.performanceConfigPath) {
179
- try {
180
- const performanceConfig = loadPerformanceConfig(options.performanceConfigPath);
181
- console.log(`📊 Performance config loaded from: ${options.performanceConfigPath}`);
182
- console.log(` Batch interval: ${performanceConfig.batchFlushIntervalMs}ms, ` +
183
- `Security batch: ${performanceConfig.securityBatchSize}`);
184
- // TODO: Wire performanceConfig through to SecurityAssessor
185
- }
186
- catch (error) {
187
- console.error(`❌ Failed to load performance config: ${error instanceof Error ? error.message : String(error)}`);
188
- throw error;
189
- }
190
- }
191
156
  const client = await connectToServer(serverConfig);
192
157
  console.log("✅ Connected successfully");
193
158
  const tools = await getTools(client, options.toolName);
@@ -197,7 +162,7 @@ async function runSecurityAssessment(options) {
197
162
  }
198
163
  const config = {
199
164
  ...DEFAULT_ASSESSMENT_CONFIG,
200
- securityPatternsToTest: 30,
165
+ securityPatternsToTest: 13,
201
166
  reviewerMode: false,
202
167
  testTimeout: 30000,
203
168
  };
@@ -206,8 +171,9 @@ async function runSecurityAssessment(options) {
206
171
  tools,
207
172
  callTool: createCallToolWrapper(client),
208
173
  config,
174
+ transportType: serverConfig.transport || "stdio",
209
175
  };
210
- console.log(`🛡️ Running security assessment with 30 attack patterns...`);
176
+ console.log(`🛡️ Running security assessment with 13 attack patterns...`);
211
177
  const assessor = new SecurityAssessor(config);
212
178
  const results = await assessor.assess(context);
213
179
  await client.close();
@@ -287,9 +253,6 @@ function parseArgs() {
287
253
  case "-v":
288
254
  options.verbose = true;
289
255
  break;
290
- case "--performance-config":
291
- options.performanceConfigPath = args[++i];
292
- break;
293
256
  case "--help":
294
257
  case "-h":
295
258
  printHelp();
@@ -326,23 +289,24 @@ function printHelp() {
326
289
  console.log(`
327
290
  Usage: mcp-assess-security [options] [server-name]
328
291
 
329
- Run security assessment against an MCP server with 30 attack patterns.
292
+ Run security assessment against an MCP server with 13 attack patterns.
330
293
 
331
294
  Options:
332
295
  --server, -s <name> Server name (required, or pass as first positional arg)
333
296
  --config, -c <path> Path to server config JSON
334
297
  --output, -o <path> Output JSON path (default: /tmp/inspector-security-assessment-<server>.json)
335
298
  --tool, -t <name> Test only specific tool (default: test all tools)
336
- --performance-config <path> Path to performance tuning JSON (batch sizes, timeouts, etc.)
337
299
  --verbose, -v Enable verbose logging
338
300
  --help, -h Show this help message
339
301
 
340
- Attack Patterns Tested (30 total):
341
- • Command Injection, SQL Injection, Path Traversal
342
- • Calculator Injection, Code Execution, XXE
343
- Data Exfiltration, Token Theft, NoSQL Injection
344
- Unicode Bypass, Nested Injection, Package Squatting
345
- Session Management, Auth Bypass, and more...
302
+ Attack Patterns Tested (13 total):
303
+ • Command Injection SQL Injection
304
+ • Calculator Injection Path Traversal
305
+ Type Safety • Boundary Testing
306
+ Required Fields • MCP Error Format
307
+ Timeout Handling • Indirect Prompt Injection
308
+ • Unicode Bypass • Nested Injection
309
+ • Package Squatting
346
310
 
347
311
  Examples:
348
312
  mcp-assess-security my-server
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bryan-thompson/inspector-assessment-cli",
3
- "version": "1.43.2",
3
+ "version": "1.43.3",
4
4
  "description": "CLI for the Enhanced MCP Inspector with assessment capabilities",
5
5
  "license": "MIT",
6
6
  "author": "Bryan Thompson <bryan@triepod.ai>",
@@ -1,282 +0,0 @@
1
- /**
2
- * Tests for Zod Error Formatting Utilities
3
- *
4
- * Validates the error formatting functions for CLI-friendly output.
5
- *
6
- * @module cli/lib/__tests__/zodErrorFormatter
7
- */
8
- // Uses Jest globals (describe, test, expect)
9
- import { jest } from "@jest/globals";
10
- import { z } from "zod";
11
- import { formatZodIssue, formatZodError, formatZodErrorIndented, printZodErrorForCli, zodErrorToArray, formatZodErrorForJson, formatUserFriendlyError, } from "../zodErrorFormatter.js";
12
- // Helper to create a ZodError with specific issues
13
- function createZodError(issues) {
14
- // Create a schema that will fail and then manipulate the error
15
- const schema = z.object({ dummy: z.string() });
16
- const result = schema.safeParse({ dummy: 123 });
17
- if (!result.success) {
18
- // Replace the errors with our custom issues
19
- result.error.issues = issues.map((issue) => ({
20
- code: issue.code || "custom",
21
- path: issue.path,
22
- message: issue.message,
23
- }));
24
- return result.error;
25
- }
26
- throw new Error("Failed to create ZodError");
27
- }
28
- describe("zodErrorFormatter", () => {
29
- describe("formatZodIssue", () => {
30
- test("formats issue with empty path (message only)", () => {
31
- const issue = { code: "custom", path: [], message: "Required" };
32
- const result = formatZodIssue(issue);
33
- expect(result).toBe("Required");
34
- });
35
- test("formats issue with single-level path", () => {
36
- const issue = {
37
- code: "custom",
38
- path: ["field"],
39
- message: "Invalid value",
40
- };
41
- const result = formatZodIssue(issue);
42
- expect(result).toBe("field: Invalid value");
43
- });
44
- test("formats issue with nested path", () => {
45
- const issue = {
46
- code: "custom",
47
- path: ["config", "nested", "field"],
48
- message: "Must be a string",
49
- };
50
- const result = formatZodIssue(issue);
51
- expect(result).toBe("config.nested.field: Must be a string");
52
- });
53
- test("formats issue with array index in path", () => {
54
- const issue = {
55
- code: "custom",
56
- path: ["items", 0, "name"],
57
- message: "Required",
58
- };
59
- const result = formatZodIssue(issue);
60
- expect(result).toBe("items.0.name: Required");
61
- });
62
- });
63
- describe("formatZodError", () => {
64
- test("formats single error", () => {
65
- const error = createZodError([
66
- { path: ["field"], message: "Invalid value" },
67
- ]);
68
- const result = formatZodError(error);
69
- expect(result).toBe("field: Invalid value");
70
- });
71
- test("formats multiple errors with newlines", () => {
72
- const error = createZodError([
73
- { path: ["field1"], message: "Error 1" },
74
- { path: ["field2"], message: "Error 2" },
75
- { path: ["field3"], message: "Error 3" },
76
- ]);
77
- const result = formatZodError(error);
78
- expect(result).toBe("field1: Error 1\nfield2: Error 2\nfield3: Error 3");
79
- });
80
- test("handles error with empty path", () => {
81
- const error = createZodError([{ path: [], message: "Global error" }]);
82
- const result = formatZodError(error);
83
- expect(result).toBe("Global error");
84
- });
85
- });
86
- describe("formatZodErrorIndented", () => {
87
- test("uses default indentation (two spaces)", () => {
88
- const error = createZodError([
89
- { path: ["field"], message: "Invalid value" },
90
- ]);
91
- const result = formatZodErrorIndented(error);
92
- expect(result).toBe(" field: Invalid value");
93
- });
94
- test("uses custom indentation", () => {
95
- const error = createZodError([
96
- { path: ["field"], message: "Invalid value" },
97
- ]);
98
- const result = formatZodErrorIndented(error, "\t");
99
- expect(result).toBe("\tfield: Invalid value");
100
- });
101
- test("uses four spaces indentation", () => {
102
- const error = createZodError([
103
- { path: ["field"], message: "Invalid value" },
104
- ]);
105
- const result = formatZodErrorIndented(error, " ");
106
- expect(result).toBe(" field: Invalid value");
107
- });
108
- test("applies indent to each line for multiple errors", () => {
109
- const error = createZodError([
110
- { path: ["field1"], message: "Error 1" },
111
- { path: ["field2"], message: "Error 2" },
112
- ]);
113
- const result = formatZodErrorIndented(error, ">> ");
114
- expect(result).toBe(">> field1: Error 1\n>> field2: Error 2");
115
- });
116
- test("uses empty indentation", () => {
117
- const error = createZodError([
118
- { path: ["field"], message: "Invalid value" },
119
- ]);
120
- const result = formatZodErrorIndented(error, "");
121
- expect(result).toBe("field: Invalid value");
122
- });
123
- });
124
- describe("printZodErrorForCli", () => {
125
- let consoleSpy;
126
- beforeEach(() => {
127
- consoleSpy = jest.spyOn(console, "error").mockImplementation(() => { });
128
- });
129
- afterEach(() => {
130
- consoleSpy.mockRestore();
131
- });
132
- test("prints with context prefix", () => {
133
- const error = createZodError([
134
- { path: ["field"], message: "Invalid value" },
135
- ]);
136
- printZodErrorForCli(error, "config file");
137
- expect(consoleSpy).toHaveBeenCalledWith("Error in config file:\n field: Invalid value");
138
- });
139
- test("prints without context (default prefix)", () => {
140
- const error = createZodError([
141
- { path: ["field"], message: "Invalid value" },
142
- ]);
143
- printZodErrorForCli(error);
144
- expect(consoleSpy).toHaveBeenCalledWith("Validation error:\n field: Invalid value");
145
- });
146
- test("outputs multiple errors with indentation", () => {
147
- const error = createZodError([
148
- { path: ["field1"], message: "Error 1" },
149
- { path: ["field2"], message: "Error 2" },
150
- ]);
151
- printZodErrorForCli(error, "CLI arguments");
152
- expect(consoleSpy).toHaveBeenCalledWith("Error in CLI arguments:\n field1: Error 1\n field2: Error 2");
153
- });
154
- });
155
- describe("zodErrorToArray", () => {
156
- test("converts single error to array", () => {
157
- const error = createZodError([
158
- { path: ["field"], message: "Invalid value" },
159
- ]);
160
- const result = zodErrorToArray(error);
161
- expect(result).toEqual(["field: Invalid value"]);
162
- });
163
- test("converts multiple errors to array", () => {
164
- const error = createZodError([
165
- { path: ["field1"], message: "Error 1" },
166
- { path: ["field2"], message: "Error 2" },
167
- { path: ["field3"], message: "Error 3" },
168
- ]);
169
- const result = zodErrorToArray(error);
170
- expect(result).toEqual([
171
- "field1: Error 1",
172
- "field2: Error 2",
173
- "field3: Error 3",
174
- ]);
175
- });
176
- test("returns array preserving order", () => {
177
- const error = createZodError([
178
- { path: ["z"], message: "Z error" },
179
- { path: ["a"], message: "A error" },
180
- { path: ["m"], message: "M error" },
181
- ]);
182
- const result = zodErrorToArray(error);
183
- expect(result).toEqual(["z: Z error", "a: A error", "m: M error"]);
184
- });
185
- });
186
- describe("formatZodErrorForJson", () => {
187
- test("includes message field", () => {
188
- const error = createZodError([
189
- { path: ["field"], message: "Invalid value" },
190
- ]);
191
- const result = formatZodErrorForJson(error);
192
- expect(result.message).toBe("Validation failed");
193
- });
194
- test("includes errors array with path, message, code", () => {
195
- const error = createZodError([
196
- { path: ["field"], message: "Invalid value", code: "invalid_type" },
197
- ]);
198
- const result = formatZodErrorForJson(error);
199
- expect(result.errors).toHaveLength(1);
200
- expect(result.errors[0]).toEqual({
201
- path: ["field"],
202
- message: "Invalid value",
203
- code: "invalid_type",
204
- });
205
- });
206
- test("preserves path as array with numbers and strings", () => {
207
- const error = createZodError([
208
- { path: ["items", 0, "nested", 1, "field"], message: "Error" },
209
- ]);
210
- const result = formatZodErrorForJson(error);
211
- expect(result.errors[0].path).toEqual(["items", 0, "nested", 1, "field"]);
212
- });
213
- test("handles multiple errors", () => {
214
- const error = createZodError([
215
- { path: ["field1"], message: "Error 1", code: "too_small" },
216
- { path: ["field2"], message: "Error 2", code: "invalid_type" },
217
- ]);
218
- const result = formatZodErrorForJson(error);
219
- expect(result.errors).toHaveLength(2);
220
- expect(result.errors[0].code).toBe("too_small");
221
- expect(result.errors[1].code).toBe("invalid_type");
222
- });
223
- });
224
- describe("formatUserFriendlyError", () => {
225
- test("single error returns plain message", () => {
226
- const error = createZodError([
227
- { path: ["username"], message: "Required" },
228
- ]);
229
- const result = formatUserFriendlyError(error);
230
- expect(result).toBe("username: Required");
231
- });
232
- test("single error with empty path returns message only", () => {
233
- const error = createZodError([{ path: [], message: "Invalid input" }]);
234
- const result = formatUserFriendlyError(error);
235
- expect(result).toBe("Invalid input");
236
- });
237
- test("multiple errors returns bulleted list", () => {
238
- const error = createZodError([
239
- { path: ["field1"], message: "Error 1" },
240
- { path: ["field2"], message: "Error 2" },
241
- ]);
242
- const result = formatUserFriendlyError(error);
243
- expect(result).toBe("Multiple validation errors:\n - field1: Error 1\n - field2: Error 2");
244
- });
245
- test("applies field label mapping", () => {
246
- const error = createZodError([
247
- { path: ["serverName"], message: "Required" },
248
- ]);
249
- const labels = { serverName: "Server Name" };
250
- const result = formatUserFriendlyError(error, labels);
251
- expect(result).toBe("Server Name: Required");
252
- });
253
- test("applies labels for multiple errors", () => {
254
- const error = createZodError([
255
- { path: ["serverName"], message: "Required" },
256
- { path: ["configPath"], message: "Invalid path" },
257
- ]);
258
- const labels = {
259
- serverName: "Server Name",
260
- configPath: "Configuration Path",
261
- };
262
- const result = formatUserFriendlyError(error, labels);
263
- expect(result).toBe("Multiple validation errors:\n - Server Name: Required\n - Configuration Path: Invalid path");
264
- });
265
- test("falls back to path when no matching label", () => {
266
- const error = createZodError([
267
- { path: ["unknownField"], message: "Error" },
268
- ]);
269
- const labels = { otherField: "Other Field" };
270
- const result = formatUserFriendlyError(error, labels);
271
- expect(result).toBe("unknownField: Error");
272
- });
273
- test("handles nested path with labels", () => {
274
- const error = createZodError([
275
- { path: ["config", "nested"], message: "Invalid" },
276
- ]);
277
- const labels = { "config.nested": "Nested Config" };
278
- const result = formatUserFriendlyError(error, labels);
279
- expect(result).toBe("Nested Config: Invalid");
280
- });
281
- });
282
- });