@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,13 @@
1
+ import * as createScanRoute from './scans/createScan.js';
2
+ import * as getScanRoute from './scans/getScan.js';
3
+ import * as getCachedResultsRoute from './scans/getCachedResults.js';
4
+ import * as createBatchScansRoute from './scans/createBatchScans.js';
5
+ import * as listScansRoute from './scans/listScans.js';
6
+ import * as clearCacheRoute from './scans/clearCache.js';
7
+
8
+ export const createScan = createScanRoute.createScan;
9
+ export const getScan = getScanRoute.getScan;
10
+ export const getCachedResults = getCachedResultsRoute.getCachedResults;
11
+ export const createBatchScans = createBatchScansRoute.createBatchScans;
12
+ export const listScans = listScansRoute.listScans;
13
+ export const clearCache = clearCacheRoute.clearCache;
@@ -0,0 +1,56 @@
1
+ import { readSmartScanToken, writeSmartScanToken } from '../../utils/smartscan-token.js';
2
+
3
+ /**
4
+ * Get stored Smart Scan token
5
+ * GET /api/smartscan/token
6
+ */
7
+ export function getToken(req, res) {
8
+ try {
9
+ const token = readSmartScanToken();
10
+ return res.json({
11
+ success: true,
12
+ token: token || null,
13
+ });
14
+ } catch (error) {
15
+ console.error('Error reading Smart Scan token:', error);
16
+ return res.status(500).json({
17
+ error: 'Failed to read token',
18
+ message: error.message,
19
+ });
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Save Smart Scan token
25
+ * POST /api/smartscan/token
26
+ */
27
+ export function saveToken(req, res) {
28
+ try {
29
+ const { token } = req.body;
30
+
31
+ if (token === undefined) {
32
+ return res.status(400).json({
33
+ error: 'Token is required',
34
+ });
35
+ }
36
+
37
+ const success = writeSmartScanToken(token);
38
+
39
+ if (!success) {
40
+ return res.status(500).json({
41
+ error: 'Failed to save token',
42
+ });
43
+ }
44
+
45
+ return res.json({
46
+ success: true,
47
+ message: 'Token saved successfully',
48
+ });
49
+ } catch (error) {
50
+ console.error('Error saving Smart Scan token:', error);
51
+ return res.status(500).json({
52
+ error: 'Failed to save token',
53
+ message: error.message,
54
+ });
55
+ }
56
+ }
@@ -0,0 +1,53 @@
1
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
2
+ import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
3
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
4
+
5
+ /**
6
+ * Create transport for MCP server based on config
7
+ */
8
+ export function createTransport(serverConfig, serverName) {
9
+ const type = serverConfig.type || (serverConfig.url ? 'http' : 'stdio');
10
+ const {
11
+ url,
12
+ headers: configHeaders = {},
13
+ command,
14
+ args = [],
15
+ env: configEnv = {},
16
+ } = serverConfig;
17
+
18
+ const env = {
19
+ ...process.env,
20
+ ...configEnv,
21
+ };
22
+
23
+ const requestInit = { headers: { ...configHeaders } };
24
+
25
+ switch (type) {
26
+ case 'stdio':
27
+ if (!command) {
28
+ throw new Error(`Server ${serverName}: command is required for stdio transport`);
29
+ }
30
+ return new StdioClientTransport({ command, args, env });
31
+
32
+ case 'http':
33
+ case 'sse':
34
+ case 'streamable-http':
35
+ if (!url) {
36
+ throw new Error(`Server ${serverName}: url is required for ${type} transport`);
37
+ }
38
+ return new StreamableHTTPClientTransport(new URL(url), { requestInit });
39
+
40
+ case 'ws':
41
+ case 'websocket':
42
+ if (!url) {
43
+ throw new Error(`Server ${serverName}: url is required for websocket transport`);
44
+ }
45
+ return new WebSocketClientTransport(new URL(url));
46
+
47
+ default:
48
+ if (command) {
49
+ return new StdioClientTransport({ command, args, env });
50
+ }
51
+ throw new Error(`Server ${serverName}: unsupported transport type: ${type}`);
52
+ }
53
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Smart Scan API proxy routes
3
+ * Proxies requests to the Smart Scan API to avoid CORS issues
4
+ */
5
+
6
+ import * as tokenRoutes from './smartscan/token.js';
7
+ import * as discoverRoutes from './smartscan/discover.js';
8
+ import * as scanRoutes from './smartscan/scans.js';
9
+
10
+ export function createSmartScanRoutes() {
11
+ const router = {};
12
+
13
+ router.getToken = tokenRoutes.getToken;
14
+ router.saveToken = tokenRoutes.saveToken;
15
+ router.discoverServers = discoverRoutes.discoverServers;
16
+ router.getCachedResults = scanRoutes.getCachedResults;
17
+ router.createScan = scanRoutes.createScan;
18
+ router.getScan = scanRoutes.getScan;
19
+ router.listScans = scanRoutes.listScans;
20
+ router.createBatchScans = scanRoutes.createBatchScans;
21
+ router.clearCache = scanRoutes.clearCache;
22
+
23
+ return router;
24
+ }
@@ -0,0 +1,83 @@
1
+ import { serializeBigInt } from '../utils/serialization.js';
2
+ import { queryRequests } from 'mcp-shark-common/db/query.js';
3
+
4
+ export function createStatisticsRoutes(db) {
5
+ const router = {};
6
+
7
+ router.getStatistics = (req, res) => {
8
+ try {
9
+ // Sanitize search parameter - convert empty strings to null
10
+ let search = req.query.search;
11
+ if (search !== undefined && search !== null) {
12
+ search = String(search).trim();
13
+ search = search.length > 0 ? search : null;
14
+ } else {
15
+ search = null;
16
+ }
17
+
18
+ // Build filters object matching the requests route
19
+ const filters = {
20
+ sessionId: (req.query.sessionId && String(req.query.sessionId).trim()) || null,
21
+ direction: (req.query.direction && String(req.query.direction).trim()) || null,
22
+ method: (req.query.method && String(req.query.method).trim()) || null,
23
+ jsonrpcMethod: (req.query.jsonrpcMethod && String(req.query.jsonrpcMethod).trim()) || null,
24
+ statusCode: req.query.statusCode ? parseInt(req.query.statusCode) : null,
25
+ jsonrpcId: (req.query.jsonrpcId && String(req.query.jsonrpcId).trim()) || null,
26
+ search: search,
27
+ serverName: (req.query.serverName && String(req.query.serverName).trim()) || null,
28
+ startTime: req.query.startTime ? BigInt(req.query.startTime) : null,
29
+ endTime: req.query.endTime ? BigInt(req.query.endTime) : null,
30
+ limit: 1000000, // Large limit for accurate statistics
31
+ offset: 0,
32
+ };
33
+
34
+ // Remove undefined values to avoid issues
35
+ Object.keys(filters).forEach((key) => {
36
+ if (filters[key] === undefined) {
37
+ filters[key] = null;
38
+ }
39
+ });
40
+
41
+ // Get all filtered requests (no limit for accurate statistics)
42
+ const allRequests = queryRequests(db, filters);
43
+
44
+ // Calculate statistics from filtered requests
45
+ const totalPackets = allRequests.length;
46
+ const totalRequests = allRequests.filter((r) => r.direction === 'request').length;
47
+ const totalResponses = allRequests.filter((r) => r.direction === 'response').length;
48
+ const totalErrors = allRequests.filter((r) => {
49
+ if (r.direction === 'response') {
50
+ const statusCode = r.status_code || r.status;
51
+ return (
52
+ statusCode >= 400 ||
53
+ (r.body_json && typeof r.body_json === 'object' && r.body_json.error)
54
+ );
55
+ }
56
+ return false;
57
+ }).length;
58
+
59
+ // Get unique sessions
60
+ const uniqueSessions = new Set();
61
+ allRequests.forEach((r) => {
62
+ if (r.session_id) {
63
+ uniqueSessions.add(r.session_id);
64
+ }
65
+ });
66
+
67
+ const stats = {
68
+ total_packets: totalPackets,
69
+ total_requests: totalRequests,
70
+ total_responses: totalResponses,
71
+ total_errors: totalErrors,
72
+ unique_sessions: uniqueSessions.size,
73
+ };
74
+
75
+ res.json(serializeBigInt(stats));
76
+ } catch (error) {
77
+ console.error('Error in getStatistics:', error);
78
+ res.status(500).json({ error: 'Failed to get statistics', details: error.message });
79
+ }
80
+ };
81
+
82
+ return router;
83
+ }
@@ -0,0 +1,212 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { storeOriginalConfig } from './config.js';
5
+
6
+ function findLatestBackup(filePath) {
7
+ const dir = path.dirname(filePath);
8
+ const basename = path.basename(filePath);
9
+ const backups = [];
10
+
11
+ if (!fs.existsSync(dir)) {
12
+ return null;
13
+ }
14
+
15
+ try {
16
+ const files = fs.readdirSync(dir);
17
+
18
+ // Find backups with new format: .mcp.json-mcpshark.<datetime>.json
19
+ files
20
+ .filter((file) => {
21
+ // Match pattern: .<basename>-mcpshark.<datetime>.json
22
+ return /^\.(.+)-mcpshark\.\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.json$/.test(file);
23
+ })
24
+ .forEach((file) => {
25
+ const match = file.match(/^\.(.+)-mcpshark\./);
26
+ if (match && match[1] === basename) {
27
+ const backupPath = path.join(dir, file);
28
+ const stats = fs.statSync(backupPath);
29
+ backups.push({
30
+ backupPath,
31
+ modifiedAt: stats.mtime,
32
+ });
33
+ }
34
+ });
35
+
36
+ // Also check for old .backup format
37
+ const oldBackupPath = `${filePath}.backup`;
38
+ if (fs.existsSync(oldBackupPath)) {
39
+ const stats = fs.statSync(oldBackupPath);
40
+ backups.push({
41
+ backupPath: oldBackupPath,
42
+ modifiedAt: stats.mtime,
43
+ });
44
+ }
45
+
46
+ if (backups.length === 0) {
47
+ return null;
48
+ }
49
+
50
+ // Sort by modifiedAt (latest first) and return the latest
51
+ backups.sort((a, b) => b.modifiedAt - a.modifiedAt);
52
+ return backups[0].backupPath;
53
+ } catch (error) {
54
+ console.error('Error finding latest backup:', error);
55
+ return null;
56
+ }
57
+ }
58
+
59
+ export function updateConfigFile(
60
+ originalConfig,
61
+ selectedServiceNames,
62
+ resolvedFilePath,
63
+ content,
64
+ mcpSharkLogs,
65
+ broadcastLogUpdate
66
+ ) {
67
+ const hasMcpServers = originalConfig.mcpServers && typeof originalConfig.mcpServers === 'object';
68
+ const hasServers = originalConfig.servers && typeof originalConfig.servers === 'object';
69
+
70
+ const updatedConfig = { ...originalConfig };
71
+
72
+ if (hasMcpServers) {
73
+ const updatedMcpServers = {};
74
+ if (selectedServiceNames.size > 0) {
75
+ updatedMcpServers['mcp-shark-server'] = {
76
+ type: 'http',
77
+ url: 'http://localhost:9851/mcp',
78
+ };
79
+ }
80
+ Object.entries(originalConfig.mcpServers).forEach(([name, cfg]) => {
81
+ if (!selectedServiceNames.has(name)) {
82
+ updatedMcpServers[name] = cfg;
83
+ }
84
+ });
85
+ updatedConfig.mcpServers = updatedMcpServers;
86
+ } else if (hasServers) {
87
+ const updatedServers = {};
88
+ if (selectedServiceNames.size > 0) {
89
+ updatedServers['mcp-shark-server'] = {
90
+ type: 'http',
91
+ url: 'http://localhost:9851/mcp',
92
+ };
93
+ }
94
+ Object.entries(originalConfig.servers).forEach(([name, cfg]) => {
95
+ if (!selectedServiceNames.has(name)) {
96
+ updatedServers[name] = cfg;
97
+ }
98
+ });
99
+ updatedConfig.servers = updatedServers;
100
+ } else {
101
+ updatedConfig.mcpServers = {
102
+ 'mcp-shark-server': {
103
+ type: 'http',
104
+ url: 'http://localhost:9851/mcp',
105
+ },
106
+ };
107
+ }
108
+
109
+ let createdBackupPath = null;
110
+ if (resolvedFilePath && fs.existsSync(resolvedFilePath)) {
111
+ // Check if we need to create a backup by comparing with latest backup
112
+ const latestBackupPath = findLatestBackup(resolvedFilePath);
113
+ let shouldCreateBackup = true;
114
+
115
+ if (latestBackupPath && fs.existsSync(latestBackupPath)) {
116
+ try {
117
+ const latestBackupContent = fs.readFileSync(latestBackupPath, 'utf-8');
118
+ const currentContent = content || fs.readFileSync(resolvedFilePath, 'utf-8');
119
+
120
+ // Normalize both contents for comparison (remove whitespace differences)
121
+ const normalizeContent = (str) => {
122
+ try {
123
+ // Try to parse as JSON and re-stringify to normalize
124
+ return JSON.stringify(JSON.parse(str), null, 2);
125
+ } catch {
126
+ // If not valid JSON, just trim
127
+ return str.trim();
128
+ }
129
+ };
130
+
131
+ const normalizedBackup = normalizeContent(latestBackupContent);
132
+ const normalizedCurrent = normalizeContent(currentContent);
133
+
134
+ if (normalizedBackup === normalizedCurrent) {
135
+ shouldCreateBackup = false;
136
+ const timestamp = new Date().toISOString();
137
+ const skipLog = {
138
+ timestamp,
139
+ type: 'stdout',
140
+ line: `[BACKUP] Skipped backup (no changes detected): ${resolvedFilePath.replace(homedir(), '~')}`,
141
+ };
142
+ mcpSharkLogs.push(skipLog);
143
+ if (mcpSharkLogs.length > 10000) {
144
+ mcpSharkLogs.shift();
145
+ }
146
+ broadcastLogUpdate(skipLog);
147
+ }
148
+ } catch (error) {
149
+ console.error('Error comparing with latest backup:', error);
150
+ // If comparison fails, create backup to be safe
151
+ shouldCreateBackup = true;
152
+ }
153
+ }
154
+
155
+ if (shouldCreateBackup) {
156
+ // Create backup with new format: .mcp.json-mcpshark.<datetime>.json
157
+ const now = new Date();
158
+ // Format: YYYY-MM-DD_HH-MM-SS
159
+ const year = now.getFullYear();
160
+ const month = String(now.getMonth() + 1).padStart(2, '0');
161
+ const day = String(now.getDate()).padStart(2, '0');
162
+ const hours = String(now.getHours()).padStart(2, '0');
163
+ const minutes = String(now.getMinutes()).padStart(2, '0');
164
+ const seconds = String(now.getSeconds()).padStart(2, '0');
165
+ const datetimeStr = `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
166
+ const dir = path.dirname(resolvedFilePath);
167
+ const basename = path.basename(resolvedFilePath);
168
+ createdBackupPath = path.join(dir, `.${basename}-mcpshark.${datetimeStr}.json`);
169
+ fs.copyFileSync(resolvedFilePath, createdBackupPath);
170
+ storeOriginalConfig(resolvedFilePath, content, createdBackupPath);
171
+
172
+ const timestamp = new Date().toISOString();
173
+ const backupLog = {
174
+ timestamp,
175
+ type: 'stdout',
176
+ line: `[BACKUP] Created backup: ${createdBackupPath.replace(homedir(), '~')}`,
177
+ };
178
+ mcpSharkLogs.push(backupLog);
179
+ if (mcpSharkLogs.length > 10000) {
180
+ mcpSharkLogs.shift();
181
+ }
182
+ broadcastLogUpdate(backupLog);
183
+ } else {
184
+ // Still store the original config reference even if we didn't create a new backup
185
+ // Use the latest backup path if available
186
+ storeOriginalConfig(resolvedFilePath, content, latestBackupPath);
187
+ }
188
+
189
+ fs.writeFileSync(resolvedFilePath, JSON.stringify(updatedConfig, null, 2));
190
+ console.log(`Updated config file: ${resolvedFilePath}`);
191
+ }
192
+
193
+ return { updatedConfig, backupPath: createdBackupPath };
194
+ }
195
+
196
+ export function getSelectedServiceNames(originalConfig, selectedServices) {
197
+ if (selectedServices && Array.isArray(selectedServices) && selectedServices.length > 0) {
198
+ return new Set(selectedServices);
199
+ }
200
+
201
+ const selectedServiceNames = new Set();
202
+ const hasMcpServers = originalConfig.mcpServers && typeof originalConfig.mcpServers === 'object';
203
+ const hasServers = originalConfig.servers && typeof originalConfig.servers === 'object';
204
+
205
+ if (hasMcpServers) {
206
+ Object.keys(originalConfig.mcpServers).forEach((name) => selectedServiceNames.add(name));
207
+ } else if (hasServers) {
208
+ Object.keys(originalConfig.servers).forEach((name) => selectedServiceNames.add(name));
209
+ }
210
+
211
+ return selectedServiceNames;
212
+ }
@@ -0,0 +1,98 @@
1
+ import * as fs from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+
4
+ const state = { originalConfigData: null };
5
+
6
+ export function storeOriginalConfig(filePath, originalContent, backupPath) {
7
+ state.originalConfigData = { filePath, originalContent, backupPath };
8
+ }
9
+
10
+ export function restoreOriginalConfig(mcpSharkLogs, broadcastLogUpdate) {
11
+ if (state.originalConfigData && state.originalConfigData.filePath) {
12
+ try {
13
+ if (fs.existsSync(state.originalConfigData.filePath)) {
14
+ fs.writeFileSync(
15
+ state.originalConfigData.filePath,
16
+ state.originalConfigData.originalContent
17
+ );
18
+ console.log(`Restored original config to: ${state.originalConfigData.filePath}`);
19
+ state.originalConfigData = null;
20
+ return true;
21
+ }
22
+ state.originalConfigData = null;
23
+ return false;
24
+ } catch (error) {
25
+ console.error('Failed to restore original config:', error);
26
+ const timestamp = new Date().toISOString();
27
+ const errorLog = {
28
+ timestamp,
29
+ type: 'error',
30
+ line: `[RESTORE ERROR] Failed to restore: ${error.message}`,
31
+ };
32
+ mcpSharkLogs.push(errorLog);
33
+ if (mcpSharkLogs.length > 10000) {
34
+ mcpSharkLogs.shift();
35
+ }
36
+ broadcastLogUpdate(errorLog);
37
+ return false;
38
+ }
39
+ }
40
+ return false;
41
+ }
42
+
43
+ export function clearOriginalConfig() {
44
+ state.originalConfigData = null;
45
+ }
46
+
47
+ export function convertMcpServersToServers(config) {
48
+ const { mcpServers, servers } = config;
49
+ const converted = { servers: {} };
50
+
51
+ if (servers) {
52
+ converted.servers = servers;
53
+ }
54
+
55
+ if (mcpServers) {
56
+ Object.entries(mcpServers).forEach(([name, cfg]) => {
57
+ const type = cfg.type || (cfg.url ? 'http' : cfg.command ? 'stdio' : 'stdio');
58
+ converted.servers[name] = { type, ...cfg };
59
+ });
60
+ }
61
+
62
+ return converted;
63
+ }
64
+
65
+ export function extractServices(config) {
66
+ const { mcpServers, servers } = config;
67
+ const servicesMap = new Map();
68
+
69
+ if (servers) {
70
+ Object.entries(servers).forEach(([name, cfg]) => {
71
+ const type = cfg.type || (cfg.url ? 'http' : cfg.command ? 'stdio' : 'stdio');
72
+ servicesMap.set(name, {
73
+ name,
74
+ type,
75
+ url: cfg.url || null,
76
+ command: cfg.command || null,
77
+ args: cfg.args || null,
78
+ });
79
+ });
80
+ }
81
+
82
+ if (mcpServers) {
83
+ Object.entries(mcpServers).forEach(([name, cfg]) => {
84
+ if (!servicesMap.has(name)) {
85
+ const type = cfg.type || (cfg.url ? 'http' : cfg.command ? 'stdio' : 'stdio');
86
+ servicesMap.set(name, {
87
+ name,
88
+ type,
89
+ url: cfg.url || null,
90
+ command: cfg.command || null,
91
+ args: cfg.args || null,
92
+ });
93
+ }
94
+ });
95
+ }
96
+
97
+ return Array.from(servicesMap.values());
98
+ }
@@ -0,0 +1,23 @@
1
+ import * as path from 'node:path';
2
+ import * as fs from 'node:fs';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ export function findMcpServerPath() {
9
+ const pathsToCheck = [
10
+ path.join(process.cwd(), '../mcp-server'),
11
+ path.join(__dirname, '../../mcp-server'),
12
+ path.join(process.cwd(), 'mcp-server'),
13
+ path.join(__dirname, '../../mcp-server'),
14
+ ];
15
+
16
+ for (const possiblePath of pathsToCheck) {
17
+ if (fs.existsSync(possiblePath)) {
18
+ return possiblePath;
19
+ }
20
+ }
21
+
22
+ return path.join(process.cwd(), '../mcp-server');
23
+ }
@@ -0,0 +1,28 @@
1
+ import { createConnection } from 'net';
2
+
3
+ export function checkPortReady(port, host = 'localhost', timeout = 10000) {
4
+ return new Promise((resolve, reject) => {
5
+ const startTime = Date.now();
6
+
7
+ const tryConnect = () => {
8
+ const socket = createConnection(port, host);
9
+
10
+ socket.on('connect', () => {
11
+ socket.destroy();
12
+ resolve(true);
13
+ });
14
+
15
+ socket.on('error', (err) => {
16
+ socket.destroy();
17
+ const elapsed = Date.now() - startTime;
18
+ if (elapsed >= timeout) {
19
+ reject(new Error(`Port ${port} not ready after ${timeout}ms`));
20
+ } else {
21
+ setTimeout(tryConnect, 200);
22
+ }
23
+ });
24
+ };
25
+
26
+ tryConnect();
27
+ });
28
+ }
@@ -0,0 +1,80 @@
1
+ import { spawn } from 'node:child_process';
2
+ import * as path from 'node:path';
3
+ import { findMcpServerPath } from './paths.js';
4
+ import { enhancePath } from '../../paths.js';
5
+ import { getMcpConfigPath, getWorkingDirectory } from 'mcp-shark-common/configs/index.js';
6
+
7
+ const MAX_LOG_LINES = 10000;
8
+
9
+ export function createLogEntry(mcpSharkLogs, broadcastLogUpdate) {
10
+ return function logEntry(type, data) {
11
+ const timestamp = new Date().toISOString();
12
+ const line = data.toString();
13
+ mcpSharkLogs.push({ timestamp, type, line });
14
+ if (mcpSharkLogs.length > MAX_LOG_LINES) {
15
+ mcpSharkLogs.shift();
16
+ }
17
+ broadcastLogUpdate({ timestamp, type, line });
18
+ };
19
+ }
20
+
21
+ export function spawnMcpSharkServer(mcpSharkJsPath, mcpsJsonPath, logEntry) {
22
+ const mcpServerPath = findMcpServerPath();
23
+ const nodeExecutable = process.execPath || 'node';
24
+ const enhancedPath = enhancePath(process.env.PATH);
25
+
26
+ logEntry('info', `[UI Server] Spawning MCP-Shark server...`);
27
+ logEntry('info', `[UI Server] Executable: ${nodeExecutable}`);
28
+ logEntry('info', `[UI Server] Script: ${mcpSharkJsPath}`);
29
+ logEntry('info', `[UI Server] Config: ${mcpsJsonPath}`);
30
+ logEntry('info', `[UI Server] CWD: ${mcpServerPath}`);
31
+ logEntry('info', `[UI Server] Data dir: ${getWorkingDirectory()}`);
32
+ logEntry('info', `[UI Server] Enhanced PATH: ${enhancedPath}`);
33
+
34
+ const processHandle = spawn(nodeExecutable, [mcpSharkJsPath, mcpsJsonPath], {
35
+ cwd: mcpServerPath,
36
+ stdio: ['ignore', 'pipe', 'pipe'],
37
+ env: {
38
+ ...process.env,
39
+ PATH: enhancedPath,
40
+ },
41
+ });
42
+
43
+ console.log(`[UI Server] MCP-Shark process spawned with PID: ${processHandle.pid}`);
44
+
45
+ processHandle.stdout.on('data', (data) => {
46
+ logEntry('stdout', data);
47
+ process.stdout.write(data);
48
+ });
49
+
50
+ processHandle.stderr.on('data', (data) => {
51
+ logEntry('stderr', data);
52
+ process.stderr.write(data);
53
+ });
54
+
55
+ return processHandle;
56
+ }
57
+
58
+ export function setupProcessHandlers(processHandle, logEntry, onError, onExit) {
59
+ processHandle.on('error', (err) => {
60
+ console.error('Failed to start mcp-shark server:', err);
61
+ logEntry('error', `Failed to start mcp-shark server: ${err.message}`);
62
+ if (onError) onError(err);
63
+ });
64
+
65
+ processHandle.on('exit', (code, signal) => {
66
+ const message = `MCP Shark server process exited with code ${code}${signal ? ` (signal: ${signal})` : ''}`;
67
+ console.log(`[UI Server] ${message}`);
68
+ logEntry('exit', message);
69
+ if (code !== 0 && code !== null) {
70
+ console.error(`[UI Server] MCP-Shark process exited with non-zero code: ${code}`);
71
+ logEntry('error', `Process exited with code ${code}`);
72
+ }
73
+ if (onExit) onExit(code, signal);
74
+ });
75
+ }
76
+
77
+ export function getMcpSharkJsPath() {
78
+ const mcpServerPath = findMcpServerPath();
79
+ return path.join(mcpServerPath, 'mcp-shark.js');
80
+ }