@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,8 @@
1
1
  import * as fs from 'node:fs';
2
- import * as path from 'node:path';
3
2
  import { homedir } from 'node:os';
3
+ import * as path from 'node:path';
4
4
  import { storeOriginalConfig } from './config.js';
5
+ import logger from './logger.js';
5
6
 
6
7
  function findLatestBackup(filePath) {
7
8
  const dir = path.dirname(filePath);
@@ -51,143 +52,144 @@ function findLatestBackup(filePath) {
51
52
  backups.sort((a, b) => b.modifiedAt - a.modifiedAt);
52
53
  return backups[0].backupPath;
53
54
  } catch (error) {
54
- console.error('Error finding latest backup:', error);
55
+ logger.error({ error: error.message }, 'Error finding latest backup');
55
56
  return null;
56
57
  }
57
58
  }
58
59
 
59
- export function updateConfigFile(
60
- originalConfig,
61
- selectedServiceNames,
60
+ function shouldCreateBackup(
61
+ latestBackupPath,
62
62
  resolvedFilePath,
63
63
  content,
64
64
  mcpSharkLogs,
65
65
  broadcastLogUpdate
66
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
- };
67
+ if (!latestBackupPath || !fs.existsSync(latestBackupPath)) {
68
+ return true;
107
69
  }
108
70
 
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;
71
+ try {
72
+ const latestBackupContent = fs.readFileSync(latestBackupPath, 'utf-8');
73
+ const currentContent = content || fs.readFileSync(resolvedFilePath, 'utf-8');
114
74
 
115
- if (latestBackupPath && fs.existsSync(latestBackupPath)) {
75
+ // Normalize both contents for comparison (remove whitespace differences)
76
+ const normalizeContent = (str) => {
116
77
  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;
78
+ // Try to parse as JSON and re-stringify to normalize
79
+ return JSON.stringify(JSON.parse(str), null, 2);
80
+ } catch {
81
+ // If not valid JSON, just trim
82
+ return str.trim();
152
83
  }
153
- }
84
+ };
154
85
 
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);
86
+ const normalizedBackup = normalizeContent(latestBackupContent);
87
+ const normalizedCurrent = normalizeContent(currentContent);
171
88
 
89
+ if (normalizedBackup === normalizedCurrent) {
172
90
  const timestamp = new Date().toISOString();
173
- const backupLog = {
91
+ const skipLog = {
174
92
  timestamp,
175
93
  type: 'stdout',
176
- line: `[BACKUP] Created backup: ${createdBackupPath.replace(homedir(), '~')}`,
94
+ line: `[BACKUP] Skipped backup (no changes detected): ${resolvedFilePath.replace(homedir(), '~')}`,
177
95
  };
178
- mcpSharkLogs.push(backupLog);
96
+ mcpSharkLogs.push(skipLog);
179
97
  if (mcpSharkLogs.length > 10000) {
180
98
  mcpSharkLogs.shift();
181
99
  }
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);
100
+ broadcastLogUpdate(skipLog);
101
+ return false;
187
102
  }
103
+ return true;
104
+ } catch (error) {
105
+ logger.error({ error: error.message }, 'Error comparing with latest backup');
106
+ // If comparison fails, create backup to be safe
107
+ return true;
108
+ }
109
+ }
188
110
 
111
+ function createBackup(resolvedFilePath, content, mcpSharkLogs, broadcastLogUpdate) {
112
+ // Create backup with new format: .mcp.json-mcpshark.<datetime>.json
113
+ const datetimeStr = formatDateTimeForBackup();
114
+ const dir = path.dirname(resolvedFilePath);
115
+ const basename = path.basename(resolvedFilePath);
116
+ const backupPath = path.join(dir, `.${basename}-mcpshark.${datetimeStr}.json`);
117
+ fs.copyFileSync(resolvedFilePath, backupPath);
118
+ storeOriginalConfig(resolvedFilePath, content, backupPath);
119
+
120
+ const timestamp = new Date().toISOString();
121
+ const backupLog = {
122
+ timestamp,
123
+ type: 'stdout',
124
+ line: `[BACKUP] Created backup: ${backupPath.replace(homedir(), '~')}`,
125
+ };
126
+ mcpSharkLogs.push(backupLog);
127
+ if (mcpSharkLogs.length > 10000) {
128
+ mcpSharkLogs.shift();
129
+ }
130
+ broadcastLogUpdate(backupLog);
131
+ return backupPath;
132
+ }
133
+
134
+ function computeBackupPath(resolvedFilePath, content, mcpSharkLogs, broadcastLogUpdate) {
135
+ if (!resolvedFilePath || !fs.existsSync(resolvedFilePath)) {
136
+ return null;
137
+ }
138
+
139
+ // Check if we need to create a backup by comparing with latest backup
140
+ const latestBackupPath = findLatestBackup(resolvedFilePath);
141
+ const needsBackup = shouldCreateBackup(
142
+ latestBackupPath,
143
+ resolvedFilePath,
144
+ content,
145
+ mcpSharkLogs,
146
+ broadcastLogUpdate
147
+ );
148
+
149
+ if (needsBackup) {
150
+ return createBackup(resolvedFilePath, content, mcpSharkLogs, broadcastLogUpdate);
151
+ }
152
+
153
+ // Still store the original config reference even if we didn't create a new backup
154
+ // Use the latest backup path if available
155
+ storeOriginalConfig(resolvedFilePath, content, latestBackupPath);
156
+ return null;
157
+ }
158
+
159
+ export function updateConfigFile(
160
+ originalConfig,
161
+ _selectedServiceNames,
162
+ resolvedFilePath,
163
+ content,
164
+ mcpSharkLogs,
165
+ broadcastLogUpdate
166
+ ) {
167
+ const [serverObject, serverType] = getServerObject(originalConfig);
168
+ const updatedConfig = { ...originalConfig };
169
+
170
+ if (serverObject) {
171
+ const updatedServers = {};
172
+ // Transform all original servers to HTTP URLs pointing to MCP shark server
173
+ // Each server gets its own endpoint to avoid tool name prefixing issues
174
+ Object.entries(serverObject).forEach(([name, _cfg]) => {
175
+ updatedServers[name] = {
176
+ type: 'http',
177
+ url: `http://localhost:9851/mcp/${encodeURIComponent(name)}`,
178
+ };
179
+ });
180
+ updatedConfig[serverType] = updatedServers;
181
+ }
182
+
183
+ const createdBackupPath = computeBackupPath(
184
+ resolvedFilePath,
185
+ content,
186
+ mcpSharkLogs,
187
+ broadcastLogUpdate
188
+ );
189
+
190
+ if (resolvedFilePath && fs.existsSync(resolvedFilePath)) {
189
191
  fs.writeFileSync(resolvedFilePath, JSON.stringify(updatedConfig, null, 2));
190
- console.log(`Updated config file: ${resolvedFilePath}`);
192
+ logger.info({ path: resolvedFilePath }, 'Updated config file');
191
193
  }
192
194
 
193
195
  return { updatedConfig, backupPath: createdBackupPath };
@@ -210,3 +212,29 @@ export function getSelectedServiceNames(originalConfig, selectedServices) {
210
212
 
211
213
  return selectedServiceNames;
212
214
  }
215
+
216
+ function getServerObject(originalConfig) {
217
+ const hasMcpServers = originalConfig.mcpServers && typeof originalConfig.mcpServers === 'object';
218
+ const hasServers = originalConfig.servers && typeof originalConfig.servers === 'object';
219
+
220
+ if (hasMcpServers) {
221
+ return [originalConfig.mcpServers, 'mcpServers'];
222
+ }
223
+
224
+ if (hasServers) {
225
+ return [originalConfig.servers, 'servers'];
226
+ }
227
+
228
+ return [null, null];
229
+ }
230
+
231
+ export function formatDateTimeForBackup() {
232
+ const now = new Date();
233
+ const year = now.getFullYear();
234
+ const month = String(now.getMonth() + 1).padStart(2, '0');
235
+ const day = String(now.getDate()).padStart(2, '0');
236
+ const hours = String(now.getHours()).padStart(2, '0');
237
+ const minutes = String(now.getMinutes()).padStart(2, '0');
238
+ const seconds = String(now.getSeconds()).padStart(2, '0');
239
+ return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
240
+ }
@@ -1,5 +1,5 @@
1
1
  import * as fs from 'node:fs';
2
- import { homedir } from 'node:os';
2
+ import logger from './logger.js';
3
3
 
4
4
  const state = { originalConfigData: null };
5
5
 
@@ -8,21 +8,21 @@ export function storeOriginalConfig(filePath, originalContent, backupPath) {
8
8
  }
9
9
 
10
10
  export function restoreOriginalConfig(mcpSharkLogs, broadcastLogUpdate) {
11
- if (state.originalConfigData && state.originalConfigData.filePath) {
11
+ if (state.originalConfigData?.filePath) {
12
12
  try {
13
13
  if (fs.existsSync(state.originalConfigData.filePath)) {
14
14
  fs.writeFileSync(
15
15
  state.originalConfigData.filePath,
16
16
  state.originalConfigData.originalContent
17
17
  );
18
- console.log(`Restored original config to: ${state.originalConfigData.filePath}`);
18
+ logger.info({ path: state.originalConfigData.filePath }, 'Restored original config');
19
19
  state.originalConfigData = null;
20
20
  return true;
21
21
  }
22
22
  state.originalConfigData = null;
23
23
  return false;
24
24
  } catch (error) {
25
- console.error('Failed to restore original config:', error);
25
+ logger.error({ error: error.message }, 'Failed to restore original config');
26
26
  const timestamp = new Date().toISOString();
27
27
  const errorLog = {
28
28
  timestamp,
@@ -0,0 +1,2 @@
1
+ // Re-export shared logger
2
+ export { default } from '../../../shared/logger.js';
@@ -1,10 +1,26 @@
1
- import * as path from 'node:path';
1
+ import { execSync } from 'node:child_process';
2
2
  import * as fs from 'node:fs';
3
- import { fileURLToPath } from 'url';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import logger from './logger.js';
4
7
 
5
8
  const __filename = fileURLToPath(import.meta.url);
6
9
  const __dirname = path.dirname(__filename);
7
10
 
11
+ function getNvmNodeBinPaths(homeDir) {
12
+ try {
13
+ const nvmVersionsPath = path.join(homeDir, '.nvm', 'versions', 'node');
14
+ if (fs.existsSync(nvmVersionsPath)) {
15
+ return fs
16
+ .readdirSync(nvmVersionsPath, { withFileTypes: true })
17
+ .filter((dirent) => dirent.isDirectory())
18
+ .map((dirent) => path.join(nvmVersionsPath, dirent.name, 'bin'));
19
+ }
20
+ } catch (_e) {}
21
+ return [];
22
+ }
23
+
8
24
  export function findMcpServerPath() {
9
25
  const pathsToCheck = [
10
26
  path.join(process.cwd(), '../mcp-server'),
@@ -21,3 +37,195 @@ export function findMcpServerPath() {
21
37
 
22
38
  return path.join(process.cwd(), '../mcp-server');
23
39
  }
40
+
41
+ /**
42
+ * Get system PATH from the host machine's shell environment
43
+ * This works in Electron by executing a shell command to get the actual PATH
44
+ * Includes both system PATH and user's custom PATH from shell config files
45
+ */
46
+ function getSystemPath() {
47
+ try {
48
+ if (process.platform === 'win32') {
49
+ const pathOutput = execSync('cmd /c echo %PATH%', {
50
+ encoding: 'utf8',
51
+ timeout: 2000,
52
+ stdio: ['ignore', 'pipe', 'ignore'],
53
+ });
54
+ return pathOutput.trim();
55
+ }
56
+ const userShell = process.env.SHELL || '/bin/zsh';
57
+ const shells = [userShell, '/bin/zsh', '/bin/bash', '/bin/sh'];
58
+
59
+ for (const shell of shells) {
60
+ if (fs.existsSync(shell)) {
61
+ try {
62
+ const shellName = path.basename(shell);
63
+
64
+ const getPathOutput = (shell, shellName) => {
65
+ const execOptions = {
66
+ encoding: 'utf8',
67
+ timeout: 2000,
68
+ stdio: ['ignore', 'pipe', 'ignore'],
69
+ maxBuffer: 1024 * 1024,
70
+ env: {
71
+ ...Object.fromEntries(
72
+ Object.entries(process.env).filter(([key]) => key !== 'PATH')
73
+ ),
74
+ },
75
+ };
76
+
77
+ if (shellName === 'zsh') {
78
+ try {
79
+ return execSync(`${shell} -i -c 'echo $PATH'`, execOptions);
80
+ } catch (_e) {
81
+ return execSync(`${shell} -l -c 'echo $PATH'`, execOptions);
82
+ }
83
+ }
84
+
85
+ return execSync(`${shell} -l -c 'echo $PATH'`, execOptions);
86
+ };
87
+
88
+ const pathOutput = getPathOutput(shell, shellName);
89
+ const systemPath = pathOutput.trim();
90
+ if (systemPath) {
91
+ logger.info({ shell, shellName }, 'Got PATH from shell');
92
+ return systemPath;
93
+ }
94
+ } catch (_e) {}
95
+ }
96
+ }
97
+
98
+ const homeDir = os.homedir();
99
+ const configFiles = [
100
+ { file: path.join(homeDir, '.zshrc'), shell: 'zsh', interactive: true },
101
+ { file: path.join(homeDir, '.zprofile'), shell: 'zsh', interactive: false },
102
+ { file: path.join(homeDir, '.zlogin'), shell: 'zsh', interactive: false },
103
+ { file: path.join(homeDir, '.bashrc'), shell: 'bash', interactive: true },
104
+ { file: path.join(homeDir, '.bash_profile'), shell: 'bash', interactive: false },
105
+ { file: path.join(homeDir, '.profile'), shell: 'sh', interactive: false },
106
+ ];
107
+
108
+ for (const { file, shell: shellName, interactive } of configFiles) {
109
+ if (fs.existsSync(file)) {
110
+ try {
111
+ const flag = shellName === 'zsh' && interactive ? '-i' : '';
112
+ const pathOutput = execSync(
113
+ `/bin/${shellName} ${flag} -c 'source ${file} 2>/dev/null; echo $PATH'`,
114
+ {
115
+ encoding: 'utf8',
116
+ timeout: 2000,
117
+ stdio: ['ignore', 'pipe', 'ignore'],
118
+ maxBuffer: 1024 * 1024,
119
+ env: {
120
+ ...Object.fromEntries(
121
+ Object.entries(process.env).filter(([key]) => key !== 'PATH')
122
+ ),
123
+ },
124
+ }
125
+ );
126
+ const systemPath = pathOutput.trim();
127
+ if (systemPath && systemPath.length > 10) {
128
+ logger.info({ file }, 'Got PATH from file');
129
+ return systemPath;
130
+ }
131
+ } catch (_e) {}
132
+ }
133
+ }
134
+ } catch (error) {
135
+ logger.warn({ error: error.message }, 'Could not get system PATH');
136
+ }
137
+ return null;
138
+ }
139
+
140
+ /**
141
+ * Enhance PATH environment variable to include system paths and user paths
142
+ * This is especially important in Electron where PATH might not include system executables
143
+ */
144
+ export function enhancePath(originalPath) {
145
+ const homeDir = os.homedir();
146
+ const pathSeparator = process.platform === 'win32' ? ';' : ':';
147
+
148
+ const systemPath = getSystemPath();
149
+ if (systemPath) {
150
+ logger.info('Using system PATH from host machine');
151
+ const userPaths = [
152
+ path.join(homeDir, '.local', 'bin'),
153
+ path.join(homeDir, '.npm-global', 'bin'),
154
+ path.join(homeDir, '.cargo', 'bin'),
155
+ path.join(homeDir, 'bin'),
156
+ path.join(homeDir, '.nvm', 'current', 'bin'),
157
+ ...getNvmNodeBinPaths(homeDir),
158
+ path.join(homeDir, '.fnm', 'node-versions', 'v20.0.0', 'install', 'bin'),
159
+ path.join(homeDir, '.pyenv', 'shims'),
160
+ path.join(homeDir, '.pyenv', 'bin'),
161
+ path.join(homeDir, '.gvm', 'bin'),
162
+ path.join(homeDir, '.gvm', 'gos', 'current', 'bin'),
163
+ path.join(homeDir, '.cargo', 'bin'),
164
+ path.join(homeDir, 'go', 'bin'),
165
+ path.join(homeDir, '.go', 'bin'),
166
+ '/Applications/iTerm.app/Contents/Resources/utilities',
167
+ ...(process.platform === 'win32'
168
+ ? [
169
+ path.join(homeDir, 'AppData', 'Local', 'Programs'),
170
+ path.join(homeDir, 'AppData', 'Roaming', 'npm'),
171
+ path.join(homeDir, 'AppData', 'Local', 'Microsoft', 'WindowsApps'),
172
+ ]
173
+ : []),
174
+ ].filter((p) => {
175
+ if (p.includes('v20.0.0') || p.includes('current')) {
176
+ return fs.existsSync(path.dirname(p));
177
+ }
178
+ return fs.existsSync(p);
179
+ });
180
+
181
+ return [systemPath, ...userPaths, originalPath || ''].filter((p) => p).join(pathSeparator);
182
+ }
183
+
184
+ logger.info('Could not get system PATH, adding common locations');
185
+ const pathsToAdd = [
186
+ '/usr/local/bin',
187
+ '/usr/bin',
188
+ '/opt/homebrew/bin',
189
+ '/usr/local/opt/node/bin',
190
+ '/opt/local/bin',
191
+ '/sbin',
192
+ '/usr/sbin',
193
+ ...(process.platform === 'darwin'
194
+ ? [
195
+ '/opt/homebrew/opt/python/bin',
196
+ '/usr/local/opt/python/bin',
197
+ '/Applications/Docker.app/Contents/Resources/bin',
198
+ ]
199
+ : []),
200
+ ...(process.platform === 'linux' ? ['/snap/bin', path.join(homeDir, '.local', 'bin')] : []),
201
+ ...(process.platform === 'win32'
202
+ ? [
203
+ path.join(process.env.ProgramFiles || '', 'nodejs'),
204
+ path.join(process.env['ProgramFiles(x86)'] || '', 'nodejs'),
205
+ path.join(homeDir, 'AppData', 'Roaming', 'npm'),
206
+ path.join(process.env.ProgramFiles || '', 'Docker', 'Docker', 'resources', 'bin'),
207
+ ]
208
+ : []),
209
+ path.join(homeDir, '.local', 'bin'),
210
+ path.join(homeDir, '.npm-global', 'bin'),
211
+ path.join(homeDir, '.cargo', 'bin'),
212
+ path.join(homeDir, 'bin'),
213
+ path.join(homeDir, '.nvm', 'current', 'bin'),
214
+ ...getNvmNodeBinPaths(homeDir),
215
+ path.join(homeDir, '.pyenv', 'shims'),
216
+ path.join(homeDir, '.pyenv', 'bin'),
217
+ path.join(homeDir, '.gvm', 'bin'),
218
+ path.join(homeDir, '.gvm', 'gos', 'current', 'bin'),
219
+ path.join(homeDir, 'go', 'bin'),
220
+ path.join(homeDir, '.go', 'bin'),
221
+ '/Applications/iTerm.app/Contents/Resources/utilities',
222
+ ...(process.platform === 'win32'
223
+ ? [
224
+ path.join(homeDir, 'AppData', 'Local', 'Programs'),
225
+ path.join(homeDir, 'AppData', 'Local', 'Microsoft', 'WindowsApps'),
226
+ ]
227
+ : []),
228
+ ].filter((p) => p && fs.existsSync(p));
229
+
230
+ return [...pathsToAdd, originalPath || ''].filter((p) => p).join(pathSeparator);
231
+ }
@@ -1,4 +1,4 @@
1
- import { createConnection } from 'net';
1
+ import { createConnection } from 'node:net';
2
2
 
3
3
  export function checkPortReady(port, host = 'localhost', timeout = 10000) {
4
4
  return new Promise((resolve, reject) => {
@@ -12,7 +12,7 @@ export function checkPortReady(port, host = 'localhost', timeout = 10000) {
12
12
  resolve(true);
13
13
  });
14
14
 
15
- socket.on('error', (err) => {
15
+ socket.on('error', (_err) => {
16
16
  socket.destroy();
17
17
  const elapsed = Date.now() - startTime;
18
18
  if (elapsed >= timeout) {
@@ -1,9 +1,3 @@
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
1
  const MAX_LOG_LINES = 10000;
8
2
 
9
3
  export function createLogEntry(mcpSharkLogs, broadcastLogUpdate) {
@@ -17,64 +11,3 @@ export function createLogEntry(mcpSharkLogs, broadcastLogUpdate) {
17
11
  broadcastLogUpdate({ timestamp, type, line });
18
12
  };
19
13
  }
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
- }