@mcp-shark/mcp-shark 1.4.2 → 1.5.2

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 (204) hide show
  1. package/README.md +84 -645
  2. package/bin/mcp-shark.js +30 -36
  3. package/mcp-server/index.js +115 -0
  4. package/mcp-server/lib/auditor/audit.js +22 -38
  5. package/mcp-server/lib/common/error.js +1 -1
  6. package/mcp-server/lib/server/external/all.js +5 -6
  7. package/mcp-server/lib/server/external/config.js +1 -3
  8. package/mcp-server/lib/server/external/kv.js +4 -12
  9. package/mcp-server/lib/server/external/single/request.js +3 -6
  10. package/mcp-server/lib/server/external/single/run.js +8 -19
  11. package/mcp-server/lib/server/internal/handlers/prompts-get.js +3 -13
  12. package/mcp-server/lib/server/internal/handlers/prompts-list.js +2 -6
  13. package/mcp-server/lib/server/internal/handlers/resources-list.js +2 -6
  14. package/mcp-server/lib/server/internal/handlers/resources-read.js +3 -12
  15. package/mcp-server/lib/server/internal/handlers/tools-call.js +3 -9
  16. package/mcp-server/lib/server/internal/handlers/tools-list.js +2 -2
  17. package/mcp-server/lib/server/internal/run.js +4 -16
  18. package/mcp-server/lib/server/internal/server.js +6 -7
  19. package/mcp-server/lib/server/internal/session.js +2 -15
  20. package/mcp-server/mcp-shark.js +16 -66
  21. package/package.json +23 -38
  22. package/shared/logger.js +90 -0
  23. package/ui/dist/assets/index-Cc-IUa83.css +1 -0
  24. package/ui/dist/assets/index-srLDlk97.js +35 -0
  25. package/ui/dist/index.html +17 -0
  26. package/ui/dist/og-image.png +0 -0
  27. package/ui/server/routes/backups/deleteBackup.js +54 -0
  28. package/ui/server/routes/backups/index.js +15 -0
  29. package/ui/server/routes/backups/listBackups.js +75 -0
  30. package/ui/server/routes/backups/restoreBackup.js +83 -0
  31. package/ui/server/routes/backups/viewBackup.js +47 -0
  32. package/ui/server/routes/composite/index.js +46 -0
  33. package/ui/server/routes/composite/servers.js +18 -0
  34. package/ui/server/routes/composite/setup.js +129 -0
  35. package/ui/server/routes/composite/status.js +7 -0
  36. package/ui/server/routes/composite/stop.js +39 -0
  37. package/ui/server/routes/composite/utils.js +45 -0
  38. package/ui/server/routes/config.js +34 -30
  39. package/ui/server/routes/conversations.js +3 -3
  40. package/ui/server/routes/help.js +2 -2
  41. package/ui/server/routes/logs.js +5 -5
  42. package/ui/server/routes/playground.js +45 -47
  43. package/ui/server/routes/requests.js +112 -108
  44. package/ui/server/routes/sessions.js +4 -4
  45. package/ui/server/routes/settings.js +199 -0
  46. package/ui/server/routes/smartscan/discover.js +7 -6
  47. package/ui/server/routes/smartscan/scans/clearCache.js +3 -2
  48. package/ui/server/routes/smartscan/scans/createBatchScans.js +4 -3
  49. package/ui/server/routes/smartscan/scans/createScan.js +2 -1
  50. package/ui/server/routes/smartscan/scans/getCachedResults.js +2 -1
  51. package/ui/server/routes/smartscan/scans/getScan.js +2 -1
  52. package/ui/server/routes/smartscan/scans/listScans.js +5 -4
  53. package/ui/server/routes/smartscan/scans.js +3 -3
  54. package/ui/server/routes/smartscan/token.js +4 -3
  55. package/ui/server/routes/smartscan/transport.js +1 -1
  56. package/ui/server/routes/smartscan.js +1 -1
  57. package/ui/server/routes/statistics.js +13 -10
  58. package/ui/server/utils/config-update.js +7 -6
  59. package/ui/server/utils/config.js +4 -4
  60. package/ui/server/utils/logger.js +2 -0
  61. package/ui/server/utils/paths.js +210 -2
  62. package/ui/server/utils/port.js +2 -2
  63. package/ui/server/utils/process.js +0 -67
  64. package/ui/server/utils/scan-cache/all-results.js +76 -59
  65. package/ui/server/utils/scan-cache/directory.js +1 -1
  66. package/ui/server/utils/scan-cache/file-operations.js +19 -16
  67. package/ui/server/utils/scan-cache/server-operations.js +14 -9
  68. package/ui/server/utils/serialization.js +9 -3
  69. package/ui/server/utils/smartscan-token.js +4 -3
  70. package/ui/server.js +86 -41
  71. package/ui/src/App.jsx +5 -5
  72. package/ui/src/CompositeLogs.jsx +20 -20
  73. package/ui/src/CompositeSetup.jsx +9 -9
  74. package/ui/src/HelpGuide/HelpGuideFooter.jsx +1 -0
  75. package/ui/src/HelpGuide/HelpGuideHeader.jsx +2 -1
  76. package/ui/src/HelpGuide.jsx +17 -4
  77. package/ui/src/IntroTour.jsx +19 -5
  78. package/ui/src/LogDetail.jsx +1 -0
  79. package/ui/src/LogTable.jsx +24 -6
  80. package/ui/src/PacketDetail.jsx +21 -16
  81. package/ui/src/PacketFilters.jsx +29 -14
  82. package/ui/src/PacketList.jsx +4 -5
  83. package/ui/src/SmartScan.jsx +5 -5
  84. package/ui/src/TabNavigation.jsx +5 -5
  85. package/ui/src/components/App/HelpButton.jsx +4 -0
  86. package/ui/src/components/App/TrafficTab.jsx +4 -4
  87. package/ui/src/components/App/useAppState.js +118 -24
  88. package/ui/src/components/BackupList.jsx +6 -2
  89. package/ui/src/components/CollapsibleSection.jsx +16 -2
  90. package/ui/src/components/ConfigViewerModal.jsx +17 -3
  91. package/ui/src/components/ConfirmationModal.jsx +20 -3
  92. package/ui/src/components/DetailsTab/BodySection.jsx +3 -1
  93. package/ui/src/components/DetailsTab/CollapsibleRequestResponse.jsx +14 -3
  94. package/ui/src/components/DetailsTab/InfoSection.jsx +4 -2
  95. package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +7 -5
  96. package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +7 -5
  97. package/ui/src/components/DetectedPathsList.jsx +5 -2
  98. package/ui/src/components/FileInput.jsx +3 -1
  99. package/ui/src/components/GroupHeader.jsx +14 -0
  100. package/ui/src/components/GroupedByMcpView.jsx +3 -4
  101. package/ui/src/components/GroupedByServerView.jsx +1 -1
  102. package/ui/src/components/GroupedBySessionView.jsx +1 -1
  103. package/ui/src/components/HexTab.jsx +17 -4
  104. package/ui/src/components/LogsToolbar.jsx +3 -1
  105. package/ui/src/components/McpPlayground/LoadingModal.jsx +7 -3
  106. package/ui/src/components/McpPlayground/PromptsSection/PromptCallPanel.jsx +5 -0
  107. package/ui/src/components/McpPlayground/PromptsSection/PromptItem.jsx +6 -2
  108. package/ui/src/components/McpPlayground/PromptsSection/PromptsList.jsx +4 -4
  109. package/ui/src/components/McpPlayground/PromptsSection.jsx +2 -1
  110. package/ui/src/components/McpPlayground/ResourcesSection/ResourceCallPanel.jsx +3 -0
  111. package/ui/src/components/McpPlayground/ResourcesSection/ResourceItem.jsx +6 -2
  112. package/ui/src/components/McpPlayground/ResourcesSection/ResourcesList.jsx +4 -4
  113. package/ui/src/components/McpPlayground/ResourcesSection.jsx +2 -1
  114. package/ui/src/components/McpPlayground/ToolsSection/ToolCallPanel.jsx +5 -0
  115. package/ui/src/components/McpPlayground/ToolsSection/ToolItem.jsx +6 -2
  116. package/ui/src/components/McpPlayground/ToolsSection/ToolsList.jsx +4 -4
  117. package/ui/src/components/McpPlayground/ToolsSection.jsx +2 -1
  118. package/ui/src/components/McpPlayground/hooks/useMcpDataLoader.js +9 -9
  119. package/ui/src/components/McpPlayground/hooks/useMcpRequest.js +10 -5
  120. package/ui/src/components/McpPlayground/hooks/useMcpServerStatus.js +43 -23
  121. package/ui/src/components/McpPlayground/useMcpPlayground.js +72 -44
  122. package/ui/src/components/McpPlayground.jsx +5 -2
  123. package/ui/src/components/PacketDetailHeader.jsx +8 -3
  124. package/ui/src/components/PacketFilters/ExportControls.jsx +2 -1
  125. package/ui/src/components/PacketFilters/FilterInput.jsx +1 -1
  126. package/ui/src/components/RawTab.jsx +15 -2
  127. package/ui/src/components/RequestRow/OrphanedResponseRow.jsx +10 -2
  128. package/ui/src/components/RequestRow/RequestRowMain.jsx +12 -3
  129. package/ui/src/components/RequestRow/ResponseRow.jsx +11 -3
  130. package/ui/src/components/RequestRow.jsx +17 -9
  131. package/ui/src/components/ServerControl.jsx +3 -1
  132. package/ui/src/components/ServiceSelector.jsx +2 -0
  133. package/ui/src/components/SmartScan/AnalysisResult.jsx +2 -2
  134. package/ui/src/components/SmartScan/BatchResultsDisplay/BatchResultItem.jsx +2 -1
  135. package/ui/src/components/SmartScan/BatchResultsDisplay/BatchResultsHeader.jsx +1 -1
  136. package/ui/src/components/SmartScan/BatchResultsDisplay.jsx +9 -3
  137. package/ui/src/components/SmartScan/EmptyState.jsx +3 -0
  138. package/ui/src/components/SmartScan/ErrorDisplay.jsx +4 -2
  139. package/ui/src/components/SmartScan/ExpandableSection.jsx +4 -0
  140. package/ui/src/components/SmartScan/FindingsTable.jsx +46 -42
  141. package/ui/src/components/SmartScan/ListViewContent.jsx +2 -2
  142. package/ui/src/components/SmartScan/NotablePatternsSection.jsx +13 -8
  143. package/ui/src/components/SmartScan/OverallSummarySection.jsx +36 -29
  144. package/ui/src/components/SmartScan/RecommendationsSection.jsx +10 -8
  145. package/ui/src/components/SmartScan/ScanDetailHeader.jsx +2 -1
  146. package/ui/src/components/SmartScan/ScanDetailView.jsx +4 -4
  147. package/ui/src/components/SmartScan/ScanListView/ScanListHeader.jsx +2 -1
  148. package/ui/src/components/SmartScan/ScanListView/ScanListItem.jsx +15 -1
  149. package/ui/src/components/SmartScan/ScanOverviewSection.jsx +3 -1
  150. package/ui/src/components/SmartScan/ScanResultsDisplay.jsx +1 -1
  151. package/ui/src/components/SmartScan/ScanViewContent.jsx +2 -2
  152. package/ui/src/components/SmartScan/ScanningProgress.jsx +4 -2
  153. package/ui/src/components/SmartScan/ServerInfoSection.jsx +3 -1
  154. package/ui/src/components/SmartScan/ServerSelectionRow.jsx +4 -2
  155. package/ui/src/components/SmartScan/SingleResultDisplay.jsx +5 -3
  156. package/ui/src/components/SmartScan/SmartScanControls.jsx +11 -7
  157. package/ui/src/components/SmartScan/SmartScanHeader.jsx +1 -1
  158. package/ui/src/components/SmartScan/ViewModeTabs.jsx +2 -0
  159. package/ui/src/components/SmartScan/hooks/useCacheManagement.js +1 -2
  160. package/ui/src/components/SmartScan/hooks/useMcpDiscovery.js +22 -26
  161. package/ui/src/components/SmartScan/hooks/useScanList.js +10 -9
  162. package/ui/src/components/SmartScan/hooks/useScanOperations.js +23 -14
  163. package/ui/src/components/SmartScan/hooks/useServerStatus.js +2 -2
  164. package/ui/src/components/SmartScan/hooks/useTokenManagement.js +2 -2
  165. package/ui/src/components/SmartScan/scanDataUtils.js +22 -17
  166. package/ui/src/components/SmartScan/useSmartScan.js +4 -4
  167. package/ui/src/components/SmartScan/utils.js +3 -1
  168. package/ui/src/components/SmartScanIcons.jsx +6 -3
  169. package/ui/src/components/TabNavigation/DesktopTabs.jsx +8 -3
  170. package/ui/src/components/TabNavigation/MobileDropdown.jsx +3 -1
  171. package/ui/src/components/TabNavigation.jsx +8 -3
  172. package/ui/src/components/TabNavigationIcons.jsx +4 -4
  173. package/ui/src/components/TourOverlay.jsx +1 -1
  174. package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +3 -0
  175. package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +1 -0
  176. package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +9 -0
  177. package/ui/src/components/TourTooltip/useTooltipPosition.js +63 -36
  178. package/ui/src/components/TourTooltip.jsx +11 -3
  179. package/ui/src/components/ViewModeTabs.jsx +3 -1
  180. package/ui/src/config/tourSteps.jsx +0 -2
  181. package/ui/src/hooks/useAnimation.js +15 -12
  182. package/ui/src/hooks/useConfigManagement.js +8 -8
  183. package/ui/src/hooks/useServiceExtraction.js +1 -1
  184. package/ui/src/index.css +3 -8
  185. package/ui/src/theme.js +3 -3
  186. package/ui/src/utils/hexUtils.js +11 -5
  187. package/ui/src/utils/mcpGroupingUtils.js +18 -10
  188. package/ui/src/utils/requestPairing.js +89 -0
  189. package/ui/src/utils/requestUtils.js +32 -101
  190. package/ui/vite.config.js +1 -1
  191. package/mcp-server/.editorconfig +0 -15
  192. package/mcp-server/.prettierignore +0 -11
  193. package/mcp-server/.prettierrc +0 -12
  194. package/mcp-server/README.md +0 -280
  195. package/mcp-server/commitlint.config.cjs +0 -42
  196. package/mcp-server/eslint.config.js +0 -131
  197. package/mcp-server/package-lock.json +0 -4784
  198. package/mcp-server/package.json +0 -30
  199. package/ui/README.md +0 -212
  200. package/ui/package-lock.json +0 -3574
  201. package/ui/package.json +0 -12
  202. package/ui/paths.js +0 -282
  203. package/ui/server/routes/backups.js +0 -251
  204. package/ui/server/routes/composite.js +0 -260
@@ -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,8 +2,10 @@ 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
10
  padding: '16px 20px',
9
11
  margin: '4px 8px',
@@ -14,6 +16,8 @@ export default function ToolItem({ tool, isSelected, onClick }) {
14
16
  boxShadow: isSelected ? `0 2px 4px ${colors.shadowSm}` : 'none',
15
17
  transition: 'all 0.2s ease',
16
18
  position: 'relative',
19
+ width: '100%',
20
+ textAlign: 'left',
17
21
  }}
18
22
  onMouseEnter={(e) => {
19
23
  if (!isSelected) {
@@ -68,6 +72,6 @@ export default function ToolItem({ tool, isSelected, onClick }) {
68
72
  )}
69
73
  </div>
70
74
  </div>
71
- </div>
75
+ </button>
72
76
  );
73
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 ToolItem from './ToolItem';
6
6
 
7
7
  export default function ToolsList({
@@ -26,7 +26,7 @@ export default function ToolsList({
26
26
  <LoadingState message="Waiting for MCP server to start..." />
27
27
  ) : toolsLoading || !toolsLoaded ? (
28
28
  <LoadingState message="Loading tools..." />
29
- ) : error && error.includes('tools:') ? (
29
+ ) : error?.includes('tools:') ? (
30
30
  <ErrorState message={`Error loading tools: ${error.replace('tools: ', '')}`} />
31
31
  ) : tools.length === 0 ? (
32
32
  <EmptyState message="No tools available." />
@@ -34,7 +34,7 @@ export default function ToolsList({
34
34
  <div style={{ padding: '8px 0' }}>
35
35
  {tools.map((tool, idx) => (
36
36
  <ToolItem
37
- key={idx}
37
+ key={tool.name || `tool-${idx}`}
38
38
  tool={tool}
39
39
  isSelected={selectedTool?.name === tool.name}
40
40
  onClick={() => onSelectTool(tool)}
@@ -1,6 +1,6 @@
1
1
  import { colors, fonts } from '../../theme';
2
- import ToolsList from './ToolsSection/ToolsList';
3
2
  import ToolCallPanel from './ToolsSection/ToolCallPanel';
3
+ import ToolsList from './ToolsSection/ToolsList';
4
4
 
5
5
  export default function ToolsSection({
6
6
  tools,
@@ -33,6 +33,7 @@ export default function ToolsSection({
33
33
  <div style={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%' }}>
34
34
  <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
35
35
  <button
36
+ type="button"
36
37
  onClick={onRefresh}
37
38
  disabled={loading || toolsLoading}
38
39
  style={{
@@ -1,4 +1,4 @@
1
- import { useState } from 'react';
1
+ import { useCallback, useState } from 'react';
2
2
 
3
3
  export function useMcpDataLoader(makeMcpRequest, selectedServer, setError) {
4
4
  const [tools, setTools] = useState([]);
@@ -11,7 +11,7 @@ export function useMcpDataLoader(makeMcpRequest, selectedServer, setError) {
11
11
  const [promptsLoaded, setPromptsLoaded] = useState(false);
12
12
  const [resourcesLoaded, setResourcesLoaded] = useState(false);
13
13
 
14
- const loadTools = async () => {
14
+ const loadTools = useCallback(async () => {
15
15
  if (!selectedServer) {
16
16
  setError('tools: No server selected');
17
17
  setToolsLoaded(true);
@@ -32,9 +32,9 @@ export function useMcpDataLoader(makeMcpRequest, selectedServer, setError) {
32
32
  } finally {
33
33
  setToolsLoading(false);
34
34
  }
35
- };
35
+ }, [selectedServer, makeMcpRequest, setError]);
36
36
 
37
- const loadPrompts = async () => {
37
+ const loadPrompts = useCallback(async () => {
38
38
  if (!selectedServer) {
39
39
  setError('prompts: No server selected');
40
40
  setPromptsLoaded(true);
@@ -55,9 +55,9 @@ export function useMcpDataLoader(makeMcpRequest, selectedServer, setError) {
55
55
  } finally {
56
56
  setPromptsLoading(false);
57
57
  }
58
- };
58
+ }, [selectedServer, makeMcpRequest, setError]);
59
59
 
60
- const loadResources = async () => {
60
+ const loadResources = useCallback(async () => {
61
61
  if (!selectedServer) {
62
62
  setError('resources: No server selected');
63
63
  setResourcesLoaded(true);
@@ -78,16 +78,16 @@ export function useMcpDataLoader(makeMcpRequest, selectedServer, setError) {
78
78
  } finally {
79
79
  setResourcesLoading(false);
80
80
  }
81
- };
81
+ }, [selectedServer, makeMcpRequest, setError]);
82
82
 
83
- const resetData = () => {
83
+ const resetData = useCallback(() => {
84
84
  setToolsLoaded(false);
85
85
  setPromptsLoaded(false);
86
86
  setResourcesLoaded(false);
87
87
  setTools([]);
88
88
  setPrompts([]);
89
89
  setResources([]);
90
- };
90
+ }, []);
91
91
 
92
92
  return {
93
93
  tools,
@@ -1,9 +1,13 @@
1
- import { useState, useCallback } from 'react';
1
+ import { useCallback, useRef, useState } from 'react';
2
2
 
3
3
  export function useMcpRequest(selectedServer) {
4
4
  const [loading, setLoading] = useState(false);
5
5
  const [error, setError] = useState(null);
6
6
  const [sessionId, setSessionId] = useState(null);
7
+ const sessionIdRef = useRef(sessionId);
8
+
9
+ // Keep ref in sync with state
10
+ sessionIdRef.current = sessionId;
7
11
 
8
12
  const makeMcpRequest = useCallback(
9
13
  async (method, params = {}) => {
@@ -16,8 +20,9 @@ export function useMcpRequest(selectedServer) {
16
20
 
17
21
  try {
18
22
  const headers = { 'Content-Type': 'application/json' };
19
- if (sessionId) {
20
- headers['Mcp-Session-Id'] = sessionId;
23
+ const currentSessionId = sessionIdRef.current;
24
+ if (currentSessionId) {
25
+ headers['Mcp-Session-Id'] = currentSessionId;
21
26
  }
22
27
 
23
28
  const response = await fetch('/api/playground/proxy', {
@@ -32,7 +37,7 @@ export function useMcpRequest(selectedServer) {
32
37
  response.headers.get('Mcp-Session-Id') ||
33
38
  response.headers.get('mcp-session-id') ||
34
39
  data._sessionId;
35
- if (responseSessionId && responseSessionId !== sessionId) {
40
+ if (responseSessionId && responseSessionId !== currentSessionId) {
36
41
  setSessionId(responseSessionId);
37
42
  }
38
43
 
@@ -48,7 +53,7 @@ export function useMcpRequest(selectedServer) {
48
53
  setLoading(false);
49
54
  }
50
55
  },
51
- [selectedServer, sessionId]
56
+ [selectedServer]
52
57
  );
53
58
 
54
59
  const resetSession = useCallback(() => {
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useCallback, useEffect, useState } from 'react';
2
2
 
3
3
  export function useMcpServerStatus() {
4
4
  const [serverStatus, setServerStatus] = useState(null);
@@ -6,52 +6,72 @@ export function useMcpServerStatus() {
6
6
  const [availableServers, setAvailableServers] = useState([]);
7
7
  const [selectedServer, setSelectedServer] = useState(null);
8
8
 
9
- const checkServerStatus = async () => {
9
+ const checkServerStatus = useCallback(async () => {
10
10
  try {
11
11
  const res = await fetch('/api/composite/status');
12
12
  if (!res.ok) {
13
13
  throw new Error('Server not available');
14
14
  }
15
15
  const data = await res.json();
16
- const wasRunning = serverStatus?.running;
17
- setServerStatus(data);
18
-
19
- if (!data.running) {
20
- if (!showLoadingModal || wasRunning) {
21
- setShowLoadingModal(true);
16
+ setServerStatus((prevStatus) => {
17
+ const wasRunning = prevStatus?.running;
18
+ if (!data.running) {
19
+ setShowLoadingModal((prevModal) => {
20
+ if (!prevModal || wasRunning) {
21
+ return true;
22
+ }
23
+ return prevModal;
24
+ });
25
+ } else {
26
+ setShowLoadingModal((prevModal) => {
27
+ if (prevModal) {
28
+ return false;
29
+ }
30
+ return prevModal;
31
+ });
22
32
  }
23
- } else if (data.running && showLoadingModal) {
24
- setShowLoadingModal(false);
25
- }
26
- } catch (err) {
33
+ return data;
34
+ });
35
+ } catch (_err) {
27
36
  setServerStatus({ running: false });
28
- if (!showLoadingModal) {
29
- setShowLoadingModal(true);
30
- }
37
+ setShowLoadingModal((prevModal) => {
38
+ if (!prevModal) {
39
+ return true;
40
+ }
41
+ return prevModal;
42
+ });
31
43
  }
32
- };
44
+ }, []);
33
45
 
34
- const loadAvailableServers = async () => {
46
+ const loadAvailableServers = useCallback(async () => {
35
47
  try {
36
48
  const res = await fetch('/api/composite/servers');
37
49
  if (res.ok) {
38
50
  const data = await res.json();
39
51
  setAvailableServers(data.servers || []);
40
- if (data.servers && data.servers.length > 0 && !selectedServer) {
41
- setSelectedServer(data.servers[0]);
42
- }
52
+ setSelectedServer((current) => {
53
+ if (!current && data.servers && data.servers.length > 0) {
54
+ return data.servers[0];
55
+ }
56
+ return current;
57
+ });
43
58
  }
44
59
  } catch (err) {
45
60
  console.error('Failed to load servers:', err);
46
61
  }
47
- };
62
+ }, []);
48
63
 
64
+ // Load servers once on mount
49
65
  useEffect(() => {
50
- checkServerStatus();
51
66
  loadAvailableServers();
67
+ }, [loadAvailableServers]);
68
+
69
+ // Poll server status every 2 seconds
70
+ useEffect(() => {
71
+ checkServerStatus();
52
72
  const interval = setInterval(checkServerStatus, 2000);
53
73
  return () => clearInterval(interval);
54
- }, []);
74
+ }, [checkServerStatus]);
55
75
 
56
76
  useEffect(() => {
57
77
  if (availableServers.length > 0 && !selectedServer) {
@@ -1,7 +1,7 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { useMcpDataLoader } from './hooks/useMcpDataLoader';
2
3
  import { useMcpRequest } from './hooks/useMcpRequest';
3
4
  import { useMcpServerStatus } from './hooks/useMcpServerStatus';
4
- import { useMcpDataLoader } from './hooks/useMcpDataLoader';
5
5
 
6
6
  export function useMcpPlayground() {
7
7
  const [activeSection, setActiveSection] = useState('tools');
@@ -35,7 +35,10 @@ export function useMcpPlayground() {
35
35
  resetData,
36
36
  } = useMcpDataLoader(makeMcpRequest, selectedServer, setError);
37
37
 
38
- // Reset and reload data when server changes
38
+ const activeSectionRef = useRef(activeSection);
39
+ activeSectionRef.current = activeSection;
40
+
41
+ // Reset and reload data when server changes (not when section changes)
39
42
  useEffect(() => {
40
43
  if (!selectedServer || !serverStatus?.running) {
41
44
  return;
@@ -54,69 +57,84 @@ export function useMcpPlayground() {
54
57
  setError(null);
55
58
 
56
59
  const timer = setTimeout(() => {
57
- if (activeSection === 'tools') {
60
+ const currentSection = activeSectionRef.current;
61
+ if (currentSection === 'tools') {
58
62
  loadTools();
59
- } else if (activeSection === 'prompts') {
63
+ } else if (currentSection === 'prompts') {
60
64
  loadPrompts();
61
- } else if (activeSection === 'resources') {
65
+ } else if (currentSection === 'resources') {
62
66
  loadResources();
63
67
  }
64
68
  }, 100);
65
69
 
66
70
  return () => clearTimeout(timer);
67
- // eslint-disable-next-line react-hooks/exhaustive-deps
68
- }, [selectedServer, serverStatus?.running, activeSection]);
71
+ }, [
72
+ selectedServer,
73
+ serverStatus?.running,
74
+ resetData,
75
+ loadTools,
76
+ loadPrompts,
77
+ loadResources,
78
+ resetSession,
79
+ setError,
80
+ ]);
69
81
 
82
+ // Load data when section changes (only if not already loaded)
70
83
  useEffect(() => {
71
- if (
72
- serverStatus?.running &&
73
- activeSection === 'tools' &&
74
- tools.length === 0 &&
75
- selectedServer
76
- ) {
84
+ if (!serverStatus?.running || !selectedServer) {
85
+ return;
86
+ }
87
+
88
+ // Only load if we haven't loaded this section yet and we're not currently loading
89
+ if (activeSection === 'tools' && !toolsLoaded && !toolsLoading) {
77
90
  const timer = setTimeout(() => {
78
91
  loadTools();
79
- }, 2000);
92
+ }, 100);
80
93
  return () => clearTimeout(timer);
81
94
  }
82
- // eslint-disable-next-line react-hooks/exhaustive-deps
83
- }, [serverStatus?.running, selectedServer, activeSection, tools.length]);
84
-
85
- useEffect(() => {
86
- if (!serverStatus?.running || !selectedServer) return;
87
-
88
- const timer = setTimeout(() => {
89
- if (activeSection === 'tools' && !toolsLoaded && !toolsLoading) {
90
- loadTools();
91
- } else if (activeSection === 'prompts' && !promptsLoaded && !promptsLoading) {
95
+ if (activeSection === 'prompts' && !promptsLoaded && !promptsLoading) {
96
+ const timer = setTimeout(() => {
92
97
  loadPrompts();
93
- } else if (activeSection === 'resources' && !resourcesLoaded && !resourcesLoading) {
98
+ }, 100);
99
+ return () => clearTimeout(timer);
100
+ }
101
+ if (activeSection === 'resources' && !resourcesLoaded && !resourcesLoading) {
102
+ const timer = setTimeout(() => {
94
103
  loadResources();
95
- }
96
- }, 100);
97
-
98
- return () => clearTimeout(timer);
99
- // eslint-disable-next-line react-hooks/exhaustive-deps
104
+ }, 100);
105
+ return () => clearTimeout(timer);
106
+ }
100
107
  }, [
101
108
  activeSection,
102
109
  serverStatus?.running,
103
110
  selectedServer,
104
111
  toolsLoaded,
105
- promptsLoaded,
106
- resourcesLoaded,
107
112
  toolsLoading,
113
+ promptsLoaded,
108
114
  promptsLoading,
115
+ resourcesLoaded,
109
116
  resourcesLoading,
117
+ loadTools,
118
+ loadPrompts,
119
+ loadResources,
110
120
  ]);
111
121
 
112
122
  const handleCallTool = async () => {
113
- if (!selectedTool) return;
123
+ if (!selectedTool) {
124
+ return;
125
+ }
114
126
 
115
127
  try {
116
- let args = {};
117
- try {
118
- args = JSON.parse(toolArgs);
119
- } catch (e) {
128
+ const parseArgs = (argsString) => {
129
+ try {
130
+ return JSON.parse(argsString);
131
+ } catch (_e) {
132
+ return null;
133
+ }
134
+ };
135
+
136
+ const args = parseArgs(toolArgs);
137
+ if (args === null) {
120
138
  setError('Invalid JSON in arguments');
121
139
  return;
122
140
  }
@@ -133,13 +151,21 @@ export function useMcpPlayground() {
133
151
  };
134
152
 
135
153
  const handleGetPrompt = async () => {
136
- if (!selectedPrompt) return;
154
+ if (!selectedPrompt) {
155
+ return;
156
+ }
137
157
 
138
158
  try {
139
- let args = {};
140
- try {
141
- args = JSON.parse(promptArgs);
142
- } catch (e) {
159
+ const parseArgs = (argsString) => {
160
+ try {
161
+ return JSON.parse(argsString);
162
+ } catch (_e) {
163
+ return null;
164
+ }
165
+ };
166
+
167
+ const args = parseArgs(promptArgs);
168
+ if (args === null) {
143
169
  setError('Invalid JSON in arguments');
144
170
  return;
145
171
  }
@@ -156,7 +182,9 @@ export function useMcpPlayground() {
156
182
  };
157
183
 
158
184
  const handleReadResource = async () => {
159
- if (!selectedResource) return;
185
+ if (!selectedResource) {
186
+ return;
187
+ }
160
188
 
161
189
  try {
162
190
  const result = await makeMcpRequest('resources/read', {
@@ -1,9 +1,9 @@
1
1
  import { colors, fonts } from '../theme';
2
- import { useMcpPlayground } from './McpPlayground/useMcpPlayground';
3
2
  import LoadingModal from './McpPlayground/LoadingModal';
4
- import ToolsSection from './McpPlayground/ToolsSection';
5
3
  import PromptsSection from './McpPlayground/PromptsSection';
6
4
  import ResourcesSection from './McpPlayground/ResourcesSection';
5
+ import ToolsSection from './McpPlayground/ToolsSection';
6
+ import { useMcpPlayground } from './McpPlayground/useMcpPlayground';
7
7
 
8
8
  function McpPlayground() {
9
9
  const {
@@ -100,6 +100,7 @@ function McpPlayground() {
100
100
  }}
101
101
  >
102
102
  <label
103
+ htmlFor="mcp-server-select"
103
104
  style={{
104
105
  fontSize: '13px',
105
106
  fontFamily: fonts.body,
@@ -119,6 +120,7 @@ function McpPlayground() {
119
120
  {availableServers.map((server) => (
120
121
  <button
121
122
  key={server}
123
+ type="button"
122
124
  onClick={() => setSelectedServer(server)}
123
125
  style={{
124
126
  padding: '10px 18px',
@@ -169,6 +171,7 @@ function McpPlayground() {
169
171
  {['tools', 'prompts', 'resources'].map((section) => (
170
172
  <button
171
173
  key={section}
174
+ type="button"
172
175
  onClick={() => setActiveSection(section)}
173
176
  style={{
174
177
  padding: '10px 18px',
@@ -1,10 +1,14 @@
1
- import { colors, fonts } from '../theme';
2
1
  import { IconX } from '@tabler/icons-react';
2
+ import { colors, fonts } from '../theme';
3
3
 
4
4
  function PacketDetailHeader({ request, onClose, matchingPair }) {
5
5
  const formatBytes = (bytes) => {
6
- if (bytes < 1024) return `${bytes} B`;
7
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
6
+ if (bytes < 1024) {
7
+ return `${bytes} B`;
8
+ }
9
+ if (bytes < 1024 * 1024) {
10
+ return `${(bytes / 1024).toFixed(2)} KB`;
11
+ }
8
12
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
9
13
  };
10
14
 
@@ -59,6 +63,7 @@ function PacketDetailHeader({ request, onClose, matchingPair }) {
59
63
  </span>
60
64
  </div>
61
65
  <button
66
+ type="button"
62
67
  onClick={onClose}
63
68
  style={{
64
69
  background: 'none',
@@ -1,6 +1,6 @@
1
- import { colors, fonts } from '../../theme';
2
1
  import { IconDownload } from '@tabler/icons-react';
3
2
  import anime from 'animejs';
3
+ import { colors, fonts } from '../../theme';
4
4
 
5
5
  export default function ExportControls({ stats, onExport }) {
6
6
  return (
@@ -50,6 +50,7 @@ export default function ExportControls({ stats, onExport }) {
50
50
  }}
51
51
  >
52
52
  <button
53
+ type="button"
53
54
  onClick={() => onExport('json')}
54
55
  style={{
55
56
  padding: '8px 14px',
@@ -1,5 +1,5 @@
1
- import { colors, fonts } from '../../theme';
2
1
  import anime from 'animejs';
2
+ import { colors, fonts } from '../../theme';
3
3
 
4
4
  export default function FilterInput({
5
5
  type = 'text',
@@ -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
  );
@@ -1,9 +1,9 @@
1
1
  import { colors, fonts, withOpacity } from '../../theme';
2
2
  import {
3
- formatRelativeTime,
4
3
  formatDateTime,
5
- getSourceDest,
4
+ formatRelativeTime,
6
5
  getEndpoint,
6
+ getSourceDest,
7
7
  } from '../../utils/requestUtils.js';
8
8
 
9
9
  export default function OrphanedResponseRow({ response, selected, firstRequestTime, onSelect }) {
@@ -14,6 +14,14 @@ export default function OrphanedResponseRow({ response, selected, firstRequestTi
14
14
  return (
15
15
  <tr
16
16
  onClick={() => onSelect(response)}
17
+ onKeyDown={(e) => {
18
+ if (e.key === 'Enter' || e.key === ' ') {
19
+ e.preventDefault();
20
+ onSelect(response);
21
+ }
22
+ }}
23
+ tabIndex={0}
24
+ aria-label={`Select orphaned response ${response.frame_number}`}
17
25
  style={{
18
26
  cursor: 'pointer',
19
27
  background: isSelected ? colors.bgSelected : colors.bgUnpaired,