@mcp-shark/mcp-shark 1.4.1 → 1.5.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.
Files changed (203) hide show
  1. package/README.md +84 -645
  2. package/bin/mcp-shark.js +30 -36
  3. package/mcp-server/index.js +115 -0
  4. package/mcp-server/lib/auditor/audit.js +34 -42
  5. package/mcp-server/lib/common/error.js +1 -1
  6. package/mcp-server/lib/server/external/all.js +5 -6
  7. package/mcp-server/lib/server/external/config.js +1 -3
  8. package/mcp-server/lib/server/external/kv.js +21 -40
  9. package/mcp-server/lib/server/external/single/request.js +3 -6
  10. package/mcp-server/lib/server/external/single/run.js +8 -19
  11. package/mcp-server/lib/server/internal/handlers/prompts-get.js +5 -7
  12. package/mcp-server/lib/server/internal/handlers/prompts-list.js +5 -4
  13. package/mcp-server/lib/server/internal/handlers/resources-list.js +5 -4
  14. package/mcp-server/lib/server/internal/handlers/resources-read.js +5 -4
  15. package/mcp-server/lib/server/internal/handlers/tools-call.js +8 -10
  16. package/mcp-server/lib/server/internal/handlers/tools-list.js +6 -5
  17. package/mcp-server/lib/server/internal/run.js +5 -17
  18. package/mcp-server/lib/server/internal/server.js +14 -15
  19. package/mcp-server/lib/server/internal/session.js +8 -13
  20. package/mcp-server/mcp-shark.js +16 -66
  21. package/package.json +23 -38
  22. package/ui/dist/assets/index-Cc-IUa83.css +1 -0
  23. package/ui/dist/assets/index-srLDlk97.js +35 -0
  24. package/ui/dist/index.html +17 -0
  25. package/ui/dist/og-image.png +0 -0
  26. package/ui/server/routes/backups/deleteBackup.js +54 -0
  27. package/ui/server/routes/backups/index.js +15 -0
  28. package/ui/server/routes/backups/listBackups.js +75 -0
  29. package/ui/server/routes/backups/restoreBackup.js +83 -0
  30. package/ui/server/routes/backups/viewBackup.js +47 -0
  31. package/ui/server/routes/composite/index.js +46 -0
  32. package/ui/server/routes/composite/servers.js +18 -0
  33. package/ui/server/routes/composite/setup.js +129 -0
  34. package/ui/server/routes/composite/status.js +7 -0
  35. package/ui/server/routes/composite/stop.js +39 -0
  36. package/ui/server/routes/composite/utils.js +45 -0
  37. package/ui/server/routes/config.js +34 -30
  38. package/ui/server/routes/conversations.js +3 -3
  39. package/ui/server/routes/help.js +2 -2
  40. package/ui/server/routes/logs.js +5 -5
  41. package/ui/server/routes/playground.js +91 -62
  42. package/ui/server/routes/requests.js +112 -108
  43. package/ui/server/routes/sessions.js +4 -4
  44. package/ui/server/routes/settings.js +199 -0
  45. package/ui/server/routes/smartscan/discover.js +7 -6
  46. package/ui/server/routes/smartscan/scans/clearCache.js +3 -2
  47. package/ui/server/routes/smartscan/scans/createBatchScans.js +4 -3
  48. package/ui/server/routes/smartscan/scans/createScan.js +2 -1
  49. package/ui/server/routes/smartscan/scans/getCachedResults.js +2 -1
  50. package/ui/server/routes/smartscan/scans/getScan.js +2 -1
  51. package/ui/server/routes/smartscan/scans/listScans.js +5 -4
  52. package/ui/server/routes/smartscan/scans.js +3 -3
  53. package/ui/server/routes/smartscan/token.js +4 -3
  54. package/ui/server/routes/smartscan/transport.js +1 -1
  55. package/ui/server/routes/smartscan.js +1 -1
  56. package/ui/server/routes/statistics.js +13 -10
  57. package/ui/server/utils/config-update.js +140 -112
  58. package/ui/server/utils/config.js +4 -4
  59. package/ui/server/utils/logger.js +2 -0
  60. package/ui/server/utils/paths.js +210 -2
  61. package/ui/server/utils/port.js +2 -2
  62. package/ui/server/utils/process.js +0 -67
  63. package/ui/server/utils/scan-cache/all-results.js +76 -59
  64. package/ui/server/utils/scan-cache/directory.js +1 -1
  65. package/ui/server/utils/scan-cache/file-operations.js +19 -16
  66. package/ui/server/utils/scan-cache/server-operations.js +14 -9
  67. package/ui/server/utils/serialization.js +9 -3
  68. package/ui/server/utils/smartscan-token.js +4 -3
  69. package/ui/server.js +87 -41
  70. package/ui/src/App.jsx +5 -5
  71. package/ui/src/CompositeLogs.jsx +20 -20
  72. package/ui/src/CompositeSetup.jsx +9 -9
  73. package/ui/src/HelpGuide/HelpGuideFooter.jsx +1 -0
  74. package/ui/src/HelpGuide/HelpGuideHeader.jsx +2 -1
  75. package/ui/src/HelpGuide.jsx +17 -4
  76. package/ui/src/IntroTour.jsx +19 -5
  77. package/ui/src/LogDetail.jsx +1 -0
  78. package/ui/src/LogTable.jsx +24 -6
  79. package/ui/src/PacketDetail.jsx +21 -16
  80. package/ui/src/PacketFilters.jsx +29 -14
  81. package/ui/src/PacketList.jsx +4 -5
  82. package/ui/src/SmartScan.jsx +5 -5
  83. package/ui/src/TabNavigation.jsx +5 -5
  84. package/ui/src/components/App/HelpButton.jsx +4 -0
  85. package/ui/src/components/App/TrafficTab.jsx +4 -4
  86. package/ui/src/components/App/useAppState.js +118 -24
  87. package/ui/src/components/BackupList.jsx +6 -2
  88. package/ui/src/components/CollapsibleSection.jsx +16 -2
  89. package/ui/src/components/ConfigViewerModal.jsx +17 -3
  90. package/ui/src/components/ConfirmationModal.jsx +20 -3
  91. package/ui/src/components/DetailsTab/BodySection.jsx +3 -1
  92. package/ui/src/components/DetailsTab/CollapsibleRequestResponse.jsx +14 -3
  93. package/ui/src/components/DetailsTab/InfoSection.jsx +4 -2
  94. package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +7 -5
  95. package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +7 -5
  96. package/ui/src/components/DetectedPathsList.jsx +5 -2
  97. package/ui/src/components/FileInput.jsx +3 -1
  98. package/ui/src/components/GroupHeader.jsx +14 -0
  99. package/ui/src/components/GroupedByMcpView.jsx +3 -10
  100. package/ui/src/components/GroupedByServerView.jsx +1 -1
  101. package/ui/src/components/GroupedBySessionView.jsx +1 -1
  102. package/ui/src/components/HexTab.jsx +17 -4
  103. package/ui/src/components/LogsToolbar.jsx +3 -1
  104. package/ui/src/components/McpPlayground/LoadingModal.jsx +7 -3
  105. package/ui/src/components/McpPlayground/PromptsSection/PromptCallPanel.jsx +5 -0
  106. package/ui/src/components/McpPlayground/PromptsSection/PromptItem.jsx +52 -23
  107. package/ui/src/components/McpPlayground/PromptsSection/PromptsList.jsx +13 -11
  108. package/ui/src/components/McpPlayground/PromptsSection.jsx +2 -1
  109. package/ui/src/components/McpPlayground/ResourcesSection/ResourceCallPanel.jsx +3 -0
  110. package/ui/src/components/McpPlayground/ResourcesSection/ResourceItem.jsx +66 -34
  111. package/ui/src/components/McpPlayground/ResourcesSection/ResourcesList.jsx +13 -11
  112. package/ui/src/components/McpPlayground/ResourcesSection.jsx +2 -1
  113. package/ui/src/components/McpPlayground/ToolsSection/ToolCallPanel.jsx +5 -0
  114. package/ui/src/components/McpPlayground/ToolsSection/ToolItem.jsx +52 -23
  115. package/ui/src/components/McpPlayground/ToolsSection/ToolsList.jsx +13 -11
  116. package/ui/src/components/McpPlayground/ToolsSection.jsx +2 -1
  117. package/ui/src/components/McpPlayground/hooks/useMcpDataLoader.js +107 -0
  118. package/ui/src/components/McpPlayground/hooks/useMcpRequest.js +70 -0
  119. package/ui/src/components/McpPlayground/hooks/useMcpServerStatus.js +90 -0
  120. package/ui/src/components/McpPlayground/useMcpPlayground.js +118 -159
  121. package/ui/src/components/McpPlayground.jsx +105 -23
  122. package/ui/src/components/PacketDetailHeader.jsx +8 -3
  123. package/ui/src/components/PacketFilters/ExportControls.jsx +2 -1
  124. package/ui/src/components/PacketFilters/FilterInput.jsx +1 -1
  125. package/ui/src/components/RawTab.jsx +15 -2
  126. package/ui/src/components/RequestRow/OrphanedResponseRow.jsx +10 -2
  127. package/ui/src/components/RequestRow/RequestRowMain.jsx +12 -3
  128. package/ui/src/components/RequestRow/ResponseRow.jsx +11 -3
  129. package/ui/src/components/RequestRow.jsx +17 -9
  130. package/ui/src/components/ServerControl.jsx +3 -1
  131. package/ui/src/components/ServiceSelector.jsx +2 -0
  132. package/ui/src/components/SmartScan/AnalysisResult.jsx +2 -2
  133. package/ui/src/components/SmartScan/BatchResultsDisplay/BatchResultItem.jsx +2 -1
  134. package/ui/src/components/SmartScan/BatchResultsDisplay/BatchResultsHeader.jsx +1 -1
  135. package/ui/src/components/SmartScan/BatchResultsDisplay.jsx +9 -3
  136. package/ui/src/components/SmartScan/EmptyState.jsx +3 -0
  137. package/ui/src/components/SmartScan/ErrorDisplay.jsx +4 -2
  138. package/ui/src/components/SmartScan/ExpandableSection.jsx +4 -0
  139. package/ui/src/components/SmartScan/FindingsTable.jsx +46 -42
  140. package/ui/src/components/SmartScan/ListViewContent.jsx +2 -2
  141. package/ui/src/components/SmartScan/NotablePatternsSection.jsx +13 -8
  142. package/ui/src/components/SmartScan/OverallSummarySection.jsx +36 -29
  143. package/ui/src/components/SmartScan/RecommendationsSection.jsx +10 -8
  144. package/ui/src/components/SmartScan/ScanDetailHeader.jsx +2 -1
  145. package/ui/src/components/SmartScan/ScanDetailView.jsx +4 -4
  146. package/ui/src/components/SmartScan/ScanListView/ScanListHeader.jsx +2 -1
  147. package/ui/src/components/SmartScan/ScanListView/ScanListItem.jsx +15 -1
  148. package/ui/src/components/SmartScan/ScanOverviewSection.jsx +3 -1
  149. package/ui/src/components/SmartScan/ScanResultsDisplay.jsx +1 -1
  150. package/ui/src/components/SmartScan/ScanViewContent.jsx +2 -2
  151. package/ui/src/components/SmartScan/ScanningProgress.jsx +4 -2
  152. package/ui/src/components/SmartScan/ServerInfoSection.jsx +3 -1
  153. package/ui/src/components/SmartScan/ServerSelectionRow.jsx +4 -2
  154. package/ui/src/components/SmartScan/SingleResultDisplay.jsx +5 -3
  155. package/ui/src/components/SmartScan/SmartScanControls.jsx +11 -7
  156. package/ui/src/components/SmartScan/SmartScanHeader.jsx +1 -1
  157. package/ui/src/components/SmartScan/ViewModeTabs.jsx +2 -0
  158. package/ui/src/components/SmartScan/hooks/useCacheManagement.js +1 -2
  159. package/ui/src/components/SmartScan/hooks/useMcpDiscovery.js +22 -26
  160. package/ui/src/components/SmartScan/hooks/useScanList.js +10 -9
  161. package/ui/src/components/SmartScan/hooks/useScanOperations.js +23 -14
  162. package/ui/src/components/SmartScan/hooks/useServerStatus.js +2 -2
  163. package/ui/src/components/SmartScan/hooks/useTokenManagement.js +2 -2
  164. package/ui/src/components/SmartScan/scanDataUtils.js +22 -17
  165. package/ui/src/components/SmartScan/useSmartScan.js +4 -4
  166. package/ui/src/components/SmartScan/utils.js +3 -1
  167. package/ui/src/components/SmartScanIcons.jsx +6 -3
  168. package/ui/src/components/TabNavigation/DesktopTabs.jsx +8 -3
  169. package/ui/src/components/TabNavigation/MobileDropdown.jsx +3 -1
  170. package/ui/src/components/TabNavigation.jsx +8 -3
  171. package/ui/src/components/TabNavigationIcons.jsx +4 -4
  172. package/ui/src/components/TourOverlay.jsx +1 -1
  173. package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +3 -0
  174. package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +1 -0
  175. package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +9 -0
  176. package/ui/src/components/TourTooltip/useTooltipPosition.js +63 -36
  177. package/ui/src/components/TourTooltip.jsx +11 -3
  178. package/ui/src/components/ViewModeTabs.jsx +3 -1
  179. package/ui/src/config/tourSteps.jsx +0 -2
  180. package/ui/src/hooks/useAnimation.js +15 -12
  181. package/ui/src/hooks/useConfigManagement.js +8 -8
  182. package/ui/src/hooks/useServiceExtraction.js +1 -1
  183. package/ui/src/index.css +3 -8
  184. package/ui/src/theme.js +3 -3
  185. package/ui/src/utils/hexUtils.js +11 -5
  186. package/ui/src/utils/mcpGroupingUtils.js +18 -10
  187. package/ui/src/utils/requestPairing.js +89 -0
  188. package/ui/src/utils/requestUtils.js +37 -105
  189. package/ui/vite.config.js +1 -1
  190. package/mcp-server/.editorconfig +0 -15
  191. package/mcp-server/.prettierignore +0 -11
  192. package/mcp-server/.prettierrc +0 -12
  193. package/mcp-server/README.md +0 -280
  194. package/mcp-server/commitlint.config.cjs +0 -42
  195. package/mcp-server/eslint.config.js +0 -131
  196. package/mcp-server/package-lock.json +0 -4784
  197. package/mcp-server/package.json +0 -30
  198. package/ui/README.md +0 -212
  199. package/ui/package-lock.json +0 -3574
  200. package/ui/package.json +0 -12
  201. package/ui/paths.js +0 -282
  202. package/ui/server/routes/backups.js +0 -251
  203. package/ui/server/routes/composite.js +0 -244
@@ -8,36 +8,32 @@ export function useMcpDiscovery(setError) {
8
8
  const [sessionId, setSessionId] = useState(null);
9
9
 
10
10
  const makeMcpRequest = async (method, params = {}) => {
11
- try {
12
- const headers = { 'Content-Type': 'application/json' };
13
- if (sessionId) {
14
- headers['Mcp-Session-Id'] = sessionId;
15
- }
16
-
17
- const response = await fetch('/api/playground/proxy', {
18
- method: 'POST',
19
- headers,
20
- body: JSON.stringify({ method, params }),
21
- });
11
+ const headers = { 'Content-Type': 'application/json' };
12
+ if (sessionId) {
13
+ headers['Mcp-Session-Id'] = sessionId;
14
+ }
22
15
 
23
- const data = await response.json();
16
+ const response = await fetch('/api/playground/proxy', {
17
+ method: 'POST',
18
+ headers,
19
+ body: JSON.stringify({ method, params }),
20
+ });
24
21
 
25
- const responseSessionId =
26
- response.headers.get('Mcp-Session-Id') ||
27
- response.headers.get('mcp-session-id') ||
28
- data._sessionId;
29
- if (responseSessionId && responseSessionId !== sessionId) {
30
- setSessionId(responseSessionId);
31
- }
22
+ const data = await response.json();
32
23
 
33
- if (!response.ok) {
34
- throw new Error(data.error?.message || data.message || 'Request failed');
35
- }
24
+ const responseSessionId =
25
+ response.headers.get('Mcp-Session-Id') ||
26
+ response.headers.get('mcp-session-id') ||
27
+ data._sessionId;
28
+ if (responseSessionId && responseSessionId !== sessionId) {
29
+ setSessionId(responseSessionId);
30
+ }
36
31
 
37
- return data.result || data;
38
- } catch (err) {
39
- throw err;
32
+ if (!response.ok) {
33
+ throw new Error(data.error?.message || data.message || 'Request failed');
40
34
  }
35
+
36
+ return data.result || data;
41
37
  };
42
38
 
43
39
  const discoverMcpData = async () => {
@@ -95,7 +91,7 @@ export function useMcpDiscovery(setError) {
95
91
  setMcpData({
96
92
  server: {
97
93
  name: firstServer.name,
98
- description: `Discovered from MCP config`,
94
+ description: 'Discovered from MCP config',
99
95
  },
100
96
  tools: firstServer.tools || [],
101
97
  resources: firstServer.resources || [],
@@ -23,7 +23,7 @@ export function useScanList(apiToken, setError) {
23
23
  if (cacheResponse.ok) {
24
24
  const scans = cacheData.scans || [];
25
25
  console.log(`[useScanList] Loaded ${scans.length} cached scans from API`);
26
- console.log(`[useScanList] Full cacheData:`, cacheData);
26
+ console.log('[useScanList] Full cacheData:', cacheData);
27
27
 
28
28
  // Debug: Log first scan structure to see what we're receiving
29
29
  if (scans.length > 0) {
@@ -54,9 +54,9 @@ export function useScanList(apiToken, setError) {
54
54
  // Extract server name from multiple possible locations
55
55
  // Handle empty strings, null, undefined
56
56
  const serverName =
57
- (scan.serverName && scan.serverName.trim()) ||
58
- (scan.server_name && scan.server_name.trim()) ||
59
- (scan.server?.name && scan.server.name.trim()) ||
57
+ scan.serverName?.trim() ||
58
+ scan.server_name?.trim() ||
59
+ scan.server?.name?.trim() ||
60
60
  'Unknown Server';
61
61
 
62
62
  console.log(`[useScanList] Extracted serverName for scan ${index}: "${serverName}"`);
@@ -79,12 +79,13 @@ export function useScanList(apiToken, setError) {
79
79
  // Get the actual scan data - it might be nested
80
80
  // scan.data could be the scan result from API which has { success, data, scan_id, ... }
81
81
  // or it could be the direct scan data
82
- let scanData = scan.data || scan.result || scan;
82
+ const baseScanData = scan.data || scan.result || scan;
83
83
 
84
84
  // If scanData has a nested 'data' property (from API response), use that
85
- if (scanData && scanData.data && typeof scanData.data === 'object') {
86
- scanData = scanData.data;
87
- }
85
+ const scanData =
86
+ baseScanData?.data && typeof baseScanData.data === 'object'
87
+ ? baseScanData.data
88
+ : baseScanData;
88
89
 
89
90
  const transformed = {
90
91
  serverName: serverName,
@@ -144,7 +145,7 @@ export function useScanList(apiToken, setError) {
144
145
  r.data?.data?.id === scanId ||
145
146
  r.data?.data?.scan_id === scanId
146
147
  );
147
- if (cachedResult && cachedResult.cached && cachedResult.data?.data) {
148
+ if (cachedResult?.cached && cachedResult.data?.data) {
148
149
  // Use the cached scan data directly
149
150
  const scanData = cachedResult.data.data;
150
151
  setSelectedScan({
@@ -43,21 +43,30 @@ export function useScanOperations(apiToken, discoveredServers, selectedServers,
43
43
  const data = await response.json();
44
44
 
45
45
  if (!response.ok) {
46
- let errorMessage = data.error || data.message || `API error: ${response.status}`;
47
-
48
- if (response.status === 400 && data.details) {
49
- if (Array.isArray(data.details)) {
50
- errorMessage = `Validation failed: ${data.details
51
- .map((d) => {
52
- if (typeof d === 'string') return d;
53
- if (d.field && d.message) return `${d.field}: ${d.message}`;
54
- return JSON.stringify(d);
55
- })
56
- .join('; ')}`;
57
- } else if (typeof data.details === 'string') {
58
- errorMessage = data.details;
46
+ const formatValidationErrors = (details) => {
47
+ if (!Array.isArray(details)) {
48
+ return null;
59
49
  }
60
- }
50
+ return `Validation failed: ${details
51
+ .map((d) => {
52
+ if (typeof d === 'string') {
53
+ return d;
54
+ }
55
+ if (d.field && d.message) {
56
+ return `${d.field}: ${d.message}`;
57
+ }
58
+ return JSON.stringify(d);
59
+ })
60
+ .join('; ')}`;
61
+ };
62
+
63
+ const errorMessage =
64
+ response.status === 400 && data.details
65
+ ? formatValidationErrors(data.details) ||
66
+ data.error ||
67
+ data.message ||
68
+ `API error: ${response.status}`
69
+ : data.error || data.message || `API error: ${response.status}`;
61
70
 
62
71
  setError(errorMessage);
63
72
  return;
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useEffect, useState } from 'react';
2
2
 
3
3
  export function useServerStatus() {
4
4
  const [serverStatus, setServerStatus] = useState(null);
@@ -17,7 +17,7 @@ export function useServerStatus() {
17
17
  }
18
18
  const data = await res.json();
19
19
  setServerStatus(data);
20
- } catch (err) {
20
+ } catch (_err) {
21
21
  setServerStatus({ running: false });
22
22
  }
23
23
  };
@@ -1,4 +1,4 @@
1
- import { useState, useRef, useEffect } from 'react';
1
+ import { useEffect, useRef, useState } from 'react';
2
2
 
3
3
  export function useTokenManagement() {
4
4
  const [apiToken, setApiToken] = useState('');
@@ -22,7 +22,7 @@ export function useTokenManagement() {
22
22
  setApiToken(data.token);
23
23
  }
24
24
  }
25
- } catch (err) {
25
+ } catch (_err) {
26
26
  console.debug('No stored token found');
27
27
  }
28
28
  };
@@ -4,15 +4,12 @@
4
4
 
5
5
  export function getScanValue(scan, path) {
6
6
  const paths = path.split('.');
7
- let value = scan;
8
- for (const p of paths) {
7
+ return paths.reduce((value, p) => {
9
8
  if (value && typeof value === 'object' && p in value) {
10
- value = value[p];
11
- } else {
12
- return null;
9
+ return value[p];
13
10
  }
14
- }
15
- return value;
11
+ return null;
12
+ }, scan);
16
13
  }
17
14
 
18
15
  export function normalizeScanData(scan) {
@@ -24,8 +21,8 @@ export function normalizeScanData(scan) {
24
21
  getScanValue(scan, 'scan_id') ||
25
22
  getScanValue(scan, 'data.id') ||
26
23
  getScanValue(scan, 'data.scan_id') ||
27
- (actualScan && actualScan.id) ||
28
- (actualScan && actualScan.scan_id);
24
+ actualScan?.id ||
25
+ actualScan?.scan_id;
29
26
 
30
27
  const serverName =
31
28
  scan.serverName || // Check top-level first (for cached scans)
@@ -35,7 +32,7 @@ export function normalizeScanData(scan) {
35
32
  getScanValue(scan, 'server.name') ||
36
33
  getScanValue(scan, 'data.server.name') ||
37
34
  getScanValue(scan, 'data.data.server.name') ||
38
- (scan.server && scan.server.name) || // Check nested server object
35
+ scan.server?.name || // Check nested server object
39
36
  'Unknown Server';
40
37
 
41
38
  const status =
@@ -61,21 +58,29 @@ export function normalizeScanData(scan) {
61
58
  getScanValue(scan, 'data.updated_at') ||
62
59
  getScanValue(scan, 'data.data.updated_at');
63
60
 
64
- let analysisResult =
61
+ const baseAnalysisResult =
65
62
  getScanValue(scan, 'result.analysis_result') ||
66
63
  getScanValue(scan, 'analysis_result') ||
67
64
  getScanValue(scan, 'data.analysis_result') ||
68
65
  getScanValue(scan, 'data.data.analysis_result') ||
69
66
  getScanValue(scan, 'data.data.data.analysis_result');
70
67
 
71
- if (!analysisResult && actualScan && typeof actualScan === 'object') {
72
- if (actualScan.tool_findings || actualScan.prompt_findings || actualScan.resource_findings) {
73
- analysisResult = actualScan;
68
+ const extractAnalysisResult = (base, actual) => {
69
+ if (base) {
70
+ return base;
74
71
  }
75
- if (!analysisResult && actualScan.analysis_result) {
76
- analysisResult = actualScan.analysis_result;
72
+ if (actual && typeof actual === 'object') {
73
+ if (actual.tool_findings || actual.prompt_findings || actual.resource_findings) {
74
+ return actual;
75
+ }
76
+ if (actual.analysis_result) {
77
+ return actual.analysis_result;
78
+ }
77
79
  }
78
- }
80
+ return null;
81
+ };
82
+
83
+ const analysisResult = extractAnalysisResult(baseAnalysisResult, actualScan);
79
84
 
80
85
  const serverData =
81
86
  getScanValue(scan, 'result.mcp_server_data.server') ||
@@ -1,10 +1,10 @@
1
1
  import { useState } from 'react';
2
- import { useTokenManagement } from './hooks/useTokenManagement';
3
- import { useServerStatus } from './hooks/useServerStatus';
2
+ import { useCacheManagement } from './hooks/useCacheManagement';
4
3
  import { useMcpDiscovery } from './hooks/useMcpDiscovery';
5
- import { useScanOperations } from './hooks/useScanOperations';
6
4
  import { useScanList } from './hooks/useScanList';
7
- import { useCacheManagement } from './hooks/useCacheManagement';
5
+ import { useScanOperations } from './hooks/useScanOperations';
6
+ import { useServerStatus } from './hooks/useServerStatus';
7
+ import { useTokenManagement } from './hooks/useTokenManagement';
8
8
 
9
9
  export function useSmartScan() {
10
10
  const [error, setError] = useState(null);
@@ -1,7 +1,9 @@
1
1
  import { colors } from '../../theme';
2
2
 
3
3
  export function getRiskLevelColor(riskLevel) {
4
- if (!riskLevel) return colors.textTertiary;
4
+ if (!riskLevel) {
5
+ return colors.textTertiary;
6
+ }
5
7
  switch (riskLevel.toLowerCase()) {
6
8
  case 'none':
7
9
  return colors.accentGreen;
@@ -1,11 +1,11 @@
1
- import { colors } from '../theme';
2
1
  import {
3
- IconShield,
4
- IconExternalLink,
5
2
  IconAlertTriangle,
6
3
  IconCheck,
7
4
  IconClock,
5
+ IconExternalLink,
6
+ IconShield,
8
7
  } from '@tabler/icons-react';
8
+ import { colors } from '../theme';
9
9
 
10
10
  export const ShieldIcon = ({ size = 24, color = 'currentColor' }) => (
11
11
  <IconShield size={size} stroke={1.5} color={color} />
@@ -51,7 +51,10 @@ export const EmptyStateIcon = () => (
51
51
  strokeLinecap="round"
52
52
  strokeLinejoin="round"
53
53
  style={{ opacity: 0.5 }}
54
+ role="img"
55
+ aria-label="Empty state icon"
54
56
  >
57
+ <title>Empty state icon</title>
55
58
  <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
56
59
  <path d="M9 12l2 2 4-4" />
57
60
  </svg>
@@ -1,6 +1,6 @@
1
- import { useEffect, useRef } from 'react';
2
- import { colors, fonts } from '../../theme';
3
1
  import anime from 'animejs';
2
+ import { useEffect } from 'react';
3
+ import { colors, fonts } from '../../theme';
4
4
 
5
5
  export default function DesktopTabs({ tabs, activeTab, onTabChange, tabRefs, indicatorRef }) {
6
6
  useEffect(() => {
@@ -21,8 +21,13 @@ export default function DesktopTabs({ tabs, activeTab, onTabChange, tabRefs, ind
21
21
  <div style={{ position: 'relative', display: 'flex', flex: 1 }}>
22
22
  {tabs.map((tab) => (
23
23
  <button
24
+ type="button"
24
25
  key={tab.id}
25
- ref={(el) => (tabRefs.current[tab.id] = el)}
26
+ ref={(el) => {
27
+ if (el) {
28
+ tabRefs.current[tab.id] = el;
29
+ }
30
+ }}
26
31
  data-tour={
27
32
  tab.id === 'traffic'
28
33
  ? 'traffic-tab'
@@ -1,5 +1,5 @@
1
1
  import { colors, fonts } from '../../theme';
2
- import { MenuIcon, ChevronDownIcon } from '../TabNavigationIcons';
2
+ import { ChevronDownIcon, MenuIcon } from '../TabNavigationIcons';
3
3
 
4
4
  export default function MobileDropdown({
5
5
  tabs,
@@ -15,6 +15,7 @@ export default function MobileDropdown({
15
15
  ref={dropdownRef}
16
16
  >
17
17
  <button
18
+ type="button"
18
19
  onClick={() => setIsDropdownOpen(!isDropdownOpen)}
19
20
  style={{
20
21
  display: 'flex',
@@ -74,6 +75,7 @@ export default function MobileDropdown({
74
75
  const Icon = tab.icon;
75
76
  return (
76
77
  <button
78
+ type="button"
77
79
  key={tab.id}
78
80
  onClick={() => {
79
81
  onTabChange(tab.id);
@@ -1,6 +1,6 @@
1
+ import anime from 'animejs';
1
2
  import { useEffect, useRef } from 'react';
2
3
  import { colors, fonts } from '../theme';
3
- import anime from 'animejs';
4
4
 
5
5
  function TabNavigation({ tabs, activeTab, onTabChange }) {
6
6
  const tabRefs = useRef({});
@@ -18,7 +18,7 @@ function TabNavigation({ tabs, activeTab, onTabChange }) {
18
18
  easing: 'easeOutExpo',
19
19
  });
20
20
  }
21
- }, [activeTab, tabs]);
21
+ }, [activeTab]);
22
22
 
23
23
  return (
24
24
  <div
@@ -35,7 +35,12 @@ function TabNavigation({ tabs, activeTab, onTabChange }) {
35
35
  {tabs.map((tab) => (
36
36
  <button
37
37
  key={tab}
38
- ref={(el) => (tabRefs.current[tab] = el)}
38
+ type="button"
39
+ ref={(el) => {
40
+ if (el) {
41
+ tabRefs.current[tab] = el;
42
+ }
43
+ }}
39
44
  onClick={() => onTabChange(tab)}
40
45
  style={{
41
46
  padding: '10px 18px',
@@ -1,13 +1,13 @@
1
1
  // Tabler Icons for Tab Navigation
2
2
  // Using @tabler/icons-react - install with: npm install @tabler/icons-react
3
3
  import {
4
- IconNetwork,
4
+ IconBrandStackoverflow,
5
+ IconChevronDown,
5
6
  IconFileText,
7
+ IconMenu2,
8
+ IconNetwork,
6
9
  IconSettings,
7
- IconBrandStackoverflow,
8
10
  IconShield,
9
- IconMenu2,
10
- IconChevronDown,
11
11
  } from '@tabler/icons-react';
12
12
 
13
13
  // Wrapper components to match existing API
@@ -1,4 +1,4 @@
1
- function TourOverlay({ elementRect, onClick }) {
1
+ function TourOverlay({ elementRect, _onClick }) {
2
2
  if (!elementRect) {
3
3
  return (
4
4
  <div
@@ -19,6 +19,7 @@ export default function TourTooltipButtons({
19
19
  }}
20
20
  >
21
21
  <button
22
+ type="button"
22
23
  onClick={(e) => {
23
24
  e.stopPropagation();
24
25
  onSkip();
@@ -52,6 +53,7 @@ export default function TourTooltipButtons({
52
53
  <div style={{ display: 'flex', gap: '8px', pointerEvents: 'auto' }}>
53
54
  {currentStep > 0 && (
54
55
  <button
56
+ type="button"
55
57
  onClick={(e) => {
56
58
  e.stopPropagation();
57
59
  onPrevious();
@@ -82,6 +84,7 @@ export default function TourTooltipButtons({
82
84
  </button>
83
85
  )}
84
86
  <button
87
+ type="button"
85
88
  onClick={(e) => {
86
89
  e.stopPropagation();
87
90
  onNext();
@@ -36,6 +36,7 @@ export default function TourTooltipHeader({ step, currentStep, totalSteps, isDra
36
36
  </p>
37
37
  </div>
38
38
  <button
39
+ type="button"
39
40
  onClick={(e) => {
40
41
  e.stopPropagation();
41
42
  onSkip();
@@ -8,7 +8,10 @@ export const CloseIcon = ({ size = 16, color = 'currentColor' }) => (
8
8
  strokeWidth="2"
9
9
  strokeLinecap="round"
10
10
  strokeLinejoin="round"
11
+ role="img"
12
+ aria-label="Close icon"
11
13
  >
14
+ <title>Close icon</title>
12
15
  <line x1="18" y1="6" x2="6" y2="18" />
13
16
  <line x1="6" y1="6" x2="18" y2="18" />
14
17
  </svg>
@@ -24,7 +27,10 @@ export const ChevronRight = ({ size = 16, color = 'currentColor' }) => (
24
27
  strokeWidth="2"
25
28
  strokeLinecap="round"
26
29
  strokeLinejoin="round"
30
+ role="img"
31
+ aria-label="Chevron right icon"
27
32
  >
33
+ <title>Chevron right icon</title>
28
34
  <polyline points="9 18 15 12 9 6" />
29
35
  </svg>
30
36
  );
@@ -39,7 +45,10 @@ export const ChevronLeft = ({ size = 16, color = 'currentColor' }) => (
39
45
  strokeWidth="2"
40
46
  strokeLinecap="round"
41
47
  strokeLinejoin="round"
48
+ role="img"
49
+ aria-label="Chevron left icon"
42
50
  >
51
+ <title>Chevron left icon</title>
43
52
  <polyline points="15 18 9 12 15 6" />
44
53
  </svg>
45
54
  );
@@ -1,13 +1,13 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useEffect, useState } from 'react';
2
2
 
3
- export function useTooltipPosition(elementRect, step, currentStep) {
3
+ export function useTooltipPosition(elementRect, step, _currentStep) {
4
4
  const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
5
5
  const [isDragging, setIsDragging] = useState(false);
6
6
  const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
7
7
 
8
8
  useEffect(() => {
9
9
  setTooltipPosition({ x: 0, y: 0 });
10
- }, [currentStep]);
10
+ }, []);
11
11
 
12
12
  const handleMouseDown = (e, tooltipRef) => {
13
13
  e.preventDefault();
@@ -23,7 +23,9 @@ export function useTooltipPosition(elementRect, step, currentStep) {
23
23
  };
24
24
 
25
25
  useEffect(() => {
26
- if (!isDragging) return;
26
+ if (!isDragging) {
27
+ return;
28
+ }
27
29
 
28
30
  const handleMouseMove = (e) => {
29
31
  setTooltipPosition({
@@ -46,7 +48,9 @@ export function useTooltipPosition(elementRect, step, currentStep) {
46
48
  }, [isDragging, dragOffset]);
47
49
 
48
50
  const calculatePosition = () => {
49
- if (!elementRect) return { left: 0, top: 0, transform: 'none' };
51
+ if (!elementRect) {
52
+ return { left: 0, top: 0, transform: 'none' };
53
+ }
50
54
 
51
55
  const tooltipWidth = 350;
52
56
  const tooltipHeight = 200;
@@ -62,42 +66,65 @@ export function useTooltipPosition(elementRect, step, currentStep) {
62
66
  }
63
67
 
64
68
  const position = step.position || 'bottom';
65
- let left, top, transform;
66
-
67
- if (position === 'left') {
68
- left = elementRect.left - tooltipWidth - spacing;
69
- top = elementRect.top + elementRect.height / 2;
70
- transform = 'translateY(-50%)';
71
- if (left < 10) {
72
- left = elementRect.right + spacing;
73
- }
74
- } else if (position === 'right') {
75
- left = elementRect.right + spacing;
76
- top = elementRect.top + elementRect.height / 2;
77
- transform = 'translateY(-50%)';
78
- if (left + tooltipWidth > window.innerWidth - 10) {
79
- left = elementRect.left - tooltipWidth - spacing;
69
+
70
+ const calculatePosition = (position, elementRect, tooltipWidth, tooltipHeight, spacing) => {
71
+ if (position === 'left') {
72
+ const baseLeft = elementRect.left - tooltipWidth - spacing;
73
+ const left = baseLeft < 10 ? elementRect.right + spacing : baseLeft;
74
+ return {
75
+ left,
76
+ top: elementRect.top + elementRect.height / 2,
77
+ transform: 'translateY(-50%)',
78
+ };
80
79
  }
81
- } else if (position === 'top') {
82
- left = elementRect.left + elementRect.width / 2;
83
- top = elementRect.top - tooltipHeight - spacing;
84
- transform = 'translate(-50%, 0)';
85
- if (top < 10) {
86
- top = elementRect.bottom + spacing;
80
+
81
+ if (position === 'right') {
82
+ const baseLeft = elementRect.right + spacing;
83
+ const left =
84
+ baseLeft + tooltipWidth > window.innerWidth - 10
85
+ ? elementRect.left - tooltipWidth - spacing
86
+ : baseLeft;
87
+ return {
88
+ left,
89
+ top: elementRect.top + elementRect.height / 2,
90
+ transform: 'translateY(-50%)',
91
+ };
87
92
  }
88
- } else {
89
- left = elementRect.left + elementRect.width / 2;
90
- top = elementRect.bottom + spacing;
91
- transform = 'translate(-50%, 0)';
92
- if (top + tooltipHeight > window.innerHeight - 10) {
93
- top = elementRect.top - tooltipHeight - spacing;
93
+
94
+ if (position === 'top') {
95
+ const baseTop = elementRect.top - tooltipHeight - spacing;
96
+ const top = baseTop < 10 ? elementRect.bottom + spacing : baseTop;
97
+ return {
98
+ left: elementRect.left + elementRect.width / 2,
99
+ top,
100
+ transform: 'translate(-50%, 0)',
101
+ };
94
102
  }
95
- }
96
103
 
97
- left = Math.max(10, Math.min(left, window.innerWidth - tooltipWidth - 10));
98
- top = Math.max(10, Math.min(top, window.innerHeight - tooltipHeight - 10));
104
+ // bottom (default)
105
+ const baseTop = elementRect.bottom + spacing;
106
+ const top =
107
+ baseTop + tooltipHeight > window.innerHeight - 10
108
+ ? elementRect.top - tooltipHeight - spacing
109
+ : baseTop;
110
+ return {
111
+ left: elementRect.left + elementRect.width / 2,
112
+ top,
113
+ transform: 'translate(-50%, 0)',
114
+ };
115
+ };
99
116
 
100
- return { left, top, transform };
117
+ const rawPosition = calculatePosition(
118
+ position,
119
+ elementRect,
120
+ tooltipWidth,
121
+ tooltipHeight,
122
+ spacing
123
+ );
124
+ const left = Math.max(10, Math.min(rawPosition.left, window.innerWidth - tooltipWidth - 10));
125
+ const top = Math.max(10, Math.min(rawPosition.top, window.innerHeight - tooltipHeight - 10));
126
+
127
+ return { left, top, transform: rawPosition.transform };
101
128
  };
102
129
 
103
130
  return {
@@ -1,7 +1,7 @@
1
1
  import { useRef } from 'react';
2
2
  import { colors, fonts } from '../theme';
3
- import TourTooltipHeader from './TourTooltip/TourTooltipHeader';
4
3
  import TourTooltipButtons from './TourTooltip/TourTooltipButtons';
4
+ import TourTooltipHeader from './TourTooltip/TourTooltipHeader';
5
5
  import { useTooltipPosition } from './TourTooltip/useTooltipPosition';
6
6
 
7
7
  function TourTooltip({ elementRect, step, currentStep, totalSteps, onNext, onPrevious, onSkip }) {
@@ -17,9 +17,17 @@ function TourTooltip({ elementRect, step, currentStep, totalSteps, onNext, onPre
17
17
  }
18
18
 
19
19
  return (
20
- <div
20
+ <button
21
+ type="button"
21
22
  ref={tooltipRef}
22
23
  onMouseDown={(e) => handleMouseDown(e, tooltipRef)}
24
+ onKeyDown={(e) => {
25
+ if (e.key === 'Enter' || e.key === ' ') {
26
+ e.preventDefault();
27
+ handleMouseDown(e, tooltipRef);
28
+ }
29
+ }}
30
+ aria-label="Draggable tooltip"
23
31
  style={{
24
32
  position: 'fixed',
25
33
  left: `${position.left}px`,
@@ -76,7 +84,7 @@ function TourTooltip({ elementRect, step, currentStep, totalSteps, onNext, onPre
76
84
  onPrevious={onPrevious}
77
85
  onSkip={onSkip}
78
86
  />
79
- </div>
87
+ </button>
80
88
  );
81
89
  }
82
90