@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
@@ -110,7 +110,7 @@ export default function FindingsTable({ findings, type }) {
110
110
  <tbody>
111
111
  {findings.map((finding, index) => (
112
112
  <tr
113
- key={index}
113
+ key={`finding-${finding.id || finding.name || index}-${index}`}
114
114
  style={{
115
115
  borderBottom: `1px solid ${colors.borderLight}`,
116
116
  transition: 'background 0.15s',
@@ -164,7 +164,7 @@ export default function FindingsTable({ findings, type }) {
164
164
  <div style={{ display: 'flex', flexWrap: 'wrap', gap: '2px' }}>
165
165
  {finding.risk_tags?.map((tag, tagIndex) => (
166
166
  <span
167
- key={tagIndex}
167
+ key={`tag-${String(tag)}-${tagIndex}`}
168
168
  style={{
169
169
  padding: '2px 6px',
170
170
  background: colors.bgCard,
@@ -192,7 +192,10 @@ export default function FindingsTable({ findings, type }) {
192
192
  style={{ listStyle: 'disc', listStylePosition: 'inside', margin: 0, padding: 0 }}
193
193
  >
194
194
  {finding.reasons?.map((reason, reasonIndex) => (
195
- <li key={reasonIndex} style={{ fontSize: '10px', marginBottom: '2px' }}>
195
+ <li
196
+ key={`reason-${reasonIndex}-${reason.substring(0, 20)}`}
197
+ style={{ fontSize: '10px', marginBottom: '2px' }}
198
+ >
196
199
  {reason}
197
200
  </li>
198
201
  ))}
@@ -209,45 +212,46 @@ export default function FindingsTable({ findings, type }) {
209
212
  >
210
213
  {finding.safe_use_notes}
211
214
  </td>
212
- {type === 'tool' && finding.hasOwnProperty('is_potentially_poisoned') && (
213
- <td style={{ padding: '8px' }}>
214
- {finding.is_potentially_poisoned ? (
215
- <span
216
- style={{
217
- display: 'inline-flex',
218
- alignItems: 'center',
219
- padding: '2px 6px',
220
- fontSize: '10px',
221
- fontWeight: '500',
222
- borderRadius: '4px',
223
- background: colors.error + '20',
224
- color: colors.error,
225
- border: `1px solid ${colors.error}40`,
226
- fontFamily: fonts.body,
227
- }}
228
- >
229
- Yes
230
- </span>
231
- ) : (
232
- <span
233
- style={{
234
- display: 'inline-flex',
235
- alignItems: 'center',
236
- padding: '2px 6px',
237
- fontSize: '10px',
238
- fontWeight: '500',
239
- borderRadius: '4px',
240
- background: colors.accentGreen + '20',
241
- color: colors.accentGreen,
242
- border: `1px solid ${colors.accentGreen}40`,
243
- fontFamily: fonts.body,
244
- }}
245
- >
246
- No
247
- </span>
248
- )}
249
- </td>
250
- )}
215
+ {type === 'tool' &&
216
+ Object.prototype.hasOwnProperty.call(finding, 'is_potentially_poisoned') && (
217
+ <td style={{ padding: '8px' }}>
218
+ {finding.is_potentially_poisoned ? (
219
+ <span
220
+ style={{
221
+ display: 'inline-flex',
222
+ alignItems: 'center',
223
+ padding: '2px 6px',
224
+ fontSize: '10px',
225
+ fontWeight: '500',
226
+ borderRadius: '4px',
227
+ background: `${colors.error}20`,
228
+ color: colors.error,
229
+ border: `1px solid ${colors.error}40`,
230
+ fontFamily: fonts.body,
231
+ }}
232
+ >
233
+ Yes
234
+ </span>
235
+ ) : (
236
+ <span
237
+ style={{
238
+ display: 'inline-flex',
239
+ alignItems: 'center',
240
+ padding: '2px 6px',
241
+ fontSize: '10px',
242
+ fontWeight: '500',
243
+ borderRadius: '4px',
244
+ background: `${colors.accentGreen}20`,
245
+ color: colors.accentGreen,
246
+ border: `1px solid ${colors.accentGreen}40`,
247
+ fontFamily: fonts.body,
248
+ }}
249
+ >
250
+ No
251
+ </span>
252
+ )}
253
+ </td>
254
+ )}
251
255
  </tr>
252
256
  ))}
253
257
  </tbody>
@@ -1,6 +1,6 @@
1
1
  import { colors, fonts } from '../../theme';
2
- import ScanResultsDisplay from './ScanResultsDisplay';
3
2
  import ScanDetailView from './ScanDetailView';
3
+ import ScanResultsDisplay from './ScanResultsDisplay';
4
4
 
5
5
  export default function ListViewContent({
6
6
  error,
@@ -50,7 +50,7 @@ export default function ListViewContent({
50
50
  <div
51
51
  style={{
52
52
  padding: '12px 16px',
53
- background: colors.error + '20',
53
+ background: `${colors.error}20`,
54
54
  border: `1px solid ${colors.error}`,
55
55
  borderRadius: '8px',
56
56
  marginBottom: '16px',
@@ -2,31 +2,33 @@ import { colors, fonts } from '../../theme';
2
2
  import ExpandableSection from './ExpandableSection';
3
3
 
4
4
  export default function NotablePatternsSection({ patterns }) {
5
- if (!patterns || patterns.length === 0) return null;
5
+ if (!patterns || patterns.length === 0) {
6
+ return null;
7
+ }
6
8
 
7
9
  return (
8
10
  <ExpandableSection title="Notable Patterns" count={patterns.length} defaultExpanded={false}>
9
11
  <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
10
12
  {patterns.map((pattern, index) => (
11
13
  <div
12
- key={index}
14
+ key={`pattern-${pattern.type || index}-${index}`}
13
15
  style={{
14
16
  display: 'flex',
15
17
  alignItems: 'flex-start',
16
18
  gap: '8px',
17
19
  padding: '8px',
18
- background: colors.accentOrange + '10',
20
+ background: `${colors.accentOrange}10`,
19
21
  borderRadius: '6px',
20
22
  border: `1px solid ${colors.accentOrange}20`,
21
23
  transition: 'all 0.15s',
22
24
  }}
23
25
  onMouseEnter={(e) => {
24
- e.currentTarget.style.background = colors.accentOrange + '15';
25
- e.currentTarget.style.borderColor = colors.accentOrange + '40';
26
+ e.currentTarget.style.background = `${colors.accentOrange}15`;
27
+ e.currentTarget.style.borderColor = `${colors.accentOrange}40`;
26
28
  }}
27
29
  onMouseLeave={(e) => {
28
- e.currentTarget.style.background = colors.accentOrange + '10';
29
- e.currentTarget.style.borderColor = colors.accentOrange + '20';
30
+ e.currentTarget.style.background = `${colors.accentOrange}10`;
31
+ e.currentTarget.style.borderColor = `${colors.accentOrange}20`;
30
32
  }}
31
33
  >
32
34
  <div
@@ -36,7 +38,7 @@ export default function NotablePatternsSection({ patterns }) {
36
38
  width: '16px',
37
39
  height: '16px',
38
40
  borderRadius: '50%',
39
- background: colors.accentOrange + '30',
41
+ background: `${colors.accentOrange}30`,
40
42
  border: `1px solid ${colors.accentOrange}60`,
41
43
  display: 'flex',
42
44
  alignItems: 'center',
@@ -47,7 +49,10 @@ export default function NotablePatternsSection({ patterns }) {
47
49
  style={{ width: '10px', height: '10px', color: colors.accentOrange }}
48
50
  fill="currentColor"
49
51
  viewBox="0 0 20 20"
52
+ role="img"
53
+ aria-label="Pattern icon"
50
54
  >
55
+ <title>Pattern icon</title>
51
56
  <path
52
57
  fillRule="evenodd"
53
58
  d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
@@ -1,9 +1,42 @@
1
1
  import { colors, fonts } from '../../theme';
2
- import { getRiskLevelColor } from './utils';
3
2
  import ExpandableSection from './ExpandableSection';
3
+ import { getRiskLevelColor } from './utils';
4
+
5
+ function renderReasonContent(overallReason) {
6
+ const separator = overallReason.includes('\n')
7
+ ? '\n'
8
+ : overallReason.includes(' | ')
9
+ ? ' | '
10
+ : null;
11
+
12
+ if (separator) {
13
+ return (
14
+ <ul
15
+ style={{
16
+ listStyle: 'disc',
17
+ listStylePosition: 'inside',
18
+ margin: 0,
19
+ paddingLeft: '8px',
20
+ }}
21
+ >
22
+ {overallReason.split(separator).map((item, index) => (
23
+ <li
24
+ key={`reason-${index}-${item.trim().substring(0, 20)}`}
25
+ style={{ fontSize: '12px', marginBottom: '2px' }}
26
+ >
27
+ {item.trim()}
28
+ </li>
29
+ ))}
30
+ </ul>
31
+ );
32
+ }
33
+ return <p style={{ fontSize: '12px' }}>{overallReason}</p>;
34
+ }
4
35
 
5
36
  export default function OverallSummarySection({ overallRiskLevel, overallReason }) {
6
- if (!overallRiskLevel) return null;
37
+ if (!overallRiskLevel) {
38
+ return null;
39
+ }
7
40
 
8
41
  return (
9
42
  <ExpandableSection title="Overall Summary" count={overallReason ? 1 : 0} defaultExpanded={true}>
@@ -37,33 +70,7 @@ export default function OverallSummarySection({ overallRiskLevel, overallReason
37
70
  </div>
38
71
  {overallReason && (
39
72
  <div style={{ fontSize: '12px', color: colors.textSecondary, fontFamily: fonts.body }}>
40
- {(() => {
41
- const separator = overallReason.includes('\n')
42
- ? '\n'
43
- : overallReason.includes(' | ')
44
- ? ' | '
45
- : null;
46
-
47
- if (separator) {
48
- return (
49
- <ul
50
- style={{
51
- listStyle: 'disc',
52
- listStylePosition: 'inside',
53
- margin: 0,
54
- paddingLeft: '8px',
55
- }}
56
- >
57
- {overallReason.split(separator).map((item, index) => (
58
- <li key={index} style={{ fontSize: '12px', marginBottom: '2px' }}>
59
- {item.trim()}
60
- </li>
61
- ))}
62
- </ul>
63
- );
64
- }
65
- return <p style={{ fontSize: '12px' }}>{overallReason}</p>;
66
- })()}
73
+ {renderReasonContent(overallReason)}
67
74
  </div>
68
75
  )}
69
76
  </div>
@@ -2,7 +2,9 @@ import { colors, fonts } from '../../theme';
2
2
  import ExpandableSection from './ExpandableSection';
3
3
 
4
4
  export default function RecommendationsSection({ recommendations }) {
5
- if (!recommendations || recommendations.length === 0) return null;
5
+ if (!recommendations || recommendations.length === 0) {
6
+ return null;
7
+ }
6
8
 
7
9
  return (
8
10
  <ExpandableSection
@@ -13,24 +15,24 @@ export default function RecommendationsSection({ recommendations }) {
13
15
  <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
14
16
  {recommendations.map((recommendation, index) => (
15
17
  <div
16
- key={index}
18
+ key={`recommendation-${recommendation.type || index}-${index}`}
17
19
  style={{
18
20
  display: 'flex',
19
21
  alignItems: 'flex-start',
20
22
  gap: '8px',
21
23
  padding: '8px',
22
- background: colors.accentBlue + '10',
24
+ background: `${colors.accentBlue}10`,
23
25
  borderRadius: '6px',
24
26
  border: `1px solid ${colors.accentBlue}20`,
25
27
  transition: 'all 0.15s',
26
28
  }}
27
29
  onMouseEnter={(e) => {
28
- e.currentTarget.style.background = colors.accentBlue + '15';
29
- e.currentTarget.style.borderColor = colors.accentBlue + '40';
30
+ e.currentTarget.style.background = `${colors.accentBlue}15`;
31
+ e.currentTarget.style.borderColor = `${colors.accentBlue}40`;
30
32
  }}
31
33
  onMouseLeave={(e) => {
32
- e.currentTarget.style.background = colors.accentBlue + '10';
33
- e.currentTarget.style.borderColor = colors.accentBlue + '20';
34
+ e.currentTarget.style.background = `${colors.accentBlue}10`;
35
+ e.currentTarget.style.borderColor = `${colors.accentBlue}20`;
34
36
  }}
35
37
  >
36
38
  <div
@@ -40,7 +42,7 @@ export default function RecommendationsSection({ recommendations }) {
40
42
  width: '16px',
41
43
  height: '16px',
42
44
  borderRadius: '50%',
43
- background: colors.accentBlue + '30',
45
+ background: `${colors.accentBlue}30`,
44
46
  border: `1px solid ${colors.accentBlue}60`,
45
47
  display: 'flex',
46
48
  alignItems: 'center',
@@ -1,5 +1,5 @@
1
+ import { IconExternalLink, IconX } from '@tabler/icons-react';
1
2
  import { colors, fonts } from '../../theme';
2
- import { IconX, IconExternalLink } from '@tabler/icons-react';
3
3
 
4
4
  export default function ScanDetailHeader({ scanId, serverName, onClose }) {
5
5
  return (
@@ -63,6 +63,7 @@ export default function ScanDetailHeader({ scanId, serverName, onClose }) {
63
63
  </a>
64
64
  )}
65
65
  <button
66
+ type="button"
66
67
  onClick={onClose}
67
68
  style={{
68
69
  padding: '6px',
@@ -1,11 +1,11 @@
1
1
  import { colors, fonts } from '../../theme';
2
2
  import AnalysisResult from './AnalysisResult';
3
- import { normalizeScanData } from './scanDataUtils';
4
- import ScanDetailHeader from './ScanDetailHeader';
5
3
  import DebugInfoSection from './DebugInfoSection';
4
+ import RawDataSection from './RawDataSection';
5
+ import ScanDetailHeader from './ScanDetailHeader';
6
6
  import ScanOverviewSection from './ScanOverviewSection';
7
7
  import ServerInfoSection from './ServerInfoSection';
8
- import RawDataSection from './RawDataSection';
8
+ import { normalizeScanData } from './scanDataUtils';
9
9
 
10
10
  export default function ScanDetailView({ scan, loading, onClose }) {
11
11
  if (loading) {
@@ -120,7 +120,7 @@ export default function ScanDetailView({ scan, loading, onClose }) {
120
120
  <div
121
121
  style={{
122
122
  padding: '12px',
123
- background: colors.bgTertiary + '80',
123
+ background: `${colors.bgTertiary}80`,
124
124
  borderRadius: '6px',
125
125
  border: `1px solid ${colors.borderLight}`,
126
126
  fontSize: '12px',
@@ -1,5 +1,5 @@
1
- import { colors, fonts } from '../../../theme';
2
1
  import { IconRefresh } from '@tabler/icons-react';
2
+ import { colors, fonts } from '../../../theme';
3
3
 
4
4
  export default function ScanListHeader({ scanCount, loading, onRefresh }) {
5
5
  return (
@@ -23,6 +23,7 @@ export default function ScanListHeader({ scanCount, loading, onRefresh }) {
23
23
  All Scans ({scanCount})
24
24
  </h2>
25
25
  <button
26
+ type="button"
26
27
  onClick={onRefresh}
27
28
  disabled={loading}
28
29
  style={{
@@ -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 };