@bryan-thompson/inspector-assessment-cli 1.26.6 ā 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/__tests__/assess-full-e2e.test.js +496 -0
- package/build/__tests__/assessment-runner/assessment-executor.test.js +248 -0
- package/build/__tests__/assessment-runner/config-builder.test.js +289 -0
- package/build/__tests__/assessment-runner/index.test.js +41 -0
- package/build/__tests__/assessment-runner/server-config.test.js +249 -0
- package/build/__tests__/assessment-runner/server-connection.test.js +221 -0
- package/build/__tests__/assessment-runner/source-loader.test.js +341 -0
- package/build/__tests__/assessment-runner/tool-wrapper.test.js +114 -0
- package/build/__tests__/assessment-runner-facade.test.js +118 -0
- package/build/assess-full.js +29 -1255
- package/build/lib/assessment-runner/assessment-executor.js +323 -0
- package/build/lib/assessment-runner/config-builder.js +127 -0
- package/build/lib/assessment-runner/index.js +20 -0
- package/build/lib/assessment-runner/server-config.js +78 -0
- package/build/lib/assessment-runner/server-connection.js +80 -0
- package/build/lib/assessment-runner/source-loader.js +139 -0
- package/build/lib/assessment-runner/tool-wrapper.js +40 -0
- package/build/lib/assessment-runner/types.js +8 -0
- package/build/lib/assessment-runner.js +6 -740
- package/build/lib/cli-parser.js +83 -2
- package/build/lib/comparison-handler.js +84 -0
- package/build/lib/result-output.js +154 -0
- package/package.json +1 -1
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assessment Executor
|
|
3
|
+
*
|
|
4
|
+
* Main orchestration for running full MCP server assessments.
|
|
5
|
+
*
|
|
6
|
+
* @module cli/lib/assessment-runner/assessment-executor
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import { AssessmentOrchestrator, } from "../../../../client/lib/services/assessment/AssessmentOrchestrator.js";
|
|
11
|
+
import { AssessmentStateManager } from "../../assessmentState.js";
|
|
12
|
+
import { emitServerConnected, emitToolDiscovered, emitToolsDiscoveryComplete, emitAssessmentComplete, emitTestBatch, emitVulnerabilityFound, emitAnnotationMissing, emitAnnotationMisaligned, emitAnnotationReviewRecommended, emitAnnotationAligned, emitModulesConfigured, } from "../jsonl-events.js";
|
|
13
|
+
import { loadServerConfig } from "./server-config.js";
|
|
14
|
+
import { loadSourceFiles } from "./source-loader.js";
|
|
15
|
+
import { connectToServer } from "./server-connection.js";
|
|
16
|
+
import { createCallToolWrapper } from "./tool-wrapper.js";
|
|
17
|
+
import { buildConfig } from "./config-builder.js";
|
|
18
|
+
/**
|
|
19
|
+
* Run full assessment against an MCP server
|
|
20
|
+
*
|
|
21
|
+
* @param options - CLI assessment options
|
|
22
|
+
* @returns Assessment results
|
|
23
|
+
*/
|
|
24
|
+
export async function runFullAssessment(options) {
|
|
25
|
+
if (!options.jsonOnly) {
|
|
26
|
+
console.log(`\nš Starting full assessment for: ${options.serverName}`);
|
|
27
|
+
}
|
|
28
|
+
const serverConfig = loadServerConfig(options.serverName, options.serverConfigPath);
|
|
29
|
+
if (!options.jsonOnly) {
|
|
30
|
+
console.log("ā
Server config loaded");
|
|
31
|
+
}
|
|
32
|
+
const client = await connectToServer(serverConfig);
|
|
33
|
+
emitServerConnected(options.serverName, serverConfig.transport || "stdio");
|
|
34
|
+
if (!options.jsonOnly) {
|
|
35
|
+
console.log("ā
Connected to MCP server");
|
|
36
|
+
}
|
|
37
|
+
// Capture server info from initialization for protocol conformance checks
|
|
38
|
+
// Apply defensive null checks for protocol conformance validation
|
|
39
|
+
const rawServerInfo = client.getServerVersion();
|
|
40
|
+
const rawServerCapabilities = client.getServerCapabilities();
|
|
41
|
+
// Build serverInfo with safe fallbacks
|
|
42
|
+
const serverInfo = rawServerInfo
|
|
43
|
+
? {
|
|
44
|
+
name: rawServerInfo.name || "unknown",
|
|
45
|
+
version: rawServerInfo.version,
|
|
46
|
+
metadata: rawServerInfo.metadata,
|
|
47
|
+
}
|
|
48
|
+
: undefined;
|
|
49
|
+
// ServerCapabilities can be undefined - that's valid per MCP spec
|
|
50
|
+
const serverCapabilities = rawServerCapabilities ?? undefined;
|
|
51
|
+
// Log warning if server didn't provide initialization info
|
|
52
|
+
if (!serverInfo && !options.jsonOnly) {
|
|
53
|
+
console.log("ā ļø Server did not provide serverInfo during initialization");
|
|
54
|
+
}
|
|
55
|
+
const response = await client.listTools();
|
|
56
|
+
const tools = response.tools || [];
|
|
57
|
+
// Emit JSONL tool discovery events for audit-worker parsing
|
|
58
|
+
for (const tool of tools) {
|
|
59
|
+
emitToolDiscovered(tool);
|
|
60
|
+
}
|
|
61
|
+
emitToolsDiscoveryComplete(tools.length);
|
|
62
|
+
if (!options.jsonOnly) {
|
|
63
|
+
console.log(`š§ Found ${tools.length} tool${tools.length !== 1 ? "s" : ""}`);
|
|
64
|
+
}
|
|
65
|
+
// Fetch resources for new capability assessments
|
|
66
|
+
let resources = [];
|
|
67
|
+
let resourceTemplates = [];
|
|
68
|
+
try {
|
|
69
|
+
const resourcesResponse = await client.listResources();
|
|
70
|
+
resources = (resourcesResponse.resources || []).map((r) => ({
|
|
71
|
+
uri: r.uri,
|
|
72
|
+
name: r.name,
|
|
73
|
+
description: r.description,
|
|
74
|
+
mimeType: r.mimeType,
|
|
75
|
+
}));
|
|
76
|
+
// resourceTemplates may be typed as unknown in some SDK versions
|
|
77
|
+
const templates = resourcesResponse.resourceTemplates;
|
|
78
|
+
if (templates) {
|
|
79
|
+
resourceTemplates = templates.map((rt) => ({
|
|
80
|
+
uriTemplate: rt.uriTemplate,
|
|
81
|
+
name: rt.name,
|
|
82
|
+
description: rt.description,
|
|
83
|
+
mimeType: rt.mimeType,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
if (!options.jsonOnly &&
|
|
87
|
+
(resources.length > 0 || resourceTemplates.length > 0)) {
|
|
88
|
+
console.log(`š¦ Found ${resources.length} resource(s) and ${resourceTemplates.length} resource template(s)`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Server may not support resources - that's okay
|
|
93
|
+
if (!options.jsonOnly) {
|
|
94
|
+
console.log("š¦ Resources not supported by server");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Fetch prompts for new capability assessments
|
|
98
|
+
let prompts = [];
|
|
99
|
+
try {
|
|
100
|
+
const promptsResponse = await client.listPrompts();
|
|
101
|
+
prompts = (promptsResponse.prompts || []).map((p) => ({
|
|
102
|
+
name: p.name,
|
|
103
|
+
description: p.description,
|
|
104
|
+
arguments: p.arguments?.map((a) => ({
|
|
105
|
+
name: a.name,
|
|
106
|
+
description: a.description,
|
|
107
|
+
required: a.required,
|
|
108
|
+
})),
|
|
109
|
+
}));
|
|
110
|
+
if (!options.jsonOnly && prompts.length > 0) {
|
|
111
|
+
console.log(`š¬ Found ${prompts.length} prompt(s)`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Server may not support prompts - that's okay
|
|
116
|
+
if (!options.jsonOnly) {
|
|
117
|
+
console.log("š¬ Prompts not supported by server");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// State management for resumable assessments
|
|
121
|
+
const stateManager = new AssessmentStateManager(options.serverName);
|
|
122
|
+
if (stateManager.exists() && !options.noResume) {
|
|
123
|
+
const summary = stateManager.getSummary();
|
|
124
|
+
if (summary) {
|
|
125
|
+
if (!options.jsonOnly) {
|
|
126
|
+
console.log(`\nš Found interrupted session from ${summary.startedAt}`);
|
|
127
|
+
console.log(` Completed modules: ${summary.completedModules.length > 0 ? summary.completedModules.join(", ") : "none"}`);
|
|
128
|
+
}
|
|
129
|
+
if (options.resume) {
|
|
130
|
+
if (!options.jsonOnly) {
|
|
131
|
+
console.log(" Resuming from previous state...");
|
|
132
|
+
}
|
|
133
|
+
// Will use partial results later
|
|
134
|
+
}
|
|
135
|
+
else if (!options.jsonOnly) {
|
|
136
|
+
console.log(" Use --resume to continue or --no-resume to start fresh");
|
|
137
|
+
// Clear state and start fresh by default
|
|
138
|
+
stateManager.clear();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (options.noResume && stateManager.exists()) {
|
|
143
|
+
stateManager.clear();
|
|
144
|
+
if (!options.jsonOnly) {
|
|
145
|
+
console.log("šļø Cleared previous assessment state");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Pre-flight validation checks
|
|
149
|
+
if (options.preflightOnly) {
|
|
150
|
+
const preflightResult = await runPreflightChecks(client, tools, options);
|
|
151
|
+
await client.close();
|
|
152
|
+
console.log(JSON.stringify(preflightResult, null, 2));
|
|
153
|
+
setTimeout(() => process.exit(preflightResult.passed ? 0 : 1), 10);
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
const config = buildConfig(options);
|
|
157
|
+
// Emit modules_configured event for consumer progress tracking
|
|
158
|
+
if (config.assessmentCategories) {
|
|
159
|
+
const enabled = [];
|
|
160
|
+
const skipped = [];
|
|
161
|
+
for (const [key, value] of Object.entries(config.assessmentCategories)) {
|
|
162
|
+
if (value) {
|
|
163
|
+
enabled.push(key);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
skipped.push(key);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const reason = options.onlyModules?.length
|
|
170
|
+
? "only-modules"
|
|
171
|
+
: options.skipModules?.length
|
|
172
|
+
? "skip-modules"
|
|
173
|
+
: "default";
|
|
174
|
+
emitModulesConfigured(enabled, skipped, reason);
|
|
175
|
+
}
|
|
176
|
+
const orchestrator = new AssessmentOrchestrator(config);
|
|
177
|
+
if (!options.jsonOnly) {
|
|
178
|
+
if (orchestrator.isClaudeEnabled()) {
|
|
179
|
+
console.log("š¤ Claude Code integration enabled");
|
|
180
|
+
}
|
|
181
|
+
else if (options.claudeEnabled) {
|
|
182
|
+
console.log("ā ļø Claude Code requested but not available");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
let sourceFiles = {};
|
|
186
|
+
if (options.sourceCodePath && fs.existsSync(options.sourceCodePath)) {
|
|
187
|
+
sourceFiles = loadSourceFiles(options.sourceCodePath);
|
|
188
|
+
if (!options.jsonOnly) {
|
|
189
|
+
console.log(`š Loaded source files from: ${options.sourceCodePath}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Create readResource wrapper for ResourceAssessor
|
|
193
|
+
const readResource = async (uri) => {
|
|
194
|
+
const response = await client.readResource({ uri });
|
|
195
|
+
// Extract text content from response
|
|
196
|
+
if (response.contents && response.contents.length > 0) {
|
|
197
|
+
const content = response.contents[0];
|
|
198
|
+
if ("text" in content && content.text) {
|
|
199
|
+
return content.text;
|
|
200
|
+
}
|
|
201
|
+
if ("blob" in content && content.blob) {
|
|
202
|
+
// Return base64 blob as string
|
|
203
|
+
return content.blob;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return "";
|
|
207
|
+
};
|
|
208
|
+
// Create getPrompt wrapper for PromptAssessor
|
|
209
|
+
const getPrompt = async (name, args) => {
|
|
210
|
+
const response = await client.getPrompt({ name, arguments: args });
|
|
211
|
+
return {
|
|
212
|
+
messages: (response.messages || []).map((m) => ({
|
|
213
|
+
role: m.role,
|
|
214
|
+
content: typeof m.content === "string" ? m.content : JSON.stringify(m.content),
|
|
215
|
+
})),
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
// Progress callback to emit JSONL events for real-time monitoring
|
|
219
|
+
const onProgress = (event) => {
|
|
220
|
+
if (event.type === "test_batch") {
|
|
221
|
+
emitTestBatch(event.module, event.completed, event.total, event.batchSize, event.elapsed);
|
|
222
|
+
}
|
|
223
|
+
else if (event.type === "vulnerability_found") {
|
|
224
|
+
emitVulnerabilityFound(event.tool, event.pattern, event.confidence, event.evidence, event.riskLevel, event.requiresReview, event.payload);
|
|
225
|
+
}
|
|
226
|
+
else if (event.type === "annotation_missing") {
|
|
227
|
+
emitAnnotationMissing(event.tool, event.title, event.description, event.parameters, event.inferredBehavior);
|
|
228
|
+
}
|
|
229
|
+
else if (event.type === "annotation_misaligned") {
|
|
230
|
+
emitAnnotationMisaligned(event.tool, event.title, event.description, event.parameters, event.field, event.actual, event.expected, event.confidence, event.reason);
|
|
231
|
+
}
|
|
232
|
+
else if (event.type === "annotation_review_recommended") {
|
|
233
|
+
emitAnnotationReviewRecommended(event.tool, event.title, event.description, event.parameters, event.field, event.actual, event.inferred, event.confidence, event.isAmbiguous, event.reason);
|
|
234
|
+
}
|
|
235
|
+
else if (event.type === "annotation_aligned") {
|
|
236
|
+
emitAnnotationAligned(event.tool, event.confidence, event.annotations);
|
|
237
|
+
}
|
|
238
|
+
// module_started and module_complete are handled by orchestrator directly
|
|
239
|
+
};
|
|
240
|
+
const context = {
|
|
241
|
+
serverName: options.serverName,
|
|
242
|
+
tools,
|
|
243
|
+
callTool: createCallToolWrapper(client),
|
|
244
|
+
listTools: async () => {
|
|
245
|
+
const response = await client.listTools();
|
|
246
|
+
return response.tools;
|
|
247
|
+
},
|
|
248
|
+
config,
|
|
249
|
+
sourceCodePath: options.sourceCodePath,
|
|
250
|
+
onProgress,
|
|
251
|
+
...sourceFiles,
|
|
252
|
+
// New capability assessment data
|
|
253
|
+
resources,
|
|
254
|
+
resourceTemplates,
|
|
255
|
+
prompts,
|
|
256
|
+
readResource,
|
|
257
|
+
getPrompt,
|
|
258
|
+
// Server info for protocol conformance checks
|
|
259
|
+
serverInfo,
|
|
260
|
+
serverCapabilities: serverCapabilities,
|
|
261
|
+
};
|
|
262
|
+
if (!options.jsonOnly) {
|
|
263
|
+
console.log(`\nš Running assessment with ${Object.keys(config.assessmentCategories || {}).length} modules...`);
|
|
264
|
+
console.log("");
|
|
265
|
+
}
|
|
266
|
+
const results = await orchestrator.runFullAssessment(context);
|
|
267
|
+
// Emit assessment complete event
|
|
268
|
+
const defaultOutputPath = `/tmp/inspector-full-assessment-${options.serverName}.json`;
|
|
269
|
+
emitAssessmentComplete(results.overallStatus, results.totalTestsRun, results.executionTime, options.outputPath || defaultOutputPath);
|
|
270
|
+
await client.close();
|
|
271
|
+
return results;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Run pre-flight validation checks
|
|
275
|
+
*
|
|
276
|
+
* @param client - Connected MCP client
|
|
277
|
+
* @param tools - Discovered tools
|
|
278
|
+
* @param options - CLI assessment options
|
|
279
|
+
* @returns Pre-flight result object
|
|
280
|
+
*/
|
|
281
|
+
async function runPreflightChecks(client, tools, options) {
|
|
282
|
+
const preflightResult = {
|
|
283
|
+
passed: true,
|
|
284
|
+
toolCount: tools.length,
|
|
285
|
+
errors: [],
|
|
286
|
+
};
|
|
287
|
+
// Check 1: Tools exist
|
|
288
|
+
if (tools.length === 0) {
|
|
289
|
+
preflightResult.passed = false;
|
|
290
|
+
preflightResult.errors.push("No tools discovered from server");
|
|
291
|
+
}
|
|
292
|
+
// Check 2: Manifest valid (if source path provided)
|
|
293
|
+
if (options.sourceCodePath) {
|
|
294
|
+
const manifestPath = path.join(options.sourceCodePath, "manifest.json");
|
|
295
|
+
if (fs.existsSync(manifestPath)) {
|
|
296
|
+
try {
|
|
297
|
+
JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
298
|
+
preflightResult.manifestValid = true;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
preflightResult.passed = false;
|
|
302
|
+
preflightResult.manifestValid = false;
|
|
303
|
+
preflightResult.errors.push("Invalid manifest.json (JSON parse error)");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Check 3: First tool responds (basic connectivity)
|
|
308
|
+
if (tools.length > 0) {
|
|
309
|
+
try {
|
|
310
|
+
const callTool = createCallToolWrapper(client);
|
|
311
|
+
const firstToolResult = await callTool(tools[0].name, {});
|
|
312
|
+
preflightResult.serverResponsive = !firstToolResult.isError;
|
|
313
|
+
if (firstToolResult.isError) {
|
|
314
|
+
preflightResult.errors.push(`First tool (${tools[0].name}) returned error - server may not be fully functional`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch (e) {
|
|
318
|
+
preflightResult.serverResponsive = false;
|
|
319
|
+
preflightResult.errors.push(`First tool call failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return preflightResult;
|
|
323
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Building
|
|
3
|
+
*
|
|
4
|
+
* Transforms CLI options into AssessmentConfiguration.
|
|
5
|
+
*
|
|
6
|
+
* @module cli/lib/assessment-runner/config-builder
|
|
7
|
+
*/
|
|
8
|
+
import { DEFAULT_ASSESSMENT_CONFIG, getAllModulesConfig, } from "../../../../client/lib/lib/assessmentTypes.js";
|
|
9
|
+
import { FULL_CLAUDE_CODE_CONFIG } from "../../../../client/lib/services/assessment/lib/claudeCodeBridge.js";
|
|
10
|
+
import { loadPerformanceConfig } from "../../../../client/lib/services/assessment/config/performanceConfig.js";
|
|
11
|
+
import { getProfileModules, resolveModuleNames, modulesToLegacyConfig, } from "../../profiles.js";
|
|
12
|
+
/**
|
|
13
|
+
* Build assessment configuration from CLI options
|
|
14
|
+
*
|
|
15
|
+
* @param options - CLI assessment options
|
|
16
|
+
* @returns Assessment configuration
|
|
17
|
+
*/
|
|
18
|
+
export function buildConfig(options) {
|
|
19
|
+
const config = {
|
|
20
|
+
...DEFAULT_ASSESSMENT_CONFIG,
|
|
21
|
+
enableExtendedAssessment: options.fullAssessment !== false,
|
|
22
|
+
parallelTesting: true,
|
|
23
|
+
testTimeout: 30000,
|
|
24
|
+
enableSourceCodeAnalysis: Boolean(options.sourceCodePath),
|
|
25
|
+
};
|
|
26
|
+
if (options.fullAssessment !== false) {
|
|
27
|
+
// Priority: --profile > --only-modules > --skip-modules > default (all)
|
|
28
|
+
if (options.profile) {
|
|
29
|
+
// Use profile-based module selection
|
|
30
|
+
const profileModules = getProfileModules(options.profile, {
|
|
31
|
+
hasSourceCode: Boolean(options.sourceCodePath),
|
|
32
|
+
skipTemporal: options.skipTemporal,
|
|
33
|
+
});
|
|
34
|
+
// Convert new-style module list to legacy config format
|
|
35
|
+
// (until orchestrator is updated to use new naming)
|
|
36
|
+
config.assessmentCategories = modulesToLegacyConfig(profileModules);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// Derive module config from ASSESSMENT_CATEGORY_METADATA (single source of truth)
|
|
40
|
+
const allModules = getAllModulesConfig({
|
|
41
|
+
sourceCodePath: Boolean(options.sourceCodePath),
|
|
42
|
+
skipTemporal: options.skipTemporal,
|
|
43
|
+
});
|
|
44
|
+
// Apply --only-modules filter (whitelist mode)
|
|
45
|
+
if (options.onlyModules?.length) {
|
|
46
|
+
// Resolve any deprecated module names
|
|
47
|
+
const resolved = resolveModuleNames(options.onlyModules);
|
|
48
|
+
for (const key of Object.keys(allModules)) {
|
|
49
|
+
// Disable all modules except those in the whitelist
|
|
50
|
+
allModules[key] = resolved.includes(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Apply --skip-modules filter (blacklist mode)
|
|
54
|
+
if (options.skipModules?.length) {
|
|
55
|
+
// Resolve any deprecated module names
|
|
56
|
+
const resolved = resolveModuleNames(options.skipModules);
|
|
57
|
+
for (const module of resolved) {
|
|
58
|
+
if (module in allModules) {
|
|
59
|
+
allModules[module] = false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
config.assessmentCategories =
|
|
64
|
+
allModules;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Temporal/rug pull detection configuration
|
|
68
|
+
if (options.temporalInvocations) {
|
|
69
|
+
config.temporalInvocations = options.temporalInvocations;
|
|
70
|
+
}
|
|
71
|
+
if (options.claudeEnabled) {
|
|
72
|
+
// Check for HTTP transport via --claude-http flag or environment variables
|
|
73
|
+
const useHttpTransport = options.claudeHttp || process.env.INSPECTOR_CLAUDE === "true";
|
|
74
|
+
const auditorUrl = options.mcpAuditorUrl ||
|
|
75
|
+
process.env.INSPECTOR_MCP_AUDITOR_URL ||
|
|
76
|
+
"http://localhost:8085";
|
|
77
|
+
config.claudeCode = {
|
|
78
|
+
enabled: true,
|
|
79
|
+
timeout: FULL_CLAUDE_CODE_CONFIG.timeout || 60000,
|
|
80
|
+
maxRetries: FULL_CLAUDE_CODE_CONFIG.maxRetries || 2,
|
|
81
|
+
// Use HTTP transport when --claude-http flag or INSPECTOR_CLAUDE env is set
|
|
82
|
+
...(useHttpTransport && {
|
|
83
|
+
transport: "http",
|
|
84
|
+
httpConfig: {
|
|
85
|
+
baseUrl: auditorUrl,
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
features: {
|
|
89
|
+
intelligentTestGeneration: true,
|
|
90
|
+
aupSemanticAnalysis: true,
|
|
91
|
+
annotationInference: true,
|
|
92
|
+
documentationQuality: true,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
if (useHttpTransport) {
|
|
96
|
+
console.log(`š Claude Bridge HTTP transport: ${auditorUrl}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Pass custom annotation pattern config path
|
|
100
|
+
if (options.patternConfigPath) {
|
|
101
|
+
config.patternConfigPath = options.patternConfigPath;
|
|
102
|
+
}
|
|
103
|
+
// Load custom performance config if provided (Issue #37)
|
|
104
|
+
// Note: Currently, modules use DEFAULT_PERFORMANCE_CONFIG directly.
|
|
105
|
+
// This validates the config file but doesn't override runtime values yet.
|
|
106
|
+
// Future enhancement: Pass performanceConfig through AssessmentContext.
|
|
107
|
+
if (options.performanceConfigPath) {
|
|
108
|
+
try {
|
|
109
|
+
const performanceConfig = loadPerformanceConfig(options.performanceConfigPath);
|
|
110
|
+
console.log(`š Performance config loaded from: ${options.performanceConfigPath}`);
|
|
111
|
+
console.log(` Batch interval: ${performanceConfig.batchFlushIntervalMs}ms, ` +
|
|
112
|
+
`Security batch: ${performanceConfig.securityBatchSize}, ` +
|
|
113
|
+
`Functionality batch: ${performanceConfig.functionalityBatchSize}`);
|
|
114
|
+
// TODO: Wire performanceConfig through AssessmentContext to modules
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error(`ā Failed to load performance config: ${error instanceof Error ? error.message : String(error)}`);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Logging configuration
|
|
122
|
+
// Precedence: CLI flags > LOG_LEVEL env var > default (info)
|
|
123
|
+
const envLogLevel = process.env.LOG_LEVEL;
|
|
124
|
+
const logLevel = options.logLevel ?? envLogLevel ?? "info";
|
|
125
|
+
config.logging = { level: logLevel };
|
|
126
|
+
return config;
|
|
127
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assessment Runner Module
|
|
3
|
+
*
|
|
4
|
+
* Aggregates and exports all assessment-runner submodules.
|
|
5
|
+
* This is the main entry point for the modular assessment-runner.
|
|
6
|
+
*
|
|
7
|
+
* @module cli/lib/assessment-runner
|
|
8
|
+
*/
|
|
9
|
+
// Server Configuration
|
|
10
|
+
export { loadServerConfig } from "./server-config.js";
|
|
11
|
+
// Source File Loading
|
|
12
|
+
export { loadSourceFiles } from "./source-loader.js";
|
|
13
|
+
// Server Connection
|
|
14
|
+
export { connectToServer } from "./server-connection.js";
|
|
15
|
+
// Tool Wrapper
|
|
16
|
+
export { createCallToolWrapper } from "./tool-wrapper.js";
|
|
17
|
+
// Configuration Building
|
|
18
|
+
export { buildConfig } from "./config-builder.js";
|
|
19
|
+
// Assessment Execution
|
|
20
|
+
export { runFullAssessment } from "./assessment-executor.js";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Configuration Loading
|
|
3
|
+
*
|
|
4
|
+
* Handles loading MCP server configuration from Claude Code settings.
|
|
5
|
+
*
|
|
6
|
+
* @module cli/lib/assessment-runner/server-config
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import * as os from "os";
|
|
11
|
+
/**
|
|
12
|
+
* Load server configuration from Claude Code's MCP settings
|
|
13
|
+
*
|
|
14
|
+
* @param serverName - Name of the server to look up
|
|
15
|
+
* @param configPath - Optional explicit config path
|
|
16
|
+
* @returns Server configuration object
|
|
17
|
+
*/
|
|
18
|
+
export function loadServerConfig(serverName, configPath) {
|
|
19
|
+
const possiblePaths = [
|
|
20
|
+
configPath,
|
|
21
|
+
path.join(os.homedir(), ".config", "mcp", "servers", `${serverName}.json`),
|
|
22
|
+
path.join(os.homedir(), ".config", "claude", "claude_desktop_config.json"),
|
|
23
|
+
].filter(Boolean);
|
|
24
|
+
for (const tryPath of possiblePaths) {
|
|
25
|
+
if (!fs.existsSync(tryPath))
|
|
26
|
+
continue;
|
|
27
|
+
let config;
|
|
28
|
+
try {
|
|
29
|
+
config = JSON.parse(fs.readFileSync(tryPath, "utf-8"));
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
throw new Error(`Invalid JSON in config file: ${tryPath}\n${e instanceof Error ? e.message : String(e)}`);
|
|
33
|
+
}
|
|
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
|
+
};
|
|
47
|
+
}
|
|
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`);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
transport: config.transport || "http",
|
|
65
|
+
url: config.url,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (config.command) {
|
|
69
|
+
return {
|
|
70
|
+
transport: "stdio",
|
|
71
|
+
command: config.command,
|
|
72
|
+
args: config.args || [],
|
|
73
|
+
env: config.env || {},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Server config not found for: ${serverName}\nTried: ${possiblePaths.join(", ")}`);
|
|
78
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Connection
|
|
3
|
+
*
|
|
4
|
+
* Handles MCP server connection via HTTP, SSE, or stdio transport.
|
|
5
|
+
*
|
|
6
|
+
* @module cli/lib/assessment-runner/server-connection
|
|
7
|
+
*/
|
|
8
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
9
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
10
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
11
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
12
|
+
/**
|
|
13
|
+
* Connect to MCP server via configured transport
|
|
14
|
+
*
|
|
15
|
+
* @param config - Server configuration
|
|
16
|
+
* @returns Connected MCP client
|
|
17
|
+
*/
|
|
18
|
+
export async function connectToServer(config) {
|
|
19
|
+
let transport;
|
|
20
|
+
let stderrData = ""; // Capture stderr for error reporting
|
|
21
|
+
switch (config.transport) {
|
|
22
|
+
case "http":
|
|
23
|
+
if (!config.url)
|
|
24
|
+
throw new Error("URL required for HTTP transport");
|
|
25
|
+
transport = new StreamableHTTPClientTransport(new URL(config.url));
|
|
26
|
+
break;
|
|
27
|
+
case "sse":
|
|
28
|
+
if (!config.url)
|
|
29
|
+
throw new Error("URL required for SSE transport");
|
|
30
|
+
transport = new SSEClientTransport(new URL(config.url));
|
|
31
|
+
break;
|
|
32
|
+
case "stdio":
|
|
33
|
+
default:
|
|
34
|
+
if (!config.command)
|
|
35
|
+
throw new Error("Command required for stdio transport");
|
|
36
|
+
transport = new StdioClientTransport({
|
|
37
|
+
command: config.command,
|
|
38
|
+
args: config.args,
|
|
39
|
+
env: {
|
|
40
|
+
...Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined)),
|
|
41
|
+
...config.env,
|
|
42
|
+
},
|
|
43
|
+
cwd: config.cwd,
|
|
44
|
+
stderr: "pipe",
|
|
45
|
+
});
|
|
46
|
+
// Capture stderr BEFORE connecting - critical for error context
|
|
47
|
+
// The MCP SDK creates a PassThrough stream immediately when stderr: "pipe"
|
|
48
|
+
// is set, allowing us to attach listeners before start() is called
|
|
49
|
+
const stderrStream = transport.stderr;
|
|
50
|
+
if (stderrStream) {
|
|
51
|
+
stderrStream.on("data", (data) => {
|
|
52
|
+
stderrData += data.toString();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
const client = new Client({
|
|
58
|
+
name: "mcp-assess-full",
|
|
59
|
+
version: "1.0.0",
|
|
60
|
+
}, {
|
|
61
|
+
capabilities: {},
|
|
62
|
+
});
|
|
63
|
+
try {
|
|
64
|
+
await client.connect(transport);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
68
|
+
// Provide helpful context when connection fails
|
|
69
|
+
if (stderrData.trim()) {
|
|
70
|
+
throw new Error(`Failed to connect to MCP server: ${errorMessage}\n\n` +
|
|
71
|
+
`Server stderr:\n${stderrData.trim()}\n\n` +
|
|
72
|
+
`Common causes:\n` +
|
|
73
|
+
` - Missing environment variables (check .env file)\n` +
|
|
74
|
+
` - Required external services not running\n` +
|
|
75
|
+
` - Missing API credentials`);
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Failed to connect to MCP server: ${errorMessage}`);
|
|
78
|
+
}
|
|
79
|
+
return client;
|
|
80
|
+
}
|