@bryan-thompson/inspector-assessment-cli 1.5.0 → 1.7.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.
@@ -0,0 +1,528 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Full Assessment Runner CLI
4
+ *
5
+ * Runs comprehensive MCP server assessment using AssessmentOrchestrator
6
+ * with all 11 assessor modules and optional Claude Code integration.
7
+ *
8
+ * Usage:
9
+ * mcp-assess-full --server <server-name> [--claude-enabled] [--full]
10
+ * mcp-assess-full my-server --source ./my-server --output ./results.json
11
+ */
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";
23
+ /**
24
+ * Load server configuration from Claude Code's MCP settings
25
+ */
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
+ };
44
+ }
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`);
50
+ }
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);
105
+ }
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
+ }
116
+ }
117
+ }
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
+ };
183
+ }
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
+ };
194
+ }
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.fullAssessment !== false) {
208
+ config.assessmentCategories = {
209
+ functionality: true,
210
+ security: true,
211
+ documentation: true,
212
+ errorHandling: true,
213
+ usability: true,
214
+ mcpSpecCompliance: true,
215
+ aupCompliance: true,
216
+ toolAnnotations: true,
217
+ prohibitedLibraries: true,
218
+ manifestValidation: true,
219
+ portability: true,
220
+ };
221
+ }
222
+ if (options.claudeEnabled) {
223
+ config.claudeCode = {
224
+ enabled: true,
225
+ timeout: FULL_CLAUDE_CODE_CONFIG.timeout || 60000,
226
+ maxRetries: FULL_CLAUDE_CODE_CONFIG.maxRetries || 2,
227
+ features: {
228
+ intelligentTestGeneration: true,
229
+ aupSemanticAnalysis: true,
230
+ annotationInference: true,
231
+ documentationQuality: true,
232
+ },
233
+ };
234
+ }
235
+ return config;
236
+ }
237
+ /**
238
+ * Run full assessment
239
+ */
240
+ async function runFullAssessment(options) {
241
+ if (!options.jsonOnly) {
242
+ console.log(`\n🔍 Starting full assessment for: ${options.serverName}`);
243
+ }
244
+ const serverConfig = loadServerConfig(options.serverName, options.serverConfigPath);
245
+ if (!options.jsonOnly) {
246
+ console.log("✅ Server config loaded");
247
+ }
248
+ const client = await connectToServer(serverConfig);
249
+ if (!options.jsonOnly) {
250
+ console.log("✅ Connected to MCP server");
251
+ }
252
+ const response = await client.listTools();
253
+ const tools = response.tools || [];
254
+ if (!options.jsonOnly) {
255
+ console.log(`🔧 Found ${tools.length} tool${tools.length !== 1 ? "s" : ""}`);
256
+ }
257
+ const config = buildConfig(options);
258
+ const orchestrator = new AssessmentOrchestrator(config);
259
+ if (!options.jsonOnly) {
260
+ if (orchestrator.isClaudeEnabled()) {
261
+ console.log("🤖 Claude Code integration enabled");
262
+ }
263
+ else if (options.claudeEnabled) {
264
+ console.log("⚠️ Claude Code requested but not available");
265
+ }
266
+ }
267
+ let sourceFiles = {};
268
+ if (options.sourceCodePath && fs.existsSync(options.sourceCodePath)) {
269
+ sourceFiles = loadSourceFiles(options.sourceCodePath);
270
+ if (!options.jsonOnly) {
271
+ console.log(`📁 Loaded source files from: ${options.sourceCodePath}`);
272
+ }
273
+ }
274
+ const context = {
275
+ serverName: options.serverName,
276
+ tools,
277
+ callTool: createCallToolWrapper(client),
278
+ config,
279
+ sourceCodePath: options.sourceCodePath,
280
+ ...sourceFiles,
281
+ };
282
+ if (!options.jsonOnly) {
283
+ console.log(`\n🏃 Running assessment with ${Object.keys(config.assessmentCategories || {}).length} modules...`);
284
+ console.log("");
285
+ }
286
+ const results = await orchestrator.runFullAssessment(context);
287
+ await client.close();
288
+ return results;
289
+ }
290
+ /**
291
+ * Save results to JSON file
292
+ */
293
+ function saveResults(serverName, results, outputPath) {
294
+ const defaultPath = `/tmp/inspector-full-assessment-${serverName}.json`;
295
+ const finalPath = outputPath || defaultPath;
296
+ const output = {
297
+ timestamp: new Date().toISOString(),
298
+ assessmentType: "full",
299
+ ...results,
300
+ };
301
+ fs.writeFileSync(finalPath, JSON.stringify(output, null, 2));
302
+ return finalPath;
303
+ }
304
+ /**
305
+ * Display summary
306
+ */
307
+ function displaySummary(results) {
308
+ const { overallStatus, summary, totalTestsRun, executionTime, functionality, security, aupCompliance, toolAnnotations, portability, documentation, errorHandling, mcpSpecCompliance, prohibitedLibraries, manifestValidation, } = results;
309
+ console.log("\n" + "=".repeat(70));
310
+ console.log("FULL ASSESSMENT RESULTS");
311
+ console.log("=".repeat(70));
312
+ console.log(`Server: ${results.serverName}`);
313
+ console.log(`Overall Status: ${overallStatus}`);
314
+ console.log(`Total Tests Run: ${totalTestsRun}`);
315
+ console.log(`Execution Time: ${executionTime}ms`);
316
+ console.log("-".repeat(70));
317
+ console.log("\n📊 MODULE STATUS:");
318
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
319
+ const modules = [
320
+ ["Functionality", functionality],
321
+ ["Security", security],
322
+ ["Documentation", documentation],
323
+ ["Error Handling", errorHandling],
324
+ ["MCP Spec Compliance", mcpSpecCompliance],
325
+ ["AUP Compliance", aupCompliance],
326
+ ["Tool Annotations", toolAnnotations],
327
+ ["Prohibited Libraries", prohibitedLibraries],
328
+ ["Manifest Validation", manifestValidation],
329
+ ["Portability", portability],
330
+ ];
331
+ for (const [name, module] of modules) {
332
+ if (module) {
333
+ const icon = module.status === "PASS"
334
+ ? "✅"
335
+ : module.status === "FAIL"
336
+ ? "❌"
337
+ : "⚠️";
338
+ console.log(` ${icon} ${name}: ${module.status}`);
339
+ }
340
+ }
341
+ console.log("\n📋 KEY FINDINGS:");
342
+ console.log(` ${summary}`);
343
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
344
+ const securityModule = security;
345
+ if (securityModule?.vulnerabilities?.length > 0) {
346
+ const vulns = securityModule.vulnerabilities;
347
+ console.log(`\n🔒 SECURITY VULNERABILITIES (${vulns.length}):`);
348
+ for (const vuln of vulns.slice(0, 5)) {
349
+ console.log(` • ${vuln}`);
350
+ }
351
+ if (vulns.length > 5) {
352
+ console.log(` ... and ${vulns.length - 5} more`);
353
+ }
354
+ }
355
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
356
+ const aupModule = aupCompliance;
357
+ if (aupModule?.violations?.length > 0) {
358
+ const violations = aupModule.violations;
359
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
360
+ const critical = violations.filter((v) => v.severity === "CRITICAL");
361
+ console.log(`\n⚖️ AUP FINDINGS:`);
362
+ console.log(` Total flagged: ${violations.length}`);
363
+ if (critical.length > 0) {
364
+ console.log(` 🚨 CRITICAL violations: ${critical.length}`);
365
+ }
366
+ }
367
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
368
+ const annotationsModule = toolAnnotations;
369
+ if (annotationsModule) {
370
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
371
+ const funcModule = functionality;
372
+ console.log(`\n🏷️ TOOL ANNOTATIONS:`);
373
+ console.log(` Annotated: ${annotationsModule.annotatedCount || 0}/${funcModule?.workingTools || 0}`);
374
+ if (annotationsModule.missingAnnotationsCount > 0) {
375
+ console.log(` Missing: ${annotationsModule.missingAnnotationsCount}`);
376
+ }
377
+ if (annotationsModule.misalignedAnnotationsCount > 0) {
378
+ console.log(` ⚠️ Misalignments: ${annotationsModule.misalignedAnnotationsCount}`);
379
+ }
380
+ }
381
+ if (results.recommendations?.length > 0) {
382
+ console.log("\n💡 RECOMMENDATIONS:");
383
+ for (const rec of results.recommendations.slice(0, 5)) {
384
+ console.log(` • ${rec}`);
385
+ }
386
+ }
387
+ console.log("\n" + "=".repeat(70));
388
+ }
389
+ /**
390
+ * Parse command-line arguments
391
+ */
392
+ function parseArgs() {
393
+ const args = process.argv.slice(2);
394
+ const options = {};
395
+ for (let i = 0; i < args.length; i++) {
396
+ const arg = args[i];
397
+ if (!arg)
398
+ continue;
399
+ switch (arg) {
400
+ case "--server":
401
+ case "-s":
402
+ options.serverName = args[++i];
403
+ break;
404
+ case "--config":
405
+ case "-c":
406
+ options.serverConfigPath = args[++i];
407
+ break;
408
+ case "--output":
409
+ case "-o":
410
+ options.outputPath = args[++i];
411
+ break;
412
+ case "--source":
413
+ options.sourceCodePath = args[++i];
414
+ break;
415
+ case "--claude-enabled":
416
+ options.claudeEnabled = true;
417
+ break;
418
+ case "--full":
419
+ options.fullAssessment = true;
420
+ break;
421
+ case "--verbose":
422
+ case "-v":
423
+ options.verbose = true;
424
+ break;
425
+ case "--json":
426
+ options.jsonOnly = true;
427
+ break;
428
+ case "--help":
429
+ case "-h":
430
+ printHelp();
431
+ options.helpRequested = true;
432
+ return options;
433
+ default:
434
+ if (!arg.startsWith("-")) {
435
+ if (!options.serverName) {
436
+ options.serverName = arg;
437
+ }
438
+ }
439
+ else {
440
+ console.error(`Unknown argument: ${arg}`);
441
+ printHelp();
442
+ setTimeout(() => process.exit(1), 10);
443
+ options.helpRequested = true;
444
+ return options;
445
+ }
446
+ }
447
+ }
448
+ if (!options.serverName) {
449
+ console.error("Error: --server is required");
450
+ printHelp();
451
+ setTimeout(() => process.exit(1), 10);
452
+ options.helpRequested = true;
453
+ return options;
454
+ }
455
+ return options;
456
+ }
457
+ /**
458
+ * Print help message
459
+ */
460
+ function printHelp() {
461
+ console.log(`
462
+ Usage: mcp-assess-full [options] [server-name]
463
+
464
+ Run comprehensive MCP server assessment with all 11 assessor modules.
465
+
466
+ Options:
467
+ --server, -s <name> Server name (required, or pass as first positional arg)
468
+ --config, -c <path> Path to server config JSON
469
+ --output, -o <path> Output JSON path (default: /tmp/inspector-full-assessment-<server>.json)
470
+ --source <path> Source code path for deep analysis (AUP, portability, etc.)
471
+ --claude-enabled Enable Claude Code integration for intelligent analysis
472
+ --full Enable all assessment modules (default)
473
+ --json Output only JSON (no console summary)
474
+ --verbose, -v Enable verbose logging
475
+ --help, -h Show this help message
476
+
477
+ Assessment Modules (11 total):
478
+ • Functionality - Tests all tools work correctly
479
+ • Security - Prompt injection & vulnerability testing
480
+ • Documentation - README completeness checks
481
+ • Error Handling - Validates error responses
482
+ • Usability - Input validation & UX
483
+ • MCP Spec - Protocol compliance
484
+ • AUP Compliance - Acceptable Use Policy checks
485
+ • Tool Annotations - readOnlyHint/destructiveHint validation
486
+ • Prohibited Libs - Dependency security checks
487
+ • Manifest - MCPB manifest.json validation
488
+ • Portability - Cross-platform compatibility
489
+
490
+ Examples:
491
+ mcp-assess-full my-server
492
+ mcp-assess-full --server broken-mcp --claude-enabled
493
+ mcp-assess-full --server my-server --source ./my-server --output ./results.json
494
+ `);
495
+ }
496
+ /**
497
+ * Main execution
498
+ */
499
+ async function main() {
500
+ try {
501
+ const options = parseArgs();
502
+ if (options.helpRequested) {
503
+ return;
504
+ }
505
+ const results = await runFullAssessment(options);
506
+ if (!options.jsonOnly) {
507
+ displaySummary(results);
508
+ }
509
+ const outputPath = saveResults(options.serverName, results, options.outputPath);
510
+ if (options.jsonOnly) {
511
+ console.log(outputPath);
512
+ }
513
+ else {
514
+ console.log(`📄 Results saved to: ${outputPath}\n`);
515
+ }
516
+ const exitCode = results.overallStatus === "FAIL" ? 1 : 0;
517
+ setTimeout(() => process.exit(exitCode), 10);
518
+ }
519
+ catch (error) {
520
+ console.error("\n❌ Error:", error instanceof Error ? error.message : String(error));
521
+ if (error instanceof Error && error.stack && process.env.DEBUG) {
522
+ console.error("\nStack trace:");
523
+ console.error(error.stack);
524
+ }
525
+ setTimeout(() => process.exit(1), 10);
526
+ }
527
+ }
528
+ main();
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone Security Assessment Runner CLI
4
+ *
5
+ * Runs security assessment against an MCP server without the web UI.
6
+ * Focuses on prompt injection and vulnerability testing.
7
+ *
8
+ * Usage:
9
+ * mcp-assess-security --server <server-name>
10
+ * mcp-assess-security --server broken-mcp --tool vulnerable_tool
11
+ */
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 { SecurityAssessor } from "../../client/lib/services/assessment/modules/SecurityAssessor.js";
21
+ import { DEFAULT_ASSESSMENT_CONFIG, } from "../../client/lib/lib/assessmentTypes.js";
22
+ /**
23
+ * Load server configuration from Claude Code's MCP settings
24
+ */
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))
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
+ };
43
+ }
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
+ };
62
+ }
63
+ }
64
+ throw new Error(`Server config not found for: ${serverName}\nTried: ${possiblePaths.join(", ")}`);
65
+ }
66
+ /**
67
+ * Connect to MCP server via configured transport
68
+ */
69
+ async function connectToServer(config) {
70
+ let transport;
71
+ switch (config.transport) {
72
+ case "http":
73
+ if (!config.url)
74
+ throw new Error("URL required for HTTP transport");
75
+ transport = new StreamableHTTPClientTransport(new URL(config.url));
76
+ break;
77
+ case "sse":
78
+ if (!config.url)
79
+ throw new Error("URL required for SSE transport");
80
+ transport = new SSEClientTransport(new URL(config.url));
81
+ break;
82
+ case "stdio":
83
+ default:
84
+ if (!config.command)
85
+ throw new Error("Command required for stdio transport");
86
+ transport = new StdioClientTransport({
87
+ command: config.command,
88
+ args: config.args,
89
+ env: {
90
+ ...Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined)),
91
+ ...config.env,
92
+ },
93
+ stderr: "pipe",
94
+ });
95
+ break;
96
+ }
97
+ const client = new Client({
98
+ name: "mcp-assess-security",
99
+ version: "1.0.0",
100
+ }, {
101
+ capabilities: {},
102
+ });
103
+ await client.connect(transport);
104
+ return client;
105
+ }
106
+ /**
107
+ * Get list of tools from MCP server
108
+ */
109
+ async function getTools(client, toolNameFilter) {
110
+ const response = await client.listTools();
111
+ let tools = response.tools || [];
112
+ if (toolNameFilter) {
113
+ tools = tools.filter((t) => t.name === toolNameFilter);
114
+ if (tools.length === 0) {
115
+ throw new Error(`Tool not found: ${toolNameFilter}`);
116
+ }
117
+ }
118
+ return tools;
119
+ }
120
+ /**
121
+ * Create callTool wrapper for assessment context
122
+ */
123
+ function createCallToolWrapper(client) {
124
+ return async (name, params) => {
125
+ try {
126
+ const response = await client.callTool({
127
+ name,
128
+ arguments: params,
129
+ });
130
+ return {
131
+ content: response.content,
132
+ isError: response.isError || false,
133
+ structuredContent: response
134
+ .structuredContent,
135
+ };
136
+ }
137
+ catch (error) {
138
+ return {
139
+ content: [
140
+ {
141
+ type: "text",
142
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
143
+ },
144
+ ],
145
+ isError: true,
146
+ };
147
+ }
148
+ };
149
+ }
150
+ /**
151
+ * Run security assessment
152
+ */
153
+ async function runSecurityAssessment(options) {
154
+ console.log(`\n🔍 Connecting to MCP server: ${options.serverName}`);
155
+ const serverConfig = loadServerConfig(options.serverName, options.serverConfigPath);
156
+ const client = await connectToServer(serverConfig);
157
+ console.log("✅ Connected successfully");
158
+ const tools = await getTools(client, options.toolName);
159
+ console.log(`🔧 Found ${tools.length} tool${tools.length !== 1 ? "s" : ""}`);
160
+ if (options.toolName) {
161
+ console.log(` Testing only: ${options.toolName}`);
162
+ }
163
+ const config = {
164
+ ...DEFAULT_ASSESSMENT_CONFIG,
165
+ securityPatternsToTest: 17,
166
+ reviewerMode: false,
167
+ testTimeout: 30000,
168
+ };
169
+ const context = {
170
+ serverName: options.serverName,
171
+ tools,
172
+ callTool: createCallToolWrapper(client),
173
+ config,
174
+ };
175
+ console.log(`🛡️ Running security assessment with 17 attack patterns...`);
176
+ const assessor = new SecurityAssessor(config);
177
+ const results = await assessor.assess(context);
178
+ await client.close();
179
+ return results;
180
+ }
181
+ /**
182
+ * Save results to JSON file
183
+ */
184
+ function saveResults(serverName, results, outputPath) {
185
+ const defaultPath = `/tmp/inspector-security-assessment-${serverName}.json`;
186
+ const finalPath = outputPath || defaultPath;
187
+ const output = {
188
+ timestamp: new Date().toISOString(),
189
+ assessmentType: "security",
190
+ serverName,
191
+ security: results,
192
+ };
193
+ fs.writeFileSync(finalPath, JSON.stringify(output, null, 2));
194
+ return finalPath;
195
+ }
196
+ /**
197
+ * Display summary
198
+ */
199
+ function displaySummary(results) {
200
+ const { promptInjectionTests, vulnerabilities, overallRiskLevel } = results;
201
+ const vulnerableCount = promptInjectionTests.filter((t) => t.vulnerable).length;
202
+ const totalTests = promptInjectionTests.length;
203
+ console.log("\n" + "=".repeat(60));
204
+ console.log("SECURITY ASSESSMENT RESULTS");
205
+ console.log("=".repeat(60));
206
+ console.log(`Total Tests: ${totalTests}`);
207
+ console.log(`Vulnerabilities Found: ${vulnerableCount}`);
208
+ console.log(`Overall Risk Level: ${overallRiskLevel}`);
209
+ console.log("=".repeat(60));
210
+ if (vulnerableCount > 0) {
211
+ console.log("\n⚠️ VULNERABILITIES DETECTED:\n");
212
+ const vulnerableTests = promptInjectionTests.filter((t) => t.vulnerable);
213
+ for (const test of vulnerableTests) {
214
+ console.log(`🚨 ${test.toolName} - ${test.testName}`);
215
+ console.log(` Risk: ${test.riskLevel}`);
216
+ console.log(` Evidence: ${test.evidence}`);
217
+ console.log("");
218
+ }
219
+ }
220
+ else {
221
+ console.log("\n✅ No vulnerabilities detected\n");
222
+ }
223
+ }
224
+ /**
225
+ * Parse command-line arguments
226
+ */
227
+ function parseArgs() {
228
+ const args = process.argv.slice(2);
229
+ const options = {};
230
+ for (let i = 0; i < args.length; i++) {
231
+ const arg = args[i];
232
+ if (!arg)
233
+ continue;
234
+ switch (arg) {
235
+ case "--server":
236
+ case "-s":
237
+ options.serverName = args[++i];
238
+ break;
239
+ case "--config":
240
+ case "-c":
241
+ options.serverConfigPath = args[++i];
242
+ break;
243
+ case "--output":
244
+ case "-o":
245
+ options.outputPath = args[++i];
246
+ break;
247
+ case "--tool":
248
+ case "-t":
249
+ options.toolName = args[++i];
250
+ break;
251
+ case "--verbose":
252
+ case "-v":
253
+ options.verbose = true;
254
+ break;
255
+ case "--help":
256
+ case "-h":
257
+ printHelp();
258
+ options.helpRequested = true;
259
+ return options;
260
+ default:
261
+ if (!arg.startsWith("-")) {
262
+ if (!options.serverName) {
263
+ options.serverName = arg;
264
+ }
265
+ }
266
+ else {
267
+ console.error(`Unknown argument: ${arg}`);
268
+ printHelp();
269
+ setTimeout(() => process.exit(1), 10);
270
+ options.helpRequested = true;
271
+ return options;
272
+ }
273
+ }
274
+ }
275
+ if (!options.serverName) {
276
+ console.error("Error: --server is required");
277
+ printHelp();
278
+ setTimeout(() => process.exit(1), 10);
279
+ options.helpRequested = true;
280
+ return options;
281
+ }
282
+ return options;
283
+ }
284
+ /**
285
+ * Print help message
286
+ */
287
+ function printHelp() {
288
+ console.log(`
289
+ Usage: mcp-assess-security [options] [server-name]
290
+
291
+ Run security assessment against an MCP server with 17 attack patterns.
292
+
293
+ Options:
294
+ --server, -s <name> Server name (required, or pass as first positional arg)
295
+ --config, -c <path> Path to server config JSON
296
+ --output, -o <path> Output JSON path (default: /tmp/inspector-security-assessment-<server>.json)
297
+ --tool, -t <name> Test only specific tool (default: test all tools)
298
+ --verbose, -v Enable verbose logging
299
+ --help, -h Show this help message
300
+
301
+ Attack Patterns Tested (17 total):
302
+ • Direct prompt injection
303
+ • Indirect prompt injection
304
+ • Instruction override
305
+ • Role-playing attacks
306
+ • Encoding bypass
307
+ • Multi-turn manipulation
308
+ • Context poisoning
309
+ • And more...
310
+
311
+ Examples:
312
+ mcp-assess-security my-server
313
+ mcp-assess-security --server broken-mcp --tool vulnerable_calculator_tool
314
+ mcp-assess-security --server my-server --output ./security-results.json
315
+ `);
316
+ }
317
+ /**
318
+ * Main execution
319
+ */
320
+ async function main() {
321
+ try {
322
+ const options = parseArgs();
323
+ if (options.helpRequested) {
324
+ return;
325
+ }
326
+ const results = await runSecurityAssessment(options);
327
+ displaySummary(results);
328
+ const outputPath = saveResults(options.serverName, results, options.outputPath);
329
+ console.log(`📄 Results saved to: ${outputPath}\n`);
330
+ const exitCode = results.vulnerabilities.length > 0 ? 1 : 0;
331
+ setTimeout(() => process.exit(exitCode), 10);
332
+ }
333
+ catch (error) {
334
+ console.error("\n❌ Error:", error instanceof Error ? error.message : String(error));
335
+ if (error instanceof Error && error.stack && process.env.DEBUG) {
336
+ console.error("\nStack trace:");
337
+ console.error(error.stack);
338
+ }
339
+ setTimeout(() => process.exit(1), 10);
340
+ }
341
+ }
342
+ main();
package/build/cli.js CHANGED
@@ -46,6 +46,10 @@ async function runWebClient(args) {
46
46
  if (args.serverUrl) {
47
47
  startArgs.push("--server-url", args.serverUrl);
48
48
  }
49
+ // Pass Claude Code flag if enabled
50
+ if (args.claudeEnabled) {
51
+ startArgs.push("--claude-enabled");
52
+ }
49
53
  // Pass command and args (using -- to separate them)
50
54
  if (args.command) {
51
55
  startArgs.push("--", args.command, ...args.args);
@@ -171,7 +175,8 @@ function parseArgs() {
171
175
  .option("--cli", "enable CLI mode")
172
176
  .option("--transport <type>", "transport type (stdio, sse, http)")
173
177
  .option("--server-url <url>", "server URL for SSE/HTTP transport")
174
- .option("--header <headers...>", 'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)', parseHeaderPair, {});
178
+ .option("--header <headers...>", 'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)', parseHeaderPair, {})
179
+ .option("--claude-enabled", "enable Claude Code integration for intelligent analysis (requires Claude CLI)");
175
180
  // Parse only the arguments before --
176
181
  program.parse(preArgs);
177
182
  const options = program.opts();
@@ -214,6 +219,7 @@ function parseArgs() {
214
219
  cli: options.cli || false,
215
220
  transport: "stdio",
216
221
  headers: options.header,
222
+ claudeEnabled: options.claudeEnabled || false,
217
223
  };
218
224
  }
219
225
  else if (config.type === "sse" || config.type === "streamable-http") {
@@ -225,6 +231,7 @@ function parseArgs() {
225
231
  transport: config.type,
226
232
  serverUrl: config.url,
227
233
  headers: options.header,
234
+ claudeEnabled: options.claudeEnabled || false,
228
235
  };
229
236
  }
230
237
  else {
@@ -236,6 +243,7 @@ function parseArgs() {
236
243
  cli: options.cli || false,
237
244
  transport: "stdio",
238
245
  headers: options.header,
246
+ claudeEnabled: options.claudeEnabled || false,
239
247
  };
240
248
  }
241
249
  }
@@ -255,6 +263,7 @@ function parseArgs() {
255
263
  transport: transport,
256
264
  serverUrl: options.serverUrl,
257
265
  headers: options.header,
266
+ claudeEnabled: options.claudeEnabled || false,
258
267
  };
259
268
  }
260
269
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bryan-thompson/inspector-assessment-cli",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "CLI for the Enhanced MCP Inspector with assessment capabilities",
5
5
  "license": "MIT",
6
6
  "author": "Bryan Thompson <bryan@triepod.ai>",
@@ -16,7 +16,9 @@
16
16
  "main": "build/cli.js",
17
17
  "type": "module",
18
18
  "bin": {
19
- "mcp-inspector-assess-cli": "build/cli.js"
19
+ "mcp-inspector-assess-cli": "build/cli.js",
20
+ "mcp-assess-full": "build/assess-full.js",
21
+ "mcp-assess-security": "build/assess-security.js"
20
22
  },
21
23
  "publishConfig": {
22
24
  "access": "public"
@@ -34,8 +36,10 @@
34
36
  },
35
37
  "devDependencies": {},
36
38
  "dependencies": {
39
+ "@bryan-thompson/inspector-assessment-client": "^1.5.0",
37
40
  "@modelcontextprotocol/sdk": "^1.23.0",
38
41
  "commander": "^13.1.0",
39
- "spawn-rx": "^5.1.2"
42
+ "spawn-rx": "^5.1.2",
43
+ "zod": "^3.25.76"
40
44
  }
41
45
  }