@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.
- package/README.md +482 -56
- package/bin/mcp-shark.js +146 -52
- package/core/cli/AutoFixEngine.js +93 -0
- package/core/cli/ConfigScanner.js +193 -0
- package/core/cli/DataLoader.js +200 -0
- package/core/cli/DeclarativeRuleEngine.js +363 -0
- package/core/cli/DoctorCommand.js +218 -0
- package/core/cli/FixHandlers.js +222 -0
- package/core/cli/HtmlReportGenerator.js +203 -0
- package/core/cli/IdeConfigPaths.js +175 -0
- package/core/cli/ListCommand.js +255 -0
- package/core/cli/LockCommand.js +164 -0
- package/core/cli/LockDiffEngine.js +152 -0
- package/core/cli/RuleRegistryConfig.js +131 -0
- package/core/cli/ScanCommand.js +244 -0
- package/core/cli/ScanService.js +200 -0
- package/core/cli/SecretDetector.js +92 -0
- package/core/cli/SharkScoreCalculator.js +109 -0
- package/core/cli/ToolClassifications.js +51 -0
- package/core/cli/ToxicFlowAnalyzer.js +212 -0
- package/core/cli/UpdateCommand.js +188 -0
- package/core/cli/WalkthroughGenerator.js +195 -0
- package/core/cli/WatchCommand.js +129 -0
- package/core/cli/YamlRuleEngine.js +197 -0
- package/core/cli/data/rule-packs/aauth-visibility.json +117 -0
- package/core/cli/data/rule-packs/agentic-security-2026.json +180 -0
- package/core/cli/data/rule-packs/general-security.json +173 -0
- package/core/cli/data/rule-packs/owasp-mcp-2026.json +244 -0
- package/core/cli/data/rule-packs/toxic-flow-heuristics.json +21 -0
- package/core/cli/data/rule-sources.json +5 -0
- package/core/cli/data/secret-patterns.json +18 -0
- package/core/cli/data/tool-classifications.json +111 -0
- package/core/cli/data/toxic-flow-rules.json +47 -0
- package/core/cli/index.js +23 -0
- package/core/cli/output/Banner.js +52 -0
- package/core/cli/output/Formatter.js +183 -0
- package/core/cli/output/JsonFormatter.js +106 -0
- package/core/cli/output/index.js +16 -0
- package/core/cli/secureRegistryFetch.js +157 -0
- package/core/cli/symbols.js +16 -0
- package/core/configs/environment.js +3 -1
- package/core/configs/index.js +3 -64
- package/core/container/DependencyContainer.js +4 -1
- package/core/mcp-server/index.js +4 -1
- package/core/mcp-server/server/external/all.js +10 -3
- package/core/mcp-server/server/external/config.js +62 -5
- package/core/models/RequestFilters.js +3 -0
- package/core/repositories/PacketRepository.js +16 -0
- package/core/services/AuditService.js +2 -0
- package/core/services/ConfigService.js +9 -1
- package/core/services/ConfigTransformService.js +34 -2
- package/core/services/RequestService.js +58 -5
- package/core/services/ServerManagementService.js +59 -4
- package/core/services/security/StaticRulesService.js +69 -13
- package/core/services/security/TrafficAnalysisService.js +19 -1
- package/core/services/security/TrafficToxicFlowService.js +154 -0
- package/core/services/security/aauthGraph.js +199 -0
- package/core/services/security/aauthParser.js +274 -0
- package/core/services/security/aauthSelfTest.js +346 -0
- package/core/services/security/index.js +2 -1
- package/core/services/security/rules/index.js +25 -59
- package/core/services/security/rules/scans/configPermissions.js +91 -0
- package/core/services/security/rules/scans/duplicateToolNames.js +85 -0
- package/core/services/security/rules/scans/insecureTransport.js +148 -0
- package/core/services/security/rules/scans/missingContainment.js +123 -0
- package/core/services/security/rules/scans/shellEnvInjection.js +101 -0
- package/core/services/security/rules/scans/unsafeDefaults.js +99 -0
- package/core/services/security/toolsListFromTrafficParser.js +70 -0
- package/core/tui/App.js +144 -0
- package/core/tui/FindingsPanel.js +115 -0
- package/core/tui/FixPanel.js +132 -0
- package/core/tui/Header.js +51 -0
- package/core/tui/HelpBar.js +42 -0
- package/core/tui/ServersPanel.js +109 -0
- package/core/tui/ToxicFlowsPanel.js +100 -0
- package/core/tui/h.js +8 -0
- package/core/tui/index.js +11 -0
- package/core/tui/render.js +22 -0
- package/package.json +24 -16
- package/ui/dist/assets/index-D6zDrtMV.js +81 -0
- package/ui/dist/index.html +1 -1
- package/ui/server/controllers/AauthController.js +279 -0
- package/ui/server/controllers/RequestController.js +12 -1
- package/ui/server/controllers/SecurityFindingsController.js +46 -1
- package/ui/server/routes/aauth.js +18 -0
- package/ui/server/routes/requests.js +8 -1
- package/ui/server/routes/security.js +5 -1
- package/ui/server/setup.js +224 -6
- package/ui/server/swagger/paths/components.js +55 -0
- package/ui/server/swagger/paths/securityTrafficFlows.js +59 -0
- package/ui/server/swagger/paths.js +2 -2
- package/ui/server/swagger/swagger.js +5 -2
- package/ui/server.js +1 -1
- package/ui/src/App.jsx +26 -52
- package/ui/src/PacketFilters.jsx +31 -1
- package/ui/src/PacketList.jsx +2 -2
- package/ui/src/Security.jsx +10 -0
- package/ui/src/TabNavigation.jsx +8 -0
- package/ui/src/components/AAuthBadge.jsx +92 -0
- package/ui/src/components/AauthExplorer/AauthExplorerGraph.jsx +231 -0
- package/ui/src/components/AauthExplorer/AauthExplorerView.jsx +387 -0
- package/ui/src/components/AauthExplorer/NodeDetailPanel.jsx +272 -0
- package/ui/src/components/App/ActionMenu.jsx +4 -31
- package/ui/src/components/App/ApiDocsButton.jsx +0 -1
- package/ui/src/components/App/ShutdownButton.jsx +0 -1
- package/ui/src/components/App/useAppState.js +19 -26
- package/ui/src/components/DetailsTab/AAuthIdentitySection.jsx +119 -0
- package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +2 -0
- package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +2 -0
- package/ui/src/components/DetectedPathsList.jsx +1 -5
- package/ui/src/components/FileInput.jsx +0 -1
- package/ui/src/components/PacketFilters/AAuthPostureFilter.jsx +81 -0
- package/ui/src/components/RequestRow/RequestRowMain.jsx +7 -1
- package/ui/src/components/Security/AAuthPosturePanel.jsx +360 -0
- package/ui/src/components/Security/ScannerContent.jsx +33 -1
- package/ui/src/components/Security/TrafficToxicFlowsPanel.jsx +253 -0
- package/ui/src/components/Security/securityApi.js +15 -0
- package/ui/src/components/Security/useSecurity.js +60 -3
- package/ui/src/components/ServerControl.jsx +0 -1
- package/ui/src/components/TabNavigation/DesktopTabs.jsx +0 -11
- package/ui/src/components/TabNavigationIcons.jsx +5 -0
- package/ui/src/components/ViewModeTabs.jsx +0 -1
- package/ui/src/utils/animations.js +26 -9
- package/core/services/security/rules/scans/agentic01GoalHijack.js +0 -130
- package/core/services/security/rules/scans/agentic02ToolMisuse.js +0 -129
- package/core/services/security/rules/scans/agentic03IdentityAbuse.js +0 -130
- package/core/services/security/rules/scans/agentic04SupplyChain.js +0 -130
- package/core/services/security/rules/scans/agentic06MemoryPoisoning.js +0 -130
- package/core/services/security/rules/scans/agentic07InsecureCommunication.js +0 -135
- package/core/services/security/rules/scans/agentic08CascadingFailures.js +0 -135
- package/core/services/security/rules/scans/agentic09TrustExploitation.js +0 -135
- package/core/services/security/rules/scans/agentic10RogueAgent.js +0 -130
- package/core/services/security/rules/scans/hardcodedSecrets.js +0 -130
- package/core/services/security/rules/scans/mcp01TokenMismanagement.js +0 -127
- package/core/services/security/rules/scans/mcp02ScopeCreep.js +0 -130
- package/core/services/security/rules/scans/mcp03ToolPoisoning.js +0 -132
- package/core/services/security/rules/scans/mcp04SupplyChain.js +0 -131
- package/core/services/security/rules/scans/mcp06PromptInjection.js +0 -200
- package/core/services/security/rules/scans/mcp07InsufficientAuth.js +0 -130
- package/core/services/security/rules/scans/mcp08LackAudit.js +0 -129
- package/core/services/security/rules/scans/mcp09ShadowServers.js +0 -129
- package/core/services/security/rules/scans/mcp10ContextInjection.js +0 -130
- package/ui/dist/assets/index-CiCSDYf-.js +0 -97
- package/ui/server/routes/help.js +0 -44
- package/ui/server/swagger/paths/help.js +0 -82
- package/ui/src/HelpGuide/HelpGuideContent.jsx +0 -118
- package/ui/src/HelpGuide/HelpGuideFooter.jsx +0 -59
- package/ui/src/HelpGuide/HelpGuideHeader.jsx +0 -57
- package/ui/src/HelpGuide.jsx +0 -78
- package/ui/src/IntroTour.jsx +0 -154
- package/ui/src/components/App/HelpButton.jsx +0 -90
- package/ui/src/components/TourOverlay.jsx +0 -117
- package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +0 -120
- package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +0 -71
- package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +0 -54
- package/ui/src/components/TourTooltip/useTooltipPosition.js +0 -135
- package/ui/src/components/TourTooltip.jsx +0 -91
- 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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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((
|
|
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.
|
|
111
|
+
logger.error('Error: UI directory not found.');
|
|
131
112
|
process.exit(1);
|
|
132
113
|
}
|
|
133
114
|
}
|
|
134
115
|
|
|
135
116
|
/**
|
|
136
|
-
*
|
|
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
|
-
|
|
140
|
-
|
|
137
|
+
applyLegacyOpenAlias();
|
|
138
|
+
|
|
139
|
+
const version = getVersion();
|
|
141
140
|
|
|
142
|
-
// Parse command line options
|
|
143
141
|
const program = new Command();
|
|
144
|
-
program.
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
+
}
|