@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
package/bin/mcp-shark.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn } from 'node:child_process';
4
- import { fileURLToPath } from 'node:url';
5
- import { dirname, join, resolve } from 'node:path';
6
4
  import { existsSync, readFileSync } from 'node:fs';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
7
  import { Command } from 'commander';
8
8
  import open from 'open';
9
+ import logger from '../shared/logger.js';
9
10
 
10
11
  const SERVER_URL = 'http://localhost:9853';
11
12
  const BROWSER_OPEN_DELAY = 1000;
@@ -34,7 +35,7 @@ function displayWelcomeBanner() {
34
35
  Version: ${version} | Homepage: https://mcpshark.sh
35
36
  `;
36
37
 
37
- console.log(banner);
38
+ logger.log(banner);
38
39
  }
39
40
 
40
41
  const uiDir = join(rootDir, 'ui');
@@ -70,25 +71,25 @@ function runCommand(command, args, options) {
70
71
  * Install dependencies in the root directory
71
72
  */
72
73
  async function installDependencies() {
73
- console.log('Installing dependencies...');
74
+ logger.info('Installing dependencies...');
74
75
  try {
75
76
  await runCommand('npm', ['install'], { cwd: rootDir });
76
- console.log('Dependencies installed successfully!\n');
77
+ logger.info('Dependencies installed successfully!\n');
77
78
  } catch (error) {
78
- console.error('Failed to install dependencies:', error.message);
79
+ logger.error({ error: error.message }, 'Failed to install dependencies');
79
80
  process.exit(1);
80
81
  }
81
82
  }
82
83
 
83
84
  /**
84
- * Build the UI for production
85
+ * Validate that UI dist directory exists
85
86
  */
86
- async function buildUI() {
87
- console.log('Building UI for production...');
88
- try {
89
- await runCommand('npm', ['run', 'build'], { cwd: uiDir });
90
- } catch (error) {
91
- console.error('Failed to build UI:', error.message);
87
+ function validateUIBuilt() {
88
+ if (!existsSync(distDir)) {
89
+ logger.error(
90
+ 'Error: UI build not found. The package should include pre-built UI files.\n' +
91
+ 'If you are developing, run: npm run build:ui'
92
+ );
92
93
  process.exit(1);
93
94
  }
94
95
  }
@@ -105,9 +106,9 @@ async function openBrowser() {
105
106
  * Start the UI server
106
107
  */
107
108
  async function startServer(shouldOpenBrowser = false) {
108
- console.log('Starting MCP Shark UI server...');
109
- console.log(`Open ${SERVER_URL} in your browser`);
110
- console.log('Press Ctrl+C to stop\n');
109
+ logger.info('Starting MCP Shark UI server...');
110
+ logger.info(`Open ${SERVER_URL} in your browser`);
111
+ logger.info('Press Ctrl+C to stop\n');
111
112
 
112
113
  const serverProcess = spawn('node', ['server.js'], {
113
114
  cwd: uiDir,
@@ -119,12 +120,12 @@ async function startServer(shouldOpenBrowser = false) {
119
120
  await openBrowser();
120
121
  }
121
122
 
122
- let isShuttingDown = false;
123
+ const shutdownState = { isShuttingDown: false };
123
124
 
124
125
  serverProcess.on('close', (code) => {
125
- if (!isShuttingDown) {
126
+ if (!shutdownState.isShuttingDown) {
126
127
  if (code !== 0 && code !== null) {
127
- console.error(`Server exited with code ${code}`);
128
+ logger.error({ code }, 'Server exited with code');
128
129
  process.exit(code);
129
130
  }
130
131
  } else {
@@ -134,17 +135,19 @@ async function startServer(shouldOpenBrowser = false) {
134
135
 
135
136
  // Handle process termination
136
137
  const shutdown = async (signal) => {
137
- if (isShuttingDown) return;
138
- isShuttingDown = true;
138
+ if (shutdownState.isShuttingDown) {
139
+ return;
140
+ }
141
+ shutdownState.isShuttingDown = true;
139
142
 
140
- console.log('Shutting down...');
143
+ logger.info('Shutting down...');
141
144
 
142
145
  // Send signal to child process
143
146
  serverProcess.kill(signal);
144
147
 
145
148
  // Wait for child process to exit, with timeout
146
149
  const timeout = setTimeout(() => {
147
- console.log('Forcefully terminating server process...');
150
+ logger.info('Forcefully terminating server process...');
148
151
  serverProcess.kill('SIGKILL');
149
152
  process.exit(1);
150
153
  }, 5000);
@@ -164,7 +167,7 @@ async function startServer(shouldOpenBrowser = false) {
164
167
  */
165
168
  function validateDirectories() {
166
169
  if (!existsSync(uiDir)) {
167
- console.error('Error: UI directory not found. Please ensure you are in the correct directory.');
170
+ logger.error('Error: UI directory not found. Please ensure you are in the correct directory.');
168
171
  process.exit(1);
169
172
  }
170
173
  }
@@ -178,15 +181,6 @@ async function ensureDependencies() {
178
181
  }
179
182
  }
180
183
 
181
- /**
182
- * Ensure UI is built
183
- */
184
- async function ensureUIBuilt() {
185
- if (!existsSync(distDir)) {
186
- await buildUI();
187
- }
188
- }
189
-
190
184
  /**
191
185
  * Main execution function
192
186
  */
@@ -206,14 +200,14 @@ async function main() {
206
200
  // Ensure dependencies are installed
207
201
  await ensureDependencies();
208
202
 
209
- // Ensure UI is built
210
- await ensureUIBuilt();
203
+ // Validate UI is built (pre-built files should be included in package)
204
+ validateUIBuilt();
211
205
 
212
206
  // Start the server
213
207
  await startServer(options.open);
214
208
  }
215
209
 
216
210
  main().catch((error) => {
217
- console.error('Unexpected error:', error);
211
+ logger.error({ error: error.message }, 'Unexpected error');
218
212
  process.exit(1);
219
213
  });
@@ -0,0 +1,115 @@
1
+ import { createServer } from 'node:http';
2
+
3
+ import serverLogger from '../shared/logger.js';
4
+ import { isError } from './lib/common/error.js';
5
+ import { runAllExternalServers } from './lib/server/external/all.js';
6
+
7
+ import {
8
+ getDatabaseFile,
9
+ getMcpConfigPath,
10
+ prepareAppDataSpaces,
11
+ } from 'mcp-shark-common/configs/index.js';
12
+ import { initDb } from 'mcp-shark-common/db/init.js';
13
+ import { getLogger } from 'mcp-shark-common/db/logger.js';
14
+ import { withAuditRequestResponseHandler } from './lib/auditor/audit.js';
15
+ import { getInternalServer } from './lib/server/internal/run.js';
16
+ import { createInternalServerFactory } from './lib/server/internal/server.js';
17
+
18
+ function initAuditLogger(serverLogger) {
19
+ serverLogger.info({ path: getDatabaseFile() }, 'Initializing audit logger at');
20
+ return getLogger(initDb(getDatabaseFile()));
21
+ }
22
+
23
+ /**
24
+ * Start the MCP Shark server
25
+ * @param {Object} options - Configuration options
26
+ * @param {string} [options.configPath] - Path to MCP config file (defaults to getMcpConfigPath())
27
+ * @param {number} [options.port=9851] - Port to listen on
28
+ * @param {Function} [options.onError] - Error callback
29
+ * @param {Function} [options.onReady] - Ready callback
30
+ * @returns {Promise<{app: Express, server: http.Server, stop: Function}>} Server instance
31
+ */
32
+ export async function startMcpSharkServer(options = {}) {
33
+ const { configPath = getMcpConfigPath(), port = 9851, onError, onReady } = options;
34
+
35
+ prepareAppDataSpaces();
36
+
37
+ serverLogger.info('[MCP-Shark] Starting MCP server...');
38
+ serverLogger.info(`[MCP-Shark] Config path: ${configPath}`);
39
+ serverLogger.info(`[MCP-Shark] Database path: ${getDatabaseFile()}`);
40
+ serverLogger.info(`[MCP-Shark] Working directory: ${process.cwd()}`);
41
+ serverLogger.info(`[MCP-Shark] PATH: ${process.env.PATH}`);
42
+
43
+ try {
44
+ const auditLogger = initAuditLogger(serverLogger);
45
+ const externalServersResult = await runAllExternalServers(serverLogger, configPath);
46
+
47
+ if (isError(externalServersResult)) {
48
+ const error = new Error(JSON.stringify(externalServersResult));
49
+ if (onError) onError(error);
50
+ throw error;
51
+ }
52
+
53
+ const { kv, servers: externalServers } = externalServersResult;
54
+
55
+ const internalServerFactory = createInternalServerFactory(serverLogger, kv);
56
+ const app = getInternalServer(
57
+ internalServerFactory,
58
+ auditLogger,
59
+ withAuditRequestResponseHandler
60
+ );
61
+
62
+ const httpServer = createServer(app);
63
+
64
+ const server = await new Promise((resolve, reject) => {
65
+ httpServer.on('error', (err) => {
66
+ if (onError) onError(err);
67
+ reject(err);
68
+ });
69
+
70
+ httpServer.listen(port, '0.0.0.0', () => {
71
+ serverLogger.info(`MCP proxy HTTP server listening on http://localhost:${port}/mcp`);
72
+ if (onReady) onReady();
73
+ resolve(httpServer);
74
+ });
75
+ });
76
+
77
+ const stop = async () => {
78
+ // Close all external server clients
79
+ for (const serverInfo of externalServers) {
80
+ if (serverInfo?.client && !isError(serverInfo)) {
81
+ try {
82
+ await serverInfo.client.close();
83
+ } catch (err) {
84
+ serverLogger.warn({ error: err.message }, 'Error closing external server client');
85
+ }
86
+ }
87
+ }
88
+
89
+ // Close all connections forcefully
90
+ if (server.closeAllConnections) {
91
+ server.closeAllConnections();
92
+ }
93
+
94
+ // Close the server
95
+ return new Promise((resolve) => {
96
+ server.close(() => {
97
+ serverLogger.info('MCP Shark server stopped');
98
+ resolve();
99
+ });
100
+ });
101
+ };
102
+
103
+ return {
104
+ app,
105
+ server,
106
+ stop,
107
+ auditLogger,
108
+ };
109
+ } catch (error) {
110
+ const errorMsg = 'Error starting MCP server';
111
+ serverLogger.error({ error: error.message, stack: error.stack }, `[MCP-Shark] ${errorMsg}`);
112
+ if (onError) onError(error);
113
+ throw error;
114
+ }
115
+ }
@@ -4,6 +4,17 @@ import { getSessionFromRequest } from '../server/internal/handlers/common.js';
4
4
 
5
5
  /* ---------- helpers ---------- */
6
6
 
7
+ /**
8
+ * Safely parse JSON string, returning null if parsing fails
9
+ */
10
+ function parseJsonSafely(jsonString) {
11
+ try {
12
+ return JSON.parse(jsonString);
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
7
18
  function toBuffer(body) {
8
19
  if (body === undefined || body === null) {
9
20
  return Buffer.alloc(0);
@@ -13,11 +24,7 @@ function toBuffer(body) {
13
24
  return body;
14
25
  }
15
26
 
16
- if (
17
- typeof body === 'object' &&
18
- body.type === 'Buffer' &&
19
- Array.isArray(body.data)
20
- ) {
27
+ if (typeof body === 'object' && body.type === 'Buffer' && Array.isArray(body.data)) {
21
28
  return Buffer.from(body.data);
22
29
  }
23
30
 
@@ -52,7 +59,7 @@ function captureResponse(res) {
52
59
  const wrapped = new Proxy(res, {
53
60
  get(target, prop, receiver) {
54
61
  if (prop === 'write') {
55
- return function (chunk, ...args) {
62
+ return (chunk, ...args) => {
56
63
  if (chunk) {
57
64
  const buf = toBuffer(chunk);
58
65
  chunks.push(buf);
@@ -62,7 +69,7 @@ function captureResponse(res) {
62
69
  }
63
70
 
64
71
  if (prop === 'end') {
65
- return function (chunk, ...args) {
72
+ return (chunk, ...args) => {
66
73
  if (chunk) {
67
74
  const buf = toBuffer(chunk);
68
75
  chunks.push(buf);
@@ -114,12 +121,12 @@ function rebuildReq(req, buf) {
114
121
  }
115
122
 
116
123
  function waitForResponseFinish(res) {
117
- return new Promise(resolve => {
118
- let done = false;
124
+ return new Promise((resolve) => {
125
+ const state = { done: false };
119
126
 
120
127
  function finishOnce() {
121
- if (!done) {
122
- done = true;
128
+ if (!state.done) {
129
+ state.done = true;
123
130
  resolve();
124
131
  }
125
132
  }
@@ -135,24 +142,26 @@ export async function withAuditRequestResponseHandler(
135
142
  transport,
136
143
  req,
137
144
  res,
138
- auditLogger
145
+ auditLogger,
146
+ requestedMcpServer,
147
+ initialSessionId
139
148
  ) {
140
149
  const reqBuf = await readBody(req);
141
150
  const reqJsonRpc = tryParseJsonRpc(reqBuf);
142
151
 
143
152
  // Extract session ID from request
144
153
  // If no session ID exists, it's an initiation request
145
- const sessionId = getSessionFromRequest(req);
154
+ const sessionIdFromRequest = getSessionFromRequest(req);
155
+ const sessionId =
156
+ sessionIdFromRequest === null ||
157
+ sessionIdFromRequest === undefined ||
158
+ sessionIdFromRequest === ''
159
+ ? initialSessionId
160
+ : sessionIdFromRequest;
146
161
 
147
162
  // Extract request body as string
148
163
  const reqBodyStr = reqBuf.toString('utf8');
149
- const reqBodyJson = (() => {
150
- try {
151
- return JSON.parse(reqBodyStr);
152
- } catch {
153
- return null;
154
- }
155
- })();
164
+ const reqBodyJson = parseJsonSafely(reqBodyStr);
156
165
 
157
166
  // Log request packet to database
158
167
  const requestResult = auditLogger.logRequestPacket({
@@ -161,7 +170,7 @@ export async function withAuditRequestResponseHandler(
161
170
  headers: req.headers,
162
171
  body: reqBodyJson || reqBodyStr,
163
172
  userAgent: req.headers['user-agent'] || req.headers['User-Agent'] || null,
164
- remoteAddress: req.socket?.remoteAddress || null,
173
+ remoteAddress: requestedMcpServer,
165
174
  sessionId: sessionId || null,
166
175
  });
167
176
 
@@ -169,15 +178,7 @@ export async function withAuditRequestResponseHandler(
169
178
 
170
179
  const replayReq = req.body ? req : rebuildReq(req, reqBuf);
171
180
 
172
- const parsedForTransport = reqJsonRpc
173
- ? (() => {
174
- try {
175
- return JSON.parse(reqBuf.toString('utf8'));
176
- } catch {
177
- return undefined;
178
- }
179
- })()
180
- : undefined;
181
+ const parsedForTransport = reqJsonRpc ? parseJsonSafely(reqBuf.toString('utf8')) : undefined;
181
182
 
182
183
  // hand over to transport
183
184
  if (!transport || typeof transport.handleRequest !== 'function') {
@@ -198,19 +199,10 @@ export async function withAuditRequestResponseHandler(
198
199
 
199
200
  // Extract response body as string
200
201
  const resBodyStr = resBuf.toString('utf8');
201
- const resBodyJson = (() => {
202
- try {
203
- return JSON.parse(resBodyStr);
204
- } catch {
205
- return null;
206
- }
207
- })();
202
+ const resBodyJson = parseJsonSafely(resBodyStr);
208
203
 
209
204
  // Extract JSON-RPC ID from request for correlation
210
- const jsonrpcId =
211
- reqJsonRpc?.payload?.id !== undefined
212
- ? String(reqJsonRpc.payload.id)
213
- : null;
205
+ const jsonrpcId = reqJsonRpc?.payload?.id !== undefined ? String(reqJsonRpc.payload.id) : null;
214
206
 
215
207
  // Log response packet to database
216
208
  // Use the same session ID from the request
@@ -223,6 +215,6 @@ export async function withAuditRequestResponseHandler(
223
215
  jsonrpcId,
224
216
  sessionId: sessionId || null,
225
217
  userAgent: req.headers['user-agent'] || req.headers['User-Agent'] || null,
226
- remoteAddress: req.socket?.remoteAddress || null,
218
+ remoteAddress: requestedMcpServer || null,
227
219
  });
228
220
  }
@@ -11,5 +11,5 @@ export function isError(error) {
11
11
  }
12
12
 
13
13
  export function getErrors(results) {
14
- return results.filter(result => isError(result));
14
+ return results.filter((result) => isError(result));
15
15
  }
@@ -1,7 +1,7 @@
1
- import { normalizeConfig } from './config.js';
2
- import { runExternalServer } from './single/run.js';
3
1
  import { CompositeError, getErrors } from '../../common/error.js';
2
+ import { normalizeConfig } from './config.js';
4
3
  import { buildKv } from './kv.js';
4
+ import { runExternalServer } from './single/run.js';
5
5
 
6
6
  export class RunAllExternalServersError extends CompositeError {
7
7
  constructor(message, error, errors = []) {
@@ -13,9 +13,7 @@ export class RunAllExternalServersError extends CompositeError {
13
13
  export async function runAllExternalServers(logger, parsedConfig) {
14
14
  const configs = normalizeConfig(parsedConfig);
15
15
  const results = await Promise.all(
16
- Object.entries(configs).map(([name, config]) =>
17
- runExternalServer({ logger, name, config })
18
- )
16
+ Object.entries(configs).map(([name, config]) => runExternalServer({ logger, name, config }))
19
17
  );
20
18
 
21
19
  const flattenedResults = results.flat();
@@ -28,5 +26,6 @@ export async function runAllExternalServers(logger, parsedConfig) {
28
26
  );
29
27
  }
30
28
 
31
- return buildKv(flattenedResults);
29
+ const kv = buildKv(flattenedResults);
30
+ return { kv, servers: flattenedResults };
32
31
  }
@@ -16,9 +16,7 @@ function parseConfig(configPath) {
16
16
  }
17
17
  return new ConfigError(
18
18
  'Invalid config file',
19
- new Error(
20
- `Invalid config file: ${configPath} - ${JSON.stringify(conf, null, 2)}`
21
- )
19
+ new Error(`Invalid config file: ${configPath} - ${JSON.stringify(conf, null, 2)}`)
22
20
  );
23
21
  } catch (error) {
24
22
  return new ConfigError(
@@ -1,25 +1,8 @@
1
1
  const kv = new Map();
2
2
 
3
- function buildName(name, typeName) {
4
- return `${name}.${typeName}`;
5
- }
6
-
7
- export function extractName(name) {
8
- const [serverName, typeName] = name.split('.');
9
- return { serverName, typeName };
10
- }
11
-
12
3
  export function buildKv(downstreamServers) {
13
4
  for (const downstreamServer of downstreamServers) {
14
- const {
15
- name,
16
- tools,
17
- resources,
18
- prompts,
19
- callTool,
20
- getPrompt,
21
- readResource,
22
- } = downstreamServer;
5
+ const { name, tools, resources, prompts, callTool, getPrompt, readResource } = downstreamServer;
23
6
 
24
7
  if (!kv.has(name)) {
25
8
  const toolsMap = new Map();
@@ -42,17 +25,17 @@ export function buildKv(downstreamServers) {
42
25
  toolsMap,
43
26
  resourcesMap,
44
27
  promptsMap,
45
- tools: tools.map(tool => {
46
- return { ...tool, name: buildName(name, tool.name) };
28
+ tools: tools.map((tool) => {
29
+ return { ...tool, name: tool.name };
47
30
  }),
48
- resources: resources.map(resource => {
31
+ resources: resources.map((resource) => {
49
32
  return {
50
33
  ...resource,
51
- name: buildName(name, resource.name),
34
+ name: resource.name,
52
35
  };
53
36
  }),
54
- prompts: prompts.map(prompt => {
55
- return { ...prompt, name: buildName(name, prompt.name) };
37
+ prompts: prompts.map((prompt) => {
38
+ return { ...prompt, name: prompt.name };
56
39
  }),
57
40
  });
58
41
  }
@@ -61,42 +44,40 @@ export function buildKv(downstreamServers) {
61
44
  return kv;
62
45
  }
63
46
 
64
- export function getBy(database, calledName, action) {
65
- const { serverName, typeName } = extractName(calledName);
66
- if (!serverName || !typeName) {
67
- return null;
68
- }
69
- const entry = database.get(serverName);
47
+ export function getBy(database, requestedMcpServer, calledName, action) {
48
+ const entry = database.get(requestedMcpServer);
70
49
  if (!entry) {
71
50
  return null;
72
51
  }
73
52
 
74
53
  // Type-based lookup
75
54
  if (action === 'getTools') {
76
- return entry.toolsMap.get(typeName);
55
+ return entry.toolsMap.get(calledName);
77
56
  }
78
57
  if (action === 'getResources') {
79
- return entry.resourcesMap.get(typeName);
58
+ return entry.resourcesMap.get(calledName);
80
59
  }
81
60
  if (action === 'getPrompts') {
82
- return entry.promptsMap.get(typeName);
61
+ return entry.promptsMap.get(calledName);
83
62
  }
84
63
 
85
64
  // Action-based lookup
86
65
  if (action === 'callTool') {
87
- return entry.toolsMap.get(typeName);
66
+ return entry.toolsMap.get(calledName);
88
67
  }
89
68
  if (action === 'readResource') {
90
- return entry.resourcesMap.get(typeName);
69
+ return entry.resourcesMap.get(calledName);
91
70
  }
92
71
  if (action === 'getPrompt') {
93
- return entry.promptsMap.get(typeName);
72
+ return entry.promptsMap.get(calledName);
94
73
  }
95
74
  return null;
96
75
  }
97
76
 
98
- export function listAll(database, type) {
99
- return Array.from(database.values())
100
- .map(entry => entry[type])
101
- .flat();
77
+ export function listAll(database, requestedMcpServer, type) {
78
+ const serverEntry = database.get(requestedMcpServer);
79
+ if (!serverEntry) {
80
+ return [];
81
+ }
82
+ return serverEntry[type];
102
83
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
- ListToolsResultSchema,
3
- ListResourcesResultSchema,
4
2
  ListPromptsResultSchema,
3
+ ListResourcesResultSchema,
4
+ ListToolsResultSchema,
5
5
  } from '@modelcontextprotocol/sdk/types.js';
6
6
 
7
7
  import { CompositeError } from '../../../common/error.js';
@@ -22,10 +22,7 @@ async function getListOf(client, typeOfList, schema) {
22
22
  [typeOfList]: [],
23
23
  };
24
24
  try {
25
- const result = await client.request(
26
- { method: `${typeOfList}/list` },
27
- schema
28
- );
25
+ const result = await client.request({ method: `${typeOfList}/list` }, schema);
29
26
  fetchedList[typeOfList] = result[typeOfList];
30
27
  return fetchedList;
31
28
  } catch (error) {
@@ -1,7 +1,7 @@
1
- import { makeTransport } from './transport.js';
1
+ import { CompositeError, getErrors, isError } from '../../../common/error.js';
2
2
  import { createClient } from './client.js';
3
3
  import * as requests from './request.js';
4
- import { isError, CompositeError, getErrors } from '../../../common/error.js';
4
+ import { makeTransport } from './transport.js';
5
5
 
6
6
  export class RunError extends CompositeError {
7
7
  constructor(message, error, errors = []) {
@@ -11,27 +11,18 @@ export class RunError extends CompositeError {
11
11
  }
12
12
 
13
13
  export async function runExternalServer({ logger, name, config }) {
14
- logger.debug(
15
- `Starting external server run for server ${name} with config:`,
16
- config
17
- );
14
+ logger.debug(`Starting external server run for server ${name} with config:`, config);
18
15
 
19
16
  // Create transport
20
17
  const transport = makeTransport(config);
21
18
  if (isError(transport)) {
22
- return new RunError(
23
- `Error creating transport for external server ${name}`,
24
- transport.error
25
- );
19
+ return new RunError(`Error creating transport for external server ${name}`, transport.error);
26
20
  }
27
21
 
28
22
  // Create client
29
23
  const client = await createClient({ transport });
30
24
  if (isError(client)) {
31
- return new RunError(
32
- `Error creating client for external server ${name}`,
33
- client.error
34
- );
25
+ return new RunError(`Error creating client for external server ${name}`, client.error);
35
26
  }
36
27
 
37
28
  // Run requests
@@ -52,17 +43,15 @@ export async function runExternalServer({ logger, name, config }) {
52
43
  );
53
44
  }
54
45
 
55
- const [{ tools }, { resources }, { prompts }] = results.map(
56
- result => result.value
57
- );
46
+ const [{ tools }, { resources }, { prompts }] = results.map((result) => result.value);
58
47
  return {
59
48
  name,
60
49
  client,
61
50
  tools,
62
51
  resources,
63
52
  prompts,
64
- callTool: args => client.callTool.bind(client)(args),
65
- readResource: resourceUri => {
53
+ callTool: (args) => client.callTool.bind(client)(args),
54
+ readResource: (resourceUri) => {
66
55
  return client.readResource.bind(client)(resourceUri);
67
56
  },
68
57
  getPrompt: (promptName, args) => {