@bryan-thompson/inspector-assessment-cli 1.26.5 → 1.26.7

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,139 @@
1
+ /**
2
+ * Source File Loading
3
+ *
4
+ * Handles recursive source file discovery with gitignore support.
5
+ *
6
+ * @module cli/lib/assessment-runner/source-loader
7
+ */
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+ /** Maximum file size (in characters) to include in source code analysis */
11
+ const MAX_SOURCE_FILE_SIZE = 100_000;
12
+ /**
13
+ * Load optional files from source code path
14
+ *
15
+ * @param sourcePath - Path to source code directory
16
+ * @returns Object containing loaded source files
17
+ */
18
+ export function loadSourceFiles(sourcePath) {
19
+ const result = {};
20
+ // Search for README in source directory and parent directories (up to 3 levels)
21
+ // This handles cases where --source points to a subdirectory but README is at repo root
22
+ const readmePaths = ["README.md", "readme.md", "Readme.md"];
23
+ let readmeFound = false;
24
+ // First try the source directory itself
25
+ for (const readmePath of readmePaths) {
26
+ const fullPath = path.join(sourcePath, readmePath);
27
+ if (fs.existsSync(fullPath)) {
28
+ result.readmeContent = fs.readFileSync(fullPath, "utf-8");
29
+ readmeFound = true;
30
+ break;
31
+ }
32
+ }
33
+ // If not found, search parent directories (up to 3 levels)
34
+ if (!readmeFound) {
35
+ let currentDir = sourcePath;
36
+ for (let i = 0; i < 3; i++) {
37
+ const parentDir = path.dirname(currentDir);
38
+ if (parentDir === currentDir)
39
+ break; // Reached filesystem root
40
+ for (const readmePath of readmePaths) {
41
+ const fullPath = path.join(parentDir, readmePath);
42
+ if (fs.existsSync(fullPath)) {
43
+ result.readmeContent = fs.readFileSync(fullPath, "utf-8");
44
+ readmeFound = true;
45
+ break;
46
+ }
47
+ }
48
+ if (readmeFound)
49
+ break;
50
+ currentDir = parentDir;
51
+ }
52
+ }
53
+ const packagePath = path.join(sourcePath, "package.json");
54
+ if (fs.existsSync(packagePath)) {
55
+ result.packageJson = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
56
+ }
57
+ const manifestPath = path.join(sourcePath, "manifest.json");
58
+ if (fs.existsSync(manifestPath)) {
59
+ result.manifestRaw = fs.readFileSync(manifestPath, "utf-8");
60
+ try {
61
+ result.manifestJson = JSON.parse(result.manifestRaw);
62
+ }
63
+ catch {
64
+ console.warn("[Assessment] Failed to parse manifest.json");
65
+ }
66
+ }
67
+ result.sourceCodeFiles = new Map();
68
+ // Include config files for portability analysis
69
+ const sourceExtensions = [
70
+ ".ts",
71
+ ".js",
72
+ ".py",
73
+ ".go",
74
+ ".rs",
75
+ ".json",
76
+ ".sh",
77
+ ".yaml",
78
+ ".yml",
79
+ ];
80
+ // Parse .gitignore patterns
81
+ const gitignorePatterns = [];
82
+ const gitignorePath = path.join(sourcePath, ".gitignore");
83
+ if (fs.existsSync(gitignorePath)) {
84
+ const gitignoreContent = fs.readFileSync(gitignorePath, "utf-8");
85
+ for (const line of gitignoreContent.split("\n")) {
86
+ const trimmed = line.trim();
87
+ if (!trimmed || trimmed.startsWith("#"))
88
+ continue;
89
+ // Convert gitignore pattern to regex
90
+ const pattern = trimmed
91
+ .replace(/\./g, "\\.")
92
+ .replace(/\*\*/g, ".*")
93
+ .replace(/\*/g, "[^/]*")
94
+ .replace(/\?/g, ".");
95
+ try {
96
+ gitignorePatterns.push(new RegExp(pattern));
97
+ }
98
+ catch {
99
+ // Skip invalid patterns
100
+ }
101
+ }
102
+ }
103
+ const isGitignored = (relativePath) => {
104
+ return gitignorePatterns.some((pattern) => pattern.test(relativePath));
105
+ };
106
+ const loadSourceDir = (dir, prefix = "") => {
107
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
108
+ for (const entry of entries) {
109
+ if (entry.name.startsWith(".") || entry.name === "node_modules")
110
+ continue;
111
+ const fullPath = path.join(dir, entry.name);
112
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
113
+ // Skip gitignored files
114
+ if (isGitignored(relativePath))
115
+ continue;
116
+ if (entry.isDirectory()) {
117
+ loadSourceDir(fullPath, relativePath);
118
+ }
119
+ else if (sourceExtensions.some((ext) => entry.name.endsWith(ext))) {
120
+ try {
121
+ const content = fs.readFileSync(fullPath, "utf-8");
122
+ if (content.length < MAX_SOURCE_FILE_SIZE) {
123
+ result.sourceCodeFiles.set(relativePath, content);
124
+ }
125
+ }
126
+ catch {
127
+ // Skip unreadable files
128
+ }
129
+ }
130
+ }
131
+ };
132
+ try {
133
+ loadSourceDir(sourcePath);
134
+ }
135
+ catch (e) {
136
+ console.warn("[Assessment] Could not load source files:", e);
137
+ }
138
+ return result;
139
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Tool Call Wrapper
3
+ *
4
+ * Creates a wrapper around MCP client.callTool() for assessment context.
5
+ *
6
+ * @module cli/lib/assessment-runner/tool-wrapper
7
+ */
8
+ /**
9
+ * Create callTool wrapper for assessment context
10
+ *
11
+ * @param client - Connected MCP client
12
+ * @returns Wrapped callTool function
13
+ */
14
+ export function createCallToolWrapper(client) {
15
+ return async (name, params) => {
16
+ try {
17
+ const response = await client.callTool({
18
+ name,
19
+ arguments: params,
20
+ });
21
+ return {
22
+ content: response.content,
23
+ isError: response.isError || false,
24
+ structuredContent: response
25
+ .structuredContent,
26
+ };
27
+ }
28
+ catch (error) {
29
+ return {
30
+ content: [
31
+ {
32
+ type: "text",
33
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
34
+ },
35
+ ],
36
+ isError: true,
37
+ };
38
+ }
39
+ };
40
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Assessment Runner Types
3
+ *
4
+ * Shared type definitions for the assessment-runner module.
5
+ *
6
+ * @module cli/lib/assessment-runner/types
7
+ */
8
+ export {};
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Assessment Runner Module (Facade)
3
+ *
4
+ * This file provides backward-compatible exports by re-exporting from the
5
+ * modular assessment-runner/ directory structure.
6
+ *
7
+ * Refactored as part of Issue #94.
8
+ *
9
+ * @module cli/lib/assessment-runner
10
+ */
11
+ // Re-export all public APIs from the modular structure
12
+ export * from "./assessment-runner/index.js";
@@ -0,0 +1,419 @@
1
+ /**
2
+ * CLI Parser Module
3
+ *
4
+ * Handles command-line argument parsing, validation, and help text for
5
+ * the mcp-assess-full CLI tool.
6
+ *
7
+ * Extracted from assess-full.ts as part of Issue #90 modularization.
8
+ *
9
+ * @module cli/lib/cli-parser
10
+ */
11
+ import { ASSESSMENT_CATEGORY_METADATA, } from "../../../client/lib/lib/assessmentTypes.js";
12
+ import { ASSESSMENT_PROFILES, isValidProfileName, getProfileHelpText, } from "../profiles.js";
13
+ // ============================================================================
14
+ // Constants
15
+ // ============================================================================
16
+ // Valid module names derived from ASSESSMENT_CATEGORY_METADATA
17
+ const VALID_MODULE_NAMES = Object.keys(ASSESSMENT_CATEGORY_METADATA);
18
+ // ============================================================================
19
+ // Validation Functions
20
+ // ============================================================================
21
+ /**
22
+ * Validate module names from CLI input
23
+ *
24
+ * @param input - Comma-separated module names
25
+ * @param flagName - Flag name for error messages (e.g., "--skip-modules")
26
+ * @returns Array of validated module names, or empty array if invalid
27
+ */
28
+ export function validateModuleNames(input, flagName) {
29
+ const names = input
30
+ .split(",")
31
+ .map((n) => n.trim())
32
+ .filter(Boolean);
33
+ const invalid = names.filter((n) => !VALID_MODULE_NAMES.includes(n));
34
+ if (invalid.length > 0) {
35
+ console.error(`Error: Invalid module name(s) for ${flagName}: ${invalid.join(", ")}`);
36
+ console.error(`Valid modules: ${VALID_MODULE_NAMES.join(", ")}`);
37
+ setTimeout(() => process.exit(1), 10);
38
+ return [];
39
+ }
40
+ return names;
41
+ }
42
+ /**
43
+ * Validate parsed options for consistency and requirements
44
+ *
45
+ * @param options - Partial assessment options to validate
46
+ * @returns Validation result with any errors
47
+ */
48
+ export function validateArgs(options) {
49
+ const errors = [];
50
+ // Server name is required
51
+ if (!options.serverName) {
52
+ errors.push("--server is required");
53
+ }
54
+ // Validate mutual exclusivity of --profile, --skip-modules, and --only-modules
55
+ if (options.profile &&
56
+ (options.skipModules?.length || options.onlyModules?.length)) {
57
+ errors.push("--profile cannot be used with --skip-modules or --only-modules");
58
+ }
59
+ if (options.skipModules?.length && options.onlyModules?.length) {
60
+ errors.push("--skip-modules and --only-modules are mutually exclusive");
61
+ }
62
+ return {
63
+ valid: errors.length === 0,
64
+ errors,
65
+ };
66
+ }
67
+ // ============================================================================
68
+ // Argument Parsing
69
+ // ============================================================================
70
+ /**
71
+ * Parse command-line arguments
72
+ *
73
+ * @param argv - Command-line arguments (defaults to process.argv.slice(2))
74
+ * @returns Parsed assessment options
75
+ */
76
+ export function parseArgs(argv) {
77
+ const args = argv ?? process.argv.slice(2);
78
+ const options = {};
79
+ for (let i = 0; i < args.length; i++) {
80
+ const arg = args[i];
81
+ if (!arg)
82
+ continue;
83
+ switch (arg) {
84
+ case "--server":
85
+ case "-s":
86
+ options.serverName = args[++i];
87
+ break;
88
+ case "--config":
89
+ case "-c":
90
+ options.serverConfigPath = args[++i];
91
+ break;
92
+ case "--output":
93
+ case "-o":
94
+ options.outputPath = args[++i];
95
+ break;
96
+ case "--source":
97
+ options.sourceCodePath = args[++i];
98
+ break;
99
+ case "--pattern-config":
100
+ case "-p":
101
+ options.patternConfigPath = args[++i];
102
+ break;
103
+ case "--performance-config":
104
+ options.performanceConfigPath = args[++i];
105
+ break;
106
+ case "--claude-enabled":
107
+ options.claudeEnabled = true;
108
+ break;
109
+ case "--claude-http":
110
+ // Enable Claude Bridge with HTTP transport (connects to mcp-auditor)
111
+ options.claudeEnabled = true;
112
+ options.claudeHttp = true;
113
+ break;
114
+ case "--mcp-auditor-url": {
115
+ const urlValue = args[++i];
116
+ if (!urlValue || urlValue.startsWith("-")) {
117
+ console.error("Error: --mcp-auditor-url requires a URL argument");
118
+ setTimeout(() => process.exit(1), 10);
119
+ options.helpRequested = true;
120
+ return options;
121
+ }
122
+ try {
123
+ new URL(urlValue); // Validate URL format
124
+ options.mcpAuditorUrl = urlValue;
125
+ }
126
+ catch {
127
+ console.error(`Error: Invalid URL for --mcp-auditor-url: ${urlValue}`);
128
+ console.error(" Expected format: http://hostname:port or https://hostname:port");
129
+ setTimeout(() => process.exit(1), 10);
130
+ options.helpRequested = true;
131
+ return options;
132
+ }
133
+ break;
134
+ }
135
+ case "--full":
136
+ options.fullAssessment = true;
137
+ break;
138
+ case "--verbose":
139
+ case "-v":
140
+ options.verbose = true;
141
+ options.logLevel = "debug";
142
+ break;
143
+ case "--silent":
144
+ options.logLevel = "silent";
145
+ break;
146
+ case "--log-level": {
147
+ const levelValue = args[++i];
148
+ const validLevels = [
149
+ "silent",
150
+ "error",
151
+ "warn",
152
+ "info",
153
+ "debug",
154
+ ];
155
+ if (!validLevels.includes(levelValue)) {
156
+ console.error(`Invalid log level: ${levelValue}. Valid options: ${validLevels.join(", ")}`);
157
+ setTimeout(() => process.exit(1), 10);
158
+ options.helpRequested = true;
159
+ return options;
160
+ }
161
+ options.logLevel = levelValue;
162
+ break;
163
+ }
164
+ case "--json":
165
+ options.jsonOnly = true;
166
+ break;
167
+ case "--format":
168
+ case "-f": {
169
+ const formatValue = args[++i];
170
+ if (formatValue !== "json" && formatValue !== "markdown") {
171
+ console.error(`Invalid format: ${formatValue}. Valid options: json, markdown`);
172
+ setTimeout(() => process.exit(1), 10);
173
+ options.helpRequested = true;
174
+ return options;
175
+ }
176
+ options.format = formatValue;
177
+ break;
178
+ }
179
+ case "--include-policy":
180
+ options.includePolicy = true;
181
+ break;
182
+ case "--preflight":
183
+ options.preflightOnly = true;
184
+ break;
185
+ case "--compare":
186
+ options.comparePath = args[++i];
187
+ break;
188
+ case "--diff-only":
189
+ options.diffOnly = true;
190
+ break;
191
+ case "--resume":
192
+ options.resume = true;
193
+ break;
194
+ case "--no-resume":
195
+ options.noResume = true;
196
+ break;
197
+ case "--temporal-invocations":
198
+ options.temporalInvocations = parseInt(args[++i], 10);
199
+ break;
200
+ case "--skip-temporal":
201
+ options.skipTemporal = true;
202
+ break;
203
+ case "--profile": {
204
+ const profileValue = args[++i];
205
+ if (!profileValue) {
206
+ console.error("Error: --profile requires a profile name");
207
+ console.error(`Valid profiles: ${Object.keys(ASSESSMENT_PROFILES).join(", ")}`);
208
+ setTimeout(() => process.exit(1), 10);
209
+ options.helpRequested = true;
210
+ return options;
211
+ }
212
+ if (!isValidProfileName(profileValue)) {
213
+ console.error(`Error: Invalid profile name: ${profileValue}`);
214
+ console.error(`Valid profiles: ${Object.keys(ASSESSMENT_PROFILES).join(", ")}`);
215
+ setTimeout(() => process.exit(1), 10);
216
+ options.helpRequested = true;
217
+ return options;
218
+ }
219
+ options.profile = profileValue;
220
+ break;
221
+ }
222
+ case "--skip-modules": {
223
+ const skipValue = args[++i];
224
+ if (!skipValue) {
225
+ console.error("Error: --skip-modules requires a comma-separated list");
226
+ setTimeout(() => process.exit(1), 10);
227
+ options.helpRequested = true;
228
+ return options;
229
+ }
230
+ options.skipModules = validateModuleNames(skipValue, "--skip-modules");
231
+ if (options.skipModules.length === 0 && skipValue) {
232
+ options.helpRequested = true;
233
+ return options;
234
+ }
235
+ break;
236
+ }
237
+ case "--only-modules": {
238
+ const onlyValue = args[++i];
239
+ if (!onlyValue) {
240
+ console.error("Error: --only-modules requires a comma-separated list");
241
+ setTimeout(() => process.exit(1), 10);
242
+ options.helpRequested = true;
243
+ return options;
244
+ }
245
+ options.onlyModules = validateModuleNames(onlyValue, "--only-modules");
246
+ if (options.onlyModules.length === 0 && onlyValue) {
247
+ options.helpRequested = true;
248
+ return options;
249
+ }
250
+ break;
251
+ }
252
+ case "--help":
253
+ case "-h":
254
+ printHelp();
255
+ options.helpRequested = true;
256
+ return options;
257
+ default:
258
+ if (!arg.startsWith("-")) {
259
+ if (!options.serverName) {
260
+ options.serverName = arg;
261
+ }
262
+ }
263
+ else {
264
+ console.error(`Unknown argument: ${arg}`);
265
+ printHelp();
266
+ setTimeout(() => process.exit(1), 10);
267
+ options.helpRequested = true;
268
+ return options;
269
+ }
270
+ }
271
+ }
272
+ // Validate mutual exclusivity of --profile, --skip-modules, and --only-modules
273
+ if (options.profile &&
274
+ (options.skipModules?.length || options.onlyModules?.length)) {
275
+ console.error("Error: --profile cannot be used with --skip-modules or --only-modules");
276
+ setTimeout(() => process.exit(1), 10);
277
+ options.helpRequested = true;
278
+ return options;
279
+ }
280
+ if (options.skipModules?.length && options.onlyModules?.length) {
281
+ console.error("Error: --skip-modules and --only-modules are mutually exclusive");
282
+ setTimeout(() => process.exit(1), 10);
283
+ options.helpRequested = true;
284
+ return options;
285
+ }
286
+ if (!options.serverName) {
287
+ console.error("Error: --server is required");
288
+ printHelp();
289
+ setTimeout(() => process.exit(1), 10);
290
+ options.helpRequested = true;
291
+ return options;
292
+ }
293
+ // Environment variable fallbacks (matches run-security-assessment.ts behavior)
294
+ // INSPECTOR_CLAUDE=true enables Claude with HTTP transport
295
+ if (process.env.INSPECTOR_CLAUDE === "true" && !options.claudeEnabled) {
296
+ options.claudeEnabled = true;
297
+ options.claudeHttp = true; // HTTP transport when enabled via env var
298
+ }
299
+ // INSPECTOR_MCP_AUDITOR_URL overrides default URL (only if not set via CLI)
300
+ if (process.env.INSPECTOR_MCP_AUDITOR_URL && !options.mcpAuditorUrl) {
301
+ const envUrl = process.env.INSPECTOR_MCP_AUDITOR_URL;
302
+ try {
303
+ new URL(envUrl);
304
+ options.mcpAuditorUrl = envUrl;
305
+ }
306
+ catch {
307
+ console.warn(`Warning: Invalid INSPECTOR_MCP_AUDITOR_URL: ${envUrl}, using default`);
308
+ }
309
+ }
310
+ return options;
311
+ }
312
+ // ============================================================================
313
+ // Help Text
314
+ // ============================================================================
315
+ /**
316
+ * Print help message to console
317
+ */
318
+ export function printHelp() {
319
+ console.log(`
320
+ Usage: mcp-assess-full [options] [server-name]
321
+
322
+ Run comprehensive MCP server assessment with 16 assessor modules organized in 4 tiers.
323
+
324
+ Options:
325
+ --server, -s <name> Server name (required, or pass as first positional arg)
326
+ --config, -c <path> Path to server config JSON
327
+ --output, -o <path> Output path (default: /tmp/inspector-full-assessment-<server>.<ext>)
328
+ --source <path> Source code path for deep analysis (AUP, portability, etc.)
329
+ --pattern-config, -p <path> Path to custom annotation pattern JSON
330
+ --performance-config <path> Path to performance tuning JSON (batch sizes, timeouts, etc.)
331
+ --format, -f <type> Output format: json (default) or markdown
332
+ --include-policy Include policy compliance mapping in report (30 requirements)
333
+ --preflight Run quick validation only (tools exist, manifest valid, server responds)
334
+ --compare <path> Compare current assessment against baseline JSON file
335
+ --diff-only Output only the comparison diff (requires --compare)
336
+ --resume Resume from previous interrupted assessment
337
+ --no-resume Force fresh start, clear any existing state
338
+ --claude-enabled Enable Claude Code integration (CLI transport: requires 'claude' binary)
339
+ --claude-http Enable Claude Code via HTTP transport (connects to mcp-auditor proxy)
340
+ --mcp-auditor-url <url> mcp-auditor URL for HTTP transport (default: http://localhost:8085)
341
+ --full Enable all assessment modules (default)
342
+ --profile <name> Use predefined module profile (quick, security, compliance, full)
343
+ --temporal-invocations <n> Number of invocations per tool for rug pull detection (default: 25)
344
+ --skip-temporal Skip temporal/rug pull testing (faster assessment)
345
+ --skip-modules <list> Skip specific modules (comma-separated)
346
+ --only-modules <list> Run only specific modules (comma-separated)
347
+ --json Output only JSON path (no console summary)
348
+ --verbose, -v Enable verbose logging (same as --log-level debug)
349
+ --silent Suppress all diagnostic logging
350
+ --log-level <level> Set log level: silent, error, warn, info (default), debug
351
+ Also supports LOG_LEVEL environment variable
352
+ --help, -h Show this help message
353
+
354
+ Environment Variables:
355
+ INSPECTOR_CLAUDE=true Enable Claude with HTTP transport (same as --claude-http)
356
+ INSPECTOR_MCP_AUDITOR_URL Override default mcp-auditor URL (default: http://localhost:8085)
357
+ LOG_LEVEL Set log level (overridden by --log-level flag)
358
+
359
+ ${getProfileHelpText()}
360
+ Module Selection:
361
+ --profile, --skip-modules, and --only-modules are mutually exclusive.
362
+ Use --profile for common assessment scenarios.
363
+ Use --skip-modules for custom runs by disabling expensive modules.
364
+ Use --only-modules to focus on specific areas (e.g., tool annotation PRs).
365
+
366
+ Valid module names (new naming):
367
+ functionality, security, errorHandling, protocolCompliance, aupCompliance,
368
+ toolAnnotations, prohibitedLibraries, manifestValidation, authentication,
369
+ temporal, resources, prompts, crossCapability, developerExperience,
370
+ portability, externalAPIScanner
371
+
372
+ Legacy module names (deprecated, will map to new names):
373
+ documentation -> developerExperience
374
+ usability -> developerExperience
375
+ mcpSpecCompliance -> protocolCompliance
376
+ protocolConformance -> protocolCompliance
377
+
378
+ Module Tiers (16 total):
379
+ Tier 1 - Core Security (Always Run):
380
+ • Functionality - Tests all tools work correctly
381
+ • Security - Prompt injection & vulnerability testing
382
+ • Error Handling - Validates error responses
383
+ • Protocol Compliance - MCP protocol + JSON-RPC validation
384
+ • AUP Compliance - Acceptable Use Policy checks
385
+ • Temporal - Rug pull/temporal behavior change detection
386
+
387
+ Tier 2 - Compliance (MCP Directory):
388
+ • Tool Annotations - readOnlyHint/destructiveHint validation
389
+ • Prohibited Libs - Dependency security checks
390
+ • Manifest - MCPB manifest.json validation
391
+ • Authentication - OAuth/auth evaluation
392
+
393
+ Tier 3 - Capability-Based (Conditional):
394
+ • Resources - Resource capability assessment
395
+ • Prompts - Prompt capability assessment
396
+ • Cross-Capability - Chained vulnerability detection
397
+
398
+ Tier 4 - Extended (Optional):
399
+ • Developer Experience - Documentation + usability assessment
400
+ • Portability - Cross-platform compatibility
401
+ • External API - External service detection
402
+
403
+ Examples:
404
+ # Profile-based (recommended):
405
+ mcp-assess-full my-server --profile quick # CI/CD fast check (~30s)
406
+ mcp-assess-full my-server --profile security # Security audit (~2-3min)
407
+ mcp-assess-full my-server --profile compliance # Directory submission (~5min)
408
+ mcp-assess-full my-server --profile full # Comprehensive audit (~10-15min)
409
+
410
+ # Custom module selection:
411
+ mcp-assess-full my-server --skip-modules temporal,resources # Skip expensive modules
412
+ mcp-assess-full my-server --only-modules functionality,toolAnnotations # Annotation PR review
413
+
414
+ # Advanced options:
415
+ mcp-assess-full --server my-server --source ./my-server --output ./results.json
416
+ mcp-assess-full --server my-server --format markdown --include-policy
417
+ mcp-assess-full --server my-server --compare ./baseline.json --diff-only
418
+ `);
419
+ }