@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,4 +1,28 @@
1
- import { useState, useEffect, useRef } from 'react';
1
+ import { useEffect, useRef, useState } from 'react';
2
+
3
+ function appendFilterParams(queryParams, filters) {
4
+ if (filters.search) {
5
+ queryParams.append('search', filters.search);
6
+ }
7
+ if (filters.serverName) {
8
+ queryParams.append('serverName', filters.serverName);
9
+ }
10
+ if (filters.sessionId) {
11
+ queryParams.append('sessionId', filters.sessionId);
12
+ }
13
+ if (filters.method) {
14
+ queryParams.append('method', filters.method);
15
+ }
16
+ if (filters.jsonrpcMethod) {
17
+ queryParams.append('jsonrpcMethod', filters.jsonrpcMethod);
18
+ }
19
+ if (filters.statusCode) {
20
+ queryParams.append('statusCode', filters.statusCode);
21
+ }
22
+ if (filters.jsonrpcId) {
23
+ queryParams.append('jsonrpcId', filters.jsonrpcId);
24
+ }
25
+ }
2
26
 
3
27
  export function useAppState() {
4
28
  const [activeTab, setActiveTab] = useState('traffic');
@@ -11,17 +35,12 @@ export function useAppState() {
11
35
  const [tourDismissed, setTourDismissed] = useState(true);
12
36
  const wsRef = useRef(null);
13
37
  const prevTabRef = useRef(activeTab);
38
+ const filtersRef = useRef(filters);
14
39
 
15
40
  const loadStatistics = async () => {
16
41
  try {
17
42
  const queryParams = new URLSearchParams();
18
- if (filters.search) queryParams.append('search', filters.search);
19
- if (filters.serverName) queryParams.append('serverName', filters.serverName);
20
- if (filters.sessionId) queryParams.append('sessionId', filters.sessionId);
21
- if (filters.method) queryParams.append('method', filters.method);
22
- if (filters.jsonrpcMethod) queryParams.append('jsonrpcMethod', filters.jsonrpcMethod);
23
- if (filters.statusCode) queryParams.append('statusCode', filters.statusCode);
24
- if (filters.jsonrpcId) queryParams.append('jsonrpcId', filters.jsonrpcId);
43
+ appendFilterParams(queryParams, filters);
25
44
 
26
45
  const statsResponse = await fetch(`/api/statistics?${queryParams}`);
27
46
  const statsData = await statsResponse.json();
@@ -34,13 +53,7 @@ export function useAppState() {
34
53
  const loadRequests = async () => {
35
54
  try {
36
55
  const queryParams = new URLSearchParams();
37
- if (filters.search) queryParams.append('search', filters.search);
38
- if (filters.serverName) queryParams.append('serverName', filters.serverName);
39
- if (filters.sessionId) queryParams.append('sessionId', filters.sessionId);
40
- if (filters.method) queryParams.append('method', filters.method);
41
- if (filters.jsonrpcMethod) queryParams.append('jsonrpcMethod', filters.jsonrpcMethod);
42
- if (filters.statusCode) queryParams.append('statusCode', filters.statusCode);
43
- if (filters.jsonrpcId) queryParams.append('jsonrpcId', filters.jsonrpcId);
56
+ appendFilterParams(queryParams, filters);
44
57
  queryParams.append('limit', '5000');
45
58
 
46
59
  const response = await fetch(`/api/requests?${queryParams}`);
@@ -81,8 +94,37 @@ export function useAppState() {
81
94
  }
82
95
  };
83
96
 
97
+ const initData = async () => {
98
+ try {
99
+ const queryParams = new URLSearchParams();
100
+ appendFilterParams(queryParams, filters);
101
+ queryParams.append('limit', '5000');
102
+
103
+ const response = await fetch(`/api/requests?${queryParams}`);
104
+ const data = await response.json();
105
+ setRequests(data);
106
+
107
+ if (data.length > 0) {
108
+ const oldest = data[data.length - 1]?.timestamp_iso;
109
+ if (oldest) {
110
+ setFirstRequestTime(oldest);
111
+ }
112
+ }
113
+
114
+ // Also load statistics
115
+ const statsQueryParams = new URLSearchParams();
116
+ appendFilterParams(statsQueryParams, filters);
117
+
118
+ const statsResponse = await fetch(`/api/statistics?${statsQueryParams}`);
119
+ const statsData = await statsResponse.json();
120
+ setStats(statsData);
121
+ } catch (error) {
122
+ console.error('Failed to load requests:', error);
123
+ }
124
+ };
125
+
84
126
  checkTourState();
85
- loadRequests();
127
+ initData();
86
128
 
87
129
  const wsUrl = import.meta.env.DEV
88
130
  ? 'ws://localhost:9853'
@@ -92,7 +134,7 @@ export function useAppState() {
92
134
  const ws = new WebSocket(wsUrl);
93
135
  wsRef.current = ws;
94
136
 
95
- ws.onmessage = (e) => {
137
+ ws.onmessage = async (e) => {
96
138
  const msg = JSON.parse(e.data);
97
139
  if (msg.type === 'update') {
98
140
  setRequests(msg.data);
@@ -103,7 +145,16 @@ export function useAppState() {
103
145
  }
104
146
  }
105
147
  // Update statistics when new data arrives via WebSocket
106
- loadStatistics();
148
+ try {
149
+ const queryParams = new URLSearchParams();
150
+ appendFilterParams(queryParams, filters);
151
+
152
+ const statsResponse = await fetch(`/api/statistics?${queryParams}`);
153
+ const statsData = await statsResponse.json();
154
+ setStats(statsData);
155
+ } catch (error) {
156
+ console.error('Failed to load statistics:', error);
157
+ }
107
158
  }
108
159
  };
109
160
 
@@ -114,7 +165,7 @@ export function useAppState() {
114
165
  ws.onclose = () => {
115
166
  // Connection closed - will attempt to reconnect on next mount if needed
116
167
  };
117
- } catch (err) {
168
+ } catch (_err) {
118
169
  // Silently handle WebSocket creation errors
119
170
  }
120
171
 
@@ -123,10 +174,44 @@ export function useAppState() {
123
174
  wsRef.current.close();
124
175
  }
125
176
  };
126
- }, []);
177
+ }, [filters]);
178
+
179
+ useEffect(() => {
180
+ const fetchData = async () => {
181
+ try {
182
+ const queryParams = new URLSearchParams();
183
+ appendFilterParams(queryParams, filters);
184
+ queryParams.append('limit', '5000');
185
+
186
+ const response = await fetch(`/api/requests?${queryParams}`);
187
+ const data = await response.json();
188
+ setRequests(data);
189
+
190
+ if (data.length > 0) {
191
+ const oldest = data[data.length - 1]?.timestamp_iso;
192
+ if (oldest) {
193
+ setFirstRequestTime(oldest);
194
+ }
195
+ }
196
+
197
+ // Also load statistics
198
+ const statsQueryParams = new URLSearchParams();
199
+ appendFilterParams(statsQueryParams, filters);
200
+
201
+ const statsResponse = await fetch(`/api/statistics?${statsQueryParams}`);
202
+ const statsData = await statsResponse.json();
203
+ setStats(statsData);
204
+ } catch (error) {
205
+ console.error('Failed to load requests:', error);
206
+ }
207
+ };
208
+
209
+ fetchData();
210
+ }, [filters]);
127
211
 
212
+ // Keep filters ref up to date
128
213
  useEffect(() => {
129
- loadRequests();
214
+ filtersRef.current = filters;
130
215
  }, [filters]);
131
216
 
132
217
  // Periodically update statistics when on traffic tab
@@ -136,12 +221,21 @@ export function useAppState() {
136
221
  }
137
222
 
138
223
  // Update statistics every 2 seconds
139
- const interval = setInterval(() => {
140
- loadStatistics();
224
+ const interval = setInterval(async () => {
225
+ try {
226
+ const queryParams = new URLSearchParams();
227
+ appendFilterParams(queryParams, filtersRef.current);
228
+
229
+ const statsResponse = await fetch(`/api/statistics?${queryParams}`);
230
+ const statsData = await statsResponse.json();
231
+ setStats(statsData);
232
+ } catch (error) {
233
+ console.error('Failed to load statistics:', error);
234
+ }
141
235
  }, 2000);
142
236
 
143
237
  return () => clearInterval(interval);
144
- }, [activeTab, filters]);
238
+ }, [activeTab]);
145
239
 
146
240
  return {
147
241
  activeTab,
@@ -1,5 +1,5 @@
1
+ import { IconEye, IconRefresh, IconRestore, IconTrash } from '@tabler/icons-react';
1
2
  import { colors, fonts, withOpacity } from '../theme';
2
- import { IconRefresh, IconEye, IconTrash, IconRestore } from '@tabler/icons-react';
3
3
 
4
4
  function BackupList({ backups, loadingBackups, onRefresh, onRestore, onView, onDelete }) {
5
5
  if (backups.length === 0) {
@@ -36,6 +36,7 @@ function BackupList({ backups, loadingBackups, onRefresh, onRestore, onView, onD
36
36
  Backup Files
37
37
  </h3>
38
38
  <button
39
+ type="button"
39
40
  onClick={onRefresh}
40
41
  disabled={loadingBackups}
41
42
  style={{
@@ -68,7 +69,7 @@ function BackupList({ backups, loadingBackups, onRefresh, onRestore, onView, onD
68
69
  <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
69
70
  {backups.map((backup, idx) => (
70
71
  <div
71
- key={idx}
72
+ key={backup.backupPath || `backup-${idx}`}
72
73
  style={{
73
74
  padding: '12px',
74
75
  background: colors.bgPrimary,
@@ -97,6 +98,7 @@ function BackupList({ backups, loadingBackups, onRefresh, onRestore, onView, onD
97
98
  </div>
98
99
  <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
99
100
  <button
101
+ type="button"
100
102
  onClick={() => onView(backup.backupPath)}
101
103
  style={{
102
104
  padding: '6px 12px',
@@ -122,6 +124,7 @@ function BackupList({ backups, loadingBackups, onRefresh, onRestore, onView, onD
122
124
  View
123
125
  </button>
124
126
  <button
127
+ type="button"
125
128
  onClick={() => {
126
129
  if (confirm('Are you sure you want to delete this backup?')) {
127
130
  onDelete(backup.backupPath);
@@ -155,6 +158,7 @@ function BackupList({ backups, loadingBackups, onRefresh, onRestore, onView, onD
155
158
  Delete
156
159
  </button>
157
160
  <button
161
+ type="button"
158
162
  onClick={() => onRestore(backup.backupPath, backup.originalPath)}
159
163
  style={{
160
164
  padding: '6px 12px',
@@ -12,7 +12,10 @@ const ChevronDown = ({ size = 12, color = 'currentColor' }) => (
12
12
  strokeLinecap="round"
13
13
  strokeLinejoin="round"
14
14
  style={{ display: 'inline-block', verticalAlign: 'middle', marginRight: '4px' }}
15
+ role="img"
16
+ aria-label="Chevron icon"
15
17
  >
18
+ <title>Chevron icon</title>
16
19
  <polyline points="6 9 12 15 18 9" />
17
20
  </svg>
18
21
  );
@@ -27,7 +30,8 @@ function CollapsibleSection({
27
30
 
28
31
  return (
29
32
  <div style={{ marginBottom: '16px' }}>
30
- <div
33
+ <button
34
+ type="button"
31
35
  style={{
32
36
  color: titleColor,
33
37
  fontWeight: '600',
@@ -41,8 +45,18 @@ function CollapsibleSection({
41
45
  gap: '6px',
42
46
  padding: '4px 0',
43
47
  transition: 'color 0.15s ease',
48
+ background: 'transparent',
49
+ border: 'none',
50
+ textAlign: 'left',
51
+ width: '100%',
44
52
  }}
45
53
  onClick={() => setIsExpanded(!isExpanded)}
54
+ onKeyDown={(e) => {
55
+ if (e.key === 'Enter' || e.key === ' ') {
56
+ e.preventDefault();
57
+ setIsExpanded(!isExpanded);
58
+ }
59
+ }}
46
60
  onMouseEnter={(e) => {
47
61
  e.currentTarget.style.color =
48
62
  titleColor === colors.accentBlue ? colors.accentBlueHover : titleColor;
@@ -61,7 +75,7 @@ function CollapsibleSection({
61
75
  <ChevronDown size={12} color={titleColor} />
62
76
  </span>
63
77
  {title}
64
- </div>
78
+ </button>
65
79
  {isExpanded && (
66
80
  <div
67
81
  style={{
@@ -1,4 +1,4 @@
1
- import { colors, fonts } from '../theme';
1
+ import { colors } from '../theme';
2
2
 
3
3
  function ConfigViewerModal({
4
4
  viewingConfig,
@@ -21,7 +21,9 @@ function ConfigViewerModal({
21
21
  const title = isViewingBackup ? 'Backup File' : 'MCP Configuration File';
22
22
 
23
23
  return (
24
- <div
24
+ <dialog
25
+ open
26
+ aria-modal="true"
25
27
  style={{
26
28
  position: 'fixed',
27
29
  top: 0,
@@ -34,10 +36,20 @@ function ConfigViewerModal({
34
36
  justifyContent: 'center',
35
37
  zIndex: 1000,
36
38
  padding: '20px',
39
+ border: 'none',
40
+ margin: 0,
41
+ width: '100%',
42
+ height: '100%',
37
43
  }}
38
44
  onClick={onClose}
45
+ onKeyDown={(e) => {
46
+ if (e.key === 'Escape') {
47
+ onClose();
48
+ }
49
+ }}
39
50
  >
40
51
  <div
52
+ role="document"
41
53
  style={{
42
54
  background: colors.bgPrimary,
43
55
  border: `1px solid ${colors.borderLight}`,
@@ -50,6 +62,7 @@ function ConfigViewerModal({
50
62
  overflow: 'hidden',
51
63
  }}
52
64
  onClick={(e) => e.stopPropagation()}
65
+ onKeyDown={(e) => e.stopPropagation()}
53
66
  >
54
67
  <div
55
68
  style={{
@@ -83,6 +96,7 @@ function ConfigViewerModal({
83
96
  )}
84
97
  </div>
85
98
  <button
99
+ type="button"
86
100
  onClick={onClose}
87
101
  style={{
88
102
  background: 'transparent',
@@ -134,7 +148,7 @@ function ConfigViewerModal({
134
148
  )}
135
149
  </div>
136
150
  </div>
137
- </div>
151
+ </dialog>
138
152
  );
139
153
  }
140
154
 
@@ -10,10 +10,14 @@ function ConfirmationModal({
10
10
  cancelText = 'Cancel',
11
11
  danger = false,
12
12
  }) {
13
- if (!isOpen) return null;
13
+ if (!isOpen) {
14
+ return null;
15
+ }
14
16
 
15
17
  return (
16
- <div
18
+ <dialog
19
+ open
20
+ aria-modal="true"
17
21
  style={{
18
22
  position: 'fixed',
19
23
  top: 0,
@@ -25,10 +29,20 @@ function ConfirmationModal({
25
29
  alignItems: 'center',
26
30
  justifyContent: 'center',
27
31
  zIndex: 1000,
32
+ border: 'none',
33
+ margin: 0,
34
+ width: '100%',
35
+ height: '100%',
28
36
  }}
29
37
  onClick={onClose}
38
+ onKeyDown={(e) => {
39
+ if (e.key === 'Escape') {
40
+ onClose();
41
+ }
42
+ }}
30
43
  >
31
44
  <div
45
+ role="document"
32
46
  style={{
33
47
  background: colors.bgCard,
34
48
  borderRadius: '12px',
@@ -39,6 +53,7 @@ function ConfirmationModal({
39
53
  fontFamily: fonts.body,
40
54
  }}
41
55
  onClick={(e) => e.stopPropagation()}
56
+ onKeyDown={(e) => e.stopPropagation()}
42
57
  >
43
58
  <h3
44
59
  style={{
@@ -68,6 +83,7 @@ function ConfirmationModal({
68
83
  }}
69
84
  >
70
85
  <button
86
+ type="button"
71
87
  onClick={onClose}
72
88
  style={{
73
89
  padding: '10px 20px',
@@ -91,6 +107,7 @@ function ConfirmationModal({
91
107
  {cancelText}
92
108
  </button>
93
109
  <button
110
+ type="button"
94
111
  onClick={() => {
95
112
  onConfirm();
96
113
  onClose();
@@ -122,7 +139,7 @@ function ConfirmationModal({
122
139
  </button>
123
140
  </div>
124
141
  </div>
125
- </div>
142
+ </dialog>
126
143
  );
127
144
  }
128
145
 
@@ -2,7 +2,9 @@ import { colors, fonts } from '../../theme';
2
2
  import CollapsibleSection from '../CollapsibleSection';
3
3
 
4
4
  export default function BodySection({ body, title, titleColor }) {
5
- if (!body) return null;
5
+ if (!body) {
6
+ return null;
7
+ }
6
8
 
7
9
  return (
8
10
  <CollapsibleSection title={title || 'Body'} titleColor={titleColor}>
@@ -1,6 +1,6 @@
1
+ import { IconChevronDown } from '@tabler/icons-react';
1
2
  import { useState } from 'react';
2
3
  import { colors, fonts } from '../../theme';
3
- import { IconChevronDown } from '@tabler/icons-react';
4
4
 
5
5
  export default function CollapsibleRequestResponse({
6
6
  title,
@@ -20,8 +20,16 @@ export default function CollapsibleRequestResponse({
20
20
  marginBottom: '20px',
21
21
  }}
22
22
  >
23
- <div
23
+ <button
24
+ type="button"
24
25
  onClick={() => setIsExpanded(!isExpanded)}
26
+ onKeyDown={(e) => {
27
+ if (e.key === 'Enter' || e.key === ' ') {
28
+ e.preventDefault();
29
+ setIsExpanded(!isExpanded);
30
+ }
31
+ }}
32
+ aria-label={`Toggle ${title} section`}
25
33
  style={{
26
34
  padding: '16px 20px',
27
35
  background: isExpanded ? colors.bgCard : colors.bgSecondary,
@@ -32,6 +40,9 @@ export default function CollapsibleRequestResponse({
32
40
  alignItems: 'center',
33
41
  justifyContent: 'space-between',
34
42
  transition: 'background-color 0.15s ease',
43
+ width: '100%',
44
+ border: 'none',
45
+ textAlign: 'left',
35
46
  }}
36
47
  onMouseEnter={(e) => {
37
48
  e.currentTarget.style.background = colors.bgHover;
@@ -63,7 +74,7 @@ export default function CollapsibleRequestResponse({
63
74
  />
64
75
  {title}
65
76
  </div>
66
- </div>
77
+ </button>
67
78
  {isExpanded && <div style={{ padding: '20px' }}>{children}</div>}
68
79
  </div>
69
80
  );
@@ -1,9 +1,11 @@
1
1
  import { colors, fonts } from '../../theme';
2
- import CollapsibleSection from '../CollapsibleSection';
3
2
  import { getInfo } from '../../utils/requestUtils';
3
+ import CollapsibleSection from '../CollapsibleSection';
4
4
 
5
5
  export default function InfoSection({ data, titleColor }) {
6
- if (!data) return null;
6
+ if (!data) {
7
+ return null;
8
+ }
7
9
 
8
10
  const info = getInfo(data);
9
11
 
@@ -1,14 +1,16 @@
1
1
  import { colors } from '../../theme';
2
+ import CollapsibleSection from '../CollapsibleSection';
3
+ import BodySection from './BodySection';
2
4
  import CollapsibleRequestResponse from './CollapsibleRequestResponse';
3
- import NetworkInfoSection from './NetworkInfoSection';
4
- import ProtocolInfoSection from './ProtocolInfoSection';
5
5
  import HeadersSection from './HeadersSection';
6
- import BodySection from './BodySection';
7
6
  import InfoSection from './InfoSection';
8
- import CollapsibleSection from '../CollapsibleSection';
7
+ import NetworkInfoSection from './NetworkInfoSection';
8
+ import ProtocolInfoSection from './ProtocolInfoSection';
9
9
 
10
10
  export default function RequestDetailsSection({ request, requestHeaders, requestBody }) {
11
- if (!request) return null;
11
+ if (!request) {
12
+ return null;
13
+ }
12
14
 
13
15
  return (
14
16
  <CollapsibleRequestResponse
@@ -1,14 +1,16 @@
1
1
  import { colors, fonts } from '../../theme';
2
+ import CollapsibleSection from '../CollapsibleSection';
3
+ import BodySection from './BodySection';
2
4
  import CollapsibleRequestResponse from './CollapsibleRequestResponse';
3
- import NetworkInfoSection from './NetworkInfoSection';
4
- import ProtocolInfoSection from './ProtocolInfoSection';
5
5
  import HeadersSection from './HeadersSection';
6
- import BodySection from './BodySection';
7
6
  import InfoSection from './InfoSection';
8
- import CollapsibleSection from '../CollapsibleSection';
7
+ import NetworkInfoSection from './NetworkInfoSection';
8
+ import ProtocolInfoSection from './ProtocolInfoSection';
9
9
 
10
10
  export default function ResponseDetailsSection({ response, responseHeaders, responseBody }) {
11
- if (!response) return null;
11
+ if (!response) {
12
+ return null;
13
+ }
12
14
 
13
15
  return (
14
16
  <CollapsibleRequestResponse
@@ -1,5 +1,5 @@
1
+ import { IconClock, IconCode, IconEye, IconRefresh } from '@tabler/icons-react';
1
2
  import { colors, fonts } from '../theme';
2
- import { IconRefresh, IconEye, IconCode, IconClock } from '@tabler/icons-react';
3
3
 
4
4
  function DetectedPathsList({ detectedPaths, detecting, onDetect, onSelect, onView }) {
5
5
  if (detectedPaths.length === 0) {
@@ -27,6 +27,7 @@ function DetectedPathsList({ detectedPaths, detecting, onDetect, onSelect, onVie
27
27
  Detected Configuration Files:
28
28
  </div>
29
29
  <button
30
+ type="button"
30
31
  onClick={onDetect}
31
32
  disabled={detecting}
32
33
  title="Refresh detection"
@@ -67,7 +68,8 @@ function DetectedPathsList({ detectedPaths, detecting, onDetect, onSelect, onVie
67
68
  >
68
69
  {detectedPaths.map((item, idx) => (
69
70
  <button
70
- key={idx}
71
+ key={`${item.editor}-${item.path}-${idx}`}
72
+ type="button"
71
73
  data-tour={idx === 0 ? 'first-detected-editor' : undefined}
72
74
  onClick={() => onSelect(item.path)}
73
75
  onDoubleClick={() => {
@@ -137,6 +139,7 @@ function DetectedPathsList({ detectedPaths, detecting, onDetect, onSelect, onVie
137
139
  Found
138
140
  </span>
139
141
  <button
142
+ type="button"
140
143
  onClick={(e) => {
141
144
  e.stopPropagation();
142
145
  onView(item.path);
@@ -1,5 +1,5 @@
1
- import { colors, fonts } from '../theme';
2
1
  import { IconFileUpload } from '@tabler/icons-react';
2
+ import { colors, fonts } from '../theme';
3
3
 
4
4
  function FileInput({
5
5
  filePath,
@@ -70,6 +70,7 @@ function FileInput({
70
70
  {fileContent && !filePath && (
71
71
  <div>
72
72
  <label
73
+ htmlFor="update-path-input"
73
74
  style={{
74
75
  display: 'block',
75
76
  fontSize: '12px',
@@ -80,6 +81,7 @@ function FileInput({
80
81
  Optional: File Path to Update
81
82
  </label>
82
83
  <input
84
+ id="update-path-input"
83
85
  type="text"
84
86
  placeholder="Enter file path to update (e.g., ~/.cursor/mcp.json)"
85
87
  value={updatePath}
@@ -11,7 +11,10 @@ const ChevronDown = ({ size = 12, color = 'currentColor' }) => (
11
11
  strokeLinecap="round"
12
12
  strokeLinejoin="round"
13
13
  style={{ display: 'inline-block', verticalAlign: 'middle' }}
14
+ role="img"
15
+ aria-label="Chevron down icon"
14
16
  >
17
+ <title>Chevron down icon</title>
15
18
  <polyline points="6 9 12 15 18 9" />
16
19
  </svg>
17
20
  );
@@ -27,7 +30,10 @@ const ChevronRight = ({ size = 12, color = 'currentColor' }) => (
27
30
  strokeLinecap="round"
28
31
  strokeLinejoin="round"
29
32
  style={{ display: 'inline-block', verticalAlign: 'middle' }}
33
+ role="img"
34
+ aria-label="Chevron right icon"
30
35
  >
36
+ <title>Chevron right icon</title>
31
37
  <polyline points="9 18 15 12 9 6" />
32
38
  </svg>
33
39
  );
@@ -36,6 +42,14 @@ export default function GroupHeader({ children, onClick, isExpanded, indent = 0
36
42
  return (
37
43
  <tr
38
44
  onClick={onClick}
45
+ onKeyDown={(e) => {
46
+ if (e.key === 'Enter' || e.key === ' ') {
47
+ e.preventDefault();
48
+ onClick();
49
+ }
50
+ }}
51
+ tabIndex={0}
52
+ aria-label={`Toggle group ${children}`}
39
53
  style={{
40
54
  cursor: 'pointer',
41
55
  background: colors.bgSecondary,
@@ -1,9 +1,8 @@
1
1
  import React from 'react';
2
- import { colors, fonts } from '../theme';
3
- import RequestRow from './RequestRow';
2
+ import { colors } from '../theme';
3
+ import { getCategoryIconComponent } from '../utils/mcpGroupingUtils';
4
4
  import GroupHeader from './GroupHeader';
5
- import { getCategoryIconComponent, getMethodDescription } from '../utils/mcpGroupingUtils';
6
- import { getJsonRpcMethod } from '../utils/requestUtils';
5
+ import RequestRow from './RequestRow';
7
6
 
8
7
  function GroupedByMcpView({
9
8
  groupedData,
@@ -15,12 +14,6 @@ function GroupedByMcpView({
15
14
  onToggleSession,
16
15
  onToggleCategory,
17
16
  }) {
18
- const getJsonRpcMethodFromPair = (pair) => {
19
- const request = pair.request || pair.response;
20
- if (!request) return null;
21
- return getJsonRpcMethod(request);
22
- };
23
-
24
17
  return (
25
18
  <tbody>
26
19
  {groupedData.map((sessionGroup) => {
@@ -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 GroupedByServerView({
7
7
  groupedData,