@bryan-thompson/inspector-assessment-cli 1.38.3 → 1.40.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.
@@ -840,3 +840,610 @@ describe("parseArgs Zod Schema Integration", () => {
840
840
  });
841
841
  });
842
842
  });
843
+ /**
844
+ * Transport Flag Tests (--http, --sse)
845
+ *
846
+ * Tests for the convenience transport flags that allow quick testing
847
+ * without creating a config file.
848
+ */
849
+ describe("Transport Flags (--http, --sse)", () => {
850
+ let processExitSpy;
851
+ let consoleErrorSpy;
852
+ beforeEach(() => {
853
+ jest.useFakeTimers();
854
+ processExitSpy = jest
855
+ .spyOn(process, "exit")
856
+ .mockImplementation((() => { }));
857
+ consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => { });
858
+ });
859
+ afterEach(() => {
860
+ jest.runAllTimers();
861
+ jest.useRealTimers();
862
+ processExitSpy?.mockRestore();
863
+ consoleErrorSpy?.mockRestore();
864
+ });
865
+ describe("--http flag", () => {
866
+ it("should accept valid HTTP URL", () => {
867
+ const result = parseArgs([
868
+ "test-server",
869
+ "--http",
870
+ "http://localhost:10900/mcp",
871
+ ]);
872
+ expect(result.httpUrl).toBe("http://localhost:10900/mcp");
873
+ expect(result.helpRequested).toBeFalsy();
874
+ });
875
+ it("should accept valid HTTPS URL", () => {
876
+ const result = parseArgs([
877
+ "test-server",
878
+ "--http",
879
+ "https://api.example.com/mcp",
880
+ ]);
881
+ expect(result.httpUrl).toBe("https://api.example.com/mcp");
882
+ expect(result.helpRequested).toBeFalsy();
883
+ });
884
+ it("should reject invalid URL", () => {
885
+ const result = parseArgs(["test-server", "--http", "not-a-valid-url"]);
886
+ expect(result.helpRequested).toBe(true);
887
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Invalid URL for --http"));
888
+ });
889
+ it("should reject missing URL argument", () => {
890
+ const result = parseArgs(["test-server", "--http"]);
891
+ expect(result.helpRequested).toBe(true);
892
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--http requires a URL argument"));
893
+ });
894
+ it("should reject when next argument is another flag", () => {
895
+ const result = parseArgs(["test-server", "--http", "--verbose"]);
896
+ expect(result.helpRequested).toBe(true);
897
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--http requires a URL argument"));
898
+ });
899
+ it("should reject non-HTTP protocol (file://)", () => {
900
+ const result = parseArgs(["test-server", "--http", "file:///etc/passwd"]);
901
+ expect(result.helpRequested).toBe(true);
902
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--http requires HTTP or HTTPS URL, got: file:"));
903
+ });
904
+ it("should reject non-HTTP protocol (ftp://)", () => {
905
+ const result = parseArgs([
906
+ "test-server",
907
+ "--http",
908
+ "ftp://example.com/file",
909
+ ]);
910
+ expect(result.helpRequested).toBe(true);
911
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--http requires HTTP or HTTPS URL, got: ftp:"));
912
+ });
913
+ });
914
+ describe("--sse flag", () => {
915
+ it("should accept valid SSE URL", () => {
916
+ const result = parseArgs([
917
+ "test-server",
918
+ "--sse",
919
+ "http://localhost:9002/sse",
920
+ ]);
921
+ expect(result.sseUrl).toBe("http://localhost:9002/sse");
922
+ expect(result.helpRequested).toBeFalsy();
923
+ });
924
+ it("should accept valid HTTPS SSE URL", () => {
925
+ const result = parseArgs([
926
+ "test-server",
927
+ "--sse",
928
+ "https://api.example.com/sse",
929
+ ]);
930
+ expect(result.sseUrl).toBe("https://api.example.com/sse");
931
+ expect(result.helpRequested).toBeFalsy();
932
+ });
933
+ it("should reject invalid URL", () => {
934
+ const result = parseArgs(["test-server", "--sse", "invalid-url"]);
935
+ expect(result.helpRequested).toBe(true);
936
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Invalid URL for --sse"));
937
+ });
938
+ it("should reject missing URL argument", () => {
939
+ const result = parseArgs(["test-server", "--sse"]);
940
+ expect(result.helpRequested).toBe(true);
941
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--sse requires a URL argument"));
942
+ });
943
+ it("should reject non-HTTP protocol (file://)", () => {
944
+ const result = parseArgs(["test-server", "--sse", "file:///etc/passwd"]);
945
+ expect(result.helpRequested).toBe(true);
946
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--sse requires HTTP or HTTPS URL, got: file:"));
947
+ });
948
+ });
949
+ describe("mutual exclusivity", () => {
950
+ it("should reject --http with --config", () => {
951
+ const result = parseArgs([
952
+ "test-server",
953
+ "--http",
954
+ "http://localhost:10900/mcp",
955
+ "--config",
956
+ "config.json",
957
+ ]);
958
+ expect(result.helpRequested).toBe(true);
959
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--http/--sse cannot be used with --config"));
960
+ });
961
+ it("should reject --sse with --config", () => {
962
+ const result = parseArgs([
963
+ "test-server",
964
+ "--sse",
965
+ "http://localhost:9002/sse",
966
+ "--config",
967
+ "config.json",
968
+ ]);
969
+ expect(result.helpRequested).toBe(true);
970
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--http/--sse cannot be used with --config"));
971
+ });
972
+ it("should reject --http with --sse", () => {
973
+ const result = parseArgs([
974
+ "test-server",
975
+ "--http",
976
+ "http://localhost:10900/mcp",
977
+ "--sse",
978
+ "http://localhost:9002/sse",
979
+ ]);
980
+ expect(result.helpRequested).toBe(true);
981
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--http and --sse are mutually exclusive"));
982
+ });
983
+ it("should allow --http without --config or --sse", () => {
984
+ const result = parseArgs([
985
+ "test-server",
986
+ "--http",
987
+ "http://localhost:10900/mcp",
988
+ ]);
989
+ expect(result.httpUrl).toBe("http://localhost:10900/mcp");
990
+ expect(result.sseUrl).toBeUndefined();
991
+ expect(result.serverConfigPath).toBeUndefined();
992
+ expect(result.helpRequested).toBeFalsy();
993
+ });
994
+ it("should allow --sse without --config or --http", () => {
995
+ const result = parseArgs([
996
+ "test-server",
997
+ "--sse",
998
+ "http://localhost:9002/sse",
999
+ ]);
1000
+ expect(result.sseUrl).toBe("http://localhost:9002/sse");
1001
+ expect(result.httpUrl).toBeUndefined();
1002
+ expect(result.serverConfigPath).toBeUndefined();
1003
+ expect(result.helpRequested).toBeFalsy();
1004
+ });
1005
+ });
1006
+ describe("combined with other options", () => {
1007
+ it("should work with --http and --temporal-invocations", () => {
1008
+ const result = parseArgs([
1009
+ "test-server",
1010
+ "--http",
1011
+ "http://localhost:10900/mcp",
1012
+ "--temporal-invocations",
1013
+ "5",
1014
+ ]);
1015
+ expect(result.httpUrl).toBe("http://localhost:10900/mcp");
1016
+ expect(result.temporalInvocations).toBe(5);
1017
+ expect(result.helpRequested).toBeFalsy();
1018
+ });
1019
+ it("should work with --sse and --profile", () => {
1020
+ const result = parseArgs([
1021
+ "test-server",
1022
+ "--sse",
1023
+ "http://localhost:9002/sse",
1024
+ "--profile",
1025
+ "quick",
1026
+ ]);
1027
+ expect(result.sseUrl).toBe("http://localhost:9002/sse");
1028
+ expect(result.profile).toBe("quick");
1029
+ expect(result.helpRequested).toBeFalsy();
1030
+ });
1031
+ it("should work with --http and --output", () => {
1032
+ const result = parseArgs([
1033
+ "test-server",
1034
+ "--http",
1035
+ "http://localhost:10900/mcp",
1036
+ "--output",
1037
+ "/tmp/results.json",
1038
+ ]);
1039
+ expect(result.httpUrl).toBe("http://localhost:10900/mcp");
1040
+ expect(result.outputPath).toBe("/tmp/results.json");
1041
+ expect(result.helpRequested).toBeFalsy();
1042
+ });
1043
+ it("should work with --http and --conformance", () => {
1044
+ const result = parseArgs([
1045
+ "test-server",
1046
+ "--http",
1047
+ "http://localhost:10900/mcp",
1048
+ "--conformance",
1049
+ ]);
1050
+ expect(result.httpUrl).toBe("http://localhost:10900/mcp");
1051
+ expect(result.conformanceEnabled).toBe(true);
1052
+ expect(result.helpRequested).toBeFalsy();
1053
+ });
1054
+ it("should work with --sse and --conformance", () => {
1055
+ const result = parseArgs([
1056
+ "test-server",
1057
+ "--sse",
1058
+ "http://localhost:9002/sse",
1059
+ "--conformance",
1060
+ ]);
1061
+ expect(result.sseUrl).toBe("http://localhost:9002/sse");
1062
+ expect(result.conformanceEnabled).toBe(true);
1063
+ expect(result.helpRequested).toBeFalsy();
1064
+ });
1065
+ });
1066
+ });
1067
+ /**
1068
+ * Module Flag Tests (--module, -m)
1069
+ *
1070
+ * Tests for the single module execution flag that bypasses orchestrator.
1071
+ * Issue #184: Single module runner for focused testing without orchestration overhead.
1072
+ */
1073
+ describe("Module Flag (--module, -m)", () => {
1074
+ let processExitSpy;
1075
+ let consoleErrorSpy;
1076
+ beforeEach(() => {
1077
+ jest.useFakeTimers();
1078
+ processExitSpy = jest
1079
+ .spyOn(process, "exit")
1080
+ .mockImplementation((() => { }));
1081
+ consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => { });
1082
+ });
1083
+ afterEach(() => {
1084
+ jest.runAllTimers();
1085
+ jest.useRealTimers();
1086
+ processExitSpy?.mockRestore();
1087
+ consoleErrorSpy?.mockRestore();
1088
+ });
1089
+ describe("valid module names", () => {
1090
+ it("should accept valid module name with long flag", () => {
1091
+ const result = parseArgs([
1092
+ "test-server",
1093
+ "--config",
1094
+ "config.json",
1095
+ "--module",
1096
+ "toolAnnotations",
1097
+ ]);
1098
+ expect(result.singleModule).toBe("toolAnnotations");
1099
+ expect(result.helpRequested).toBeFalsy();
1100
+ });
1101
+ it("should accept valid module name with short flag", () => {
1102
+ const result = parseArgs([
1103
+ "test-server",
1104
+ "--config",
1105
+ "config.json",
1106
+ "-m",
1107
+ "security",
1108
+ ]);
1109
+ expect(result.singleModule).toBe("security");
1110
+ expect(result.helpRequested).toBeFalsy();
1111
+ });
1112
+ it("should accept all valid core module names", () => {
1113
+ const coreModules = [
1114
+ "functionality",
1115
+ "security",
1116
+ "documentation",
1117
+ "errorHandling",
1118
+ "usability",
1119
+ "mcpSpecCompliance",
1120
+ "aupCompliance",
1121
+ "toolAnnotations",
1122
+ "prohibitedLibraries",
1123
+ "externalAPIScanner",
1124
+ "authentication",
1125
+ "temporal",
1126
+ "resources",
1127
+ "prompts",
1128
+ "crossCapability",
1129
+ "protocolConformance",
1130
+ ];
1131
+ for (const moduleName of coreModules) {
1132
+ consoleErrorSpy.mockClear();
1133
+ const result = parseArgs([
1134
+ "test-server",
1135
+ "--config",
1136
+ "config.json",
1137
+ "--module",
1138
+ moduleName,
1139
+ ]);
1140
+ expect(result.singleModule).toBe(moduleName);
1141
+ expect(result.helpRequested).toBeFalsy();
1142
+ }
1143
+ });
1144
+ it("should accept optional module names", () => {
1145
+ const optionalModules = ["manifestValidation", "portability"];
1146
+ for (const moduleName of optionalModules) {
1147
+ consoleErrorSpy.mockClear();
1148
+ const result = parseArgs([
1149
+ "test-server",
1150
+ "--config",
1151
+ "config.json",
1152
+ "--module",
1153
+ moduleName,
1154
+ ]);
1155
+ expect(result.singleModule).toBe(moduleName);
1156
+ expect(result.helpRequested).toBeFalsy();
1157
+ }
1158
+ });
1159
+ });
1160
+ describe("invalid module names", () => {
1161
+ it("should reject invalid module name", () => {
1162
+ const result = parseArgs([
1163
+ "test-server",
1164
+ "--config",
1165
+ "config.json",
1166
+ "--module",
1167
+ "invalidModule",
1168
+ ]);
1169
+ expect(result.helpRequested).toBe(true);
1170
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Invalid module name"));
1171
+ });
1172
+ it("should reject missing module argument", () => {
1173
+ const result = parseArgs([
1174
+ "test-server",
1175
+ "--config",
1176
+ "config.json",
1177
+ "--module",
1178
+ ]);
1179
+ expect(result.helpRequested).toBe(true);
1180
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--module requires a module name"));
1181
+ });
1182
+ it("should reject when next argument is another flag", () => {
1183
+ const result = parseArgs([
1184
+ "test-server",
1185
+ "--config",
1186
+ "config.json",
1187
+ "--module",
1188
+ "--verbose",
1189
+ ]);
1190
+ expect(result.helpRequested).toBe(true);
1191
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--module requires a module name"));
1192
+ });
1193
+ it("should reject case-sensitive mismatch", () => {
1194
+ const result = parseArgs([
1195
+ "test-server",
1196
+ "--config",
1197
+ "config.json",
1198
+ "--module",
1199
+ "SECURITY",
1200
+ ]);
1201
+ expect(result.helpRequested).toBe(true);
1202
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Invalid module name"));
1203
+ });
1204
+ it("should reject module name with typo", () => {
1205
+ const result = parseArgs([
1206
+ "test-server",
1207
+ "--config",
1208
+ "config.json",
1209
+ "--module",
1210
+ "functionalaty",
1211
+ ]);
1212
+ expect(result.helpRequested).toBe(true);
1213
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Invalid module name"));
1214
+ });
1215
+ });
1216
+ describe("mutual exclusivity with orchestrator flags", () => {
1217
+ it("should reject --module with --profile", () => {
1218
+ const result = parseArgs([
1219
+ "test-server",
1220
+ "--config",
1221
+ "config.json",
1222
+ "--module",
1223
+ "security",
1224
+ "--profile",
1225
+ "quick",
1226
+ ]);
1227
+ expect(result.helpRequested).toBe(true);
1228
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--module cannot be used with --skip-modules, --only-modules, or --profile"));
1229
+ });
1230
+ it("should reject --module with --skip-modules", () => {
1231
+ const result = parseArgs([
1232
+ "test-server",
1233
+ "--config",
1234
+ "config.json",
1235
+ "--module",
1236
+ "security",
1237
+ "--skip-modules",
1238
+ "temporal",
1239
+ ]);
1240
+ expect(result.helpRequested).toBe(true);
1241
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--module cannot be used with"));
1242
+ });
1243
+ it("should reject --module with --only-modules", () => {
1244
+ const result = parseArgs([
1245
+ "test-server",
1246
+ "--config",
1247
+ "config.json",
1248
+ "--module",
1249
+ "security",
1250
+ "--only-modules",
1251
+ "functionality",
1252
+ ]);
1253
+ expect(result.helpRequested).toBe(true);
1254
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--module cannot be used with"));
1255
+ });
1256
+ it("should reject --profile with --module (order reversed)", () => {
1257
+ const result = parseArgs([
1258
+ "test-server",
1259
+ "--config",
1260
+ "config.json",
1261
+ "--profile",
1262
+ "quick",
1263
+ "--module",
1264
+ "security",
1265
+ ]);
1266
+ expect(result.helpRequested).toBe(true);
1267
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--module cannot be used with"));
1268
+ });
1269
+ });
1270
+ describe("combined with transport flags", () => {
1271
+ it("should work with --http", () => {
1272
+ const result = parseArgs([
1273
+ "test-server",
1274
+ "--http",
1275
+ "http://localhost:10900/mcp",
1276
+ "--module",
1277
+ "security",
1278
+ ]);
1279
+ expect(result.httpUrl).toBe("http://localhost:10900/mcp");
1280
+ expect(result.singleModule).toBe("security");
1281
+ expect(result.helpRequested).toBeFalsy();
1282
+ });
1283
+ it("should work with --sse", () => {
1284
+ const result = parseArgs([
1285
+ "test-server",
1286
+ "--sse",
1287
+ "http://localhost:9002/sse",
1288
+ "--module",
1289
+ "functionality",
1290
+ ]);
1291
+ expect(result.sseUrl).toBe("http://localhost:9002/sse");
1292
+ expect(result.singleModule).toBe("functionality");
1293
+ expect(result.helpRequested).toBeFalsy();
1294
+ });
1295
+ it("should work with --config", () => {
1296
+ const result = parseArgs([
1297
+ "test-server",
1298
+ "--config",
1299
+ "config.json",
1300
+ "--module",
1301
+ "temporal",
1302
+ ]);
1303
+ expect(result.serverConfigPath).toBe("config.json");
1304
+ expect(result.singleModule).toBe("temporal");
1305
+ expect(result.helpRequested).toBeFalsy();
1306
+ });
1307
+ });
1308
+ describe("combined with other compatible flags", () => {
1309
+ it("should work with --output", () => {
1310
+ const result = parseArgs([
1311
+ "test-server",
1312
+ "--config",
1313
+ "config.json",
1314
+ "--module",
1315
+ "security",
1316
+ "--output",
1317
+ "/tmp/results.json",
1318
+ ]);
1319
+ expect(result.singleModule).toBe("security");
1320
+ expect(result.outputPath).toBe("/tmp/results.json");
1321
+ expect(result.helpRequested).toBeFalsy();
1322
+ });
1323
+ it("should work with --verbose", () => {
1324
+ const result = parseArgs([
1325
+ "test-server",
1326
+ "--config",
1327
+ "config.json",
1328
+ "--module",
1329
+ "toolAnnotations",
1330
+ "--verbose",
1331
+ ]);
1332
+ expect(result.singleModule).toBe("toolAnnotations");
1333
+ expect(result.verbose).toBe(true);
1334
+ expect(result.helpRequested).toBeFalsy();
1335
+ });
1336
+ it("should work with --log-level", () => {
1337
+ const result = parseArgs([
1338
+ "test-server",
1339
+ "--config",
1340
+ "config.json",
1341
+ "--module",
1342
+ "errorHandling",
1343
+ "--log-level",
1344
+ "debug",
1345
+ ]);
1346
+ expect(result.singleModule).toBe("errorHandling");
1347
+ expect(result.logLevel).toBe("debug");
1348
+ expect(result.helpRequested).toBeFalsy();
1349
+ });
1350
+ it("should work with --temporal-invocations", () => {
1351
+ const result = parseArgs([
1352
+ "test-server",
1353
+ "--config",
1354
+ "config.json",
1355
+ "--module",
1356
+ "temporal",
1357
+ "--temporal-invocations",
1358
+ "10",
1359
+ ]);
1360
+ expect(result.singleModule).toBe("temporal");
1361
+ expect(result.temporalInvocations).toBe(10);
1362
+ expect(result.helpRequested).toBeFalsy();
1363
+ });
1364
+ it("should work with --conformance", () => {
1365
+ const result = parseArgs([
1366
+ "test-server",
1367
+ "--http",
1368
+ "http://localhost:10900/mcp",
1369
+ "--module",
1370
+ "protocolConformance",
1371
+ "--conformance",
1372
+ ]);
1373
+ expect(result.singleModule).toBe("protocolConformance");
1374
+ expect(result.conformanceEnabled).toBe(true);
1375
+ expect(result.helpRequested).toBeFalsy();
1376
+ });
1377
+ it("should work with --format", () => {
1378
+ const result = parseArgs([
1379
+ "test-server",
1380
+ "--config",
1381
+ "config.json",
1382
+ "--module",
1383
+ "security",
1384
+ "--format",
1385
+ "markdown",
1386
+ ]);
1387
+ expect(result.singleModule).toBe("security");
1388
+ expect(result.format).toBe("markdown");
1389
+ expect(result.helpRequested).toBeFalsy();
1390
+ });
1391
+ });
1392
+ describe("short flag behavior", () => {
1393
+ it("should accept -m with all transport types", () => {
1394
+ // Test with --http
1395
+ let result = parseArgs([
1396
+ "test-server",
1397
+ "--http",
1398
+ "http://localhost:10900/mcp",
1399
+ "-m",
1400
+ "security",
1401
+ ]);
1402
+ expect(result.singleModule).toBe("security");
1403
+ expect(result.helpRequested).toBeFalsy();
1404
+ // Test with --sse
1405
+ consoleErrorSpy.mockClear();
1406
+ result = parseArgs([
1407
+ "test-server",
1408
+ "--sse",
1409
+ "http://localhost:9002/sse",
1410
+ "-m",
1411
+ "functionality",
1412
+ ]);
1413
+ expect(result.singleModule).toBe("functionality");
1414
+ expect(result.helpRequested).toBeFalsy();
1415
+ // Test with --config
1416
+ consoleErrorSpy.mockClear();
1417
+ result = parseArgs([
1418
+ "test-server",
1419
+ "--config",
1420
+ "config.json",
1421
+ "-m",
1422
+ "temporal",
1423
+ ]);
1424
+ expect(result.singleModule).toBe("temporal");
1425
+ expect(result.helpRequested).toBeFalsy();
1426
+ });
1427
+ it("should reject -m with missing argument", () => {
1428
+ const result = parseArgs([
1429
+ "test-server",
1430
+ "--config",
1431
+ "config.json",
1432
+ "-m",
1433
+ ]);
1434
+ expect(result.helpRequested).toBe(true);
1435
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--module requires a module name"));
1436
+ });
1437
+ it("should reject -m with invalid module", () => {
1438
+ const result = parseArgs([
1439
+ "test-server",
1440
+ "--config",
1441
+ "config.json",
1442
+ "-m",
1443
+ "notAModule",
1444
+ ]);
1445
+ expect(result.helpRequested).toBe(true);
1446
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Invalid module name"));
1447
+ });
1448
+ });
1449
+ });
@@ -12,8 +12,8 @@
12
12
  import { ScopedListenerConfig } from "./lib/event-config.js";
13
13
  // Import from extracted modules
14
14
  import { parseArgs } from "./lib/cli-parser.js";
15
- import { runFullAssessment } from "./lib/assessment-runner.js";
16
- import { saveResults, saveTieredResults, saveSummaryOnly, displaySummary, } from "./lib/result-output.js";
15
+ import { runFullAssessment, runSingleModule } from "./lib/assessment-runner.js";
16
+ import { saveResults, saveTieredResults, saveSummaryOnly, displaySummary, saveSingleModuleResults, displaySingleModuleSummary, } from "./lib/result-output.js";
17
17
  import { handleComparison, displayComparisonSummary, } from "./lib/comparison-handler.js";
18
18
  import { shouldAutoTier, formatTokenEstimate, } from "../../client/lib/lib/assessment/summarizer/index.js";
19
19
  // ============================================================================
@@ -35,6 +35,23 @@ async function main() {
35
35
  }
36
36
  // Apply scoped listener configuration for assessment
37
37
  listenerConfig.apply();
38
+ // Single module mode - bypass orchestrator for lightweight execution (Issue #184)
39
+ if (options.singleModule) {
40
+ const result = await runSingleModule(options.singleModule, options);
41
+ if (!options.jsonOnly) {
42
+ displaySingleModuleSummary(result);
43
+ }
44
+ const outputPath = saveSingleModuleResults(options.serverName, options.singleModule, result, options);
45
+ if (options.jsonOnly) {
46
+ console.log(outputPath);
47
+ }
48
+ else {
49
+ console.log(`\n📄 Results saved to: ${outputPath}\n`);
50
+ }
51
+ const exitCode = result.status === "FAIL" ? 1 : 0;
52
+ setTimeout(() => process.exit(exitCode), 10);
53
+ return;
54
+ }
38
55
  const results = await runFullAssessment(options);
39
56
  // Pre-flight mode handles its own output and exit
40
57
  if (options.preflightOnly) {
@@ -43,9 +43,34 @@ export async function runFullAssessment(options) {
43
43
  if (!options.jsonOnly) {
44
44
  console.log(`\n🔍 Starting full assessment for: ${options.serverName}`);
45
45
  }
46
- const serverConfig = loadServerConfig(options.serverName, options.serverConfigPath);
47
- if (!options.jsonOnly) {
48
- console.log("✅ Server config loaded");
46
+ // Determine server config: --http/--sse flags take precedence over --config
47
+ let serverConfig;
48
+ if (options.httpUrl) {
49
+ // Direct HTTP URL provided via --http flag (no config file needed)
50
+ serverConfig = {
51
+ transport: "http",
52
+ url: options.httpUrl,
53
+ };
54
+ if (!options.jsonOnly) {
55
+ console.log(`✅ Using HTTP transport: ${options.httpUrl}`);
56
+ }
57
+ }
58
+ else if (options.sseUrl) {
59
+ // Direct SSE URL provided via --sse flag (no config file needed)
60
+ serverConfig = {
61
+ transport: "sse",
62
+ url: options.sseUrl,
63
+ };
64
+ if (!options.jsonOnly) {
65
+ console.log(`✅ Using SSE transport: ${options.sseUrl}`);
66
+ }
67
+ }
68
+ else {
69
+ // Load from config file (existing behavior)
70
+ serverConfig = loadServerConfig(options.serverName, options.serverConfigPath);
71
+ if (!options.jsonOnly) {
72
+ console.log("✅ Server config loaded");
73
+ }
49
74
  }
50
75
  // Phase 1: Discovery
51
76
  const discoveryStart = Date.now();
@@ -20,3 +20,5 @@ export { createCallToolWrapper } from "./tool-wrapper.js";
20
20
  export { buildConfig } from "./config-builder.js";
21
21
  // Assessment Execution
22
22
  export { runFullAssessment } from "./assessment-executor.js";
23
+ // Single Module Execution (Issue #184)
24
+ export { runSingleModule, getValidModuleNames, } from "./single-module-runner.js";
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Single Module Runner
3
+ *
4
+ * Lightweight execution for individual assessment modules via --module flag.
5
+ * Bypasses full orchestration for faster, targeted assessment.
6
+ *
7
+ * @module cli/lib/assessment-runner/single-module-runner
8
+ * @see GitHub Issue #184
9
+ */
10
+ import { ASSESSOR_DEFINITION_MAP, ASSESSOR_DEFINITIONS, } from "../../../../client/lib/services/assessment/registry/AssessorDefinitions.js";
11
+ import { DEFAULT_CONTEXT_REQUIREMENTS } from "../../../../client/lib/services/assessment/registry/types.js";
12
+ import { loadServerConfig } from "./server-config.js";
13
+ import { loadSourceFiles } from "./source-loader.js";
14
+ import { resolveSourcePath } from "./path-resolver.js";
15
+ import { connectToServer } from "./server-connection.js";
16
+ import { createCallToolWrapper } from "./tool-wrapper.js";
17
+ import { buildConfig } from "./config-builder.js";
18
+ import { getToolsWithPreservedHints } from "./tools-with-hints.js";
19
+ /**
20
+ * Get all valid module names for --module validation.
21
+ */
22
+ export function getValidModuleNames() {
23
+ return ASSESSOR_DEFINITIONS.map((def) => def.id);
24
+ }
25
+ /**
26
+ * Run a single assessment module directly without orchestration.
27
+ *
28
+ * This provides lightweight execution for targeted validation:
29
+ * - Builds only the context required by the specific module
30
+ * - Skips orchestrator phase ordering
31
+ * - Returns focused output
32
+ *
33
+ * @param moduleName - The module ID to run (e.g., 'toolAnnotations', 'functionality')
34
+ * @param options - CLI assessment options
35
+ * @returns Single module result
36
+ */
37
+ export async function runSingleModule(moduleName, options) {
38
+ const definition = ASSESSOR_DEFINITION_MAP.get(moduleName);
39
+ if (!definition) {
40
+ const validModules = getValidModuleNames().join(", ");
41
+ throw new Error(`Unknown module: ${moduleName}\nValid modules: ${validModules}`);
42
+ }
43
+ if (!options.jsonOnly) {
44
+ console.log(`\n🎯 Running single module: ${definition.displayName}`);
45
+ }
46
+ // Build server config (respects --http/--sse from Issue #183)
47
+ let serverConfig;
48
+ if (options.httpUrl) {
49
+ serverConfig = { transport: "http", url: options.httpUrl };
50
+ if (!options.jsonOnly) {
51
+ console.log(`✅ Using HTTP transport: ${options.httpUrl}`);
52
+ }
53
+ }
54
+ else if (options.sseUrl) {
55
+ serverConfig = { transport: "sse", url: options.sseUrl };
56
+ if (!options.jsonOnly) {
57
+ console.log(`✅ Using SSE transport: ${options.sseUrl}`);
58
+ }
59
+ }
60
+ else {
61
+ serverConfig = loadServerConfig(options.serverName, options.serverConfigPath);
62
+ if (!options.jsonOnly) {
63
+ console.log("✅ Server config loaded");
64
+ }
65
+ }
66
+ const client = await connectToServer(serverConfig);
67
+ try {
68
+ if (!options.jsonOnly) {
69
+ console.log("✅ Connected to MCP server");
70
+ }
71
+ // Get context requirements (use default if not specified)
72
+ const requirements = definition.contextRequirements || DEFAULT_CONTEXT_REQUIREMENTS;
73
+ // Build minimal context based on module requirements
74
+ const context = await buildMinimalContext(client, definition, requirements, options, serverConfig);
75
+ // Instantiate and run the assessor
76
+ const config = buildConfig(options);
77
+ const assessor = new definition.assessorClass(config);
78
+ // Apply custom setup if defined (e.g., ToolAnnotationAssessor pattern loading)
79
+ if (definition.customSetup) {
80
+ const { createLogger } = await import("../../../../client/lib/services/assessment/lib/logger.js");
81
+ const logger = createLogger(config.logging?.level || "info");
82
+ definition.customSetup(assessor, config, logger);
83
+ }
84
+ if (!options.jsonOnly) {
85
+ console.log(`\n🏃 Running ${definition.displayName} assessment...`);
86
+ }
87
+ const startTime = Date.now();
88
+ const result = await assessor.assess(context);
89
+ const executionTime = Date.now() - startTime;
90
+ // Estimate test count using the definition's estimator
91
+ const estimatedTestCount = definition.estimateTests(context, config);
92
+ const singleResult = {
93
+ timestamp: new Date().toISOString(),
94
+ serverName: options.serverName,
95
+ module: moduleName,
96
+ displayName: definition.displayName,
97
+ result,
98
+ status: extractStatus(result),
99
+ executionTime,
100
+ estimatedTestCount,
101
+ phase: definition.phase,
102
+ };
103
+ return singleResult;
104
+ }
105
+ finally {
106
+ await client.close();
107
+ }
108
+ }
109
+ /**
110
+ * Build minimal AssessmentContext based on module requirements.
111
+ * Only fetches/prepares what the specific module needs.
112
+ */
113
+ async function buildMinimalContext(client, definition, requirements, options, serverConfig) {
114
+ const config = buildConfig(options);
115
+ // Start with minimal context
116
+ const context = {
117
+ serverName: options.serverName,
118
+ config,
119
+ };
120
+ // Fetch tools if needed
121
+ if (requirements.needsTools) {
122
+ context.tools = await getToolsWithPreservedHints(client);
123
+ if (!options.jsonOnly) {
124
+ console.log(`🔧 Found ${context.tools.length} tool${context.tools.length !== 1 ? "s" : ""}`);
125
+ }
126
+ }
127
+ else {
128
+ context.tools = [];
129
+ }
130
+ // Setup callTool wrapper if needed
131
+ if (requirements.needsCallTool) {
132
+ context.callTool = createCallToolWrapper(client);
133
+ }
134
+ // Setup listTools function if needed (for TemporalAssessor baseline)
135
+ if (requirements.needsListTools) {
136
+ context.listTools = async () => {
137
+ return getToolsWithPreservedHints(client);
138
+ };
139
+ }
140
+ // Fetch resources if needed
141
+ if (requirements.needsResources) {
142
+ try {
143
+ const resourcesResponse = await client.listResources();
144
+ context.resources = (resourcesResponse.resources || []).map((r) => ({
145
+ uri: r.uri,
146
+ name: r.name,
147
+ description: r.description,
148
+ mimeType: r.mimeType,
149
+ }));
150
+ // Also get resource templates
151
+ try {
152
+ const templatesResponse = await client.listResourceTemplates();
153
+ context.resourceTemplates = (templatesResponse.resourceTemplates || []).map((rt) => ({
154
+ uriTemplate: rt.uriTemplate,
155
+ name: rt.name,
156
+ description: rt.description,
157
+ mimeType: rt.mimeType,
158
+ }));
159
+ }
160
+ catch {
161
+ context.resourceTemplates = [];
162
+ }
163
+ // Setup readResource wrapper
164
+ context.readResource = async (uri) => {
165
+ const response = await client.readResource({ uri });
166
+ if (response.contents && response.contents.length > 0) {
167
+ const content = response.contents[0];
168
+ if ("text" in content && content.text) {
169
+ return content.text;
170
+ }
171
+ if ("blob" in content && content.blob) {
172
+ return content.blob;
173
+ }
174
+ }
175
+ return "";
176
+ };
177
+ if (!options.jsonOnly && context.resources.length > 0) {
178
+ console.log(`📦 Found ${context.resources.length} resource(s) and ${context.resourceTemplates?.length || 0} template(s)`);
179
+ }
180
+ }
181
+ catch {
182
+ context.resources = [];
183
+ context.resourceTemplates = [];
184
+ if (!options.jsonOnly) {
185
+ console.log("📦 Resources not supported by server");
186
+ }
187
+ }
188
+ }
189
+ // Fetch prompts if needed
190
+ if (requirements.needsPrompts) {
191
+ try {
192
+ const promptsResponse = await client.listPrompts();
193
+ context.prompts = (promptsResponse.prompts || []).map((p) => ({
194
+ name: p.name,
195
+ description: p.description,
196
+ arguments: p.arguments?.map((a) => ({
197
+ name: a.name,
198
+ description: a.description,
199
+ required: a.required,
200
+ })),
201
+ }));
202
+ // Setup getPrompt wrapper
203
+ context.getPrompt = async (name, args) => {
204
+ const response = await client.getPrompt({ name, arguments: args });
205
+ return {
206
+ messages: (response.messages || []).map((m) => ({
207
+ role: m.role,
208
+ content: typeof m.content === "string"
209
+ ? m.content
210
+ : JSON.stringify(m.content),
211
+ })),
212
+ };
213
+ };
214
+ if (!options.jsonOnly && context.prompts.length > 0) {
215
+ console.log(`💬 Found ${context.prompts.length} prompt(s)`);
216
+ }
217
+ }
218
+ catch {
219
+ context.prompts = [];
220
+ if (!options.jsonOnly) {
221
+ console.log("💬 Prompts not supported by server");
222
+ }
223
+ }
224
+ }
225
+ // Load source code if needed and path provided
226
+ if (requirements.needsSourceCode && options.sourceCodePath) {
227
+ const resolvedSourcePath = resolveSourcePath(options.sourceCodePath);
228
+ const { existsSync } = await import("fs");
229
+ if (existsSync(resolvedSourcePath)) {
230
+ const sourceFiles = loadSourceFiles(resolvedSourcePath, options.debugSource);
231
+ context.sourceCodeFiles = sourceFiles.sourceCodeFiles;
232
+ context.sourceCodePath = options.sourceCodePath;
233
+ context.manifestJson = sourceFiles.manifestJson;
234
+ context.manifestRaw = sourceFiles.manifestRaw;
235
+ context.packageJson = sourceFiles.packageJson;
236
+ context.readmeContent = sourceFiles.readmeContent;
237
+ if (!options.jsonOnly) {
238
+ console.log(`📁 Loaded source files from: ${resolvedSourcePath}`);
239
+ }
240
+ }
241
+ else if (!options.jsonOnly) {
242
+ console.log(`⚠️ Source path not found: ${options.sourceCodePath} (module may have reduced coverage)`);
243
+ }
244
+ }
245
+ // Load manifest specifically if needed (for ManifestValidationAssessor)
246
+ if (requirements.needsManifest && options.sourceCodePath) {
247
+ const resolvedSourcePath = resolveSourcePath(options.sourceCodePath);
248
+ const { existsSync } = await import("fs");
249
+ if (existsSync(resolvedSourcePath)) {
250
+ const sourceFiles = loadSourceFiles(resolvedSourcePath, options.debugSource);
251
+ context.manifestJson = sourceFiles.manifestJson;
252
+ context.manifestRaw = sourceFiles.manifestRaw;
253
+ }
254
+ }
255
+ // Get server info if needed (for ProtocolComplianceAssessor)
256
+ if (requirements.needsServerInfo) {
257
+ const rawServerInfo = client.getServerVersion();
258
+ const rawServerCapabilities = client.getServerCapabilities();
259
+ context.serverInfo = rawServerInfo
260
+ ? {
261
+ name: rawServerInfo.name || "unknown",
262
+ version: rawServerInfo.version,
263
+ metadata: rawServerInfo.metadata,
264
+ }
265
+ : undefined;
266
+ context.serverCapabilities =
267
+ rawServerCapabilities ??
268
+ undefined;
269
+ // Set serverUrl for conformance tests when HTTP/SSE transport
270
+ if (serverConfig.url && !config.serverUrl) {
271
+ config.serverUrl = serverConfig.url;
272
+ }
273
+ }
274
+ return context;
275
+ }
276
+ /**
277
+ * Extract status from assessment result.
278
+ * Handles various result structures from different assessors.
279
+ */
280
+ function extractStatus(result) {
281
+ if (!result || typeof result !== "object") {
282
+ return "UNKNOWN";
283
+ }
284
+ const r = result;
285
+ // Check for direct status field
286
+ if (typeof r.status === "string") {
287
+ return r.status;
288
+ }
289
+ // Check for overallStatus field
290
+ if (typeof r.overallStatus === "string") {
291
+ return r.overallStatus;
292
+ }
293
+ // Check for overall field with status
294
+ if (r.overall && typeof r.overall === "object") {
295
+ const overall = r.overall;
296
+ if (typeof overall.status === "string") {
297
+ return overall.status;
298
+ }
299
+ }
300
+ // Derive from vulnerabilities/issues count
301
+ if (Array.isArray(r.vulnerabilities) && r.vulnerabilities.length > 0) {
302
+ return "FAIL";
303
+ }
304
+ if (Array.isArray(r.issues) && r.issues.length > 0) {
305
+ return "FAIL";
306
+ }
307
+ // Check for pass/fail counts
308
+ if (typeof r.passCount === "number" && typeof r.failCount === "number") {
309
+ if (r.failCount > 0)
310
+ return "FAIL";
311
+ if (r.passCount > 0)
312
+ return "PASS";
313
+ }
314
+ return "UNKNOWN";
315
+ }
@@ -200,6 +200,89 @@ export function parseArgs(argv) {
200
200
  case "--skip-temporal":
201
201
  options.skipTemporal = true;
202
202
  break;
203
+ case "--http": {
204
+ const httpUrlValue = args[++i];
205
+ if (!httpUrlValue || httpUrlValue.startsWith("-")) {
206
+ console.error("Error: --http requires a URL argument");
207
+ console.error(" Example: --http http://localhost:10900/mcp");
208
+ setTimeout(() => process.exit(1), 10);
209
+ options.helpRequested = true;
210
+ return options;
211
+ }
212
+ try {
213
+ const parsedHttpUrl = new URL(httpUrlValue);
214
+ // Validate protocol is HTTP or HTTPS (reject file://, ftp://, etc.)
215
+ if (parsedHttpUrl.protocol !== "http:" &&
216
+ parsedHttpUrl.protocol !== "https:") {
217
+ console.error(`Error: --http requires HTTP or HTTPS URL, got: ${parsedHttpUrl.protocol}`);
218
+ console.error(" Expected format: http://hostname:port/path or https://hostname:port/path");
219
+ setTimeout(() => process.exit(1), 10);
220
+ options.helpRequested = true;
221
+ return options;
222
+ }
223
+ options.httpUrl = httpUrlValue;
224
+ }
225
+ catch {
226
+ console.error(`Error: Invalid URL for --http: ${httpUrlValue}`);
227
+ console.error(" Expected format: http://hostname:port/path or https://hostname:port/path");
228
+ setTimeout(() => process.exit(1), 10);
229
+ options.helpRequested = true;
230
+ return options;
231
+ }
232
+ break;
233
+ }
234
+ case "--sse": {
235
+ const sseUrlValue = args[++i];
236
+ if (!sseUrlValue || sseUrlValue.startsWith("-")) {
237
+ console.error("Error: --sse requires a URL argument");
238
+ console.error(" Example: --sse http://localhost:9002/sse");
239
+ setTimeout(() => process.exit(1), 10);
240
+ options.helpRequested = true;
241
+ return options;
242
+ }
243
+ try {
244
+ const parsedSseUrl = new URL(sseUrlValue);
245
+ // Validate protocol is HTTP or HTTPS (reject file://, ftp://, etc.)
246
+ if (parsedSseUrl.protocol !== "http:" &&
247
+ parsedSseUrl.protocol !== "https:") {
248
+ console.error(`Error: --sse requires HTTP or HTTPS URL, got: ${parsedSseUrl.protocol}`);
249
+ console.error(" Expected format: http://hostname:port/path or https://hostname:port/path");
250
+ setTimeout(() => process.exit(1), 10);
251
+ options.helpRequested = true;
252
+ return options;
253
+ }
254
+ options.sseUrl = sseUrlValue;
255
+ }
256
+ catch {
257
+ console.error(`Error: Invalid URL for --sse: ${sseUrlValue}`);
258
+ console.error(" Expected format: http://hostname:port/path or https://hostname:port/path");
259
+ setTimeout(() => process.exit(1), 10);
260
+ options.helpRequested = true;
261
+ return options;
262
+ }
263
+ break;
264
+ }
265
+ case "--module":
266
+ case "-m": {
267
+ // Issue #184: Single module execution (bypasses orchestrator)
268
+ const moduleValue = args[++i];
269
+ if (!moduleValue || moduleValue.startsWith("-")) {
270
+ console.error("Error: --module requires a module name");
271
+ console.error(` Valid modules: ${VALID_MODULE_NAMES.join(", ")}`);
272
+ setTimeout(() => process.exit(1), 10);
273
+ options.helpRequested = true;
274
+ return options;
275
+ }
276
+ // Validate the module name
277
+ const validated = validateModuleNames(moduleValue, "--module");
278
+ if (validated.length === 0) {
279
+ options.helpRequested = true;
280
+ return options;
281
+ }
282
+ // Only accept a single module (first one if multiple provided)
283
+ options.singleModule = validated[0];
284
+ break;
285
+ }
203
286
  case "--conformance":
204
287
  // Enable official MCP conformance tests (requires HTTP/SSE transport with serverUrl)
205
288
  options.conformanceEnabled = true;
@@ -326,6 +409,32 @@ export function parseArgs(argv) {
326
409
  options.helpRequested = true;
327
410
  return options;
328
411
  }
412
+ // Validate mutual exclusivity of --module with orchestrator options (Issue #184)
413
+ if (options.singleModule &&
414
+ (options.skipModules?.length ||
415
+ options.onlyModules?.length ||
416
+ options.profile)) {
417
+ console.error("Error: --module cannot be used with --skip-modules, --only-modules, or --profile");
418
+ console.error(" Use --module for single-module runs, or the other flags for orchestrated runs");
419
+ setTimeout(() => process.exit(1), 10);
420
+ options.helpRequested = true;
421
+ return options;
422
+ }
423
+ // Validate mutual exclusivity of --http, --sse, and --config
424
+ if ((options.httpUrl || options.sseUrl) && options.serverConfigPath) {
425
+ console.error("Error: --http/--sse cannot be used with --config (they are mutually exclusive)");
426
+ console.error(" Use --http or --sse for direct URL, or --config for JSON file");
427
+ setTimeout(() => process.exit(1), 10);
428
+ options.helpRequested = true;
429
+ return options;
430
+ }
431
+ if (options.httpUrl && options.sseUrl) {
432
+ console.error("Error: --http and --sse are mutually exclusive");
433
+ console.error(" Use --http for HTTP transport or --sse for SSE transport");
434
+ setTimeout(() => process.exit(1), 10);
435
+ options.helpRequested = true;
436
+ return options;
437
+ }
329
438
  if (!options.serverName) {
330
439
  console.error("Error: --server is required");
331
440
  printHelp();
@@ -379,6 +488,8 @@ Run comprehensive MCP server assessment with 16 assessor modules organized in 4
379
488
  Options:
380
489
  --server, -s <name> Server name (required, or pass as first positional arg)
381
490
  --config, -c <path> Path to server config JSON
491
+ --http <url> Use HTTP transport with specified URL (no config file needed)
492
+ --sse <url> Use SSE transport with specified URL (no config file needed)
382
493
  --output, -o <path> Output path (default: /tmp/inspector-full-assessment-<server>.<ext>)
383
494
  --source <path> Source code path for deep analysis (AUP, portability, etc.)
384
495
  --debug-source Enable debug logging for source file loading (Issue #151)
@@ -398,7 +509,7 @@ Options:
398
509
  --mcp-auditor-url <url> mcp-auditor URL for HTTP transport (default: http://localhost:8085)
399
510
  --full Enable all assessment modules (default)
400
511
  --profile <name> Use predefined module profile (quick, security, compliance, full)
401
- --temporal-invocations <n> Number of invocations per tool for rug pull detection (default: 25)
512
+ --temporal-invocations <n> Number of invocations per tool for rug pull detection (default: 3)
402
513
  --skip-temporal Skip temporal/rug pull testing (faster assessment)
403
514
  --conformance Enable official MCP conformance tests (experimental, requires HTTP/SSE transport)
404
515
  --output-format <fmt> Output format: full (default), tiered, summary-only
@@ -409,6 +520,8 @@ Options:
409
520
  --stage-b-verbose Enable Stage B enrichment for Claude semantic analysis
410
521
  Adds evidence samples, payload correlations, and confidence
411
522
  breakdowns to tiered output (Tier 2 + Tier 3)
523
+ --module, -m <name> Run single module directly (bypasses orchestrator for faster execution)
524
+ Mutually exclusive with --skip-modules, --only-modules, --profile
412
525
  --skip-modules <list> Skip specific modules (comma-separated)
413
526
  --only-modules <list> Run only specific modules (comma-separated)
414
527
  --json Output only JSON path (no console summary)
@@ -426,7 +539,8 @@ Environment Variables:
426
539
 
427
540
  ${getProfileHelpText()}
428
541
  Module Selection:
429
- --profile, --skip-modules, and --only-modules are mutually exclusive.
542
+ --module, --profile, --skip-modules, and --only-modules are mutually exclusive.
543
+ Use --module for single-module runs (fastest, bypasses orchestrator).
430
544
  Use --profile for common assessment scenarios.
431
545
  Use --skip-modules for custom runs by disabling expensive modules.
432
546
  Use --only-modules to focus on specific areas (e.g., tool annotation PRs).
@@ -468,13 +582,27 @@ Module Tiers (16 total):
468
582
  • Portability - Cross-platform compatibility
469
583
  • External API - External service detection
470
584
 
585
+ Transport Options:
586
+ --config, --http, and --sse are mutually exclusive.
587
+ Use --http or --sse for quick testing without a config file.
588
+ Use --config for complex setups (STDIO, env vars, etc.).
589
+
471
590
  Examples:
591
+ # Quick HTTP/SSE testing (no config file needed):
592
+ mcp-assess-full my-server --http http://localhost:10900/mcp
593
+ mcp-assess-full my-server --sse http://localhost:9002/sse
594
+
472
595
  # Profile-based (recommended):
473
596
  mcp-assess-full my-server --profile quick # CI/CD fast check (~30s)
474
597
  mcp-assess-full my-server --profile security # Security audit (~2-3min)
475
598
  mcp-assess-full my-server --profile compliance # Directory submission (~5min)
476
599
  mcp-assess-full my-server --profile full # Comprehensive audit (~10-15min)
477
600
 
601
+ # Single module (fastest - bypasses orchestrator):
602
+ mcp-assess-full my-server --http http://localhost:10900/mcp --module toolAnnotations
603
+ mcp-assess-full my-server --http http://localhost:10900/mcp --module functionality
604
+ mcp-assess-full my-server --http http://localhost:10900/mcp --module security
605
+
478
606
  # Custom module selection:
479
607
  mcp-assess-full my-server --skip-modules temporal,resources # Skip expensive modules
480
608
  mcp-assess-full my-server --only-modules functionality,toolAnnotations # Annotation PR review
@@ -305,3 +305,133 @@ export function displaySummary(results) {
305
305
  }
306
306
  console.log("\n" + "=".repeat(70));
307
307
  }
308
+ // ============================================================================
309
+ // Single Module Output (Issue #184)
310
+ // ============================================================================
311
+ /**
312
+ * Save single module results to file.
313
+ *
314
+ * @param serverName - Server name
315
+ * @param moduleName - Module ID that was executed
316
+ * @param result - Single module result
317
+ * @param options - Assessment options
318
+ * @returns Path to output file
319
+ */
320
+ export function saveSingleModuleResults(serverName, moduleName, result, options) {
321
+ const defaultPath = `/tmp/inspector-${moduleName}-${serverName}.json`;
322
+ const finalPath = options.outputPath || defaultPath;
323
+ fs.writeFileSync(finalPath, JSON.stringify(result, null, 2));
324
+ return finalPath;
325
+ }
326
+ /**
327
+ * Display single module summary to console.
328
+ *
329
+ * @param result - Single module result
330
+ */
331
+ export function displaySingleModuleSummary(result) {
332
+ const statusIcon = result.status === "PASS"
333
+ ? "✅"
334
+ : result.status === "FAIL"
335
+ ? "❌"
336
+ : result.status === "PARTIAL"
337
+ ? "⚠️"
338
+ : "❓";
339
+ console.log("\n" + "=".repeat(60));
340
+ console.log(`MODULE: ${result.displayName.toUpperCase()}`);
341
+ console.log("=".repeat(60));
342
+ console.log(`Server: ${result.serverName}`);
343
+ console.log(`Status: ${statusIcon} ${result.status}`);
344
+ console.log(`Estimated Tests: ${result.estimatedTestCount}`);
345
+ console.log(`Execution Time: ${result.executionTime}ms`);
346
+ console.log("=".repeat(60));
347
+ // Display module-specific highlights based on result type
348
+ displayModuleHighlights(result);
349
+ }
350
+ /**
351
+ * Display module-specific highlights from the result.
352
+ */
353
+ function displayModuleHighlights(result) {
354
+ const r = result.result;
355
+ if (!r)
356
+ return;
357
+ // Security module
358
+ if (result.module === "security") {
359
+ const vulnCount = Array.isArray(r.vulnerabilities)
360
+ ? r.vulnerabilities.length
361
+ : 0;
362
+ if (vulnCount > 0) {
363
+ console.log(`\n🔒 VULNERABILITIES FOUND: ${vulnCount}`);
364
+ const vulns = r.vulnerabilities;
365
+ for (const vuln of vulns.slice(0, 5)) {
366
+ console.log(` • ${vuln.toolName || "unknown"}: ${vuln.testName || "unknown"} (${vuln.riskLevel || "unknown"})`);
367
+ }
368
+ if (vulnCount > 5) {
369
+ console.log(` ... and ${vulnCount - 5} more`);
370
+ }
371
+ }
372
+ else {
373
+ console.log("\n✅ No vulnerabilities detected");
374
+ }
375
+ }
376
+ // Functionality module
377
+ if (result.module === "functionality") {
378
+ const working = r.workingTools;
379
+ const broken = r.brokenTools;
380
+ if (typeof working === "number" || typeof broken === "number") {
381
+ console.log(`\n📊 TOOL STATUS: ${working || 0} working, ${Array.isArray(broken) ? broken.length : 0} broken`);
382
+ }
383
+ }
384
+ // Tool Annotations module
385
+ if (result.module === "toolAnnotations") {
386
+ const missing = r.missingAnnotationsCount;
387
+ const misaligned = r.misalignedAnnotationsCount;
388
+ const annotated = r.annotatedCount;
389
+ console.log(`\n🏷️ ANNOTATION STATUS:`);
390
+ if (typeof annotated === "number") {
391
+ console.log(` Annotated tools: ${annotated}`);
392
+ }
393
+ if (typeof missing === "number" && missing > 0) {
394
+ console.log(` ⚠️ Missing annotations: ${missing}`);
395
+ }
396
+ if (typeof misaligned === "number" && misaligned > 0) {
397
+ console.log(` ⚠️ Misaligned annotations: ${misaligned}`);
398
+ }
399
+ }
400
+ // Error Handling module
401
+ if (result.module === "errorHandling") {
402
+ const metrics = r.metrics;
403
+ if (metrics) {
404
+ console.log(`\n🛡️ ERROR HANDLING METRICS:`);
405
+ if (typeof metrics.invalidParamHandledCorrectly === "number") {
406
+ console.log(` Invalid param handling: ${metrics.invalidParamHandledCorrectly}%`);
407
+ }
408
+ if (typeof metrics.missingParamHandledCorrectly === "number") {
409
+ console.log(` Missing param handling: ${metrics.missingParamHandledCorrectly}%`);
410
+ }
411
+ }
412
+ }
413
+ // AUP Compliance module
414
+ if (result.module === "aupCompliance") {
415
+ const violations = r.violations;
416
+ if (Array.isArray(violations) && violations.length > 0) {
417
+ const critical = violations.filter((v) => v.severity === "CRITICAL");
418
+ console.log(`\n⚖️ AUP FINDINGS:`);
419
+ console.log(` Total flagged: ${violations.length}`);
420
+ if (critical.length > 0) {
421
+ console.log(` 🚨 CRITICAL violations: ${critical.length}`);
422
+ }
423
+ }
424
+ }
425
+ // Temporal module
426
+ if (result.module === "temporal") {
427
+ const overallRisk = r.overallRisk;
428
+ const detectedChanges = r.detectedChanges;
429
+ console.log(`\n⏱️ TEMPORAL ANALYSIS:`);
430
+ if (typeof overallRisk === "string") {
431
+ console.log(` Overall Risk: ${overallRisk}`);
432
+ }
433
+ if (typeof detectedChanges === "number") {
434
+ console.log(` Detected Changes: ${detectedChanges}`);
435
+ }
436
+ }
437
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bryan-thompson/inspector-assessment-cli",
3
- "version": "1.38.3",
3
+ "version": "1.40.0",
4
4
  "description": "CLI for the Enhanced MCP Inspector with assessment capabilities",
5
5
  "license": "MIT",
6
6
  "author": "Bryan Thompson <bryan@triepod.ai>",