@bryan-thompson/inspector-assessment 1.2.1 → 1.3.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/README.md CHANGED
@@ -972,6 +972,65 @@ mcp-inspector-assess-cli https://my-mcp-server.example.com --method tools/call -
972
972
  mcp-inspector-assess-cli https://my-mcp-server.example.com --method resources/list
973
973
  ```
974
974
 
975
+ ### Security Testing: Pure Behavior Detection
976
+
977
+ The inspector uses **pure behavior-based detection** for security assessment, analyzing tool responses to identify actual code execution vs safe data handling. This approach works on any MCP server without requiring special security metadata.
978
+
979
+ **How It Works**:
980
+
981
+ ```bash
982
+ # Run security assessment against any MCP server
983
+ npm run assess -- --server myserver --config config.json
984
+ ```
985
+
986
+ **Detection Strategy**:
987
+
988
+ 1. **Reflection Detection**: Identifies when tools safely echo malicious input as data
989
+ - Pattern: "Stored query: ../../../etc/passwd" → SAFE (reflection)
990
+ - Pattern: "Query results for: ..." → SAFE (search results)
991
+
992
+ 2. **Execution Evidence**: Detects actual code execution
993
+ - Pattern: Response contains "root:x:0:0" → VULNERABLE (file accessed)
994
+ - Pattern: Response contains "total 42 drwx" → VULNERABLE (directory listed)
995
+
996
+ 3. **Category Classification**: Distinguishes safe tool types
997
+ - Search/retrieval tools return data, not code execution
998
+ - CRUD operations create resources, not execute code
999
+ - Safe storage tools treat input as pure data
1000
+
1001
+ **Validation with Testbed**:
1002
+
1003
+ The inspector has been validated against purpose-built testbed servers with ground-truth labeled tools:
1004
+
1005
+ ```bash
1006
+ # Test against broken-mcp testbed (10 vulnerable + 6 safe tools)
1007
+ npm run assess -- --server broken-mcp --config testbed.json
1008
+
1009
+ # Results: 20 vulnerabilities detected, 0 false positives (100% precision)
1010
+ ```
1011
+
1012
+ **Why Behavior Detection Matters**:
1013
+
1014
+ Real-world MCP servers don't provide security metadata - the inspector must detect vulnerabilities by analyzing actual tool behavior. Testbed validation proves this approach works reliably.
1015
+
1016
+ **For Inspector Developers**:
1017
+
1018
+ When modifying detection logic, validate against the testbed:
1019
+
1020
+ ```bash
1021
+ # Before changes: Record baseline
1022
+ npm run assess -- --server broken-mcp --output /tmp/baseline.json
1023
+
1024
+ # After changes: Verify no regressions
1025
+ npm run assess -- --server broken-mcp --output /tmp/after.json
1026
+
1027
+ # Expected: 0 false positives on safe tools
1028
+ cat /tmp/after.json | jq '[.security.promptInjectionTests[] | select(.toolName | startswith("safe_")) | select(.vulnerable == true)] | length'
1029
+ # Output: 0
1030
+ ```
1031
+
1032
+ See [docs/mcp_vulnerability_testbed.md](docs/mcp_vulnerability_testbed.md) for detailed validation results and testbed usage guide.
1033
+
975
1034
  ### UI Mode vs CLI Mode: When to Use Each
976
1035
 
977
1036
  | Use Case | UI Mode | CLI Mode |
@@ -1,4 +1,4 @@
1
- import { u as useToast, r as reactExports, j as jsxRuntimeExports, p as parseOAuthCallbackParams, g as generateOAuthErrorDescription, S as SESSION_KEYS, I as InspectorOAuthClientProvider, a as auth } from "./index-D12b6zCd.js";
1
+ import { u as useToast, r as reactExports, j as jsxRuntimeExports, p as parseOAuthCallbackParams, g as generateOAuthErrorDescription, S as SESSION_KEYS, I as InspectorOAuthClientProvider, a as auth } from "./index-BsOrK-Nh.js";
2
2
  const OAuthCallback = ({ onConnect }) => {
3
3
  const { toast } = useToast();
4
4
  const hasProcessedRef = reactExports.useRef(false);
@@ -1,4 +1,4 @@
1
- import { r as reactExports, S as SESSION_KEYS, p as parseOAuthCallbackParams, j as jsxRuntimeExports, g as generateOAuthErrorDescription } from "./index-D12b6zCd.js";
1
+ import { r as reactExports, S as SESSION_KEYS, p as parseOAuthCallbackParams, j as jsxRuntimeExports, g as generateOAuthErrorDescription } from "./index-BsOrK-Nh.js";
2
2
  const OAuthDebugCallback = ({ onConnect }) => {
3
3
  reactExports.useEffect(() => {
4
4
  let isProcessed = false;
@@ -16205,7 +16205,7 @@ objectType({
16205
16205
  token_type_hint: stringType().optional()
16206
16206
  }).strip();
16207
16207
  const name = "@bryan-thompson/inspector-assessment-client";
16208
- const version$1 = "1.2.1";
16208
+ const version$1 = "1.3.0";
16209
16209
  const packageJson = {
16210
16210
  name,
16211
16211
  version: version$1
@@ -16986,8 +16986,8 @@ class InspectorOAuthClientProvider {
16986
16986
  token_endpoint_auth_method: "none",
16987
16987
  grant_types: ["authorization_code", "refresh_token"],
16988
16988
  response_types: ["code"],
16989
- client_name: "MCP Inspector",
16990
- client_uri: "https://github.com/modelcontextprotocol/inspector",
16989
+ client_name: "MCP Assessor",
16990
+ client_uri: "https://github.com/triepod-ai/inspector-assessment",
16991
16991
  scope: this.scope ?? ""
16992
16992
  };
16993
16993
  }
@@ -41736,7 +41736,7 @@ const useTheme = () => {
41736
41736
  [theme, setThemeWithSideEffect]
41737
41737
  );
41738
41738
  };
41739
- const version = "1.2.1";
41739
+ const version = "1.3.0";
41740
41740
  var [createTooltipContext] = createContextScope("Tooltip", [
41741
41741
  createPopperScope
41742
41742
  ]);
@@ -42732,7 +42732,7 @@ const Sidebar = ({
42732
42732
  }, [generateMCPServerFile, toast2, reportError2]);
42733
42733
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-card border-r border-border flex flex-col h-full", children: [
42734
42734
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-between p-4 border-b border-gray-200 dark:border-border", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("h1", { className: "ml-2 text-lg font-semibold", children: [
42735
- "MCP Inspector v",
42735
+ "MCP Assessor v",
42736
42736
  version
42737
42737
  ] }) }) }),
42738
42738
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 flex-1 overflow-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
@@ -43288,7 +43288,7 @@ const Sidebar = ({
43288
43288
  children: /* @__PURE__ */ jsxRuntimeExports.jsx(
43289
43289
  "a",
43290
43290
  {
43291
- href: "https://github.com/modelcontextprotocol/inspector",
43291
+ href: "https://github.com/triepod-ai/inspector-assessment",
43292
43292
  target: "_blank",
43293
43293
  rel: "noopener noreferrer",
43294
43294
  children: /* @__PURE__ */ jsxRuntimeExports.jsx(Github, { className: "w-4 h-4 text-foreground" })
@@ -47002,10 +47002,20 @@ class SecurityAssessor extends BaseAssessor {
47002
47002
  async assess(context) {
47003
47003
  const toolsToTest = this.selectToolsForTesting(context.tools);
47004
47004
  const allTests = await this.runUniversalSecurityTests(context);
47005
+ const connectionErrors = allTests.filter((t) => t.connectionError === true);
47006
+ const validTests = allTests.filter((t) => !t.connectionError);
47007
+ if (connectionErrors.length > 0) {
47008
+ this.log(
47009
+ `⚠️ WARNING: ${connectionErrors.length} test${connectionErrors.length !== 1 ? "s" : ""} failed due to connection/server errors`
47010
+ );
47011
+ this.log(
47012
+ `Connection errors: ${connectionErrors.map((e) => `${e.toolName}:${e.testName} (${e.errorType})`).join(", ")}`
47013
+ );
47014
+ }
47005
47015
  const vulnerabilities = [];
47006
47016
  let highRiskCount = 0;
47007
47017
  let mediumRiskCount = 0;
47008
- for (const test of allTests) {
47018
+ for (const test of validTests) {
47009
47019
  if (test.vulnerable) {
47010
47020
  let vulnerability;
47011
47021
  if (test.confidence === "high" || !test.confidence) {
@@ -47030,12 +47040,14 @@ class SecurityAssessor extends BaseAssessor {
47030
47040
  vulnerabilities.length
47031
47041
  );
47032
47042
  const status = this.determineSecurityStatus(
47033
- allTests,
47043
+ validTests,
47034
47044
  vulnerabilities.length,
47035
- allTests.length
47045
+ validTests.length,
47046
+ connectionErrors.length
47036
47047
  );
47037
47048
  const explanation = this.generateSecurityExplanation(
47038
- allTests,
47049
+ validTests,
47050
+ connectionErrors,
47039
47051
  vulnerabilities,
47040
47052
  overallRiskLevel
47041
47053
  );
@@ -47256,6 +47268,24 @@ class SecurityAssessor extends BaseAssessor {
47256
47268
  callTool(tool.name, params),
47257
47269
  5e3
47258
47270
  );
47271
+ if (this.isConnectionError(response)) {
47272
+ return {
47273
+ testName: attackName,
47274
+ description: payload.description,
47275
+ payload: payload.payload,
47276
+ riskLevel: payload.riskLevel,
47277
+ toolName: tool.name,
47278
+ vulnerable: true,
47279
+ // Mark as failed (test could not complete)
47280
+ evidence: `CONNECTION ERROR: Test could not complete due to server/network failure`,
47281
+ response: this.extractResponseContent(response),
47282
+ connectionError: true,
47283
+ errorType: this.classifyError(response),
47284
+ testReliability: "failed",
47285
+ confidence: "high",
47286
+ requiresManualReview: true
47287
+ };
47288
+ }
47259
47289
  const { isVulnerable, evidence } = this.analyzeResponse(
47260
47290
  response,
47261
47291
  payload,
@@ -47280,6 +47310,23 @@ class SecurityAssessor extends BaseAssessor {
47280
47310
  ...confidenceResult
47281
47311
  };
47282
47312
  } catch (error) {
47313
+ if (this.isConnectionErrorFromException(error)) {
47314
+ return {
47315
+ testName: attackName,
47316
+ description: payload.description,
47317
+ payload: payload.payload,
47318
+ riskLevel: payload.riskLevel,
47319
+ toolName: tool.name,
47320
+ vulnerable: false,
47321
+ evidence: `CONNECTION ERROR: Test could not complete due to server/network failure`,
47322
+ response: this.extractErrorMessage(error),
47323
+ connectionError: true,
47324
+ errorType: this.classifyErrorFromException(error),
47325
+ testReliability: "failed",
47326
+ confidence: "high",
47327
+ requiresManualReview: true
47328
+ };
47329
+ }
47283
47330
  return {
47284
47331
  testName: attackName,
47285
47332
  description: payload.description,
@@ -47292,16 +47339,183 @@ class SecurityAssessor extends BaseAssessor {
47292
47339
  }
47293
47340
  }
47294
47341
  /**
47295
- * Try to parse JSON response and extract structured data
47296
- * Returns null if response is not JSON
47342
+ * Check if response indicates connection/server failure
47343
+ * Returns true if test couldn't complete due to infrastructure issues
47344
+ *
47345
+ * CRITICAL: Only match transport/infrastructure errors, NOT tool business logic
47297
47346
  */
47298
- tryParseResponseJSON(response) {
47299
- try {
47300
- const responseText = this.extractResponseContent(response);
47301
- return JSON.parse(responseText);
47302
- } catch {
47303
- return null;
47347
+ isConnectionError(response) {
47348
+ const text = this.extractResponseContent(response).toLowerCase();
47349
+ const unambiguousPatterns = [
47350
+ /MCP error -32001/i,
47351
+ // MCP transport errors
47352
+ /MCP error -32603/i,
47353
+ // MCP internal error
47354
+ /MCP error -32000/i,
47355
+ // MCP server error
47356
+ /MCP error -32700/i,
47357
+ // MCP parse error
47358
+ /socket hang up/i,
47359
+ // Network socket errors
47360
+ /ECONNREFUSED/i,
47361
+ // Connection refused
47362
+ /ETIMEDOUT/i,
47363
+ // Network timeout
47364
+ /ERR_CONNECTION/i,
47365
+ // Connection errors
47366
+ /fetch failed/i,
47367
+ // HTTP fetch failures
47368
+ /connection reset/i,
47369
+ // Connection reset
47370
+ /error POSTing to endpoint/i,
47371
+ // Transport layer POST errors
47372
+ /error GETting.*endpoint/i,
47373
+ // Transport layer GET errors (requires 'endpoint' to avoid false positives)
47374
+ /service unavailable/i,
47375
+ // HTTP 503 (server down)
47376
+ /gateway timeout/i,
47377
+ // HTTP 504 (gateway timeout)
47378
+ /unknown tool:/i,
47379
+ // Tool name not in current server's tool list (stale tool list)
47380
+ /tool.*not found/i,
47381
+ // Alternative phrasing for missing tool
47382
+ /tool.*does not exist/i,
47383
+ // Alternative phrasing for missing tool
47384
+ /no such tool/i
47385
+ // Alternative phrasing for missing tool
47386
+ ];
47387
+ if (unambiguousPatterns.some((pattern2) => pattern2.test(text))) {
47388
+ return true;
47304
47389
  }
47390
+ const mcpPrefix = /^mcp error -\d+:/i.test(text);
47391
+ if (mcpPrefix) {
47392
+ const contextualPatterns = [
47393
+ /bad request/i,
47394
+ // HTTP 400 (only if in MCP error)
47395
+ /unauthorized/i,
47396
+ // HTTP 401 (only if in MCP error)
47397
+ /forbidden/i,
47398
+ // HTTP 403 (only if in MCP error)
47399
+ /no valid session/i,
47400
+ // Session errors (only if in MCP error)
47401
+ /session.*expired/i,
47402
+ // Session expiration (only if in MCP error)
47403
+ /internal server error/i,
47404
+ // HTTP 500 (only if in MCP error)
47405
+ /HTTP [45]\d\d/i
47406
+ // HTTP status codes (only if in MCP error)
47407
+ ];
47408
+ return contextualPatterns.some((pattern2) => pattern2.test(text));
47409
+ }
47410
+ return false;
47411
+ }
47412
+ /**
47413
+ * Check if caught exception indicates connection/server failure
47414
+ * CRITICAL: Only match transport/infrastructure errors, NOT tool business logic
47415
+ */
47416
+ isConnectionErrorFromException(error) {
47417
+ if (error instanceof Error) {
47418
+ const message = error.message.toLowerCase();
47419
+ const unambiguousPatterns = [
47420
+ /MCP error -32001/i,
47421
+ // MCP transport errors
47422
+ /MCP error -32603/i,
47423
+ // MCP internal error
47424
+ /MCP error -32000/i,
47425
+ // MCP server error
47426
+ /MCP error -32700/i,
47427
+ // MCP parse error
47428
+ /socket hang up/i,
47429
+ // Network socket errors
47430
+ /ECONNREFUSED/i,
47431
+ // Connection refused
47432
+ /ETIMEDOUT/i,
47433
+ // Network timeout
47434
+ /network error/i,
47435
+ // Generic network errors
47436
+ /ERR_CONNECTION/i,
47437
+ // Connection errors
47438
+ /fetch failed/i,
47439
+ // HTTP fetch failures
47440
+ /connection reset/i,
47441
+ // Connection reset
47442
+ /error POSTing to endpoint/i,
47443
+ // Transport layer POST errors
47444
+ /error GETting/i,
47445
+ // Transport layer GET errors
47446
+ /service unavailable/i,
47447
+ // HTTP 503 (server down)
47448
+ /gateway timeout/i,
47449
+ // HTTP 504 (gateway timeout)
47450
+ /unknown tool:/i,
47451
+ // Tool name not in current server's tool list (stale tool list)
47452
+ /tool.*not found/i,
47453
+ // Alternative phrasing for missing tool
47454
+ /tool.*does not exist/i,
47455
+ // Alternative phrasing for missing tool
47456
+ /no such tool/i
47457
+ // Alternative phrasing for missing tool
47458
+ ];
47459
+ if (unambiguousPatterns.some((pattern2) => pattern2.test(message))) {
47460
+ return true;
47461
+ }
47462
+ const mcpPrefix = /^mcp error -\d+:/i.test(message);
47463
+ if (mcpPrefix) {
47464
+ const contextualPatterns = [
47465
+ /bad request/i,
47466
+ /unauthorized/i,
47467
+ /forbidden/i,
47468
+ /no valid session/i,
47469
+ /session.*expired/i,
47470
+ /internal server error/i,
47471
+ /HTTP [45]\d\d/i
47472
+ ];
47473
+ return contextualPatterns.some((pattern2) => pattern2.test(message));
47474
+ }
47475
+ }
47476
+ return false;
47477
+ }
47478
+ /**
47479
+ * Classify error type for reporting
47480
+ */
47481
+ classifyError(response) {
47482
+ const text = this.extractResponseContent(response).toLowerCase();
47483
+ if (/socket|ECONNREFUSED|ETIMEDOUT|network|fetch failed|connection reset/i.test(
47484
+ text
47485
+ )) {
47486
+ return "connection";
47487
+ }
47488
+ if (/-32603|-32000|-32700|internal server error|service unavailable|gateway timeout|HTTP 5\d\d|error POSTing.*endpoint|error GETting.*endpoint|bad request|HTTP 400|unauthorized|forbidden|no valid session|session.*expired/i.test(
47489
+ text
47490
+ )) {
47491
+ return "server";
47492
+ }
47493
+ if (/-32001/i.test(text)) {
47494
+ return "protocol";
47495
+ }
47496
+ return "protocol";
47497
+ }
47498
+ /**
47499
+ * Classify error type from caught exception
47500
+ */
47501
+ classifyErrorFromException(error) {
47502
+ if (error instanceof Error) {
47503
+ const message = error.message.toLowerCase();
47504
+ if (/socket|ECONNREFUSED|ETIMEDOUT|network|fetch failed|connection reset/i.test(
47505
+ message
47506
+ )) {
47507
+ return "connection";
47508
+ }
47509
+ if (/-32603|-32000|-32700|internal server error|service unavailable|gateway timeout|HTTP 5\d\d|error POSTing|error GETting|bad request|HTTP 400|unauthorized|forbidden|no valid session|session.*expired/i.test(
47510
+ message
47511
+ )) {
47512
+ return "server";
47513
+ }
47514
+ if (/-32001/i.test(message)) {
47515
+ return "protocol";
47516
+ }
47517
+ }
47518
+ return "protocol";
47305
47519
  }
47306
47520
  /**
47307
47521
  * Analyze response with evidence-based detection
@@ -47310,27 +47524,6 @@ class SecurityAssessor extends BaseAssessor {
47310
47524
  analyzeResponse(response, payload, tool) {
47311
47525
  const responseText = this.extractResponseContent(response).toLowerCase();
47312
47526
  const payloadText = payload.payload.toLowerCase();
47313
- const parsedResponse = this.tryParseResponseJSON(response);
47314
- if (parsedResponse) {
47315
- if (parsedResponse.safe === true) {
47316
- return {
47317
- isVulnerable: false,
47318
- evidence: "Tool explicitly marked response as safe (safe: true flag in JSON response)"
47319
- };
47320
- }
47321
- if (parsedResponse.hardened === true) {
47322
- return {
47323
- isVulnerable: false,
47324
- evidence: "Tool explicitly marked response as hardened (hardened: true flag in JSON response)"
47325
- };
47326
- }
47327
- if (parsedResponse.vulnerable === true) {
47328
- return {
47329
- isVulnerable: true,
47330
- evidence: "Tool explicitly marked response as vulnerable (vulnerable: true flag in JSON response)"
47331
- };
47332
- }
47333
- }
47334
47527
  const errorInfo = this.extractErrorInfo(response);
47335
47528
  if (this.isMCPValidationError(errorInfo, responseText)) {
47336
47529
  const errorCode = errorInfo.code ? ` (${errorInfo.code})` : "";
@@ -47363,20 +47556,33 @@ class SecurityAssessor extends BaseAssessor {
47363
47556
  evidence: "Read-only info tool returned user/workspace data (intended data exposure, not vulnerability)"
47364
47557
  };
47365
47558
  }
47559
+ if (classification.categories.includes(ToolCategory.SAFE_STORAGE)) {
47560
+ return {
47561
+ isVulnerable: false,
47562
+ evidence: "Safe storage control tool (validated safe implementation, control group for testing)"
47563
+ };
47564
+ }
47366
47565
  if (this.isApiWrapperResponse(responseText)) {
47367
47566
  return {
47368
47567
  isVulnerable: false,
47369
47568
  evidence: "API wrapper response - returned external content as data"
47370
47569
  };
47371
47570
  }
47372
- if (responseText.includes(payloadText)) {
47373
- const isJustReflection = this.isReflectionResponse(responseText);
47374
- if (isJustReflection) {
47375
- return {
47376
- isVulnerable: false,
47377
- evidence: "Tool safely reflected input without execution"
47378
- };
47379
- }
47571
+ const isJustReflection = this.isReflectionResponse(
47572
+ responseText,
47573
+ payloadText
47574
+ );
47575
+ if (isJustReflection) {
47576
+ return {
47577
+ isVulnerable: false,
47578
+ evidence: "Tool safely reflected input without execution"
47579
+ };
47580
+ }
47581
+ if (this.isValidationRejection(response)) {
47582
+ return {
47583
+ isVulnerable: false,
47584
+ evidence: "Tool correctly rejected invalid input with validation error"
47585
+ };
47380
47586
  }
47381
47587
  if (payload.evidence && payload.evidence.test(responseText)) {
47382
47588
  const evidenceMatchesInput = payload.evidence.test(payloadText);
@@ -47402,6 +47608,54 @@ class SecurityAssessor extends BaseAssessor {
47402
47608
  }
47403
47609
  return this.analyzeInjectionResponse(response, payload.payload);
47404
47610
  }
47611
+ /**
47612
+ * Check if tool explicitly rejected input with validation error (SAFE)
47613
+ * Returns true if tool properly rejected invalid input (SECURE)
47614
+ *
47615
+ * Distinguishes between:
47616
+ * - SAFE: {"valid": false, "errors": ["Input too long"]} (rejection)
47617
+ * - VULNERABLE: {"result": "deleted 5 rows"} (execution)
47618
+ */
47619
+ isValidationRejection(response) {
47620
+ const responseText = this.extractResponseContent(response);
47621
+ try {
47622
+ const parsed = JSON.parse(responseText);
47623
+ if (parsed.valid === false || parsed.error === true || parsed.error === "true" || // Handle string boolean
47624
+ parsed.error && parsed.error !== false || // Any truthy error value
47625
+ parsed.status === "rejected" || parsed.status === "invalid" || parsed.status === "failed") {
47626
+ return true;
47627
+ }
47628
+ if (parsed.errors && Array.isArray(parsed.errors) && parsed.errors.length > 0) {
47629
+ return true;
47630
+ }
47631
+ if (parsed.error && typeof parsed.error === "string") {
47632
+ return true;
47633
+ }
47634
+ if (typeof parsed.result === "string") {
47635
+ const resultRejectionPatterns = [
47636
+ /validation (failed|error)/i,
47637
+ /rejected/i,
47638
+ /not.*approved/i,
47639
+ /not.*in.*list/i,
47640
+ /invalid.*input/i,
47641
+ /error:.*invalid/i
47642
+ ];
47643
+ if (resultRejectionPatterns.some((p2) => p2.test(parsed.result))) {
47644
+ return true;
47645
+ }
47646
+ }
47647
+ } catch {
47648
+ }
47649
+ const rejectionPatterns = [
47650
+ /validation failed/i,
47651
+ /rejected/i,
47652
+ /not.*approved/i,
47653
+ /not.*in.*list/i,
47654
+ /invalid.*input/i,
47655
+ /error:.*invalid/i
47656
+ ];
47657
+ return rejectionPatterns.some((pattern2) => pattern2.test(responseText));
47658
+ }
47405
47659
  /**
47406
47660
  * Check if response is an MCP validation error (safe rejection)
47407
47661
  * Returns true if tool rejected input before processing (SECURE)
@@ -47535,7 +47789,8 @@ class SecurityAssessor extends BaseAssessor {
47535
47789
  /**
47536
47790
  * Determine security status based on confidence levels
47537
47791
  */
47538
- determineSecurityStatus(tests, vulnerabilityCount, testCount) {
47792
+ determineSecurityStatus(tests, vulnerabilityCount, testCount, connectionErrorCount = 0) {
47793
+ if (connectionErrorCount > 0) return "FAIL";
47539
47794
  if (testCount === 0) return "NEED_MORE_INFO";
47540
47795
  if (vulnerabilityCount === 0) return "PASS";
47541
47796
  const hasHighConfidence = tests.some(
@@ -47547,30 +47802,38 @@ class SecurityAssessor extends BaseAssessor {
47547
47802
  /**
47548
47803
  * Generate security explanation
47549
47804
  */
47550
- generateSecurityExplanation(tests, vulnerabilities, riskLevel) {
47805
+ generateSecurityExplanation(validTests, connectionErrors, vulnerabilities, riskLevel) {
47551
47806
  const vulnCount = vulnerabilities.length;
47552
- const testCount = tests.length;
47553
- if (testCount === 0) {
47807
+ const testCount = validTests.length;
47808
+ const errorCount = connectionErrors.length;
47809
+ let explanation = "";
47810
+ if (errorCount > 0) {
47811
+ explanation += `⚠️ ${errorCount} test${errorCount !== 1 ? "s" : ""} failed due to connection/server errors. `;
47812
+ }
47813
+ if (testCount === 0 && errorCount > 0) {
47814
+ return explanation + `No valid tests completed. Check server connectivity and retry assessment.`;
47815
+ }
47816
+ if (testCount === 0 && errorCount === 0) {
47554
47817
  return `No tools selected for security testing. Select tools to run security assessments.`;
47555
47818
  }
47556
47819
  if (vulnCount === 0) {
47557
- return `Tested ${testCount} security patterns across selected tools. No vulnerabilities detected. All tools properly handle malicious inputs.`;
47820
+ return explanation + `Tested ${testCount} security patterns across selected tools. No vulnerabilities detected. All tools properly handle malicious inputs.`;
47558
47821
  }
47559
- const highConfidenceCount = tests.filter(
47822
+ const highConfidenceCount = validTests.filter(
47560
47823
  (t) => t.vulnerable && (!t.confidence || t.confidence === "high")
47561
47824
  ).length;
47562
- const mediumConfidenceCount = tests.filter(
47825
+ const mediumConfidenceCount = validTests.filter(
47563
47826
  (t) => t.vulnerable && t.confidence === "medium"
47564
47827
  ).length;
47565
- const lowConfidenceCount = tests.filter(
47828
+ const lowConfidenceCount = validTests.filter(
47566
47829
  (t) => t.vulnerable && t.confidence === "low"
47567
47830
  ).length;
47568
47831
  if (highConfidenceCount > 0) {
47569
- return `Found ${highConfidenceCount} confirmed vulnerability${highConfidenceCount !== 1 ? "s" : ""} across ${testCount} security tests. Risk level: ${riskLevel}. Tools may execute malicious commands or leak sensitive data.`;
47832
+ return explanation + `Found ${highConfidenceCount} confirmed vulnerability${highConfidenceCount !== 1 ? "s" : ""} across ${testCount} security tests. Risk level: ${riskLevel}. Tools may execute malicious commands or leak sensitive data.`;
47570
47833
  } else if (mediumConfidenceCount > 0) {
47571
- return `Detected ${mediumConfidenceCount} potential security concern${mediumConfidenceCount !== 1 ? "s" : ""} across ${testCount} security tests requiring manual review. Tools showed suspicious behavior that needs verification.`;
47834
+ return explanation + `Detected ${mediumConfidenceCount} potential security concern${mediumConfidenceCount !== 1 ? "s" : ""} across ${testCount} security tests requiring manual review. Tools showed suspicious behavior that needs verification.`;
47572
47835
  } else {
47573
- return `Flagged ${lowConfidenceCount} uncertain detection${lowConfidenceCount !== 1 ? "s" : ""} across ${testCount} security tests. Manual verification needed to confirm if these are actual vulnerabilities or false positives.`;
47836
+ return explanation + `Flagged ${lowConfidenceCount} uncertain detection${lowConfidenceCount !== 1 ? "s" : ""} across ${testCount} security tests. Manual verification needed to confirm if these are actual vulnerabilities or false positives.`;
47574
47837
  }
47575
47838
  }
47576
47839
  /**
@@ -47665,9 +47928,13 @@ class SecurityAssessor extends BaseAssessor {
47665
47928
  /**
47666
47929
  * Check if response is just reflection (safe)
47667
47930
  * Expanded to catch more reflection patterns including echo, repeat, display
47668
- * IMPROVED: Bidirectional patterns and safety indicators for broader coverage
47931
+ * IMPROVED: Bidirectional patterns, safety indicators, and payload-aware JSON analysis
47932
+ *
47933
+ * CRITICAL: This check distinguishes between:
47934
+ * - SAFE: Tool stores/echoes malicious input as data (reflection)
47935
+ * - VULNERABLE: Tool executes malicious input and returns results (execution)
47669
47936
  */
47670
- isReflectionResponse(responseText) {
47937
+ isReflectionResponse(responseText, payload) {
47671
47938
  const reflectionPatterns = [
47672
47939
  // Original patterns (keep all existing)
47673
47940
  /stored.*query/i,
@@ -47685,16 +47952,34 @@ class SecurityAssessor extends BaseAssessor {
47685
47952
  /safely.*as.*data/i,
47686
47953
  // NEW: Bidirectional patterns (catch "Query stored" and "stored query")
47687
47954
  /query.*stored/i,
47955
+ /stored.*query/i,
47956
+ // Bidirectional: "Stored query"
47688
47957
  /input.*saved/i,
47689
47958
  /parameter.*received/i,
47690
47959
  /command.*stored/i,
47960
+ /stored.*command/i,
47961
+ // Bidirectional: "Stored command"
47691
47962
  /data.*stored/i,
47963
+ /stored.*data/i,
47964
+ // Bidirectional: "Stored data"
47692
47965
  /action.*stored/i,
47966
+ /stored.*action/i,
47967
+ // Bidirectional: "Stored action"
47693
47968
  /text.*stored/i,
47969
+ /stored.*text/i,
47970
+ // Bidirectional: "Stored text"
47694
47971
  /setting.*stored/i,
47972
+ /stored.*setting/i,
47973
+ // Bidirectional: "Stored setting"
47695
47974
  /instruction.*stored/i,
47975
+ /stored.*instruction/i,
47976
+ // Bidirectional: "Stored instruction"
47696
47977
  /url.*stored/i,
47978
+ /stored.*url/i,
47979
+ // Bidirectional: "Stored URL"
47697
47980
  /package.*stored/i,
47981
+ /stored.*package/i,
47982
+ // Bidirectional: "Stored package"
47698
47983
  // NEW: Safety indicators (common in hardened implementations)
47699
47984
  /stored.*safely/i,
47700
47985
  /safely.*stored/i,
@@ -47708,14 +47993,66 @@ class SecurityAssessor extends BaseAssessor {
47708
47993
  /stored.*successfully/i,
47709
47994
  /saved.*to/i,
47710
47995
  /recorded\s+in/i,
47711
- /added\s+to/i
47996
+ /added\s+to/i,
47997
+ // NEW: Storage/logging confirmations (high confidence)
47998
+ /logged successfully:/i,
47999
+ /queued for processing:/i,
48000
+ /saved (for|successfully)/i,
48001
+ /stored for (admin review|configuration|processing)/i,
48002
+ // NEW: Processing confirmations (high confidence)
48003
+ /processed successfully/i,
48004
+ /validated successfully/i,
48005
+ /parsed successfully/i,
48006
+ /(validation|processing) (passed|completed)/i,
48007
+ // NEW: Error messages with input reflection (common safe pattern)
48008
+ /error:.*not (found|in approved list|recognized)/i,
48009
+ /error getting info for ['"].*['"]/i,
48010
+ /invalid .* format.*stored as text/i,
48011
+ /error:.*too (long|short|large)/i
47712
48012
  ];
47713
- return reflectionPatterns.some((pattern2) => pattern2.test(responseText));
48013
+ if (reflectionPatterns.some((pattern2) => pattern2.test(responseText))) {
48014
+ return true;
48015
+ }
48016
+ try {
48017
+ const parsed = JSON.parse(responseText);
48018
+ const safeFields = [
48019
+ "command",
48020
+ "query",
48021
+ "text",
48022
+ "data",
48023
+ "action",
48024
+ "instruction",
48025
+ "setting",
48026
+ "url",
48027
+ "package",
48028
+ "input"
48029
+ ];
48030
+ const executionFields = ["output", "stdout", "stderr", "contents"];
48031
+ const hasPayloadInExecution = executionFields.some((execField) => {
48032
+ const value = parsed[execField];
48033
+ return value && typeof value === "string" && value.includes(payload);
48034
+ });
48035
+ if (hasPayloadInExecution) {
48036
+ return false;
48037
+ }
48038
+ const isStoredOnly = safeFields.some((field) => {
48039
+ const value = parsed[field];
48040
+ return value === payload || typeof value === "string" && value.includes(payload);
48041
+ });
48042
+ if (isStoredOnly) {
48043
+ return true;
48044
+ }
48045
+ if (parsed.status && /(stored|logged|queued|pending|processed|validated)/.test(parsed.status)) {
48046
+ return true;
48047
+ }
48048
+ } catch {
48049
+ }
48050
+ return false;
47714
48051
  }
47715
48052
  /**
47716
48053
  * Analyze injection response (existing logic)
47717
48054
  */
47718
- analyzeInjectionResponse(response, _payload) {
48055
+ analyzeInjectionResponse(response, payload) {
47719
48056
  const responseText = this.extractResponseContent(response);
47720
48057
  const executionPatterns = [
47721
48058
  /executed/i,
@@ -47727,7 +48064,7 @@ class SecurityAssessor extends BaseAssessor {
47727
48064
  const hasExecution = executionPatterns.some(
47728
48065
  (pattern2) => pattern2.test(responseText)
47729
48066
  );
47730
- if (hasExecution && !this.isReflectionResponse(responseText)) {
48067
+ if (hasExecution && !this.isReflectionResponse(responseText, payload)) {
47731
48068
  return {
47732
48069
  isVulnerable: true,
47733
48070
  evidence: "Tool executed instruction: found execution keywords"
@@ -49744,6 +50081,7 @@ function ToolSelector({
49744
50081
  const AssessmentTab = ({
49745
50082
  tools,
49746
50083
  isLoadingTools = false,
50084
+ listTools,
49747
50085
  callTool,
49748
50086
  serverName = "MCP Server"
49749
50087
  }) => {
@@ -49761,6 +50099,7 @@ const AssessmentTab = ({
49761
50099
  const [showJson, setShowJson] = reactExports.useState(false);
49762
50100
  const [collapsedTools, setCollapsedTools] = reactExports.useState(/* @__PURE__ */ new Set());
49763
50101
  const [allToolsCollapsed, setAllToolsCollapsed] = reactExports.useState(false);
50102
+ const [showOnlyErrors, setShowOnlyErrors] = reactExports.useState(false);
49764
50103
  const [expandedToolDescriptions, setExpandedToolDescriptions] = reactExports.useState(/* @__PURE__ */ new Set());
49765
50104
  const [categoryFilter, setCategoryFilter] = reactExports.useState({
49766
50105
  functionality: true,
@@ -49780,11 +50119,28 @@ const AssessmentTab = ({
49780
50119
  [config]
49781
50120
  );
49782
50121
  reactExports.useEffect(() => {
49783
- if (tools.length > 0 && !config.selectedToolsForTesting) {
50122
+ if (tools.length === 0) {
50123
+ return;
50124
+ }
50125
+ const currentToolNames = tools.map((t) => t.name);
50126
+ if (!config.selectedToolsForTesting) {
49784
50127
  setConfig({
49785
50128
  ...config,
49786
- selectedToolsForTesting: tools.map((t) => t.name)
50129
+ selectedToolsForTesting: currentToolNames
49787
50130
  });
50131
+ } else {
50132
+ const existingSelections = config.selectedToolsForTesting.filter(
50133
+ (name2) => currentToolNames.includes(name2)
50134
+ );
50135
+ const newTools = currentToolNames.filter(
50136
+ (name2) => !config.selectedToolsForTesting.includes(name2)
50137
+ );
50138
+ if (newTools.length > 0 || existingSelections.length !== config.selectedToolsForTesting.length) {
50139
+ setConfig({
50140
+ ...config,
50141
+ selectedToolsForTesting: [...existingSelections, ...newTools]
50142
+ });
50143
+ }
49788
50144
  }
49789
50145
  }, [tools, config, setConfig]);
49790
50146
  const calculateFilteredOverallStatus = reactExports.useCallback(
@@ -49955,7 +50311,23 @@ const AssessmentTab = ({
49955
50311
  )
49956
50312
  ] }),
49957
50313
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
49958
- /* @__PURE__ */ jsxRuntimeExports.jsx(Label$1, { htmlFor: "tool-selector", children: "Select tools for testing:" }),
50314
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between", children: [
50315
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Label$1, { htmlFor: "tool-selector", children: "Select tools for testing:" }),
50316
+ listTools && /* @__PURE__ */ jsxRuntimeExports.jsxs(
50317
+ Button,
50318
+ {
50319
+ variant: "ghost",
50320
+ size: "sm",
50321
+ onClick: () => listTools(),
50322
+ disabled: isLoadingTools || isRunning,
50323
+ className: "h-7 px-2",
50324
+ children: [
50325
+ isLoadingTools ? /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "w-4 h-4 mr-1 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { className: "w-4 h-4 mr-1" }),
50326
+ "Refresh"
50327
+ ]
50328
+ }
50329
+ )
50330
+ ] }),
49959
50331
  isLoadingTools ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
49960
50332
  /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "w-4 h-4 animate-spin" }),
49961
50333
  "Loading tools..."
@@ -50232,74 +50604,129 @@ const AssessmentTab = ({
50232
50604
  children: [
50233
50605
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm mb-2", children: assessment.security.explanation }),
50234
50606
  (() => {
50235
- var _a2, _b2, _c;
50236
- const highConfidenceCount = ((_a2 = assessment.security.promptInjectionTests) == null ? void 0 : _a2.filter(
50607
+ var _a2, _b2, _c, _d;
50608
+ const connectionErrors = ((_a2 = assessment.security.promptInjectionTests) == null ? void 0 : _a2.filter(
50609
+ (t) => t.connectionError === true
50610
+ )) || [];
50611
+ const highConfidenceCount = ((_b2 = assessment.security.promptInjectionTests) == null ? void 0 : _b2.filter(
50237
50612
  (t) => t.vulnerable && (!t.confidence || t.confidence === "high")
50238
50613
  ).length) || 0;
50239
- const mediumConfidenceCount = ((_b2 = assessment.security.promptInjectionTests) == null ? void 0 : _b2.filter(
50614
+ const mediumConfidenceCount = ((_c = assessment.security.promptInjectionTests) == null ? void 0 : _c.filter(
50240
50615
  (t) => t.vulnerable && t.confidence === "medium"
50241
50616
  ).length) || 0;
50242
- const lowConfidenceCount = ((_c = assessment.security.promptInjectionTests) == null ? void 0 : _c.filter(
50617
+ const lowConfidenceCount = ((_d = assessment.security.promptInjectionTests) == null ? void 0 : _d.filter(
50243
50618
  (t) => t.vulnerable && t.confidence === "low"
50244
50619
  ).length) || 0;
50245
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-sm space-y-1", children: [
50246
- highConfidenceCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-red-700", children: [
50247
- /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Confirmed Issues:" }),
50248
- " ",
50249
- highConfidenceCount
50250
- ] }),
50251
- mediumConfidenceCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-amber-700", children: [
50252
- /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Need Review:" }),
50253
- " ",
50254
- mediumConfidenceCount
50255
- ] }),
50256
- lowConfidenceCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-blue-700", children: [
50257
- /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Uncertain (Verification Needed):" }),
50258
- " ",
50259
- lowConfidenceCount
50620
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
50621
+ connectionErrors.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-yellow-50 border-l-4 border-yellow-500 p-4 mb-4 rounded", children: [
50622
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mb-2", children: [
50623
+ /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { className: "w-5 h-5 text-yellow-600" }),
50624
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("h5", { className: "text-sm font-semibold text-yellow-900", children: [
50625
+ "Connection Errors (",
50626
+ connectionErrors.length,
50627
+ ")"
50628
+ ] })
50629
+ ] }),
50630
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-sm text-yellow-800 mb-2", children: [
50631
+ connectionErrors.length,
50632
+ " test",
50633
+ connectionErrors.length !== 1 ? "s" : "",
50634
+ " could not complete due to server/network failures. These tests are excluded from vulnerability counts."
50635
+ ] }),
50636
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-yellow-700 space-y-1 max-h-32 overflow-y-auto", children: connectionErrors.map((err, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
50637
+ "div",
50638
+ {
50639
+ className: "flex items-start gap-2",
50640
+ children: [
50641
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-yellow-600", children: "•" }),
50642
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
50643
+ /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: err.testName }),
50644
+ " on",
50645
+ " ",
50646
+ /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "bg-yellow-100 px-1 rounded", children: err.toolName }),
50647
+ ": ",
50648
+ err.errorType
50649
+ ] })
50650
+ ]
50651
+ },
50652
+ i
50653
+ )) }),
50654
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-yellow-700 mt-2 font-medium", children: "✅ Fix connectivity issues and re-run assessment for accurate results" })
50260
50655
  ] }),
50261
- highConfidenceCount === 0 && mediumConfidenceCount === 0 && lowConfidenceCount === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-green-700", children: /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "All tests passed" }) })
50656
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-sm space-y-1", children: [
50657
+ highConfidenceCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-red-700", children: [
50658
+ /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Confirmed Issues:" }),
50659
+ " ",
50660
+ highConfidenceCount
50661
+ ] }),
50662
+ mediumConfidenceCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-amber-700", children: [
50663
+ /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Need Review:" }),
50664
+ " ",
50665
+ mediumConfidenceCount
50666
+ ] }),
50667
+ lowConfidenceCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-blue-700", children: [
50668
+ /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Uncertain (Verification Needed):" }),
50669
+ " ",
50670
+ lowConfidenceCount
50671
+ ] }),
50672
+ highConfidenceCount === 0 && mediumConfidenceCount === 0 && lowConfidenceCount === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-green-700", children: /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "All tests passed" }) })
50673
+ ] })
50262
50674
  ] });
50263
50675
  })(),
50264
50676
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2", children: [
50265
50677
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
50266
50678
  /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { className: "text-sm", children: "Security Test Results:" }),
50267
- /* @__PURE__ */ jsxRuntimeExports.jsx(
50268
- Button,
50269
- {
50270
- variant: "outline",
50271
- size: "sm",
50272
- className: "text-xs h-6 px-2",
50273
- onClick: () => {
50274
- const toolGroups = /* @__PURE__ */ new Map();
50275
- assessment.security.promptInjectionTests.forEach(
50276
- (testResult) => {
50277
- const toolName = testResult.toolName || "Unknown Tool";
50278
- if (!toolGroups.has(toolName)) {
50279
- toolGroups.set(toolName, []);
50679
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
50680
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
50681
+ Button,
50682
+ {
50683
+ variant: showOnlyErrors ? "default" : "outline",
50684
+ size: "sm",
50685
+ className: "text-xs h-6 px-2",
50686
+ onClick: () => setShowOnlyErrors(!showOnlyErrors),
50687
+ children: [
50688
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Funnel, { className: "h-3 w-3 mr-1" }),
50689
+ showOnlyErrors ? "Show All" : "Filter Errors"
50690
+ ]
50691
+ }
50692
+ ),
50693
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
50694
+ Button,
50695
+ {
50696
+ variant: "outline",
50697
+ size: "sm",
50698
+ className: "text-xs h-6 px-2",
50699
+ onClick: () => {
50700
+ const toolGroups = /* @__PURE__ */ new Map();
50701
+ assessment.security.promptInjectionTests.forEach(
50702
+ (testResult) => {
50703
+ const toolName = testResult.toolName || "Unknown Tool";
50704
+ if (!toolGroups.has(toolName)) {
50705
+ toolGroups.set(toolName, []);
50706
+ }
50280
50707
  }
50281
- }
50282
- );
50283
- if (allToolsCollapsed) {
50284
- setCollapsedTools(/* @__PURE__ */ new Set());
50285
- setAllToolsCollapsed(false);
50286
- } else {
50287
- const allToolNames = Array.from(
50288
- toolGroups.keys()
50289
50708
  );
50290
- setCollapsedTools(new Set(allToolNames));
50291
- setAllToolsCollapsed(true);
50292
- }
50293
- },
50294
- children: allToolsCollapsed ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
50295
- /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { className: "h-3 w-3 mr-1" }),
50296
- "Expand All"
50297
- ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
50298
- /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { className: "h-3 w-3 mr-1" }),
50299
- "Collapse All"
50300
- ] })
50301
- }
50302
- )
50709
+ if (allToolsCollapsed) {
50710
+ setCollapsedTools(/* @__PURE__ */ new Set());
50711
+ setAllToolsCollapsed(false);
50712
+ } else {
50713
+ const allToolNames = Array.from(
50714
+ toolGroups.keys()
50715
+ );
50716
+ setCollapsedTools(new Set(allToolNames));
50717
+ setAllToolsCollapsed(true);
50718
+ }
50719
+ },
50720
+ children: allToolsCollapsed ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
50721
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { className: "h-3 w-3 mr-1" }),
50722
+ "Expand All"
50723
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
50724
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { className: "h-3 w-3 mr-1" }),
50725
+ "Collapse All"
50726
+ ] })
50727
+ }
50728
+ )
50729
+ ] })
50303
50730
  ] }),
50304
50731
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 space-y-1", children: (() => {
50305
50732
  const toolGroups = /* @__PURE__ */ new Map();
@@ -50324,18 +50751,26 @@ const AssessmentTab = ({
50324
50751
  newCollapsed.size === toolGroups.size
50325
50752
  );
50326
50753
  };
50327
- return Array.from(toolGroups.entries()).map(
50328
- ([toolName, toolTests]) => /* @__PURE__ */ jsxRuntimeExports.jsx(
50329
- CollapsibleToolSection,
50330
- {
50331
- toolName,
50332
- toolTests,
50333
- isCollapsed: collapsedTools.has(toolName),
50334
- onToggle: handleToggleTool
50335
- },
50336
- toolName
50337
- )
50338
- );
50754
+ let filteredGroups = Array.from(toolGroups.entries());
50755
+ if (showOnlyErrors) {
50756
+ filteredGroups = filteredGroups.filter(
50757
+ ([, toolTests]) => {
50758
+ return toolTests.some(
50759
+ (test) => test.vulnerable === true
50760
+ );
50761
+ }
50762
+ );
50763
+ }
50764
+ return filteredGroups.map(([toolName, toolTests]) => /* @__PURE__ */ jsxRuntimeExports.jsx(
50765
+ CollapsibleToolSection,
50766
+ {
50767
+ toolName,
50768
+ toolTests,
50769
+ isCollapsed: collapsedTools.has(toolName),
50770
+ onToggle: handleToggleTool
50771
+ },
50772
+ toolName
50773
+ ));
50339
50774
  })() }),
50340
50775
  assessment.security.vulnerabilities.length > 0 && (() => {
50341
50776
  var _a2, _b2, _c;
@@ -50589,43 +51024,58 @@ const AssessmentTab = ({
50589
51024
  assessment.errorHandling.metrics.testDetails && assessment.errorHandling.metrics.testDetails.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-3 border-t pt-3", children: [
50590
51025
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
50591
51026
  /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { className: "text-sm", children: "Error Handling Test Results:" }),
50592
- /* @__PURE__ */ jsxRuntimeExports.jsx(
50593
- Button,
50594
- {
50595
- variant: "outline",
50596
- size: "sm",
50597
- className: "text-xs h-6 px-2",
50598
- onClick: () => {
50599
- var _a2;
50600
- const toolGroups = /* @__PURE__ */ new Map();
50601
- (_a2 = assessment.errorHandling.metrics.testDetails) == null ? void 0 : _a2.forEach(
50602
- (testResult) => {
50603
- const toolName = testResult.toolName || "Unknown Tool";
50604
- if (!toolGroups.has(toolName)) {
50605
- toolGroups.set(toolName, []);
51027
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
51028
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
51029
+ Button,
51030
+ {
51031
+ variant: showOnlyErrors ? "default" : "outline",
51032
+ size: "sm",
51033
+ className: "text-xs h-6 px-2",
51034
+ onClick: () => setShowOnlyErrors(!showOnlyErrors),
51035
+ children: [
51036
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Funnel, { className: "h-3 w-3 mr-1" }),
51037
+ showOnlyErrors ? "Show All" : "Filter Errors"
51038
+ ]
51039
+ }
51040
+ ),
51041
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
51042
+ Button,
51043
+ {
51044
+ variant: "outline",
51045
+ size: "sm",
51046
+ className: "text-xs h-6 px-2",
51047
+ onClick: () => {
51048
+ var _a2;
51049
+ const toolGroups = /* @__PURE__ */ new Map();
51050
+ (_a2 = assessment.errorHandling.metrics.testDetails) == null ? void 0 : _a2.forEach(
51051
+ (testResult) => {
51052
+ const toolName = testResult.toolName || "Unknown Tool";
51053
+ if (!toolGroups.has(toolName)) {
51054
+ toolGroups.set(toolName, []);
51055
+ }
50606
51056
  }
50607
- }
50608
- );
50609
- if (allToolsCollapsed) {
50610
- setCollapsedTools(/* @__PURE__ */ new Set());
50611
- setAllToolsCollapsed(false);
50612
- } else {
50613
- const allToolNames = Array.from(
50614
- toolGroups.keys()
50615
51057
  );
50616
- setCollapsedTools(new Set(allToolNames));
50617
- setAllToolsCollapsed(true);
50618
- }
50619
- },
50620
- children: allToolsCollapsed ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
50621
- /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { className: "h-3 w-3 mr-1" }),
50622
- "Expand All"
50623
- ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
50624
- /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { className: "h-3 w-3 mr-1" }),
50625
- "Collapse All"
50626
- ] })
50627
- }
50628
- )
51058
+ if (allToolsCollapsed) {
51059
+ setCollapsedTools(/* @__PURE__ */ new Set());
51060
+ setAllToolsCollapsed(false);
51061
+ } else {
51062
+ const allToolNames = Array.from(
51063
+ toolGroups.keys()
51064
+ );
51065
+ setCollapsedTools(new Set(allToolNames));
51066
+ setAllToolsCollapsed(true);
51067
+ }
51068
+ },
51069
+ children: allToolsCollapsed ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
51070
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { className: "h-3 w-3 mr-1" }),
51071
+ "Expand All"
51072
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
51073
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { className: "h-3 w-3 mr-1" }),
51074
+ "Collapse All"
51075
+ ] })
51076
+ }
51077
+ )
51078
+ ] })
50629
51079
  ] }),
50630
51080
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 space-y-1", children: (() => {
50631
51081
  var _a2;
@@ -50651,12 +51101,27 @@ const AssessmentTab = ({
50651
51101
  newCollapsed.size === toolGroups.size
50652
51102
  );
50653
51103
  };
50654
- return Array.from(toolGroups.entries()).map(
51104
+ let filteredGroups = Array.from(
51105
+ toolGroups.entries()
51106
+ );
51107
+ if (showOnlyErrors) {
51108
+ filteredGroups = filteredGroups.filter(
51109
+ ([, toolTests]) => {
51110
+ return toolTests.some(
51111
+ (test) => test.passed === false
51112
+ );
51113
+ }
51114
+ );
51115
+ }
51116
+ return filteredGroups.map(
50655
51117
  ([toolName, toolTests]) => {
50656
- const passedCount = toolTests.filter(
51118
+ const scoredTests = toolTests.filter(
51119
+ (t) => t.testType !== "invalid_values"
51120
+ );
51121
+ const passedCount = scoredTests.filter(
50657
51122
  (t) => t.passed
50658
51123
  ).length;
50659
- const totalCount = toolTests.length;
51124
+ const totalCount = scoredTests.length;
50660
51125
  const allPassed = passedCount === totalCount;
50661
51126
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
50662
51127
  "div",
@@ -52497,13 +52962,13 @@ const App = () => {
52497
52962
  ) });
52498
52963
  if (window.location.pathname === "/oauth/callback") {
52499
52964
  const OAuthCallback = React.lazy(
52500
- () => __vitePreload(() => import("./OAuthCallback-C8iZSwWO.js"), true ? [] : void 0)
52965
+ () => __vitePreload(() => import("./OAuthCallback-CiSJznN1.js"), true ? [] : void 0)
52501
52966
  );
52502
52967
  return /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: "Loading..." }), children: /* @__PURE__ */ jsxRuntimeExports.jsx(OAuthCallback, { onConnect: onOAuthConnect }) });
52503
52968
  }
52504
52969
  if (window.location.pathname === "/oauth/callback/debug") {
52505
52970
  const OAuthDebugCallback = React.lazy(
52506
- () => __vitePreload(() => import("./OAuthDebugCallback-Br9U2vZs.js"), true ? [] : void 0)
52971
+ () => __vitePreload(() => import("./OAuthDebugCallback-D_XkKc3n.js"), true ? [] : void 0)
52507
52972
  );
52508
52973
  return /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: "Loading..." }), children: /* @__PURE__ */ jsxRuntimeExports.jsx(OAuthDebugCallback, { onConnect: onOAuthDebugConnect }) });
52509
52974
  }
@@ -52790,6 +53255,10 @@ const App = () => {
52790
53255
  {
52791
53256
  tools,
52792
53257
  isLoadingTools,
53258
+ listTools: () => {
53259
+ clearError("tools");
53260
+ listTools();
53261
+ },
52793
53262
  callTool: async (name2, params) => {
52794
53263
  const result = await callTool(name2, params);
52795
53264
  return result;
@@ -2014,6 +2014,10 @@ video {
2014
2014
  --tw-text-opacity: 1;
2015
2015
  color: rgb(133 77 14 / var(--tw-text-opacity, 1));
2016
2016
  }
2017
+ .text-yellow-900 {
2018
+ --tw-text-opacity: 1;
2019
+ color: rgb(113 63 18 / var(--tw-text-opacity, 1));
2020
+ }
2017
2021
  .underline-offset-4 {
2018
2022
  text-underline-offset: 4px;
2019
2023
  }
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/mcp.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>MCP Inspector</title>
8
- <script type="module" crossorigin src="/assets/index-D12b6zCd.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-DrjsuFb9.css">
8
+ <script type="module" crossorigin src="/assets/index-BsOrK-Nh.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-Cz-lwW4x.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root" class="w-full"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bryan-thompson/inspector-assessment",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Enhanced MCP Inspector with comprehensive assessment capabilities for server validation",
5
5
  "license": "MIT",
6
6
  "author": "Bryan Thompson <bryan@triepod.ai>",