@mcp-shark/mcp-shark 1.4.2 → 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 +22 -38
  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 +4 -12
  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 +3 -13
  12. package/mcp-server/lib/server/internal/handlers/prompts-list.js +2 -6
  13. package/mcp-server/lib/server/internal/handlers/resources-list.js +2 -6
  14. package/mcp-server/lib/server/internal/handlers/resources-read.js +3 -12
  15. package/mcp-server/lib/server/internal/handlers/tools-call.js +3 -9
  16. package/mcp-server/lib/server/internal/handlers/tools-list.js +2 -2
  17. package/mcp-server/lib/server/internal/run.js +4 -16
  18. package/mcp-server/lib/server/internal/server.js +6 -7
  19. package/mcp-server/lib/server/internal/session.js +2 -15
  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 +45 -47
  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 +7 -6
  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 +86 -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 -4
  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 +6 -2
  107. package/ui/src/components/McpPlayground/PromptsSection/PromptsList.jsx +4 -4
  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 +6 -2
  111. package/ui/src/components/McpPlayground/ResourcesSection/ResourcesList.jsx +4 -4
  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 +6 -2
  115. package/ui/src/components/McpPlayground/ToolsSection/ToolsList.jsx +4 -4
  116. package/ui/src/components/McpPlayground/ToolsSection.jsx +2 -1
  117. package/ui/src/components/McpPlayground/hooks/useMcpDataLoader.js +9 -9
  118. package/ui/src/components/McpPlayground/hooks/useMcpRequest.js +10 -5
  119. package/ui/src/components/McpPlayground/hooks/useMcpServerStatus.js +43 -23
  120. package/ui/src/components/McpPlayground/useMcpPlayground.js +72 -44
  121. package/ui/src/components/McpPlayground.jsx +5 -2
  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 +32 -101
  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 -260
@@ -1,6 +1,6 @@
1
+ import { IconChevronRight, IconExternalLink, IconEye } from '@tabler/icons-react';
1
2
  import { useState } from 'react';
2
3
  import { colors, fonts } from '../../../theme';
3
- import { IconChevronRight, IconEye, IconExternalLink } from '@tabler/icons-react';
4
4
  import { getRiskLevelColor } from '../utils';
5
5
 
6
6
  export default function ScanListItem({ scan, onSelectScan }) {
@@ -25,6 +25,13 @@ export default function ScanListItem({ scan, onSelectScan }) {
25
25
  cursor: 'pointer',
26
26
  }}
27
27
  onClick={() => setIsExpanded(!isExpanded)}
28
+ onKeyDown={(e) => {
29
+ if (e.key === 'Enter' || e.key === ' ') {
30
+ e.preventDefault();
31
+ setIsExpanded(!isExpanded);
32
+ }
33
+ }}
34
+ aria-label={`Toggle scan ${scan.scanId || 'details'}`}
28
35
  >
29
36
  <div
30
37
  style={{
@@ -101,8 +108,15 @@ export default function ScanListItem({ scan, onSelectScan }) {
101
108
  flexShrink: 0,
102
109
  }}
103
110
  onClick={(e) => e.stopPropagation()}
111
+ onKeyDown={(e) => {
112
+ if (e.key === 'Enter' || e.key === ' ') {
113
+ e.preventDefault();
114
+ e.stopPropagation();
115
+ }
116
+ }}
104
117
  >
105
118
  <button
119
+ type="button"
106
120
  onClick={() => {
107
121
  onSelectScan(scan.id);
108
122
  }}
@@ -3,7 +3,9 @@ import { getRiskLevelColor } from './utils';
3
3
 
4
4
  export default function ScanOverviewSection({ status, overallRiskLevel, createdAt, updatedAt }) {
5
5
  const formatDate = (dateString) => {
6
- if (!dateString) return 'N/A';
6
+ if (!dateString) {
7
+ return 'N/A';
8
+ }
7
9
  return new Date(dateString).toLocaleString();
8
10
  };
9
11
 
@@ -1,8 +1,8 @@
1
1
  import { colors } from '../../theme';
2
+ import BatchResultsDisplay from './BatchResultsDisplay';
2
3
  import EmptyState from './EmptyState';
3
4
  import ErrorDisplay from './ErrorDisplay';
4
5
  import ScanningProgress from './ScanningProgress';
5
- import BatchResultsDisplay from './BatchResultsDisplay';
6
6
  import SingleResultDisplay from './SingleResultDisplay';
7
7
 
8
8
  export default function ScanResultsDisplay({
@@ -1,7 +1,7 @@
1
1
  import { colors } from '../../theme';
2
- import ServerSelectionRow from './ServerSelectionRow';
3
- import ScanResultsDisplay from './ScanResultsDisplay';
4
2
  import ScanDetailView from './ScanDetailView';
3
+ import ScanResultsDisplay from './ScanResultsDisplay';
4
+ import ServerSelectionRow from './ServerSelectionRow';
5
5
 
6
6
  export default function ScanViewContent({
7
7
  discoveredServers,
@@ -1,8 +1,10 @@
1
- import { LoadingSpinner } from '../SmartScanIcons';
2
1
  import { colors, fonts } from '../../theme';
2
+ import { LoadingSpinner } from '../SmartScanIcons';
3
3
 
4
4
  export default function ScanningProgress({ scanning, selectedServers }) {
5
- if (!scanning) return null;
5
+ if (!scanning) {
6
+ return null;
7
+ }
6
8
 
7
9
  return (
8
10
  <div
@@ -1,7 +1,9 @@
1
1
  import { colors, fonts } from '../../theme';
2
2
 
3
3
  export default function ServerInfoSection({ serverData }) {
4
- if (!serverData) return null;
4
+ if (!serverData) {
5
+ return null;
6
+ }
5
7
 
6
8
  return (
7
9
  <div style={{ marginBottom: '24px' }}>
@@ -1,5 +1,5 @@
1
- import { CheckIcon, ShieldIcon, LoadingSpinner } from '../SmartScanIcons';
2
1
  import { colors, fonts } from '../../theme';
2
+ import { LoadingSpinner, ShieldIcon } from '../SmartScanIcons';
3
3
 
4
4
  export default function ServerSelectionRow({
5
5
  discoveredServers,
@@ -57,7 +57,7 @@ export default function ServerSelectionRow({
57
57
  const isSelected = selectedServers.has(server.name);
58
58
  return (
59
59
  <label
60
- key={idx}
60
+ key={server.name || `server-${idx}`}
61
61
  style={{
62
62
  display: 'flex',
63
63
  alignItems: 'center',
@@ -119,6 +119,7 @@ export default function ServerSelectionRow({
119
119
  })}
120
120
  </div>
121
121
  <button
122
+ type="button"
122
123
  onClick={toggleSelectAll}
123
124
  style={{
124
125
  padding: '6px 12px',
@@ -143,6 +144,7 @@ export default function ServerSelectionRow({
143
144
  {selectedServers.size === discoveredServers.length ? 'Deselect All' : 'Select All'}
144
145
  </button>
145
146
  <button
147
+ type="button"
146
148
  onClick={runScan}
147
149
  disabled={!apiToken || selectedServers.size === 0 || scanning}
148
150
  style={{
@@ -1,9 +1,11 @@
1
- import { ExternalLinkIcon } from '../SmartScanIcons';
2
1
  import { colors, fonts } from '../../theme';
2
+ import { ExternalLinkIcon } from '../SmartScanIcons';
3
3
  import { getRiskLevelColor } from './utils';
4
4
 
5
5
  export default function SingleResultDisplay({ scanResult }) {
6
- if (!scanResult) return null;
6
+ if (!scanResult) {
7
+ return null;
8
+ }
7
9
 
8
10
  return (
9
11
  <div
@@ -218,7 +220,7 @@ export default function SingleResultDisplay({ scanResult }) {
218
220
  }}
219
221
  >
220
222
  {scanResult.data.recommendations.map((rec, idx) => (
221
- <li key={idx}>{rec}</li>
223
+ <li key={`recommendation-${idx}-${rec.substring(0, 30)}`}>{rec}</li>
222
224
  ))}
223
225
  </ul>
224
226
  </div>
@@ -1,9 +1,7 @@
1
- import { useState, useRef } from 'react';
1
+ import { useRef, useState } from 'react';
2
2
  import { colors, fonts } from '../../theme';
3
- import { CheckIcon, LoadingSpinner, CacheIcon } from '../SmartScanIcons';
4
- import { ExternalLinkIcon } from '../SmartScanIcons';
5
- import { IconTrash } from '@tabler/icons-react';
6
3
  import ConfirmationModal from '../ConfirmationModal';
4
+ import { CheckIcon, ExternalLinkIcon, LoadingSpinner } from '../SmartScanIcons';
7
5
 
8
6
  export default function SmartScanControls({
9
7
  apiToken,
@@ -12,13 +10,14 @@ export default function SmartScanControls({
12
10
  loadingData,
13
11
  discoverMcpData,
14
12
  discoveredServers,
15
- selectedServers,
16
- setSelectedServers,
17
- runScan,
13
+ _selectedServers,
14
+ _setSelectedServers,
18
15
  scanning,
19
16
  clearCache,
20
17
  clearingCache,
18
+ ...rest
21
19
  }) {
20
+ const { runScan: _runScan } = rest;
22
21
  const saveTokenTimeoutRef = useRef(null);
23
22
  const [showClearCacheModal, setShowClearCacheModal] = useState(false);
24
23
 
@@ -56,6 +55,7 @@ export default function SmartScanControls({
56
55
  }}
57
56
  >
58
57
  <label
58
+ htmlFor="api-token-input"
59
59
  style={{
60
60
  fontSize: '12px',
61
61
  fontWeight: '600',
@@ -68,6 +68,7 @@ export default function SmartScanControls({
68
68
  </label>
69
69
  <div style={{ position: 'relative', width: '200px' }}>
70
70
  <input
71
+ id="api-token-input"
71
72
  type="password"
72
73
  value={apiToken}
73
74
  onChange={(e) => handleTokenChange(e.target.value)}
@@ -145,6 +146,7 @@ export default function SmartScanControls({
145
146
  }}
146
147
  >
147
148
  <label
149
+ htmlFor="servers-label"
148
150
  style={{
149
151
  fontSize: '12px',
150
152
  fontWeight: '600',
@@ -156,6 +158,7 @@ export default function SmartScanControls({
156
158
  Servers:
157
159
  </label>
158
160
  <button
161
+ type="button"
159
162
  onClick={discoverMcpData}
160
163
  disabled={loadingData}
161
164
  style={{
@@ -224,6 +227,7 @@ export default function SmartScanControls({
224
227
 
225
228
  {/* Clear Cache Button */}
226
229
  <button
230
+ type="button"
227
231
  onClick={() => setShowClearCacheModal(true)}
228
232
  disabled={clearingCache}
229
233
  style={{
@@ -1,5 +1,5 @@
1
- import { ShieldIcon, ExternalLinkIcon } from '../SmartScanIcons';
2
1
  import { colors, fonts } from '../../theme';
2
+ import { ExternalLinkIcon, ShieldIcon } from '../SmartScanIcons';
3
3
 
4
4
  export default function SmartScanHeader() {
5
5
  return (
@@ -13,6 +13,7 @@ export default function ViewModeTabs({ viewMode, setViewMode, onSwitchToScan, on
13
13
  }}
14
14
  >
15
15
  <button
16
+ type="button"
16
17
  onClick={() => {
17
18
  setViewMode('scan');
18
19
  onSwitchToScan?.();
@@ -33,6 +34,7 @@ export default function ViewModeTabs({ viewMode, setViewMode, onSwitchToScan, on
33
34
  Scan Servers
34
35
  </button>
35
36
  <button
37
+ type="button"
36
38
  onClick={() => {
37
39
  setViewMode('list');
38
40
  onSwitchToList?.();
@@ -16,9 +16,8 @@ export function useCacheManagement(discoveredServers, discoverMcpData, setError)
16
16
  await discoverMcpData();
17
17
  }
18
18
  return { success: true, message: data.message };
19
- } else {
20
- throw new Error(data.error || 'Failed to clear cache');
21
19
  }
20
+ throw new Error(data.error || 'Failed to clear cache');
22
21
  } catch (err) {
23
22
  setError(err.message || 'Failed to clear cache');
24
23
  return { success: false, error: err.message };
@@ -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',