@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
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { colors, fonts } from '../theme';
3
- import RequestRow from './RequestRow';
4
3
  import GroupHeader from './GroupHeader';
4
+ import RequestRow from './RequestRow';
5
5
 
6
6
  export default function GroupedBySessionView({
7
7
  groupedData,
@@ -17,7 +17,10 @@ const ChevronDown = ({ size = 14, color = 'currentColor', rotated = false }) =>
17
17
  transform: rotated ? 'rotate(-90deg)' : 'rotate(0deg)',
18
18
  transition: 'transform 0.2s ease',
19
19
  }}
20
+ role="img"
21
+ aria-label="Chevron down icon"
20
22
  >
23
+ <title>Chevron down icon</title>
21
24
  <polyline points="6 9 12 15 18 9" />
22
25
  </svg>
23
26
  );
@@ -35,8 +38,15 @@ function CollapsibleRequestResponse({ title, titleColor, children, defaultExpand
35
38
  marginBottom: '20px',
36
39
  }}
37
40
  >
38
- <div
41
+ <button
42
+ type="button"
39
43
  onClick={() => setIsExpanded(!isExpanded)}
44
+ onKeyDown={(e) => {
45
+ if (e.key === 'Enter' || e.key === ' ') {
46
+ e.preventDefault();
47
+ setIsExpanded(!isExpanded);
48
+ }
49
+ }}
40
50
  style={{
41
51
  padding: '16px 20px',
42
52
  background: isExpanded ? colors.bgCard : colors.bgSecondary,
@@ -47,6 +57,9 @@ function CollapsibleRequestResponse({ title, titleColor, children, defaultExpand
47
57
  alignItems: 'center',
48
58
  justifyContent: 'space-between',
49
59
  transition: 'background-color 0.15s ease',
60
+ width: '100%',
61
+ border: 'none',
62
+ textAlign: 'left',
50
63
  }}
51
64
  onMouseEnter={(e) => {
52
65
  e.currentTarget.style.background = colors.bgHover;
@@ -71,7 +84,7 @@ function CollapsibleRequestResponse({ title, titleColor, children, defaultExpand
71
84
  <ChevronDown size={14} color={titleColor} rotated={!isExpanded} />
72
85
  {title}
73
86
  </div>
74
- </div>
87
+ </button>
75
88
  {isExpanded && <div style={{ padding: '20px' }}>{children}</div>}
76
89
  </div>
77
90
  );
@@ -100,7 +113,7 @@ function HexTab({ requestHexLines, responseHexLines, hasRequest, hasResponse })
100
113
  {requestHexLines.length > 0 ? (
101
114
  requestHexLines.map((line, i) => (
102
115
  <div
103
- key={i}
116
+ key={`request-${line.offset}-${i}`}
104
117
  style={{
105
118
  display: 'flex',
106
119
  gap: '16px',
@@ -150,7 +163,7 @@ function HexTab({ requestHexLines, responseHexLines, hasRequest, hasResponse })
150
163
  {responseHexLines.length > 0 ? (
151
164
  responseHexLines.map((line, i) => (
152
165
  <div
153
- key={i}
166
+ key={`response-${line.offset}-${i}`}
154
167
  style={{
155
168
  display: 'flex',
156
169
  gap: '16px',
@@ -1,5 +1,5 @@
1
+ import { IconDownload, IconSearch, IconTrash } from '@tabler/icons-react';
1
2
  import { colors, fonts } from '../theme';
2
- import { IconTrash, IconDownload, IconSearch } from '@tabler/icons-react';
3
3
 
4
4
  export default function LogsToolbar({
5
5
  filter,
@@ -117,6 +117,7 @@ export default function LogsToolbar({
117
117
  </label>
118
118
 
119
119
  <button
120
+ type="button"
120
121
  onClick={onClearLogs}
121
122
  style={{
122
123
  padding: '8px 14px',
@@ -148,6 +149,7 @@ export default function LogsToolbar({
148
149
  </button>
149
150
 
150
151
  <button
152
+ type="button"
151
153
  onClick={onExportLogs}
152
154
  style={{
153
155
  padding: '8px 14px',
@@ -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
  export default function LoadingModal({ show }) {
6
6
  const loadingModalRef = useRef(null);
@@ -42,7 +42,9 @@ export default function LoadingModal({ show }) {
42
42
  }
43
43
  }, [show]);
44
44
 
45
- if (!show) return null;
45
+ if (!show) {
46
+ return null;
47
+ }
46
48
 
47
49
  return (
48
50
  <div
@@ -96,7 +98,9 @@ export default function LoadingModal({ show }) {
96
98
  <div
97
99
  key={i}
98
100
  ref={(el) => {
99
- if (el) dotsRef.current[i] = el;
101
+ if (el) {
102
+ dotsRef.current[i] = el;
103
+ }
100
104
  }}
101
105
  style={{
102
106
  width: '12px',
@@ -43,6 +43,7 @@ export default function PromptCallPanel({
43
43
  >
44
44
  <div>
45
45
  <label
46
+ htmlFor="prompt-args-textarea"
46
47
  style={{
47
48
  display: 'block',
48
49
  fontSize: '12px',
@@ -54,6 +55,7 @@ export default function PromptCallPanel({
54
55
  Arguments (JSON)
55
56
  </label>
56
57
  <textarea
58
+ id="prompt-args-textarea"
57
59
  value={promptArgs}
58
60
  onChange={(e) => onPromptArgsChange(e.target.value)}
59
61
  style={{
@@ -71,6 +73,7 @@ export default function PromptCallPanel({
71
73
  />
72
74
  </div>
73
75
  <button
76
+ type="button"
74
77
  onClick={onGetPrompt}
75
78
  disabled={loading}
76
79
  style={{
@@ -91,6 +94,7 @@ export default function PromptCallPanel({
91
94
  {promptResult && (
92
95
  <div>
93
96
  <label
97
+ htmlFor="prompt-result-pre"
94
98
  style={{
95
99
  display: 'block',
96
100
  fontSize: '12px',
@@ -102,6 +106,7 @@ export default function PromptCallPanel({
102
106
  Result
103
107
  </label>
104
108
  <pre
109
+ id="prompt-result-pre"
105
110
  style={{
106
111
  padding: '12px',
107
112
  background: colors.bgSecondary,
@@ -2,47 +2,76 @@ import { colors } from '../../../theme';
2
2
 
3
3
  export default function PromptItem({ prompt, isSelected, onClick }) {
4
4
  return (
5
- <div
5
+ <button
6
+ type="button"
6
7
  onClick={onClick}
8
+ aria-label={`Select prompt ${prompt.name}`}
7
9
  style={{
8
- padding: '12px',
9
- borderBottom: `1px solid ${colors.borderLight}`,
10
+ padding: '16px 20px',
11
+ margin: '4px 8px',
12
+ borderRadius: '8px',
10
13
  cursor: 'pointer',
11
- background: isSelected ? colors.bgSecondary : colors.bgCard,
12
- transition: 'background 0.2s',
14
+ background: isSelected ? colors.bgSelected : colors.bgCard,
15
+ border: isSelected ? `2px solid ${colors.accentBlue}` : `1px solid ${colors.borderLight}`,
16
+ boxShadow: isSelected ? `0 2px 4px ${colors.shadowSm}` : 'none',
17
+ transition: 'all 0.2s ease',
18
+ position: 'relative',
19
+ width: '100%',
20
+ textAlign: 'left',
13
21
  }}
14
22
  onMouseEnter={(e) => {
15
23
  if (!isSelected) {
16
24
  e.currentTarget.style.background = colors.bgHover;
25
+ e.currentTarget.style.borderColor = colors.borderMedium;
26
+ e.currentTarget.style.boxShadow = `0 2px 4px ${colors.shadowSm}`;
17
27
  }
18
28
  }}
19
29
  onMouseLeave={(e) => {
20
30
  if (!isSelected) {
21
31
  e.currentTarget.style.background = colors.bgCard;
32
+ e.currentTarget.style.borderColor = colors.borderLight;
33
+ e.currentTarget.style.boxShadow = 'none';
22
34
  }
23
35
  }}
24
36
  >
25
- <div
26
- style={{
27
- fontWeight: '500',
28
- fontSize: '13px',
29
- color: colors.textPrimary,
30
- marginBottom: '4px',
31
- }}
32
- >
33
- {prompt.name}
34
- </div>
35
- {prompt.description && (
37
+ <div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
36
38
  <div
37
39
  style={{
38
- fontSize: '12px',
39
- color: colors.textSecondary,
40
- marginTop: '4px',
40
+ width: '6px',
41
+ height: '6px',
42
+ borderRadius: '50%',
43
+ background: isSelected ? colors.accentBlue : colors.textTertiary,
44
+ marginTop: '6px',
45
+ flexShrink: 0,
46
+ transition: 'background 0.2s',
41
47
  }}
42
- >
43
- {prompt.description}
48
+ />
49
+ <div style={{ flex: 1, minWidth: 0 }}>
50
+ <div
51
+ style={{
52
+ fontWeight: isSelected ? '600' : '500',
53
+ fontSize: '14px',
54
+ color: colors.textPrimary,
55
+ marginBottom: prompt.description ? '6px' : '0',
56
+ lineHeight: '1.4',
57
+ }}
58
+ >
59
+ {prompt.name}
60
+ </div>
61
+ {prompt.description && (
62
+ <div
63
+ style={{
64
+ fontSize: '12px',
65
+ color: colors.textSecondary,
66
+ lineHeight: '1.5',
67
+ marginTop: '4px',
68
+ }}
69
+ >
70
+ {prompt.description}
71
+ </div>
72
+ )}
44
73
  </div>
45
- )}
46
- </div>
74
+ </div>
75
+ </button>
47
76
  );
48
77
  }
@@ -1,7 +1,7 @@
1
1
  import { colors } from '../../../theme';
2
- import LoadingState from '../common/LoadingState';
3
- import ErrorState from '../common/ErrorState';
4
2
  import EmptyState from '../common/EmptyState';
3
+ import ErrorState from '../common/ErrorState';
4
+ import LoadingState from '../common/LoadingState';
5
5
  import PromptItem from './PromptItem';
6
6
 
7
7
  export default function PromptsList({
@@ -26,19 +26,21 @@ export default function PromptsList({
26
26
  <LoadingState message="Waiting for MCP server to start..." />
27
27
  ) : promptsLoading || !promptsLoaded ? (
28
28
  <LoadingState message="Loading prompts..." />
29
- ) : error && error.includes('prompts:') ? (
29
+ ) : error?.includes('prompts:') ? (
30
30
  <ErrorState message={`Error loading prompts: ${error.replace('prompts: ', '')}`} />
31
31
  ) : prompts.length === 0 ? (
32
32
  <EmptyState message="No prompts available." />
33
33
  ) : (
34
- prompts.map((prompt, idx) => (
35
- <PromptItem
36
- key={idx}
37
- prompt={prompt}
38
- isSelected={selectedPrompt?.name === prompt.name}
39
- onClick={() => onSelectPrompt(prompt)}
40
- />
41
- ))
34
+ <div style={{ padding: '8px 0' }}>
35
+ {prompts.map((prompt, idx) => (
36
+ <PromptItem
37
+ key={prompt.name || `prompt-${idx}`}
38
+ prompt={prompt}
39
+ isSelected={selectedPrompt?.name === prompt.name}
40
+ onClick={() => onSelectPrompt(prompt)}
41
+ />
42
+ ))}
43
+ </div>
42
44
  )}
43
45
  </div>
44
46
  );
@@ -1,6 +1,6 @@
1
1
  import { colors, fonts } from '../../theme';
2
- import PromptsList from './PromptsSection/PromptsList';
3
2
  import PromptCallPanel from './PromptsSection/PromptCallPanel';
3
+ import PromptsList from './PromptsSection/PromptsList';
4
4
 
5
5
  export default function PromptsSection({
6
6
  prompts,
@@ -32,6 +32,7 @@ export default function PromptsSection({
32
32
  <div style={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%' }}>
33
33
  <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
34
34
  <button
35
+ type="button"
35
36
  onClick={onRefresh}
36
37
  disabled={loading || promptsLoading}
37
38
  style={{
@@ -35,6 +35,7 @@ export default function ResourceCallPanel({ resource, resourceResult, onReadReso
35
35
  }}
36
36
  >
37
37
  <button
38
+ type="button"
38
39
  onClick={onReadResource}
39
40
  disabled={loading}
40
41
  style={{
@@ -55,6 +56,7 @@ export default function ResourceCallPanel({ resource, resourceResult, onReadReso
55
56
  {resourceResult && (
56
57
  <div>
57
58
  <label
59
+ htmlFor="resource-result-pre"
58
60
  style={{
59
61
  display: 'block',
60
62
  fontSize: '12px',
@@ -66,6 +68,7 @@ export default function ResourceCallPanel({ resource, resourceResult, onReadReso
66
68
  Result
67
69
  </label>
68
70
  <pre
71
+ id="resource-result-pre"
69
72
  style={{
70
73
  padding: '12px',
71
74
  background: colors.bgSecondary,
@@ -2,58 +2,90 @@ import { colors } from '../../../theme';
2
2
 
3
3
  export default function ResourceItem({ resource, isSelected, onClick }) {
4
4
  return (
5
- <div
5
+ <button
6
+ type="button"
6
7
  onClick={onClick}
8
+ aria-label={`Select resource ${resource.uri}`}
7
9
  style={{
8
- padding: '12px',
9
- borderBottom: `1px solid ${colors.borderLight}`,
10
+ padding: '16px 20px',
11
+ margin: '4px 8px',
12
+ borderRadius: '8px',
10
13
  cursor: 'pointer',
11
- background: isSelected ? colors.bgSecondary : colors.bgCard,
12
- transition: 'background 0.2s',
14
+ background: isSelected ? colors.bgSelected : colors.bgCard,
15
+ border: isSelected ? `2px solid ${colors.accentBlue}` : `1px solid ${colors.borderLight}`,
16
+ boxShadow: isSelected ? `0 2px 4px ${colors.shadowSm}` : 'none',
17
+ transition: 'all 0.2s ease',
18
+ position: 'relative',
19
+ width: '100%',
20
+ textAlign: 'left',
13
21
  }}
14
22
  onMouseEnter={(e) => {
15
23
  if (!isSelected) {
16
24
  e.currentTarget.style.background = colors.bgHover;
25
+ e.currentTarget.style.borderColor = colors.borderMedium;
26
+ e.currentTarget.style.boxShadow = `0 2px 4px ${colors.shadowSm}`;
17
27
  }
18
28
  }}
19
29
  onMouseLeave={(e) => {
20
30
  if (!isSelected) {
21
31
  e.currentTarget.style.background = colors.bgCard;
32
+ e.currentTarget.style.borderColor = colors.borderLight;
33
+ e.currentTarget.style.boxShadow = 'none';
22
34
  }
23
35
  }}
24
36
  >
25
- <div
26
- style={{
27
- fontWeight: '500',
28
- fontSize: '13px',
29
- color: colors.textPrimary,
30
- marginBottom: '4px',
31
- }}
32
- >
33
- {resource.uri}
34
- </div>
35
- {resource.name && (
36
- <div
37
- style={{
38
- fontSize: '12px',
39
- color: colors.textSecondary,
40
- marginTop: '4px',
41
- }}
42
- >
43
- {resource.name}
44
- </div>
45
- )}
46
- {resource.description && (
37
+ <div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
47
38
  <div
48
39
  style={{
49
- fontSize: '12px',
50
- color: colors.textSecondary,
51
- marginTop: '4px',
40
+ width: '6px',
41
+ height: '6px',
42
+ borderRadius: '50%',
43
+ background: isSelected ? colors.accentBlue : colors.textTertiary,
44
+ marginTop: '6px',
45
+ flexShrink: 0,
46
+ transition: 'background 0.2s',
52
47
  }}
53
- >
54
- {resource.description}
48
+ />
49
+ <div style={{ flex: 1, minWidth: 0 }}>
50
+ <div
51
+ style={{
52
+ fontWeight: isSelected ? '600' : '500',
53
+ fontSize: '14px',
54
+ color: colors.textPrimary,
55
+ marginBottom: resource.name || resource.description ? '6px' : '0',
56
+ lineHeight: '1.4',
57
+ wordBreak: 'break-word',
58
+ }}
59
+ >
60
+ {resource.uri}
61
+ </div>
62
+ {resource.name && (
63
+ <div
64
+ style={{
65
+ fontSize: '13px',
66
+ color: colors.textPrimary,
67
+ fontWeight: '500',
68
+ lineHeight: '1.5',
69
+ marginTop: '4px',
70
+ }}
71
+ >
72
+ {resource.name}
73
+ </div>
74
+ )}
75
+ {resource.description && (
76
+ <div
77
+ style={{
78
+ fontSize: '12px',
79
+ color: colors.textSecondary,
80
+ lineHeight: '1.5',
81
+ marginTop: resource.name ? '2px' : '4px',
82
+ }}
83
+ >
84
+ {resource.description}
85
+ </div>
86
+ )}
55
87
  </div>
56
- )}
57
- </div>
88
+ </div>
89
+ </button>
58
90
  );
59
91
  }
@@ -1,7 +1,7 @@
1
1
  import { colors } from '../../../theme';
2
- import LoadingState from '../common/LoadingState';
3
- import ErrorState from '../common/ErrorState';
4
2
  import EmptyState from '../common/EmptyState';
3
+ import ErrorState from '../common/ErrorState';
4
+ import LoadingState from '../common/LoadingState';
5
5
  import ResourceItem from './ResourceItem';
6
6
 
7
7
  export default function ResourcesList({
@@ -26,19 +26,21 @@ export default function ResourcesList({
26
26
  <LoadingState message="Waiting for MCP server to start..." />
27
27
  ) : resourcesLoading || !resourcesLoaded ? (
28
28
  <LoadingState message="Loading resources..." />
29
- ) : error && error.includes('resources:') ? (
29
+ ) : error?.includes('resources:') ? (
30
30
  <ErrorState message={`Error loading resources: ${error.replace('resources: ', '')}`} />
31
31
  ) : resources.length === 0 ? (
32
32
  <EmptyState message="No resources available." />
33
33
  ) : (
34
- resources.map((resource, idx) => (
35
- <ResourceItem
36
- key={idx}
37
- resource={resource}
38
- isSelected={selectedResource?.uri === resource.uri}
39
- onClick={() => onSelectResource(resource)}
40
- />
41
- ))
34
+ <div style={{ padding: '8px 0' }}>
35
+ {resources.map((resource, idx) => (
36
+ <ResourceItem
37
+ key={resource.uri || `resource-${idx}`}
38
+ resource={resource}
39
+ isSelected={selectedResource?.uri === resource.uri}
40
+ onClick={() => onSelectResource(resource)}
41
+ />
42
+ ))}
43
+ </div>
42
44
  )}
43
45
  </div>
44
46
  );
@@ -1,6 +1,6 @@
1
1
  import { colors, fonts } from '../../theme';
2
- import ResourcesList from './ResourcesSection/ResourcesList';
3
2
  import ResourceCallPanel from './ResourcesSection/ResourceCallPanel';
3
+ import ResourcesList from './ResourcesSection/ResourcesList';
4
4
 
5
5
  export default function ResourcesSection({
6
6
  resources,
@@ -19,6 +19,7 @@ export default function ResourcesSection({
19
19
  <div style={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%' }}>
20
20
  <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
21
21
  <button
22
+ type="button"
22
23
  onClick={onRefresh}
23
24
  disabled={loading || resourcesLoading}
24
25
  style={{
@@ -43,6 +43,7 @@ export default function ToolCallPanel({
43
43
  >
44
44
  <div>
45
45
  <label
46
+ htmlFor="tool-args-textarea"
46
47
  style={{
47
48
  display: 'block',
48
49
  fontSize: '12px',
@@ -54,6 +55,7 @@ export default function ToolCallPanel({
54
55
  Arguments (JSON)
55
56
  </label>
56
57
  <textarea
58
+ id="tool-args-textarea"
57
59
  value={toolArgs}
58
60
  onChange={(e) => onToolArgsChange(e.target.value)}
59
61
  style={{
@@ -71,6 +73,7 @@ export default function ToolCallPanel({
71
73
  />
72
74
  </div>
73
75
  <button
76
+ type="button"
74
77
  onClick={onCallTool}
75
78
  disabled={loading}
76
79
  style={{
@@ -91,6 +94,7 @@ export default function ToolCallPanel({
91
94
  {toolResult && (
92
95
  <div>
93
96
  <label
97
+ htmlFor="tool-result-pre"
94
98
  style={{
95
99
  display: 'block',
96
100
  fontSize: '12px',
@@ -102,6 +106,7 @@ export default function ToolCallPanel({
102
106
  Result
103
107
  </label>
104
108
  <pre
109
+ id="tool-result-pre"
105
110
  style={{
106
111
  padding: '12px',
107
112
  background: colors.bgSecondary,
@@ -2,47 +2,76 @@ import { colors } from '../../../theme';
2
2
 
3
3
  export default function ToolItem({ tool, isSelected, onClick }) {
4
4
  return (
5
- <div
5
+ <button
6
+ type="button"
6
7
  onClick={onClick}
8
+ aria-label={`Select tool ${tool.name}`}
7
9
  style={{
8
- padding: '12px',
9
- borderBottom: `1px solid ${colors.borderLight}`,
10
+ padding: '16px 20px',
11
+ margin: '4px 8px',
12
+ borderRadius: '8px',
10
13
  cursor: 'pointer',
11
- background: isSelected ? colors.bgSecondary : colors.bgCard,
12
- transition: 'background 0.2s',
14
+ background: isSelected ? colors.bgSelected : colors.bgCard,
15
+ border: isSelected ? `2px solid ${colors.accentBlue}` : `1px solid ${colors.borderLight}`,
16
+ boxShadow: isSelected ? `0 2px 4px ${colors.shadowSm}` : 'none',
17
+ transition: 'all 0.2s ease',
18
+ position: 'relative',
19
+ width: '100%',
20
+ textAlign: 'left',
13
21
  }}
14
22
  onMouseEnter={(e) => {
15
23
  if (!isSelected) {
16
24
  e.currentTarget.style.background = colors.bgHover;
25
+ e.currentTarget.style.borderColor = colors.borderMedium;
26
+ e.currentTarget.style.boxShadow = `0 2px 4px ${colors.shadowSm}`;
17
27
  }
18
28
  }}
19
29
  onMouseLeave={(e) => {
20
30
  if (!isSelected) {
21
31
  e.currentTarget.style.background = colors.bgCard;
32
+ e.currentTarget.style.borderColor = colors.borderLight;
33
+ e.currentTarget.style.boxShadow = 'none';
22
34
  }
23
35
  }}
24
36
  >
25
- <div
26
- style={{
27
- fontWeight: '500',
28
- fontSize: '13px',
29
- color: colors.textPrimary,
30
- marginBottom: '4px',
31
- }}
32
- >
33
- {tool.name}
34
- </div>
35
- {tool.description && (
37
+ <div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
36
38
  <div
37
39
  style={{
38
- fontSize: '12px',
39
- color: colors.textSecondary,
40
- marginTop: '4px',
40
+ width: '6px',
41
+ height: '6px',
42
+ borderRadius: '50%',
43
+ background: isSelected ? colors.accentBlue : colors.textTertiary,
44
+ marginTop: '6px',
45
+ flexShrink: 0,
46
+ transition: 'background 0.2s',
41
47
  }}
42
- >
43
- {tool.description}
48
+ />
49
+ <div style={{ flex: 1, minWidth: 0 }}>
50
+ <div
51
+ style={{
52
+ fontWeight: isSelected ? '600' : '500',
53
+ fontSize: '14px',
54
+ color: colors.textPrimary,
55
+ marginBottom: tool.description ? '6px' : '0',
56
+ lineHeight: '1.4',
57
+ }}
58
+ >
59
+ {tool.name}
60
+ </div>
61
+ {tool.description && (
62
+ <div
63
+ style={{
64
+ fontSize: '12px',
65
+ color: colors.textSecondary,
66
+ lineHeight: '1.5',
67
+ marginTop: '4px',
68
+ }}
69
+ >
70
+ {tool.description}
71
+ </div>
72
+ )}
44
73
  </div>
45
- )}
46
- </div>
74
+ </div>
75
+ </button>
47
76
  );
48
77
  }