@bryan-thompson/inspector-assessment-cli 1.32.1 → 1.32.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.
@@ -2,12 +2,15 @@
2
2
  * Server Configuration Loading
3
3
  *
4
4
  * Handles loading MCP server configuration from Claude Code settings.
5
+ * Uses Zod schemas for runtime validation (Issue #84).
5
6
  *
6
7
  * @module cli/lib/assessment-runner/server-config
7
8
  */
8
9
  import * as fs from "fs";
9
10
  import * as path from "path";
10
11
  import * as os from "os";
12
+ import { ServerEntrySchema, isHttpSseConfig, isStdioConfig, } from "./server-configSchemas.js";
13
+ import { formatZodError } from "../zodErrorFormatter.js";
11
14
  /**
12
15
  * Load server configuration from Claude Code's MCP settings
13
16
  *
@@ -24,55 +27,60 @@ export function loadServerConfig(serverName, configPath) {
24
27
  for (const tryPath of possiblePaths) {
25
28
  if (!fs.existsSync(tryPath))
26
29
  continue;
27
- let config;
30
+ let rawConfig;
28
31
  try {
29
- config = JSON.parse(fs.readFileSync(tryPath, "utf-8"));
32
+ rawConfig = JSON.parse(fs.readFileSync(tryPath, "utf-8"));
30
33
  }
31
34
  catch (e) {
32
35
  throw new Error(`Invalid JSON in config file: ${tryPath}\n${e instanceof Error ? e.message : String(e)}`);
33
36
  }
34
- if (config.mcpServers && config.mcpServers[serverName]) {
35
- const serverConfig = config.mcpServers[serverName];
36
- // Check if serverConfig specifies http/sse transport
37
- if (serverConfig.url ||
38
- serverConfig.transport === "http" ||
39
- serverConfig.transport === "sse") {
40
- if (!serverConfig.url) {
41
- throw new Error(`Invalid server config: transport is '${serverConfig.transport}' but 'url' is missing`);
42
- }
43
- return {
44
- transport: serverConfig.transport || "http",
45
- url: serverConfig.url,
46
- };
37
+ // Determine which config entry to validate
38
+ let serverEntry;
39
+ let configSource;
40
+ // Check for Claude Desktop format (mcpServers object)
41
+ if (typeof rawConfig === "object" &&
42
+ rawConfig !== null &&
43
+ "mcpServers" in rawConfig &&
44
+ typeof rawConfig.mcpServers === "object") {
45
+ const mcpServers = rawConfig
46
+ .mcpServers;
47
+ if (mcpServers && serverName in mcpServers) {
48
+ serverEntry = mcpServers[serverName];
49
+ configSource = `${tryPath} (mcpServers.${serverName})`;
47
50
  }
48
- // Default to stdio transport
49
- return {
50
- transport: "stdio",
51
- command: serverConfig.command,
52
- args: serverConfig.args || [],
53
- env: serverConfig.env || {},
54
- cwd: serverConfig.cwd,
55
- };
56
- }
57
- if (config.url ||
58
- config.transport === "http" ||
59
- config.transport === "sse") {
60
- if (!config.url) {
61
- throw new Error(`Invalid server config: transport is '${config.transport}' but 'url' is missing`);
51
+ else {
52
+ continue; // Server not in this file, try next path
62
53
  }
54
+ }
55
+ else {
56
+ // Standalone config format
57
+ serverEntry = rawConfig;
58
+ configSource = tryPath;
59
+ }
60
+ // Validate server entry with Zod schema
61
+ const validationResult = ServerEntrySchema.safeParse(serverEntry);
62
+ if (!validationResult.success) {
63
+ throw new Error(`Invalid server config in ${configSource}:\n${formatZodError(validationResult.error)}`);
64
+ }
65
+ const validatedEntry = validationResult.data;
66
+ // Convert to ServerConfig using type guards
67
+ if (isHttpSseConfig(validatedEntry)) {
63
68
  return {
64
- transport: config.transport || "http",
65
- url: config.url,
69
+ transport: validatedEntry.transport || "http",
70
+ url: validatedEntry.url,
66
71
  };
67
72
  }
68
- if (config.command) {
73
+ if (isStdioConfig(validatedEntry)) {
69
74
  return {
70
75
  transport: "stdio",
71
- command: config.command,
72
- args: config.args || [],
73
- env: config.env || {},
76
+ command: validatedEntry.command,
77
+ args: validatedEntry.args || [],
78
+ env: validatedEntry.env || {},
79
+ cwd: validatedEntry.cwd,
74
80
  };
75
81
  }
82
+ // This should never happen due to schema validation, but TypeScript needs it
83
+ throw new Error(`Unable to determine transport type for config: ${configSource}`);
76
84
  }
77
85
  throw new Error(`Server config not found for: ${serverName}\nTried: ${possiblePaths.join(", ")}`);
78
86
  }
@@ -34,7 +34,10 @@ export { TransportTypeSchema };
34
34
  */
35
35
  export const HttpSseServerConfigSchema = z.object({
36
36
  transport: z.enum(["http", "sse"]).optional(),
37
- url: z.string().url("url must be a valid URL"),
37
+ url: z
38
+ .string()
39
+ .min(1, "'url' is required for HTTP/SSE transport")
40
+ .url("url must be a valid URL"),
38
41
  });
39
42
  /**
40
43
  * Schema for stdio transport server configuration.
@@ -6,6 +6,10 @@
6
6
  *
7
7
  * This is a CLI-local version that imports from the built client lib
8
8
  * to avoid rootDir conflicts in TypeScript compilation.
9
+ *
10
+ * NOTE: Phase 7 events (tool_test_complete, validation_summary, phase_started,
11
+ * phase_complete) are re-exported from scripts/lib/jsonl-events.ts to maintain
12
+ * a single source of truth. See Issue #88.
9
13
  */
10
14
  import { INSPECTOR_VERSION, SCHEMA_VERSION, } from "../../../client/lib/lib/moduleScoring.js";
11
15
  // Re-export for consumers of this module
@@ -196,3 +200,58 @@ export function emitModulesConfigured(enabled, skipped, reason) {
196
200
  reason,
197
201
  });
198
202
  }
203
+ // ============================================================================
204
+ // Phase 7 Events - Per-Tool Testing & Phase Lifecycle
205
+ // ============================================================================
206
+ /**
207
+ * Emit tool_test_complete event after all tests for a single tool finish.
208
+ * Provides per-tool summary for real-time progress in auditor UI.
209
+ */
210
+ export function emitToolTestComplete(tool, module, scenariosPassed, scenariosExecuted, confidence, status, executionTime) {
211
+ emitJSONL({
212
+ event: "tool_test_complete",
213
+ tool,
214
+ module,
215
+ scenariosPassed,
216
+ scenariosExecuted,
217
+ confidence,
218
+ status,
219
+ executionTime,
220
+ });
221
+ }
222
+ /**
223
+ * Emit validation_summary event with per-tool input validation metrics.
224
+ * Tracks how tools handle invalid inputs (wrong types, missing required, etc.)
225
+ */
226
+ export function emitValidationSummary(tool, wrongType, missingRequired, extraParams, nullValues, invalidValues) {
227
+ emitJSONL({
228
+ event: "validation_summary",
229
+ tool,
230
+ wrongType,
231
+ missingRequired,
232
+ extraParams,
233
+ nullValues,
234
+ invalidValues,
235
+ });
236
+ }
237
+ /**
238
+ * Emit phase_started event when an assessment phase begins.
239
+ * Used for high-level progress tracking (discovery, assessment, analysis).
240
+ */
241
+ export function emitPhaseStarted(phase) {
242
+ emitJSONL({
243
+ event: "phase_started",
244
+ phase,
245
+ });
246
+ }
247
+ /**
248
+ * Emit phase_complete event when an assessment phase finishes.
249
+ * Includes duration for performance tracking.
250
+ */
251
+ export function emitPhaseComplete(phase, duration) {
252
+ emitJSONL({
253
+ event: "phase_complete",
254
+ phase,
255
+ duration,
256
+ });
257
+ }
@@ -17,11 +17,42 @@ export function formatZodIssue(issue) {
17
17
  }
18
18
  /**
19
19
  * Format a ZodError into a readable string.
20
+ * Detects union validation failures and extracts the most specific error messages.
20
21
  *
21
22
  * @param error - Zod validation error
22
23
  * @returns Formatted error messages joined by newlines
23
24
  */
24
25
  export function formatZodError(error) {
26
+ // Check if this is a union validation error (code: invalid_union)
27
+ const unionErrors = error.errors.filter((e) => e.code === "invalid_union");
28
+ if (unionErrors.length > 0) {
29
+ // Extract detailed errors from union attempts
30
+ const detailedErrors = [];
31
+ for (const unionError of unionErrors) {
32
+ // Union errors have unionErrors property with detailed failures
33
+ if ("unionErrors" in unionError &&
34
+ Array.isArray(unionError.unionErrors)) {
35
+ for (const subError of unionError.unionErrors) {
36
+ // Find the most specific error message (not "Required")
37
+ const specificErrors = subError.errors.filter((e) => e.message !== "Required" && e.code !== "invalid_type");
38
+ if (specificErrors.length > 0) {
39
+ detailedErrors.push(...specificErrors.map(formatZodIssue));
40
+ }
41
+ else {
42
+ // Fallback to any error from this branch
43
+ detailedErrors.push(...subError.errors.map(formatZodIssue));
44
+ }
45
+ }
46
+ }
47
+ }
48
+ // If we found detailed errors, use them; otherwise fall back to generic message
49
+ if (detailedErrors.length > 0) {
50
+ // Deduplicate errors and return all unique errors
51
+ const uniqueErrors = Array.from(new Set(detailedErrors));
52
+ return uniqueErrors.join("\n");
53
+ }
54
+ }
55
+ // Standard formatting for non-union errors
25
56
  return error.errors.map(formatZodIssue).join("\n");
26
57
  }
27
58
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bryan-thompson/inspector-assessment-cli",
3
- "version": "1.32.1",
3
+ "version": "1.32.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>",