@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 { 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,19 +26,21 @@ 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." />
33
33
  ) : (
34
- tools.map((tool, idx) => (
35
- <ToolItem
36
- key={idx}
37
- tool={tool}
38
- isSelected={selectedTool?.name === tool.name}
39
- onClick={() => onSelectTool(tool)}
40
- />
41
- ))
34
+ <div style={{ padding: '8px 0' }}>
35
+ {tools.map((tool, idx) => (
36
+ <ToolItem
37
+ key={tool.name || `tool-${idx}`}
38
+ tool={tool}
39
+ isSelected={selectedTool?.name === tool.name}
40
+ onClick={() => onSelectTool(tool)}
41
+ />
42
+ ))}
43
+ </div>
42
44
  )}
43
45
  </div>
44
46
  );
@@ -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={{
@@ -0,0 +1,107 @@
1
+ import { useCallback, useState } from 'react';
2
+
3
+ export function useMcpDataLoader(makeMcpRequest, selectedServer, setError) {
4
+ const [tools, setTools] = useState([]);
5
+ const [prompts, setPrompts] = useState([]);
6
+ const [resources, setResources] = useState([]);
7
+ const [toolsLoading, setToolsLoading] = useState(false);
8
+ const [promptsLoading, setPromptsLoading] = useState(false);
9
+ const [resourcesLoading, setResourcesLoading] = useState(false);
10
+ const [toolsLoaded, setToolsLoaded] = useState(false);
11
+ const [promptsLoaded, setPromptsLoaded] = useState(false);
12
+ const [resourcesLoaded, setResourcesLoaded] = useState(false);
13
+
14
+ const loadTools = useCallback(async () => {
15
+ if (!selectedServer) {
16
+ setError('tools: No server selected');
17
+ setToolsLoaded(true);
18
+ return;
19
+ }
20
+
21
+ setToolsLoading(true);
22
+ setError(null);
23
+ try {
24
+ const result = await makeMcpRequest('tools/list');
25
+ setTools(result?.tools || []);
26
+ setToolsLoaded(true);
27
+ } catch (err) {
28
+ const errorMsg = err.message || 'Failed to load tools';
29
+ setError(`tools: ${errorMsg}`);
30
+ setToolsLoaded(true);
31
+ console.error('Failed to load tools:', err);
32
+ } finally {
33
+ setToolsLoading(false);
34
+ }
35
+ }, [selectedServer, makeMcpRequest, setError]);
36
+
37
+ const loadPrompts = useCallback(async () => {
38
+ if (!selectedServer) {
39
+ setError('prompts: No server selected');
40
+ setPromptsLoaded(true);
41
+ return;
42
+ }
43
+
44
+ setPromptsLoading(true);
45
+ setError(null);
46
+ try {
47
+ const result = await makeMcpRequest('prompts/list');
48
+ setPrompts(result?.prompts || []);
49
+ setPromptsLoaded(true);
50
+ } catch (err) {
51
+ const errorMsg = err.message || 'Failed to load prompts';
52
+ setError(`prompts: ${errorMsg}`);
53
+ setPromptsLoaded(true);
54
+ console.error('Failed to load prompts:', err);
55
+ } finally {
56
+ setPromptsLoading(false);
57
+ }
58
+ }, [selectedServer, makeMcpRequest, setError]);
59
+
60
+ const loadResources = useCallback(async () => {
61
+ if (!selectedServer) {
62
+ setError('resources: No server selected');
63
+ setResourcesLoaded(true);
64
+ return;
65
+ }
66
+
67
+ setResourcesLoading(true);
68
+ setError(null);
69
+ try {
70
+ const result = await makeMcpRequest('resources/list');
71
+ setResources(result?.resources || []);
72
+ setResourcesLoaded(true);
73
+ } catch (err) {
74
+ const errorMsg = err.message || 'Failed to load resources';
75
+ setError(`resources: ${errorMsg}`);
76
+ setResourcesLoaded(true);
77
+ console.error('Failed to load resources:', err);
78
+ } finally {
79
+ setResourcesLoading(false);
80
+ }
81
+ }, [selectedServer, makeMcpRequest, setError]);
82
+
83
+ const resetData = useCallback(() => {
84
+ setToolsLoaded(false);
85
+ setPromptsLoaded(false);
86
+ setResourcesLoaded(false);
87
+ setTools([]);
88
+ setPrompts([]);
89
+ setResources([]);
90
+ }, []);
91
+
92
+ return {
93
+ tools,
94
+ prompts,
95
+ resources,
96
+ toolsLoading,
97
+ promptsLoading,
98
+ resourcesLoading,
99
+ toolsLoaded,
100
+ promptsLoaded,
101
+ resourcesLoaded,
102
+ loadTools,
103
+ loadPrompts,
104
+ loadResources,
105
+ resetData,
106
+ };
107
+ }
@@ -0,0 +1,70 @@
1
+ import { useCallback, useRef, useState } from 'react';
2
+
3
+ export function useMcpRequest(selectedServer) {
4
+ const [loading, setLoading] = useState(false);
5
+ const [error, setError] = useState(null);
6
+ const [sessionId, setSessionId] = useState(null);
7
+ const sessionIdRef = useRef(sessionId);
8
+
9
+ // Keep ref in sync with state
10
+ sessionIdRef.current = sessionId;
11
+
12
+ const makeMcpRequest = useCallback(
13
+ async (method, params = {}) => {
14
+ if (!selectedServer) {
15
+ throw new Error('No server selected');
16
+ }
17
+
18
+ setError(null);
19
+ setLoading(true);
20
+
21
+ try {
22
+ const headers = { 'Content-Type': 'application/json' };
23
+ const currentSessionId = sessionIdRef.current;
24
+ if (currentSessionId) {
25
+ headers['Mcp-Session-Id'] = currentSessionId;
26
+ }
27
+
28
+ const response = await fetch('/api/playground/proxy', {
29
+ method: 'POST',
30
+ headers,
31
+ body: JSON.stringify({ method, params, serverName: selectedServer }),
32
+ });
33
+
34
+ const data = await response.json();
35
+
36
+ const responseSessionId =
37
+ response.headers.get('Mcp-Session-Id') ||
38
+ response.headers.get('mcp-session-id') ||
39
+ data._sessionId;
40
+ if (responseSessionId && responseSessionId !== currentSessionId) {
41
+ setSessionId(responseSessionId);
42
+ }
43
+
44
+ if (!response.ok) {
45
+ throw new Error(data.error?.message || data.message || 'Request failed');
46
+ }
47
+
48
+ return data.result || data;
49
+ } catch (err) {
50
+ setError(err.message);
51
+ throw err;
52
+ } finally {
53
+ setLoading(false);
54
+ }
55
+ },
56
+ [selectedServer]
57
+ );
58
+
59
+ const resetSession = useCallback(() => {
60
+ setSessionId(null);
61
+ }, []);
62
+
63
+ return {
64
+ makeMcpRequest,
65
+ loading,
66
+ error,
67
+ setError,
68
+ resetSession,
69
+ };
70
+ }
@@ -0,0 +1,90 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+
3
+ export function useMcpServerStatus() {
4
+ const [serverStatus, setServerStatus] = useState(null);
5
+ const [showLoadingModal, setShowLoadingModal] = useState(false);
6
+ const [availableServers, setAvailableServers] = useState([]);
7
+ const [selectedServer, setSelectedServer] = useState(null);
8
+
9
+ const checkServerStatus = useCallback(async () => {
10
+ try {
11
+ const res = await fetch('/api/composite/status');
12
+ if (!res.ok) {
13
+ throw new Error('Server not available');
14
+ }
15
+ const data = await res.json();
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
+ });
32
+ }
33
+ return data;
34
+ });
35
+ } catch (_err) {
36
+ setServerStatus({ running: false });
37
+ setShowLoadingModal((prevModal) => {
38
+ if (!prevModal) {
39
+ return true;
40
+ }
41
+ return prevModal;
42
+ });
43
+ }
44
+ }, []);
45
+
46
+ const loadAvailableServers = useCallback(async () => {
47
+ try {
48
+ const res = await fetch('/api/composite/servers');
49
+ if (res.ok) {
50
+ const data = await res.json();
51
+ setAvailableServers(data.servers || []);
52
+ setSelectedServer((current) => {
53
+ if (!current && data.servers && data.servers.length > 0) {
54
+ return data.servers[0];
55
+ }
56
+ return current;
57
+ });
58
+ }
59
+ } catch (err) {
60
+ console.error('Failed to load servers:', err);
61
+ }
62
+ }, []);
63
+
64
+ // Load servers once on mount
65
+ useEffect(() => {
66
+ loadAvailableServers();
67
+ }, [loadAvailableServers]);
68
+
69
+ // Poll server status every 2 seconds
70
+ useEffect(() => {
71
+ checkServerStatus();
72
+ const interval = setInterval(checkServerStatus, 2000);
73
+ return () => clearInterval(interval);
74
+ }, [checkServerStatus]);
75
+
76
+ useEffect(() => {
77
+ if (availableServers.length > 0 && !selectedServer) {
78
+ setSelectedServer(availableServers[0]);
79
+ }
80
+ }, [availableServers, selectedServer]);
81
+
82
+ return {
83
+ serverStatus,
84
+ showLoadingModal,
85
+ availableServers,
86
+ selectedServer,
87
+ setSelectedServer,
88
+ checkServerStatus,
89
+ };
90
+ }
@@ -1,12 +1,10 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { useMcpDataLoader } from './hooks/useMcpDataLoader';
3
+ import { useMcpRequest } from './hooks/useMcpRequest';
4
+ import { useMcpServerStatus } from './hooks/useMcpServerStatus';
2
5
 
3
6
  export function useMcpPlayground() {
4
7
  const [activeSection, setActiveSection] = useState('tools');
5
- const [tools, setTools] = useState([]);
6
- const [prompts, setPrompts] = useState([]);
7
- const [resources, setResources] = useState([]);
8
- const [loading, setLoading] = useState(false);
9
- const [error, setError] = useState(null);
10
8
  const [selectedTool, setSelectedTool] = useState(null);
11
9
  const [toolArgs, setToolArgs] = useState('{}');
12
10
  const [toolResult, setToolResult] = useState(null);
@@ -15,180 +13,128 @@ export function useMcpPlayground() {
15
13
  const [promptResult, setPromptResult] = useState(null);
16
14
  const [selectedResource, setSelectedResource] = useState(null);
17
15
  const [resourceResult, setResourceResult] = useState(null);
18
- const [serverStatus, setServerStatus] = useState(null);
19
- const [sessionId, setSessionId] = useState(null);
20
- const [showLoadingModal, setShowLoadingModal] = useState(false);
21
- const [toolsLoading, setToolsLoading] = useState(false);
22
- const [promptsLoading, setPromptsLoading] = useState(false);
23
- const [resourcesLoading, setResourcesLoading] = useState(false);
24
- const [toolsLoaded, setToolsLoaded] = useState(false);
25
- const [promptsLoaded, setPromptsLoaded] = useState(false);
26
- const [resourcesLoaded, setResourcesLoaded] = useState(false);
27
16
 
28
- useEffect(() => {
29
- checkServerStatus();
30
- const interval = setInterval(checkServerStatus, 2000);
31
- return () => clearInterval(interval);
32
- }, []);
17
+ const { serverStatus, showLoadingModal, availableServers, selectedServer, setSelectedServer } =
18
+ useMcpServerStatus();
19
+
20
+ const { makeMcpRequest, loading, error, setError, resetSession } = useMcpRequest(selectedServer);
33
21
 
22
+ const {
23
+ tools,
24
+ prompts,
25
+ resources,
26
+ toolsLoading,
27
+ promptsLoading,
28
+ resourcesLoading,
29
+ toolsLoaded,
30
+ promptsLoaded,
31
+ resourcesLoaded,
32
+ loadTools,
33
+ loadPrompts,
34
+ loadResources,
35
+ resetData,
36
+ } = useMcpDataLoader(makeMcpRequest, selectedServer, setError);
37
+
38
+ const activeSectionRef = useRef(activeSection);
39
+ activeSectionRef.current = activeSection;
40
+
41
+ // Reset and reload data when server changes (not when section changes)
34
42
  useEffect(() => {
35
- if (serverStatus?.running && activeSection === 'tools' && tools.length === 0) {
36
- const timer = setTimeout(() => {
37
- loadTools();
38
- }, 2000);
39
- return () => clearTimeout(timer);
43
+ if (!selectedServer || !serverStatus?.running) {
44
+ return;
40
45
  }
41
- }, [serverStatus?.running]);
42
46
 
43
- useEffect(() => {
44
- if (!serverStatus?.running) return;
47
+ resetData();
48
+ setSelectedTool(null);
49
+ setSelectedPrompt(null);
50
+ setSelectedResource(null);
51
+ setToolResult(null);
52
+ setPromptResult(null);
53
+ setResourceResult(null);
54
+ setToolArgs('{}');
55
+ setPromptArgs('{}');
56
+ resetSession();
57
+ setError(null);
45
58
 
46
59
  const timer = setTimeout(() => {
47
- if (activeSection === 'tools' && !toolsLoaded && !toolsLoading) {
60
+ const currentSection = activeSectionRef.current;
61
+ if (currentSection === 'tools') {
48
62
  loadTools();
49
- } else if (activeSection === 'prompts' && !promptsLoaded && !promptsLoading) {
63
+ } else if (currentSection === 'prompts') {
50
64
  loadPrompts();
51
- } else if (activeSection === 'resources' && !resourcesLoaded && !resourcesLoading) {
65
+ } else if (currentSection === 'resources') {
52
66
  loadResources();
53
67
  }
54
68
  }, 100);
55
69
 
56
70
  return () => clearTimeout(timer);
57
71
  }, [
58
- activeSection,
72
+ selectedServer,
59
73
  serverStatus?.running,
60
- toolsLoaded,
61
- promptsLoaded,
62
- resourcesLoaded,
63
- toolsLoading,
64
- promptsLoading,
65
- resourcesLoading,
74
+ resetData,
75
+ loadTools,
76
+ loadPrompts,
77
+ loadResources,
78
+ resetSession,
79
+ setError,
66
80
  ]);
67
81
 
68
- const checkServerStatus = async () => {
69
- try {
70
- const res = await fetch('/api/composite/status');
71
- if (!res.ok) {
72
- throw new Error('Server not available');
73
- }
74
- const data = await res.json();
75
- const wasRunning = serverStatus?.running;
76
- setServerStatus(data);
77
-
78
- if (!data.running) {
79
- if (!showLoadingModal || wasRunning) {
80
- setShowLoadingModal(true);
81
- }
82
- } else if (data.running && showLoadingModal) {
83
- setShowLoadingModal(false);
84
- }
85
- } catch (err) {
86
- // Silently handle connection errors - server is not running
87
- setServerStatus({ running: false });
88
- if (!showLoadingModal) {
89
- setShowLoadingModal(true);
90
- }
91
- }
92
- };
93
-
94
- const makeMcpRequest = async (method, params = {}) => {
95
- setError(null);
96
- setLoading(true);
97
-
98
- try {
99
- const headers = { 'Content-Type': 'application/json' };
100
- if (sessionId) {
101
- headers['Mcp-Session-Id'] = sessionId;
102
- }
103
-
104
- const response = await fetch('/api/playground/proxy', {
105
- method: 'POST',
106
- headers,
107
- body: JSON.stringify({ method, params }),
108
- });
109
-
110
- const data = await response.json();
111
-
112
- const responseSessionId =
113
- response.headers.get('Mcp-Session-Id') ||
114
- response.headers.get('mcp-session-id') ||
115
- data._sessionId;
116
- if (responseSessionId && responseSessionId !== sessionId) {
117
- setSessionId(responseSessionId);
118
- }
119
-
120
- if (!response.ok) {
121
- throw new Error(data.error?.message || data.message || 'Request failed');
122
- }
123
-
124
- return data.result || data;
125
- } catch (err) {
126
- setError(err.message);
127
- throw err;
128
- } finally {
129
- setLoading(false);
82
+ // Load data when section changes (only if not already loaded)
83
+ useEffect(() => {
84
+ if (!serverStatus?.running || !selectedServer) {
85
+ return;
130
86
  }
131
- };
132
87
 
133
- const loadTools = async () => {
134
- setToolsLoading(true);
135
- setError(null);
136
- try {
137
- const result = await makeMcpRequest('tools/list');
138
- setTools(result?.tools || []);
139
- setToolsLoaded(true);
140
- } catch (err) {
141
- const errorMsg = err.message || 'Failed to load tools';
142
- setError(`tools: ${errorMsg}`);
143
- setToolsLoaded(true);
144
- console.error('Failed to load tools:', err);
145
- } finally {
146
- setToolsLoading(false);
88
+ // Only load if we haven't loaded this section yet and we're not currently loading
89
+ if (activeSection === 'tools' && !toolsLoaded && !toolsLoading) {
90
+ const timer = setTimeout(() => {
91
+ loadTools();
92
+ }, 100);
93
+ return () => clearTimeout(timer);
147
94
  }
148
- };
149
-
150
- const loadPrompts = async () => {
151
- setPromptsLoading(true);
152
- setError(null);
153
- try {
154
- const result = await makeMcpRequest('prompts/list');
155
- setPrompts(result?.prompts || []);
156
- setPromptsLoaded(true);
157
- } catch (err) {
158
- const errorMsg = err.message || 'Failed to load prompts';
159
- setError(`prompts: ${errorMsg}`);
160
- setPromptsLoaded(true);
161
- console.error('Failed to load prompts:', err);
162
- } finally {
163
- setPromptsLoading(false);
95
+ if (activeSection === 'prompts' && !promptsLoaded && !promptsLoading) {
96
+ const timer = setTimeout(() => {
97
+ loadPrompts();
98
+ }, 100);
99
+ return () => clearTimeout(timer);
164
100
  }
165
- };
166
-
167
- const loadResources = async () => {
168
- setResourcesLoading(true);
169
- setError(null);
170
- try {
171
- const result = await makeMcpRequest('resources/list');
172
- setResources(result?.resources || []);
173
- setResourcesLoaded(true);
174
- } catch (err) {
175
- const errorMsg = err.message || 'Failed to load resources';
176
- setError(`resources: ${errorMsg}`);
177
- setResourcesLoaded(true);
178
- console.error('Failed to load resources:', err);
179
- } finally {
180
- setResourcesLoading(false);
101
+ if (activeSection === 'resources' && !resourcesLoaded && !resourcesLoading) {
102
+ const timer = setTimeout(() => {
103
+ loadResources();
104
+ }, 100);
105
+ return () => clearTimeout(timer);
181
106
  }
182
- };
107
+ }, [
108
+ activeSection,
109
+ serverStatus?.running,
110
+ selectedServer,
111
+ toolsLoaded,
112
+ toolsLoading,
113
+ promptsLoaded,
114
+ promptsLoading,
115
+ resourcesLoaded,
116
+ resourcesLoading,
117
+ loadTools,
118
+ loadPrompts,
119
+ loadResources,
120
+ ]);
183
121
 
184
122
  const handleCallTool = async () => {
185
- if (!selectedTool) return;
123
+ if (!selectedTool) {
124
+ return;
125
+ }
186
126
 
187
127
  try {
188
- let args = {};
189
- try {
190
- args = JSON.parse(toolArgs);
191
- } 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) {
192
138
  setError('Invalid JSON in arguments');
193
139
  return;
194
140
  }
@@ -205,13 +151,21 @@ export function useMcpPlayground() {
205
151
  };
206
152
 
207
153
  const handleGetPrompt = async () => {
208
- if (!selectedPrompt) return;
154
+ if (!selectedPrompt) {
155
+ return;
156
+ }
209
157
 
210
158
  try {
211
- let args = {};
212
- try {
213
- args = JSON.parse(promptArgs);
214
- } 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) {
215
169
  setError('Invalid JSON in arguments');
216
170
  return;
217
171
  }
@@ -228,7 +182,9 @@ export function useMcpPlayground() {
228
182
  };
229
183
 
230
184
  const handleReadResource = async () => {
231
- if (!selectedResource) return;
185
+ if (!selectedResource) {
186
+ return;
187
+ }
232
188
 
233
189
  try {
234
190
  const result = await makeMcpRequest('resources/read', {
@@ -276,5 +232,8 @@ export function useMcpPlayground() {
276
232
  handleCallTool,
277
233
  handleGetPrompt,
278
234
  handleReadResource,
235
+ availableServers,
236
+ selectedServer,
237
+ setSelectedServer,
279
238
  };
280
239
  }