@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.
- package/build/assess-full.js +532 -106
- package/build/assess-security.js +54 -90
- package/build/validate-testbed.js +0 -0
- package/package.json +1 -1
- package/build/lib/__tests__/zodErrorFormatter.test.js +0 -282
package/build/assess-full.js
CHANGED
|
@@ -3,131 +3,561 @@
|
|
|
3
3
|
* Full Assessment Runner CLI
|
|
4
4
|
*
|
|
5
5
|
* Runs comprehensive MCP server assessment using AssessmentOrchestrator
|
|
6
|
-
* with all
|
|
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
|
|
13
|
-
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
//
|
|
20
|
-
|
|
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
|
-
*
|
|
24
|
+
* Load server configuration from Claude Code's MCP settings
|
|
24
25
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
displayComparisonSummary(comparison.diff);
|
|
279
|
+
else if (options.claudeEnabled) {
|
|
280
|
+
console.log("⚠️ Claude Code requested but not available");
|
|
76
281
|
}
|
|
77
|
-
|
|
282
|
+
}
|
|
283
|
+
let sourceFiles = {};
|
|
284
|
+
if (options.sourceCodePath && fs.existsSync(options.sourceCodePath)) {
|
|
285
|
+
sourceFiles = loadSourceFiles(options.sourceCodePath);
|
|
78
286
|
if (!options.jsonOnly) {
|
|
79
|
-
|
|
287
|
+
console.log(`📁 Loaded source files from: ${options.sourceCodePath}`);
|
|
80
288
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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();
|
package/build/assess-security.js
CHANGED
|
@@ -11,73 +11,58 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as fs from "fs";
|
|
13
13
|
import * as path from "path";
|
|
14
|
-
import
|
|
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
|
|
20
|
-
import {
|
|
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
|
-
*
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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:
|
|
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
|
|
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
|
|
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 (
|
|
341
|
-
• Command Injection
|
|
342
|
-
• Calculator Injection
|
|
343
|
-
•
|
|
344
|
-
•
|
|
345
|
-
•
|
|
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,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
|
-
});
|