@mcp-shark/mcp-shark 1.4.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 (212) hide show
  1. package/LICENSE +85 -0
  2. package/README.md +724 -0
  3. package/bin/mcp-shark.js +93 -0
  4. package/mcp-server/.editorconfig +15 -0
  5. package/mcp-server/.prettierignore +11 -0
  6. package/mcp-server/.prettierrc +12 -0
  7. package/mcp-server/README.md +280 -0
  8. package/mcp-server/commitlint.config.cjs +42 -0
  9. package/mcp-server/eslint.config.js +131 -0
  10. package/mcp-server/lib/auditor/audit.js +228 -0
  11. package/mcp-server/lib/common/error.js +15 -0
  12. package/mcp-server/lib/server/external/all.js +32 -0
  13. package/mcp-server/lib/server/external/config.js +59 -0
  14. package/mcp-server/lib/server/external/kv.js +102 -0
  15. package/mcp-server/lib/server/external/single/client.js +35 -0
  16. package/mcp-server/lib/server/external/single/request.js +49 -0
  17. package/mcp-server/lib/server/external/single/run.js +75 -0
  18. package/mcp-server/lib/server/external/single/transport.js +57 -0
  19. package/mcp-server/lib/server/internal/handlers/common.js +20 -0
  20. package/mcp-server/lib/server/internal/handlers/error.js +7 -0
  21. package/mcp-server/lib/server/internal/handlers/prompts-get.js +22 -0
  22. package/mcp-server/lib/server/internal/handlers/prompts-list.js +12 -0
  23. package/mcp-server/lib/server/internal/handlers/resources-list.js +12 -0
  24. package/mcp-server/lib/server/internal/handlers/resources-read.js +19 -0
  25. package/mcp-server/lib/server/internal/handlers/tools-call.js +37 -0
  26. package/mcp-server/lib/server/internal/handlers/tools-list.js +14 -0
  27. package/mcp-server/lib/server/internal/run.js +49 -0
  28. package/mcp-server/lib/server/internal/server.js +63 -0
  29. package/mcp-server/lib/server/internal/session.js +39 -0
  30. package/mcp-server/mcp-shark.js +72 -0
  31. package/mcp-server/package-lock.json +4784 -0
  32. package/mcp-server/package.json +30 -0
  33. package/package.json +103 -0
  34. package/ui/README.md +212 -0
  35. package/ui/index.html +16 -0
  36. package/ui/package-lock.json +3574 -0
  37. package/ui/package.json +12 -0
  38. package/ui/paths.js +282 -0
  39. package/ui/public/og-image.png +0 -0
  40. package/ui/server/routes/backups.js +251 -0
  41. package/ui/server/routes/composite.js +244 -0
  42. package/ui/server/routes/config.js +175 -0
  43. package/ui/server/routes/conversations.js +25 -0
  44. package/ui/server/routes/help.js +43 -0
  45. package/ui/server/routes/logs.js +32 -0
  46. package/ui/server/routes/playground.js +152 -0
  47. package/ui/server/routes/requests.js +235 -0
  48. package/ui/server/routes/sessions.js +27 -0
  49. package/ui/server/routes/smartscan/discover.js +117 -0
  50. package/ui/server/routes/smartscan/scans/clearCache.js +22 -0
  51. package/ui/server/routes/smartscan/scans/createBatchScans.js +123 -0
  52. package/ui/server/routes/smartscan/scans/createScan.js +42 -0
  53. package/ui/server/routes/smartscan/scans/getCachedResults.js +51 -0
  54. package/ui/server/routes/smartscan/scans/getScan.js +41 -0
  55. package/ui/server/routes/smartscan/scans/listScans.js +24 -0
  56. package/ui/server/routes/smartscan/scans.js +13 -0
  57. package/ui/server/routes/smartscan/token.js +56 -0
  58. package/ui/server/routes/smartscan/transport.js +53 -0
  59. package/ui/server/routes/smartscan.js +24 -0
  60. package/ui/server/routes/statistics.js +83 -0
  61. package/ui/server/utils/config-update.js +212 -0
  62. package/ui/server/utils/config.js +98 -0
  63. package/ui/server/utils/paths.js +23 -0
  64. package/ui/server/utils/port.js +28 -0
  65. package/ui/server/utils/process.js +80 -0
  66. package/ui/server/utils/scan-cache/all-results.js +180 -0
  67. package/ui/server/utils/scan-cache/directory.js +35 -0
  68. package/ui/server/utils/scan-cache/file-operations.js +104 -0
  69. package/ui/server/utils/scan-cache/hash.js +47 -0
  70. package/ui/server/utils/scan-cache/server-operations.js +80 -0
  71. package/ui/server/utils/scan-cache.js +12 -0
  72. package/ui/server/utils/serialization.js +13 -0
  73. package/ui/server/utils/smartscan-token.js +42 -0
  74. package/ui/server.js +199 -0
  75. package/ui/src/App.jsx +153 -0
  76. package/ui/src/CompositeLogs.jsx +164 -0
  77. package/ui/src/CompositeSetup.jsx +285 -0
  78. package/ui/src/HelpGuide/HelpGuideContent.jsx +118 -0
  79. package/ui/src/HelpGuide/HelpGuideFooter.jsx +58 -0
  80. package/ui/src/HelpGuide/HelpGuideHeader.jsx +56 -0
  81. package/ui/src/HelpGuide.jsx +65 -0
  82. package/ui/src/IntroTour.jsx +140 -0
  83. package/ui/src/LogDetail.jsx +122 -0
  84. package/ui/src/LogTable.jsx +242 -0
  85. package/ui/src/PacketDetail.jsx +190 -0
  86. package/ui/src/PacketFilters.jsx +222 -0
  87. package/ui/src/PacketList.jsx +183 -0
  88. package/ui/src/SmartScan.jsx +178 -0
  89. package/ui/src/TabNavigation.jsx +143 -0
  90. package/ui/src/components/App/HelpButton.jsx +64 -0
  91. package/ui/src/components/App/TrafficTab.jsx +69 -0
  92. package/ui/src/components/App/useAppState.js +163 -0
  93. package/ui/src/components/BackupList.jsx +192 -0
  94. package/ui/src/components/CollapsibleSection.jsx +82 -0
  95. package/ui/src/components/ConfigFileSection.jsx +84 -0
  96. package/ui/src/components/ConfigViewerModal.jsx +141 -0
  97. package/ui/src/components/ConfirmationModal.jsx +129 -0
  98. package/ui/src/components/DetailsTab/BodySection.jsx +27 -0
  99. package/ui/src/components/DetailsTab/CollapsibleRequestResponse.jsx +70 -0
  100. package/ui/src/components/DetailsTab/HeadersSection.jsx +25 -0
  101. package/ui/src/components/DetailsTab/InfoSection.jsx +28 -0
  102. package/ui/src/components/DetailsTab/NetworkInfoSection.jsx +63 -0
  103. package/ui/src/components/DetailsTab/ProtocolInfoSection.jsx +75 -0
  104. package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +46 -0
  105. package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +66 -0
  106. package/ui/src/components/DetailsTab.jsx +31 -0
  107. package/ui/src/components/DetectedPathsList.jsx +171 -0
  108. package/ui/src/components/FileInput.jsx +144 -0
  109. package/ui/src/components/GroupHeader.jsx +76 -0
  110. package/ui/src/components/GroupedByMcpView.jsx +103 -0
  111. package/ui/src/components/GroupedByServerView.jsx +134 -0
  112. package/ui/src/components/GroupedBySessionView.jsx +127 -0
  113. package/ui/src/components/GroupedViews.jsx +2 -0
  114. package/ui/src/components/HexTab.jsx +188 -0
  115. package/ui/src/components/LogsDisplay.jsx +93 -0
  116. package/ui/src/components/LogsToolbar.jsx +193 -0
  117. package/ui/src/components/McpPlayground/LoadingModal.jsx +113 -0
  118. package/ui/src/components/McpPlayground/PromptsSection/PromptCallPanel.jsx +125 -0
  119. package/ui/src/components/McpPlayground/PromptsSection/PromptItem.jsx +48 -0
  120. package/ui/src/components/McpPlayground/PromptsSection/PromptsList.jsx +45 -0
  121. package/ui/src/components/McpPlayground/PromptsSection.jsx +106 -0
  122. package/ui/src/components/McpPlayground/ResourcesSection/ResourceCallPanel.jsx +89 -0
  123. package/ui/src/components/McpPlayground/ResourcesSection/ResourceItem.jsx +59 -0
  124. package/ui/src/components/McpPlayground/ResourcesSection/ResourcesList.jsx +45 -0
  125. package/ui/src/components/McpPlayground/ResourcesSection.jsx +91 -0
  126. package/ui/src/components/McpPlayground/ToolsSection/ToolCallPanel.jsx +125 -0
  127. package/ui/src/components/McpPlayground/ToolsSection/ToolItem.jsx +48 -0
  128. package/ui/src/components/McpPlayground/ToolsSection/ToolsList.jsx +45 -0
  129. package/ui/src/components/McpPlayground/ToolsSection.jsx +107 -0
  130. package/ui/src/components/McpPlayground/common/EmptyState.jsx +17 -0
  131. package/ui/src/components/McpPlayground/common/ErrorState.jsx +17 -0
  132. package/ui/src/components/McpPlayground/common/LoadingState.jsx +17 -0
  133. package/ui/src/components/McpPlayground/useMcpPlayground.js +280 -0
  134. package/ui/src/components/McpPlayground.jsx +171 -0
  135. package/ui/src/components/MessageDisplay.jsx +28 -0
  136. package/ui/src/components/PacketDetailHeader.jsx +88 -0
  137. package/ui/src/components/PacketFilters/ExportControls.jsx +126 -0
  138. package/ui/src/components/PacketFilters/FilterInput.jsx +59 -0
  139. package/ui/src/components/RawTab.jsx +142 -0
  140. package/ui/src/components/RequestRow/OrphanedResponseRow.jsx +155 -0
  141. package/ui/src/components/RequestRow/RequestRowMain.jsx +240 -0
  142. package/ui/src/components/RequestRow/ResponseRow.jsx +158 -0
  143. package/ui/src/components/RequestRow.jsx +70 -0
  144. package/ui/src/components/ServerControl.jsx +133 -0
  145. package/ui/src/components/ServiceSelector.jsx +209 -0
  146. package/ui/src/components/SetupHeader.jsx +30 -0
  147. package/ui/src/components/SharkLogo.jsx +21 -0
  148. package/ui/src/components/SmartScan/AnalysisResult.jsx +64 -0
  149. package/ui/src/components/SmartScan/BatchResultsDisplay/BatchResultItem.jsx +215 -0
  150. package/ui/src/components/SmartScan/BatchResultsDisplay/BatchResultsHeader.jsx +94 -0
  151. package/ui/src/components/SmartScan/BatchResultsDisplay.jsx +26 -0
  152. package/ui/src/components/SmartScan/DebugInfoSection.jsx +53 -0
  153. package/ui/src/components/SmartScan/EmptyState.jsx +57 -0
  154. package/ui/src/components/SmartScan/ErrorDisplay.jsx +48 -0
  155. package/ui/src/components/SmartScan/ExpandableSection.jsx +93 -0
  156. package/ui/src/components/SmartScan/FindingsTable.jsx +257 -0
  157. package/ui/src/components/SmartScan/ListViewContent.jsx +75 -0
  158. package/ui/src/components/SmartScan/NotablePatternsSection.jsx +75 -0
  159. package/ui/src/components/SmartScan/OverallSummarySection.jsx +72 -0
  160. package/ui/src/components/SmartScan/RawDataSection.jsx +52 -0
  161. package/ui/src/components/SmartScan/RecommendationsSection.jsx +78 -0
  162. package/ui/src/components/SmartScan/ScanDetailHeader.jsx +92 -0
  163. package/ui/src/components/SmartScan/ScanDetailView.jsx +141 -0
  164. package/ui/src/components/SmartScan/ScanListView/ScanListHeader.jsx +49 -0
  165. package/ui/src/components/SmartScan/ScanListView/ScanListItem.jsx +201 -0
  166. package/ui/src/components/SmartScan/ScanListView.jsx +73 -0
  167. package/ui/src/components/SmartScan/ScanOverviewSection.jsx +123 -0
  168. package/ui/src/components/SmartScan/ScanResultsDisplay.jsx +35 -0
  169. package/ui/src/components/SmartScan/ScanViewContent.jsx +68 -0
  170. package/ui/src/components/SmartScan/ScanningProgress.jsx +47 -0
  171. package/ui/src/components/SmartScan/ServerInfoSection.jsx +43 -0
  172. package/ui/src/components/SmartScan/ServerSelectionRow.jsx +207 -0
  173. package/ui/src/components/SmartScan/SingleResultDisplay.jsx +269 -0
  174. package/ui/src/components/SmartScan/SmartScanControls.jsx +290 -0
  175. package/ui/src/components/SmartScan/SmartScanHeader.jsx +77 -0
  176. package/ui/src/components/SmartScan/ViewModeTabs.jsx +57 -0
  177. package/ui/src/components/SmartScan/hooks/useCacheManagement.js +34 -0
  178. package/ui/src/components/SmartScan/hooks/useMcpDiscovery.js +121 -0
  179. package/ui/src/components/SmartScan/hooks/useScanList.js +193 -0
  180. package/ui/src/components/SmartScan/hooks/useScanOperations.js +87 -0
  181. package/ui/src/components/SmartScan/hooks/useServerStatus.js +26 -0
  182. package/ui/src/components/SmartScan/hooks/useTokenManagement.js +53 -0
  183. package/ui/src/components/SmartScan/scanDataUtils.js +98 -0
  184. package/ui/src/components/SmartScan/useSmartScan.js +72 -0
  185. package/ui/src/components/SmartScan/utils.js +19 -0
  186. package/ui/src/components/SmartScanIcons.jsx +58 -0
  187. package/ui/src/components/TabNavigation/DesktopTabs.jsx +111 -0
  188. package/ui/src/components/TabNavigation/MobileDropdown.jsx +140 -0
  189. package/ui/src/components/TabNavigation.jsx +97 -0
  190. package/ui/src/components/TabNavigationIcons.jsx +40 -0
  191. package/ui/src/components/TableHeader.jsx +164 -0
  192. package/ui/src/components/TourOverlay.jsx +117 -0
  193. package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +117 -0
  194. package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +70 -0
  195. package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +45 -0
  196. package/ui/src/components/TourTooltip/useTooltipPosition.js +108 -0
  197. package/ui/src/components/TourTooltip.jsx +83 -0
  198. package/ui/src/components/ViewModeTabs.jsx +91 -0
  199. package/ui/src/components/WhatThisDoesSection.jsx +61 -0
  200. package/ui/src/config/tourSteps.jsx +141 -0
  201. package/ui/src/hooks/useAnimation.js +92 -0
  202. package/ui/src/hooks/useConfigManagement.js +124 -0
  203. package/ui/src/hooks/useServiceExtraction.js +51 -0
  204. package/ui/src/index.css +42 -0
  205. package/ui/src/main.jsx +10 -0
  206. package/ui/src/theme.js +65 -0
  207. package/ui/src/utils/animations.js +170 -0
  208. package/ui/src/utils/groupingUtils.js +93 -0
  209. package/ui/src/utils/hexUtils.js +24 -0
  210. package/ui/src/utils/mcpGroupingUtils.js +262 -0
  211. package/ui/src/utils/requestUtils.js +297 -0
  212. package/ui/vite.config.js +18 -0
@@ -0,0 +1,143 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { colors, fonts } from './theme';
3
+ import { SharkLogo } from './components/SharkLogo';
4
+ import {
5
+ NetworkIcon,
6
+ LogsIcon,
7
+ SettingsIcon,
8
+ PlaygroundIcon,
9
+ ShieldIcon,
10
+ } from './components/TabNavigationIcons';
11
+ import MobileDropdown from './components/TabNavigation/MobileDropdown';
12
+ import DesktopTabs from './components/TabNavigation/DesktopTabs';
13
+
14
+ function TabNavigation({ activeTab, onTabChange }) {
15
+ const tabs = [
16
+ {
17
+ id: 'traffic',
18
+ label: 'Traffic Capture',
19
+ icon: NetworkIcon,
20
+ description: 'Wireshark-like HTTP request/response analysis for forensic investigation',
21
+ },
22
+ {
23
+ id: 'logs',
24
+ label: 'MCP Shark Logs',
25
+ icon: LogsIcon,
26
+ description: 'View MCP Shark server console output and debug logs',
27
+ },
28
+ {
29
+ id: 'setup',
30
+ label: 'MCP Server Setup',
31
+ icon: SettingsIcon,
32
+ description: 'Configure and manage MCP Shark server',
33
+ },
34
+ {
35
+ id: 'playground',
36
+ label: 'MCP Playground',
37
+ icon: PlaygroundIcon,
38
+ description: 'Test and interact with MCP tools, prompts, and resources',
39
+ },
40
+ {
41
+ id: 'smart-scan',
42
+ label: 'Smart Scan',
43
+ icon: ShieldIcon,
44
+ description: 'AI-powered security analysis for MCP servers',
45
+ },
46
+ ];
47
+
48
+ const tabRefs = useRef({});
49
+ const indicatorRef = useRef(null);
50
+ const [isMobile, setIsMobile] = useState(false);
51
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
52
+ const dropdownRef = useRef(null);
53
+
54
+ // Check window size and handle resize
55
+ useEffect(() => {
56
+ const checkMobile = () => {
57
+ setIsMobile(window.innerWidth < 1200);
58
+ };
59
+
60
+ checkMobile();
61
+ window.addEventListener('resize', checkMobile);
62
+
63
+ return () => window.removeEventListener('resize', checkMobile);
64
+ }, []);
65
+
66
+ // Close dropdown when clicking outside
67
+ useEffect(() => {
68
+ const handleClickOutside = (event) => {
69
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
70
+ setIsDropdownOpen(false);
71
+ }
72
+ };
73
+
74
+ if (isDropdownOpen) {
75
+ document.addEventListener('mousedown', handleClickOutside);
76
+ }
77
+
78
+ return () => {
79
+ document.removeEventListener('mousedown', handleClickOutside);
80
+ };
81
+ }, [isDropdownOpen]);
82
+
83
+ return (
84
+ <div
85
+ style={{
86
+ borderBottom: `1px solid ${colors.borderLight}`,
87
+ background: colors.bgCard,
88
+ boxShadow: `0 1px 3px ${colors.shadowSm}`,
89
+ }}
90
+ >
91
+ <div
92
+ style={{
93
+ display: 'flex',
94
+ alignItems: 'center',
95
+ padding: '0 16px',
96
+ gap: '12px',
97
+ }}
98
+ >
99
+ <div
100
+ style={{
101
+ display: 'flex',
102
+ alignItems: 'center',
103
+ gap: '8px',
104
+ paddingRight: '12px',
105
+ borderRight: `1px solid ${colors.borderLight}`,
106
+ }}
107
+ >
108
+ <SharkLogo size={24} />
109
+ <span
110
+ style={{
111
+ fontSize: '16px',
112
+ fontWeight: '600',
113
+ color: colors.textPrimary,
114
+ fontFamily: fonts.body,
115
+ }}
116
+ >
117
+ MCP Shark
118
+ </span>
119
+ </div>
120
+ {isMobile ? (
121
+ <MobileDropdown
122
+ tabs={tabs}
123
+ activeTab={activeTab}
124
+ onTabChange={onTabChange}
125
+ isDropdownOpen={isDropdownOpen}
126
+ setIsDropdownOpen={setIsDropdownOpen}
127
+ dropdownRef={dropdownRef}
128
+ />
129
+ ) : (
130
+ <DesktopTabs
131
+ tabs={tabs}
132
+ activeTab={activeTab}
133
+ onTabChange={onTabChange}
134
+ tabRefs={tabRefs}
135
+ indicatorRef={indicatorRef}
136
+ />
137
+ )}
138
+ </div>
139
+ </div>
140
+ );
141
+ }
142
+
143
+ export default TabNavigation;
@@ -0,0 +1,64 @@
1
+ import { colors, fonts } from '../../theme';
2
+
3
+ const TourIcon = ({ size = 16, color = 'currentColor' }) => (
4
+ <svg
5
+ width={size}
6
+ height={size}
7
+ viewBox="0 0 24 24"
8
+ fill="none"
9
+ stroke={color}
10
+ strokeWidth="2"
11
+ strokeLinecap="round"
12
+ strokeLinejoin="round"
13
+ >
14
+ <path d="M12 2L2 7l10 5 10-5-10-5z" />
15
+ <path d="M2 17l10 5 10-5" />
16
+ <path d="M2 12l10 5 10-5" />
17
+ </svg>
18
+ );
19
+
20
+ export default function HelpButton({ onClick }) {
21
+ return (
22
+ <button
23
+ onClick={onClick}
24
+ data-tour="help-button"
25
+ style={{
26
+ position: 'fixed',
27
+ bottom: '20px',
28
+ right: '20px',
29
+ background: colors.bgCard,
30
+ border: `1px solid ${colors.borderLight}`,
31
+ borderRadius: '50%',
32
+ width: '48px',
33
+ height: '48px',
34
+ padding: 0,
35
+ color: colors.textSecondary,
36
+ cursor: 'pointer',
37
+ fontFamily: fonts.body,
38
+ boxShadow: `0 4px 12px ${colors.shadowMd}`,
39
+ display: 'flex',
40
+ alignItems: 'center',
41
+ justifyContent: 'center',
42
+ zIndex: 9999,
43
+ transition: 'all 0.2s ease',
44
+ }}
45
+ onMouseEnter={(e) => {
46
+ e.currentTarget.style.background = colors.bgHover;
47
+ e.currentTarget.style.color = colors.textPrimary;
48
+ e.currentTarget.style.borderColor = colors.borderMedium;
49
+ e.currentTarget.style.transform = 'scale(1.1)';
50
+ e.currentTarget.style.boxShadow = `0 6px 16px ${colors.shadowLg}`;
51
+ }}
52
+ onMouseLeave={(e) => {
53
+ e.currentTarget.style.background = colors.bgCard;
54
+ e.currentTarget.style.color = colors.textSecondary;
55
+ e.currentTarget.style.borderColor = colors.borderLight;
56
+ e.currentTarget.style.transform = 'scale(1)';
57
+ e.currentTarget.style.boxShadow = `0 4px 12px ${colors.shadowMd}`;
58
+ }}
59
+ title="Start tour"
60
+ >
61
+ <TourIcon size={20} />
62
+ </button>
63
+ );
64
+ }
@@ -0,0 +1,69 @@
1
+ import { useRef, useEffect } from 'react';
2
+ import { colors } from '../../theme';
3
+ import { slideInRight } from '../../utils/animations';
4
+ import RequestList from '../../PacketList';
5
+ import RequestDetail from '../../PacketDetail';
6
+ import RequestFilters from '../../PacketFilters';
7
+
8
+ export default function TrafficTab({
9
+ requests,
10
+ selected,
11
+ onSelect,
12
+ filters,
13
+ onFilterChange,
14
+ stats,
15
+ firstRequestTime,
16
+ onClear,
17
+ }) {
18
+ const detailPanelRef = useRef(null);
19
+
20
+ useEffect(() => {
21
+ if (selected && detailPanelRef.current) {
22
+ setTimeout(() => {
23
+ if (detailPanelRef.current) {
24
+ detailPanelRef.current.style.opacity = '0';
25
+ detailPanelRef.current.style.transform = 'translateX(600px)';
26
+ slideInRight(detailPanelRef.current, { width: 600 });
27
+ }
28
+ }, 10);
29
+ }
30
+ }, [selected]);
31
+
32
+ return (
33
+ <div data-tab-content style={{ display: 'flex', flex: 1, overflow: 'hidden', minHeight: 0 }}>
34
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 }}>
35
+ <RequestFilters
36
+ filters={filters}
37
+ onFilterChange={onFilterChange}
38
+ stats={stats}
39
+ onExport={() => {}}
40
+ onClear={onClear}
41
+ />
42
+ <RequestList
43
+ requests={requests}
44
+ selected={selected}
45
+ onSelect={onSelect}
46
+ firstRequestTime={firstRequestTime}
47
+ />
48
+ </div>
49
+ {selected && (
50
+ <div
51
+ ref={detailPanelRef}
52
+ style={{
53
+ width: '40%',
54
+ minWidth: '500px',
55
+ maxWidth: '700px',
56
+ borderLeft: `1px solid ${colors.borderLight}`,
57
+ overflow: 'hidden',
58
+ display: 'flex',
59
+ flexDirection: 'column',
60
+ background: colors.bgCard,
61
+ flexShrink: 0,
62
+ }}
63
+ >
64
+ <RequestDetail request={selected} onClose={() => onSelect(null)} requests={requests} />
65
+ </div>
66
+ )}
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1,163 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+
3
+ export function useAppState() {
4
+ const [activeTab, setActiveTab] = useState('traffic');
5
+ const [requests, setRequests] = useState([]);
6
+ const [selected, setSelected] = useState(null);
7
+ const [filters, setFilters] = useState({});
8
+ const [stats, setStats] = useState(null);
9
+ const [firstRequestTime, setFirstRequestTime] = useState(null);
10
+ const [showTour, setShowTour] = useState(false);
11
+ const [tourDismissed, setTourDismissed] = useState(true);
12
+ const wsRef = useRef(null);
13
+ const prevTabRef = useRef(activeTab);
14
+
15
+ const loadStatistics = async () => {
16
+ try {
17
+ 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);
25
+
26
+ const statsResponse = await fetch(`/api/statistics?${queryParams}`);
27
+ const statsData = await statsResponse.json();
28
+ setStats(statsData);
29
+ } catch (error) {
30
+ console.error('Failed to load statistics:', error);
31
+ }
32
+ };
33
+
34
+ const loadRequests = async () => {
35
+ try {
36
+ 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);
44
+ queryParams.append('limit', '5000');
45
+
46
+ const response = await fetch(`/api/requests?${queryParams}`);
47
+ const data = await response.json();
48
+ setRequests(data);
49
+
50
+ if (data.length > 0) {
51
+ const oldest = data[data.length - 1]?.timestamp_iso;
52
+ if (oldest) {
53
+ setFirstRequestTime(oldest);
54
+ }
55
+ }
56
+
57
+ // Also load statistics when loading requests
58
+ await loadStatistics();
59
+ } catch (error) {
60
+ console.error('Failed to load requests:', error);
61
+ }
62
+ };
63
+
64
+ useEffect(() => {
65
+ const checkTourState = async () => {
66
+ try {
67
+ const response = await fetch('/api/help/state');
68
+ const data = await response.json();
69
+ setTourDismissed(data.dismissed || data.tourCompleted);
70
+ if (!data.dismissed && !data.tourCompleted) {
71
+ setTimeout(() => {
72
+ setShowTour(true);
73
+ }, 500);
74
+ }
75
+ } catch (error) {
76
+ console.error('Failed to load tour state:', error);
77
+ setTimeout(() => {
78
+ setShowTour(true);
79
+ }, 500);
80
+ setTourDismissed(false);
81
+ }
82
+ };
83
+
84
+ checkTourState();
85
+ loadRequests();
86
+
87
+ const wsUrl = import.meta.env.DEV
88
+ ? 'ws://localhost:9853'
89
+ : `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}`;
90
+
91
+ try {
92
+ const ws = new WebSocket(wsUrl);
93
+ wsRef.current = ws;
94
+
95
+ ws.onmessage = (e) => {
96
+ const msg = JSON.parse(e.data);
97
+ if (msg.type === 'update') {
98
+ setRequests(msg.data);
99
+ if (msg.data.length > 0) {
100
+ const oldest = msg.data[msg.data.length - 1]?.timestamp_iso;
101
+ if (oldest) {
102
+ setFirstRequestTime(oldest);
103
+ }
104
+ }
105
+ // Update statistics when new data arrives via WebSocket
106
+ loadStatistics();
107
+ }
108
+ };
109
+
110
+ ws.onerror = () => {
111
+ // Silently handle WebSocket errors - server may not be running
112
+ };
113
+
114
+ ws.onclose = () => {
115
+ // Connection closed - will attempt to reconnect on next mount if needed
116
+ };
117
+ } catch (err) {
118
+ // Silently handle WebSocket creation errors
119
+ }
120
+
121
+ return () => {
122
+ if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
123
+ wsRef.current.close();
124
+ }
125
+ };
126
+ }, []);
127
+
128
+ useEffect(() => {
129
+ loadRequests();
130
+ }, [filters]);
131
+
132
+ // Periodically update statistics when on traffic tab
133
+ useEffect(() => {
134
+ if (activeTab !== 'traffic') {
135
+ return;
136
+ }
137
+
138
+ // Update statistics every 2 seconds
139
+ const interval = setInterval(() => {
140
+ loadStatistics();
141
+ }, 2000);
142
+
143
+ return () => clearInterval(interval);
144
+ }, [activeTab, filters]);
145
+
146
+ return {
147
+ activeTab,
148
+ setActiveTab,
149
+ requests,
150
+ selected,
151
+ setSelected,
152
+ filters,
153
+ setFilters,
154
+ stats,
155
+ firstRequestTime,
156
+ showTour,
157
+ setShowTour,
158
+ tourDismissed,
159
+ prevTabRef,
160
+ wsRef,
161
+ loadRequests,
162
+ };
163
+ }
@@ -0,0 +1,192 @@
1
+ import { colors, fonts, withOpacity } from '../theme';
2
+ import { IconRefresh, IconEye, IconTrash, IconRestore } from '@tabler/icons-react';
3
+
4
+ function BackupList({ backups, loadingBackups, onRefresh, onRestore, onView, onDelete }) {
5
+ if (backups.length === 0) {
6
+ return null;
7
+ }
8
+
9
+ return (
10
+ <div
11
+ style={{
12
+ background: colors.bgCard,
13
+ border: `1px solid ${colors.borderLight}`,
14
+ borderRadius: '12px',
15
+ padding: '24px',
16
+ marginBottom: '20px',
17
+ boxShadow: `0 2px 4px ${colors.shadowSm}`,
18
+ }}
19
+ >
20
+ <div
21
+ style={{
22
+ display: 'flex',
23
+ justifyContent: 'space-between',
24
+ alignItems: 'center',
25
+ marginBottom: '12px',
26
+ }}
27
+ >
28
+ <h3
29
+ style={{
30
+ fontSize: '15px',
31
+ fontWeight: '600',
32
+ color: colors.textPrimary,
33
+ fontFamily: fonts.body,
34
+ }}
35
+ >
36
+ Backup Files
37
+ </h3>
38
+ <button
39
+ onClick={onRefresh}
40
+ disabled={loadingBackups}
41
+ style={{
42
+ padding: '4px 8px',
43
+ background: 'transparent',
44
+ border: `1px solid ${colors.borderMedium}`,
45
+ color: colors.textSecondary,
46
+ cursor: loadingBackups ? 'not-allowed' : 'pointer',
47
+ fontSize: '11px',
48
+ borderRadius: '8px',
49
+ opacity: loadingBackups ? 0.5 : 1,
50
+ }}
51
+ title="Refresh backups"
52
+ >
53
+ <IconRefresh size={14} stroke={1.5} style={{ marginRight: '4px' }} />
54
+ {loadingBackups ? 'Loading...' : 'Refresh'}
55
+ </button>
56
+ </div>
57
+ <p
58
+ style={{
59
+ fontSize: '12px',
60
+ color: colors.textSecondary,
61
+ marginBottom: '12px',
62
+ lineHeight: '1.4',
63
+ }}
64
+ >
65
+ View, restore, or delete backups of your MCP configuration files. Backups are created
66
+ automatically when starting MCP Shark.
67
+ </p>
68
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
69
+ {backups.map((backup, idx) => (
70
+ <div
71
+ key={idx}
72
+ style={{
73
+ padding: '12px',
74
+ background: colors.bgPrimary,
75
+ border: `1px solid ${colors.borderMedium}`,
76
+ borderRadius: '8px',
77
+ display: 'flex',
78
+ justifyContent: 'space-between',
79
+ alignItems: 'center',
80
+ }}
81
+ >
82
+ <div style={{ flex: 1 }}>
83
+ <div
84
+ style={{
85
+ color: colors.textPrimary,
86
+ fontSize: '12px',
87
+ fontWeight: '500',
88
+ marginBottom: '4px',
89
+ }}
90
+ >
91
+ {backup.displayPath}
92
+ </div>
93
+ <div style={{ color: colors.textTertiary, fontSize: '11px', fontFamily: fonts.body }}>
94
+ Created: {new Date(backup.modifiedAt || backup.createdAt).toLocaleString()} • Size:{' '}
95
+ {(backup.size / 1024).toFixed(2)} KB
96
+ </div>
97
+ </div>
98
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
99
+ <button
100
+ onClick={() => onView(backup.backupPath)}
101
+ style={{
102
+ padding: '6px 12px',
103
+ background: 'transparent',
104
+ border: `1px solid ${colors.borderMedium}`,
105
+ color: colors.textSecondary,
106
+ cursor: 'pointer',
107
+ fontSize: '12px',
108
+ borderRadius: '8px',
109
+ fontWeight: '500',
110
+ }}
111
+ onMouseEnter={(e) => {
112
+ e.currentTarget.style.background = colors.bgCard;
113
+ e.currentTarget.style.borderColor = colors.borderLight;
114
+ }}
115
+ onMouseLeave={(e) => {
116
+ e.currentTarget.style.background = 'transparent';
117
+ e.currentTarget.style.borderColor = colors.borderMedium;
118
+ }}
119
+ title="View backup content"
120
+ >
121
+ <IconEye size={14} stroke={1.5} style={{ marginRight: '4px' }} />
122
+ View
123
+ </button>
124
+ <button
125
+ onClick={() => {
126
+ if (confirm('Are you sure you want to delete this backup?')) {
127
+ onDelete(backup.backupPath);
128
+ }
129
+ }}
130
+ style={{
131
+ padding: '6px 12px',
132
+ background: 'transparent',
133
+ border: `1px solid ${colors.borderMedium}`,
134
+ color: colors.error,
135
+ cursor: 'pointer',
136
+ fontSize: '12px',
137
+ borderRadius: '8px',
138
+ fontWeight: '500',
139
+ fontFamily: fonts.body,
140
+ display: 'flex',
141
+ alignItems: 'center',
142
+ gap: '4px',
143
+ }}
144
+ onMouseEnter={(e) => {
145
+ e.currentTarget.style.background = withOpacity(colors.error, 0.15);
146
+ e.currentTarget.style.borderColor = colors.error;
147
+ }}
148
+ onMouseLeave={(e) => {
149
+ e.currentTarget.style.background = 'transparent';
150
+ e.currentTarget.style.borderColor = colors.borderMedium;
151
+ }}
152
+ title="Delete backup"
153
+ >
154
+ <IconTrash size={14} stroke={1.5} />
155
+ Delete
156
+ </button>
157
+ <button
158
+ onClick={() => onRestore(backup.backupPath, backup.originalPath)}
159
+ style={{
160
+ padding: '6px 12px',
161
+ background: colors.buttonPrimary,
162
+ border: `1px solid ${colors.buttonPrimary}`,
163
+ color: colors.textInverse,
164
+ cursor: 'pointer',
165
+ fontSize: '12px',
166
+ borderRadius: '8px',
167
+ fontWeight: '500',
168
+ fontFamily: fonts.body,
169
+ display: 'flex',
170
+ alignItems: 'center',
171
+ gap: '4px',
172
+ }}
173
+ onMouseEnter={(e) => {
174
+ e.currentTarget.style.background = colors.buttonPrimaryHover;
175
+ }}
176
+ onMouseLeave={(e) => {
177
+ e.currentTarget.style.background = colors.buttonPrimary;
178
+ }}
179
+ title="Restore this backup"
180
+ >
181
+ <IconRestore size={14} stroke={1.5} />
182
+ Restore
183
+ </button>
184
+ </div>
185
+ </div>
186
+ ))}
187
+ </div>
188
+ </div>
189
+ );
190
+ }
191
+
192
+ export default BackupList;