@mcp-shark/mcp-shark 1.5.13 → 1.7.2

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 (158) hide show
  1. package/README.md +482 -56
  2. package/bin/mcp-shark.js +146 -52
  3. package/core/cli/AutoFixEngine.js +93 -0
  4. package/core/cli/ConfigScanner.js +193 -0
  5. package/core/cli/DataLoader.js +200 -0
  6. package/core/cli/DeclarativeRuleEngine.js +363 -0
  7. package/core/cli/DoctorCommand.js +218 -0
  8. package/core/cli/FixHandlers.js +222 -0
  9. package/core/cli/HtmlReportGenerator.js +203 -0
  10. package/core/cli/IdeConfigPaths.js +175 -0
  11. package/core/cli/ListCommand.js +255 -0
  12. package/core/cli/LockCommand.js +164 -0
  13. package/core/cli/LockDiffEngine.js +152 -0
  14. package/core/cli/RuleRegistryConfig.js +131 -0
  15. package/core/cli/ScanCommand.js +244 -0
  16. package/core/cli/ScanService.js +200 -0
  17. package/core/cli/SecretDetector.js +92 -0
  18. package/core/cli/SharkScoreCalculator.js +109 -0
  19. package/core/cli/ToolClassifications.js +51 -0
  20. package/core/cli/ToxicFlowAnalyzer.js +212 -0
  21. package/core/cli/UpdateCommand.js +188 -0
  22. package/core/cli/WalkthroughGenerator.js +195 -0
  23. package/core/cli/WatchCommand.js +129 -0
  24. package/core/cli/YamlRuleEngine.js +197 -0
  25. package/core/cli/data/rule-packs/aauth-visibility.json +117 -0
  26. package/core/cli/data/rule-packs/agentic-security-2026.json +180 -0
  27. package/core/cli/data/rule-packs/general-security.json +173 -0
  28. package/core/cli/data/rule-packs/owasp-mcp-2026.json +244 -0
  29. package/core/cli/data/rule-packs/toxic-flow-heuristics.json +21 -0
  30. package/core/cli/data/rule-sources.json +5 -0
  31. package/core/cli/data/secret-patterns.json +18 -0
  32. package/core/cli/data/tool-classifications.json +111 -0
  33. package/core/cli/data/toxic-flow-rules.json +47 -0
  34. package/core/cli/index.js +23 -0
  35. package/core/cli/output/Banner.js +52 -0
  36. package/core/cli/output/Formatter.js +183 -0
  37. package/core/cli/output/JsonFormatter.js +106 -0
  38. package/core/cli/output/index.js +16 -0
  39. package/core/cli/secureRegistryFetch.js +157 -0
  40. package/core/cli/symbols.js +16 -0
  41. package/core/configs/environment.js +3 -1
  42. package/core/configs/index.js +3 -64
  43. package/core/container/DependencyContainer.js +4 -1
  44. package/core/mcp-server/index.js +4 -1
  45. package/core/mcp-server/server/external/all.js +10 -3
  46. package/core/mcp-server/server/external/config.js +62 -5
  47. package/core/models/RequestFilters.js +3 -0
  48. package/core/repositories/PacketRepository.js +16 -0
  49. package/core/services/AuditService.js +2 -0
  50. package/core/services/ConfigService.js +9 -1
  51. package/core/services/ConfigTransformService.js +34 -2
  52. package/core/services/RequestService.js +58 -5
  53. package/core/services/ServerManagementService.js +59 -4
  54. package/core/services/security/StaticRulesService.js +69 -13
  55. package/core/services/security/TrafficAnalysisService.js +19 -1
  56. package/core/services/security/TrafficToxicFlowService.js +154 -0
  57. package/core/services/security/aauthGraph.js +199 -0
  58. package/core/services/security/aauthParser.js +274 -0
  59. package/core/services/security/aauthSelfTest.js +346 -0
  60. package/core/services/security/index.js +2 -1
  61. package/core/services/security/rules/index.js +25 -59
  62. package/core/services/security/rules/scans/configPermissions.js +91 -0
  63. package/core/services/security/rules/scans/duplicateToolNames.js +85 -0
  64. package/core/services/security/rules/scans/insecureTransport.js +148 -0
  65. package/core/services/security/rules/scans/missingContainment.js +123 -0
  66. package/core/services/security/rules/scans/shellEnvInjection.js +101 -0
  67. package/core/services/security/rules/scans/unsafeDefaults.js +99 -0
  68. package/core/services/security/toolsListFromTrafficParser.js +70 -0
  69. package/core/tui/App.js +144 -0
  70. package/core/tui/FindingsPanel.js +115 -0
  71. package/core/tui/FixPanel.js +132 -0
  72. package/core/tui/Header.js +51 -0
  73. package/core/tui/HelpBar.js +42 -0
  74. package/core/tui/ServersPanel.js +109 -0
  75. package/core/tui/ToxicFlowsPanel.js +100 -0
  76. package/core/tui/h.js +8 -0
  77. package/core/tui/index.js +11 -0
  78. package/core/tui/render.js +22 -0
  79. package/package.json +24 -16
  80. package/ui/dist/assets/index-D6zDrtMV.js +81 -0
  81. package/ui/dist/index.html +1 -1
  82. package/ui/server/controllers/AauthController.js +279 -0
  83. package/ui/server/controllers/RequestController.js +12 -1
  84. package/ui/server/controllers/SecurityFindingsController.js +46 -1
  85. package/ui/server/routes/aauth.js +18 -0
  86. package/ui/server/routes/requests.js +8 -1
  87. package/ui/server/routes/security.js +5 -1
  88. package/ui/server/setup.js +224 -6
  89. package/ui/server/swagger/paths/components.js +55 -0
  90. package/ui/server/swagger/paths/securityTrafficFlows.js +59 -0
  91. package/ui/server/swagger/paths.js +2 -2
  92. package/ui/server/swagger/swagger.js +5 -2
  93. package/ui/server.js +1 -1
  94. package/ui/src/App.jsx +26 -52
  95. package/ui/src/PacketFilters.jsx +31 -1
  96. package/ui/src/PacketList.jsx +2 -2
  97. package/ui/src/Security.jsx +10 -0
  98. package/ui/src/TabNavigation.jsx +8 -0
  99. package/ui/src/components/AAuthBadge.jsx +92 -0
  100. package/ui/src/components/AauthExplorer/AauthExplorerGraph.jsx +231 -0
  101. package/ui/src/components/AauthExplorer/AauthExplorerView.jsx +387 -0
  102. package/ui/src/components/AauthExplorer/NodeDetailPanel.jsx +272 -0
  103. package/ui/src/components/App/ActionMenu.jsx +4 -31
  104. package/ui/src/components/App/ApiDocsButton.jsx +0 -1
  105. package/ui/src/components/App/ShutdownButton.jsx +0 -1
  106. package/ui/src/components/App/useAppState.js +19 -26
  107. package/ui/src/components/DetailsTab/AAuthIdentitySection.jsx +119 -0
  108. package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +2 -0
  109. package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +2 -0
  110. package/ui/src/components/DetectedPathsList.jsx +1 -5
  111. package/ui/src/components/FileInput.jsx +0 -1
  112. package/ui/src/components/PacketFilters/AAuthPostureFilter.jsx +81 -0
  113. package/ui/src/components/RequestRow/RequestRowMain.jsx +7 -1
  114. package/ui/src/components/Security/AAuthPosturePanel.jsx +360 -0
  115. package/ui/src/components/Security/ScannerContent.jsx +33 -1
  116. package/ui/src/components/Security/TrafficToxicFlowsPanel.jsx +253 -0
  117. package/ui/src/components/Security/securityApi.js +15 -0
  118. package/ui/src/components/Security/useSecurity.js +60 -3
  119. package/ui/src/components/ServerControl.jsx +0 -1
  120. package/ui/src/components/TabNavigation/DesktopTabs.jsx +0 -11
  121. package/ui/src/components/TabNavigationIcons.jsx +5 -0
  122. package/ui/src/components/ViewModeTabs.jsx +0 -1
  123. package/ui/src/utils/animations.js +26 -9
  124. package/core/services/security/rules/scans/agentic01GoalHijack.js +0 -130
  125. package/core/services/security/rules/scans/agentic02ToolMisuse.js +0 -129
  126. package/core/services/security/rules/scans/agentic03IdentityAbuse.js +0 -130
  127. package/core/services/security/rules/scans/agentic04SupplyChain.js +0 -130
  128. package/core/services/security/rules/scans/agentic06MemoryPoisoning.js +0 -130
  129. package/core/services/security/rules/scans/agentic07InsecureCommunication.js +0 -135
  130. package/core/services/security/rules/scans/agentic08CascadingFailures.js +0 -135
  131. package/core/services/security/rules/scans/agentic09TrustExploitation.js +0 -135
  132. package/core/services/security/rules/scans/agentic10RogueAgent.js +0 -130
  133. package/core/services/security/rules/scans/hardcodedSecrets.js +0 -130
  134. package/core/services/security/rules/scans/mcp01TokenMismanagement.js +0 -127
  135. package/core/services/security/rules/scans/mcp02ScopeCreep.js +0 -130
  136. package/core/services/security/rules/scans/mcp03ToolPoisoning.js +0 -132
  137. package/core/services/security/rules/scans/mcp04SupplyChain.js +0 -131
  138. package/core/services/security/rules/scans/mcp06PromptInjection.js +0 -200
  139. package/core/services/security/rules/scans/mcp07InsufficientAuth.js +0 -130
  140. package/core/services/security/rules/scans/mcp08LackAudit.js +0 -129
  141. package/core/services/security/rules/scans/mcp09ShadowServers.js +0 -129
  142. package/core/services/security/rules/scans/mcp10ContextInjection.js +0 -130
  143. package/ui/dist/assets/index-CiCSDYf-.js +0 -97
  144. package/ui/server/routes/help.js +0 -44
  145. package/ui/server/swagger/paths/help.js +0 -82
  146. package/ui/src/HelpGuide/HelpGuideContent.jsx +0 -118
  147. package/ui/src/HelpGuide/HelpGuideFooter.jsx +0 -59
  148. package/ui/src/HelpGuide/HelpGuideHeader.jsx +0 -57
  149. package/ui/src/HelpGuide.jsx +0 -78
  150. package/ui/src/IntroTour.jsx +0 -154
  151. package/ui/src/components/App/HelpButton.jsx +0 -90
  152. package/ui/src/components/TourOverlay.jsx +0 -117
  153. package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +0 -120
  154. package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +0 -71
  155. package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +0 -54
  156. package/ui/src/components/TourTooltip/useTooltipPosition.js +0 -135
  157. package/ui/src/components/TourTooltip.jsx +0 -91
  158. package/ui/src/config/tourSteps.jsx +0 -140
package/bin/mcp-shark.js CHANGED
@@ -1,12 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ /**
4
+ * MCP Shark CLI Entry Point
5
+ */
3
6
  import { spawn } from 'node:child_process';
4
7
  import { existsSync, readFileSync } from 'node:fs';
5
8
  import { dirname, join, resolve } from 'node:path';
6
9
  import { fileURLToPath } from 'node:url';
7
10
  import { Command } from 'commander';
8
11
  import open from 'open';
12
+ import { executeDoctor } from '#core/cli/DoctorCommand.js';
13
+ import { executeList } from '#core/cli/ListCommand.js';
14
+ import { executeDiff, executeLock, executeLockVerify } from '#core/cli/LockCommand.js';
15
+ import { executeScan } from '#core/cli/ScanCommand.js';
16
+ import { executeUpdateRules } from '#core/cli/UpdateCommand.js';
17
+ import { executeWatch } from '#core/cli/WatchCommand.js';
18
+ import { displayServeBanner } from '#core/cli/output/Banner.js';
9
19
  import { bootstrapLogger as logger } from '#core/libraries/index.js';
20
+ import { launchTui } from '#core/tui/render.js';
10
21
 
11
22
  const SERVER_URL = 'http://localhost:9853';
12
23
  const BROWSER_OPEN_DELAY = 1000;
@@ -15,35 +26,19 @@ const __filename = fileURLToPath(import.meta.url);
15
26
  const __dirname = dirname(__filename);
16
27
  const rootDir = resolve(__dirname, '..');
17
28
 
18
- /**
19
- * Display welcome banner
20
- */
21
- function displayWelcomeBanner() {
22
- const pkgPath = join(rootDir, 'package.json');
23
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
24
- const version = pkg.version;
25
-
26
- const banner = `
27
- ███╗ ███╗ ██████╗ ██████╗ ███████╗██╗ ██╗ █████╗ ██████╗ ██╗ ██╗
28
- ████╗ ████║██╔════╝██╔══██╗ ██╔════╝██║ ██║██╔══██╗██╔══██╗██║ ██╔╝
29
- ██╔████╔██║██║ ██████╔╝ ███████╗███████║███████║██████╔╝█████╔╝
30
- ██║╚██╔╝██║██║ ██╔═══╝ ╚════██║██╔══██║██╔══██║██╔══██╗██╔═██╗
31
- ██║ ╚═╝ ██║╚██████╗██║ ███████║██║ ██║██║ ██║██║ ██║██║ ██╗
32
- ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
33
-
34
- Aggregate multiple MCP servers into a unified interface
35
- Version: ${version} | Homepage: https://mcpshark.sh
36
- `;
37
-
38
- logger.log(banner);
29
+ function getVersion() {
30
+ try {
31
+ const pkgPath = join(rootDir, 'package.json');
32
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
33
+ return pkg.version;
34
+ } catch (_err) {
35
+ return '0.0.0';
36
+ }
39
37
  }
40
38
 
41
39
  const uiDir = join(rootDir, 'ui');
42
40
  const distDir = join(uiDir, 'dist');
43
41
 
44
- /**
45
- * Validate that UI dist directory exists
46
- */
47
42
  function validateUIBuilt() {
48
43
  if (!existsSync(distDir)) {
49
44
  logger.error(
@@ -54,17 +49,11 @@ function validateUIBuilt() {
54
49
  }
55
50
  }
56
51
 
57
- /**
58
- * Open the browser after a short delay
59
- */
60
52
  async function openBrowser() {
61
- await new Promise((resolve) => setTimeout(resolve, BROWSER_OPEN_DELAY));
53
+ await new Promise((r) => setTimeout(r, BROWSER_OPEN_DELAY));
62
54
  open(SERVER_URL);
63
55
  }
64
56
 
65
- /**
66
- * Start the UI server
67
- */
68
57
  async function startServer(shouldOpenBrowser = false) {
69
58
  logger.info('Starting MCP Shark UI server...');
70
59
  logger.info(`Open ${SERVER_URL} in your browser`);
@@ -93,19 +82,14 @@ async function startServer(shouldOpenBrowser = false) {
93
82
  }
94
83
  });
95
84
 
96
- // Handle process termination
97
85
  const shutdown = async (signal) => {
98
86
  if (shutdownState.isShuttingDown) {
99
87
  return;
100
88
  }
101
89
  shutdownState.isShuttingDown = true;
102
-
103
90
  logger.info('Shutting down...');
104
-
105
- // Send signal to child process
106
91
  serverProcess.kill(signal);
107
92
 
108
- // Wait for child process to exit, with timeout
109
93
  const timeout = setTimeout(() => {
110
94
  logger.info('Forcefully terminating server process...');
111
95
  serverProcess.kill('SIGKILL');
@@ -122,37 +106,147 @@ async function startServer(shouldOpenBrowser = false) {
122
106
  process.on('SIGTERM', () => shutdown('SIGTERM'));
123
107
  }
124
108
 
125
- /**
126
- * Validate that required directories exist
127
- */
128
109
  function validateDirectories() {
129
110
  if (!existsSync(uiDir)) {
130
- logger.error('Error: UI directory not found. Please ensure you are in the correct directory.');
111
+ logger.error('Error: UI directory not found.');
131
112
  process.exit(1);
132
113
  }
133
114
  }
134
115
 
135
116
  /**
136
- * Main execution function
117
+ * Legacy: `npx mcp-shark --open` / `-o` → `serve --open` (before Commander parses argv).
118
+ * Skipped when the first argument is a subcommand (e.g. `serve`, `scan`).
137
119
  */
120
+ function applyLegacyOpenAlias() {
121
+ const argv = process.argv.slice(2);
122
+ if (argv.length === 0) {
123
+ return;
124
+ }
125
+ const first = argv[0];
126
+ if (!first.startsWith('-')) {
127
+ return;
128
+ }
129
+ if (!argv.includes('--open') && !argv.includes('-o')) {
130
+ return;
131
+ }
132
+ const filtered = argv.filter((x) => x !== '--open' && x !== '-o');
133
+ process.argv = [process.argv[0], process.argv[1], 'serve', '--open', ...filtered];
134
+ }
135
+
138
136
  async function main() {
139
- // Display welcome banner
140
- displayWelcomeBanner();
137
+ applyLegacyOpenAlias();
138
+
139
+ const version = getVersion();
141
140
 
142
- // Parse command line options
143
141
  const program = new Command();
144
- program.option('-o, --open', 'Open the browser', false).parse(process.argv);
142
+ program.name('mcp-shark').description('Security scanner for AI agent tools').version(version);
143
+
144
+ program
145
+ .command('scan', { isDefault: true })
146
+ .description('Scan MCP configurations for security issues (default)')
147
+ .option('--fix', 'Auto-fix fixable issues')
148
+ .option('--undo', 'Undo previous --fix changes (use with --fix)')
149
+ .option('--yes', 'Skip confirmation prompt for --fix')
150
+ .option('--walkthrough', 'Show full attack chain narratives')
151
+ .option('--ci', 'CI mode: exit code 1 on critical/high findings')
152
+ .option('--format <format>', 'Output format: terminal, json, sarif, html', 'terminal')
153
+ .option('--output <path>', 'Write report to file (for html format)')
154
+ .option('--strict', 'Count advisory findings in score')
155
+ .option('--ide <name>', 'Scan specific IDE only')
156
+ .option('--rules <path>', 'Load custom YAML rules from directory')
157
+ .option(
158
+ '--refresh-rules',
159
+ 'Download rule packs from registry before scan (HTTPS; configure via env or .mcp-shark/rule-registry.json)'
160
+ )
161
+ .action(async (options) => {
162
+ const exitCode = await executeScan(options);
163
+ if (exitCode !== 0) {
164
+ process.exit(exitCode);
165
+ }
166
+ });
145
167
 
146
- const options = program.opts();
168
+ program
169
+ .command('lock')
170
+ .description('Create or update .mcp-shark.lock (pin tool definitions)')
171
+ .option('--verify', 'Verify current state matches lockfile')
172
+ .action((options) => {
173
+ const exitCode = options.verify ? executeLockVerify() : executeLock(options);
174
+ if (exitCode !== 0) {
175
+ process.exit(exitCode);
176
+ }
177
+ });
147
178
 
148
- // Validate environment
149
- validateDirectories();
179
+ program
180
+ .command('diff')
181
+ .description('Show tool definition changes since last lock')
182
+ .action(() => {
183
+ const exitCode = executeDiff();
184
+ if (exitCode !== 0) {
185
+ process.exit(exitCode);
186
+ }
187
+ });
150
188
 
151
- // Validate UI is built (pre-built files should be included in package)
152
- validateUIBuilt();
189
+ program
190
+ .command('doctor')
191
+ .description('Run environment health checks')
192
+ .action(() => {
193
+ const exitCode = executeDoctor();
194
+ if (exitCode !== 0) {
195
+ process.exit(exitCode);
196
+ }
197
+ });
198
+
199
+ program
200
+ .command('tui')
201
+ .description('Launch interactive terminal UI (lazygit-style)')
202
+ .action(async () => {
203
+ await launchTui();
204
+ });
205
+
206
+ program
207
+ .command('watch')
208
+ .description('Watch config files and re-scan on changes')
209
+ .action(() => {
210
+ const exitCode = executeWatch();
211
+ if (exitCode !== 0) {
212
+ process.exit(exitCode);
213
+ }
214
+ });
215
+
216
+ program
217
+ .command('list')
218
+ .description('Show inventory of all detected MCP servers')
219
+ .option('--format <format>', 'Output format: terminal, json', 'terminal')
220
+ .action((options) => {
221
+ const exitCode = executeList(options);
222
+ if (exitCode !== 0) {
223
+ process.exit(exitCode);
224
+ }
225
+ });
226
+
227
+ program
228
+ .command('update-rules')
229
+ .description('Download latest rule packs from remote registry')
230
+ .option('--source <url>', 'Custom manifest URL (enterprise registries)')
231
+ .action(async (options) => {
232
+ const code = await executeUpdateRules(options);
233
+ if (code !== 0) {
234
+ process.exit(code);
235
+ }
236
+ });
237
+
238
+ program
239
+ .command('serve')
240
+ .description('Start the web UI on localhost:9853')
241
+ .option('-o, --open', 'Open the browser', false)
242
+ .action(async (options) => {
243
+ displayServeBanner();
244
+ validateDirectories();
245
+ validateUIBuilt();
246
+ await startServer(options.open);
247
+ });
153
248
 
154
- // Start the server
155
- await startServer(options.open);
249
+ await program.parseAsync(process.argv);
156
250
  }
157
251
 
158
252
  main().catch((error) => {
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Auto-Fix Engine
3
+ * Orchestrates fix application and renders results.
4
+ * Delegates actual fix logic to FixHandlers.
5
+ */
6
+ import kleur from 'kleur';
7
+ import { applyFix, createEnvExample, undoFixes } from './FixHandlers.js';
8
+ import { S } from './symbols.js';
9
+
10
+ /**
11
+ * Apply fixes for all fixable findings
12
+ * @param {Array} findings - Findings from ScanService
13
+ * @param {object} options
14
+ * @param {boolean} [options.undo] - Undo previous fixes
15
+ * @returns {{ fixed: Array, skipped: Array, errors: Array }}
16
+ */
17
+ export function applyFixes(findings, options = {}) {
18
+ if (options.undo) {
19
+ return undoFixes(findings);
20
+ }
21
+
22
+ const fixable = findings.filter((f) => f.fixable);
23
+ const result = { fixed: [], skipped: [], errors: [] };
24
+
25
+ const envVarsCollected = [];
26
+
27
+ for (const finding of fixable) {
28
+ const fixResult = applyFix(finding, envVarsCollected);
29
+ if (fixResult.success) {
30
+ result.fixed.push(fixResult);
31
+ } else if (fixResult.error) {
32
+ result.errors.push(fixResult);
33
+ } else {
34
+ result.skipped.push(fixResult);
35
+ }
36
+ }
37
+
38
+ if (envVarsCollected.length > 0) {
39
+ createEnvExample(envVarsCollected, result);
40
+ }
41
+
42
+ return result;
43
+ }
44
+
45
+ /**
46
+ * Render fix results to terminal
47
+ */
48
+ export function renderFixResults(fixResult, scoreBefore, scoreAfter) {
49
+ const total = fixResult.fixed.length;
50
+ const attempted = fixResult.fixed.length + fixResult.skipped.length + fixResult.errors.length;
51
+
52
+ if (attempted === 0) {
53
+ console.log(` ${kleur.dim('No auto-fixable issues found')}`);
54
+ return;
55
+ }
56
+
57
+ console.log('');
58
+ console.log(kleur.bold(' Applying fixes...'));
59
+ console.log('');
60
+
61
+ fixResult.fixed.forEach((fix, index) => {
62
+ console.log(` ${kleur.green(S.pass)} [${index + 1}/${total}] ${fix.message}`);
63
+ if (fix.backupPath) {
64
+ console.log(` ${kleur.dim(`Backup: ${fix.backupPath}`)}`);
65
+ }
66
+ });
67
+
68
+ for (const err of fixResult.errors) {
69
+ console.log(` ${kleur.red(S.fail)} ${err.finding?.title || 'Fix'}: ${err.error}`);
70
+ }
71
+
72
+ const remaining = fixResult.skipped.length + fixResult.errors.length;
73
+ console.log('');
74
+ console.log(` ${total} fixed · ${remaining} remaining (manual fix needed)`);
75
+
76
+ if (scoreBefore !== null && scoreAfter !== null) {
77
+ const diff = scoreAfter - scoreBefore;
78
+ const bar = renderProgressBar(scoreAfter);
79
+ console.log(` Shark Score: ${scoreBefore} → ${scoreAfter} (+${diff} points) ${bar}`);
80
+ }
81
+
82
+ console.log('');
83
+ console.log(kleur.dim(' Undo all: npx mcp-shark scan --fix --undo'));
84
+ }
85
+
86
+ /**
87
+ * Render a simple progress bar
88
+ */
89
+ function renderProgressBar(score) {
90
+ const filled = Math.round((score / 100) * 30);
91
+ const empty = 30 - filled;
92
+ return kleur.green('█'.repeat(filled)) + kleur.dim('░'.repeat(empty));
93
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Scans IDE config files and extracts MCP server definitions
3
+ * Supports JSON, TOML, and embedded JSON (settings files with mcpServers key)
4
+ */
5
+ import { existsSync, readFileSync, statSync } from 'node:fs';
6
+ import { basename } from 'node:path';
7
+ import TOML from '@iarna/toml';
8
+ import { IDE_CONFIGS } from './IdeConfigPaths.js';
9
+
10
+ /**
11
+ * Parse a JSON config file for MCP servers
12
+ * @returns {{ servers: object, raw: object } | null}
13
+ */
14
+ function parseJsonConfig(filePath) {
15
+ try {
16
+ const content = readFileSync(filePath, 'utf-8');
17
+ const parsed = JSON.parse(content);
18
+ const servers = parsed.mcpServers || parsed.servers || {};
19
+ return { servers, raw: parsed };
20
+ } catch (_err) {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Parse a TOML config file (Codex) for MCP servers
27
+ * @returns {{ servers: object, raw: object } | null}
28
+ */
29
+ function parseTOMLConfig(filePath) {
30
+ try {
31
+ const content = readFileSync(filePath, 'utf-8');
32
+ const parsed = TOML.parse(content);
33
+ const servers = parsed.mcpServers || parsed.mcp_servers || {};
34
+ return { servers, raw: parsed };
35
+ } catch (_err) {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Parse a JSON settings file that embeds MCP config under a key
42
+ * (Gemini CLI, Continue, Zed)
43
+ * @returns {{ servers: object, raw: object } | null}
44
+ */
45
+ function parseEmbeddedJsonConfig(filePath) {
46
+ try {
47
+ const content = readFileSync(filePath, 'utf-8');
48
+ const parsed = JSON.parse(content);
49
+ const servers = parsed.mcpServers || parsed.mcp_servers || parsed.mcp?.servers || {};
50
+ return { servers, raw: parsed };
51
+ } catch (_err) {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Get file permissions as octal string (Unix only)
58
+ */
59
+ function getFilePermissions(filePath) {
60
+ try {
61
+ const stats = statSync(filePath);
62
+ return (stats.mode & 0o777).toString(8);
63
+ } catch (_err) {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Extract tool definitions from a server config entry
70
+ */
71
+ function extractToolsFromServer(serverConfig) {
72
+ if (!serverConfig || typeof serverConfig !== 'object') {
73
+ return [];
74
+ }
75
+ if (serverConfig.tools) {
76
+ return serverConfig.tools;
77
+ }
78
+ return [];
79
+ }
80
+
81
+ /**
82
+ * Scan all IDEs and return discovered MCP configurations
83
+ * @param {object} options
84
+ * @param {string} [options.ide] - Filter to specific IDE name
85
+ * @returns {Array<object>} Array of discovered IDE configs with servers
86
+ */
87
+ export function scanIdeConfigs(options = {}) {
88
+ const results = [];
89
+ const ideFilter = options.ide ? options.ide.toLowerCase() : null;
90
+
91
+ for (const ideConfig of IDE_CONFIGS) {
92
+ if (ideFilter && ideConfig.name.toLowerCase() !== ideFilter) {
93
+ continue;
94
+ }
95
+
96
+ const detected = detectIdeConfig(ideConfig);
97
+ results.push(detected);
98
+ }
99
+
100
+ return results;
101
+ }
102
+
103
+ /**
104
+ * Detect and parse a single IDE config
105
+ */
106
+ function detectIdeConfig(ideConfig) {
107
+ const result = {
108
+ name: ideConfig.name,
109
+ found: false,
110
+ configPath: null,
111
+ displayPath: null,
112
+ permissions: null,
113
+ servers: {},
114
+ serverCount: 0,
115
+ toolCount: 0,
116
+ error: null,
117
+ };
118
+
119
+ for (const configPath of ideConfig.paths) {
120
+ if (!existsSync(configPath)) {
121
+ continue;
122
+ }
123
+
124
+ result.found = true;
125
+ result.configPath = configPath;
126
+ result.displayPath = configPath.replace(process.env.HOME || '', '~');
127
+ result.permissions = getFilePermissions(configPath);
128
+
129
+ const parsed = parseConfigByType(ideConfig.parser, configPath);
130
+ if (!parsed) {
131
+ result.error = `Failed to parse ${basename(configPath)}`;
132
+ break;
133
+ }
134
+
135
+ result.servers = parsed.servers;
136
+ result.serverCount = Object.keys(parsed.servers).length;
137
+
138
+ const toolCount = countTools(parsed.servers);
139
+ result.toolCount = toolCount;
140
+ break;
141
+ }
142
+
143
+ return result;
144
+ }
145
+
146
+ /**
147
+ * Route to the correct parser based on config type
148
+ */
149
+ function parseConfigByType(parser, filePath) {
150
+ if (parser === 'json') {
151
+ return parseJsonConfig(filePath);
152
+ }
153
+ if (parser === 'toml') {
154
+ return parseTOMLConfig(filePath);
155
+ }
156
+ if (parser === 'jsonEmbedded') {
157
+ return parseEmbeddedJsonConfig(filePath);
158
+ }
159
+ return null;
160
+ }
161
+
162
+ /**
163
+ * Count total tools across all servers
164
+ */
165
+ function countTools(servers) {
166
+ const total = Object.values(servers).reduce((sum, server) => {
167
+ const tools = extractToolsFromServer(server);
168
+ return sum + (Array.isArray(tools) ? tools.length : Object.keys(tools).length);
169
+ }, 0);
170
+ return total;
171
+ }
172
+
173
+ /**
174
+ * Get a flat list of all servers across all IDEs
175
+ */
176
+ export function getAllServers(ideResults) {
177
+ const servers = [];
178
+ for (const ide of ideResults) {
179
+ if (!ide.found) {
180
+ continue;
181
+ }
182
+ for (const [serverName, serverConfig] of Object.entries(ide.servers)) {
183
+ servers.push({
184
+ name: serverName,
185
+ ide: ide.name,
186
+ configPath: ide.configPath,
187
+ config: serverConfig,
188
+ tools: extractToolsFromServer(serverConfig),
189
+ });
190
+ }
191
+ }
192
+ return servers;
193
+ }