@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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IDE configuration paths for MCP server detection
|
|
3
|
+
* Maps IDE names to their known config file locations across platforms
|
|
4
|
+
*/
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { Environment } from '#core/configs/environment.js';
|
|
8
|
+
|
|
9
|
+
const HOME = homedir();
|
|
10
|
+
const PLATFORM = process.platform;
|
|
11
|
+
|
|
12
|
+
function macAppSupport(appName) {
|
|
13
|
+
return join(HOME, 'Library', 'Application Support', appName);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function winAppData(appName) {
|
|
17
|
+
const appData = process.env.APPDATA || join(Environment.getUserProfile(), 'AppData', 'Roaming');
|
|
18
|
+
return join(appData, appName);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function linuxConfig(appName) {
|
|
22
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || join(HOME, '.config');
|
|
23
|
+
return join(xdgConfig, appName);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build platform-aware config paths for an IDE
|
|
28
|
+
*/
|
|
29
|
+
function buildPaths(mac, win, linux) {
|
|
30
|
+
const paths = [];
|
|
31
|
+
if (PLATFORM === 'darwin' && mac) {
|
|
32
|
+
paths.push(...(Array.isArray(mac) ? mac : [mac]));
|
|
33
|
+
}
|
|
34
|
+
if (PLATFORM === 'win32' && win) {
|
|
35
|
+
paths.push(...(Array.isArray(win) ? win : [win]));
|
|
36
|
+
}
|
|
37
|
+
if (PLATFORM === 'linux' && linux) {
|
|
38
|
+
paths.push(...(Array.isArray(linux) ? linux : [linux]));
|
|
39
|
+
}
|
|
40
|
+
return paths;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* All known IDE config definitions
|
|
45
|
+
* Each entry: { name, paths: string[], parser: 'json' | 'toml' | 'jsonEmbedded' }
|
|
46
|
+
*/
|
|
47
|
+
export const IDE_CONFIGS = [
|
|
48
|
+
{
|
|
49
|
+
name: 'Cursor',
|
|
50
|
+
parser: 'json',
|
|
51
|
+
paths: [
|
|
52
|
+
join(HOME, '.cursor', 'mcp.json'),
|
|
53
|
+
join(process.cwd(), '.cursor', 'mcp.json'),
|
|
54
|
+
...buildPaths(null, join(Environment.getUserProfile(), '.cursor', 'mcp.json'), null),
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'Claude Desktop',
|
|
59
|
+
parser: 'json',
|
|
60
|
+
paths: buildPaths(
|
|
61
|
+
join(macAppSupport('Claude'), 'claude_desktop_config.json'),
|
|
62
|
+
join(winAppData('Claude'), 'claude_desktop_config.json'),
|
|
63
|
+
join(linuxConfig('Claude'), 'claude_desktop_config.json')
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'Claude Code',
|
|
68
|
+
parser: 'json',
|
|
69
|
+
paths: [join(HOME, '.claude.json'), join(HOME, '.claude', 'settings.json')],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'VS Code',
|
|
73
|
+
parser: 'json',
|
|
74
|
+
paths: [join(process.cwd(), '.vscode', 'mcp.json'), join(HOME, '.vscode', 'mcp.json')],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'Windsurf',
|
|
78
|
+
parser: 'json',
|
|
79
|
+
paths: [
|
|
80
|
+
join(HOME, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
81
|
+
...buildPaths(
|
|
82
|
+
null,
|
|
83
|
+
join(Environment.getUserProfile(), '.codeium', 'windsurf', 'mcp_config.json'),
|
|
84
|
+
null
|
|
85
|
+
),
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'Codex',
|
|
90
|
+
parser: 'toml',
|
|
91
|
+
paths: [
|
|
92
|
+
join(Environment.getCodexHome(), 'config.toml'),
|
|
93
|
+
join(HOME, '.codex', 'config.toml'),
|
|
94
|
+
...buildPaths(null, join(Environment.getUserProfile(), '.codex', 'config.toml'), null),
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'Gemini CLI',
|
|
99
|
+
parser: 'jsonEmbedded',
|
|
100
|
+
paths: [join(HOME, '.gemini', 'settings.json')],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'Continue',
|
|
104
|
+
parser: 'jsonEmbedded',
|
|
105
|
+
paths: [join(HOME, '.continue', 'config.json')],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'Cline',
|
|
109
|
+
parser: 'json',
|
|
110
|
+
paths: buildPaths(
|
|
111
|
+
join(
|
|
112
|
+
macAppSupport('Code'),
|
|
113
|
+
'User',
|
|
114
|
+
'globalStorage',
|
|
115
|
+
'saoudrizwan.claude-dev',
|
|
116
|
+
'settings',
|
|
117
|
+
'cline_mcp_settings.json'
|
|
118
|
+
),
|
|
119
|
+
join(
|
|
120
|
+
winAppData('Code'),
|
|
121
|
+
'User',
|
|
122
|
+
'globalStorage',
|
|
123
|
+
'saoudrizwan.claude-dev',
|
|
124
|
+
'settings',
|
|
125
|
+
'cline_mcp_settings.json'
|
|
126
|
+
),
|
|
127
|
+
join(
|
|
128
|
+
linuxConfig('Code'),
|
|
129
|
+
'User',
|
|
130
|
+
'globalStorage',
|
|
131
|
+
'saoudrizwan.claude-dev',
|
|
132
|
+
'settings',
|
|
133
|
+
'cline_mcp_settings.json'
|
|
134
|
+
)
|
|
135
|
+
),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'Amp',
|
|
139
|
+
parser: 'json',
|
|
140
|
+
paths: [join(HOME, '.amp', 'mcp.json')],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'Kiro',
|
|
144
|
+
parser: 'json',
|
|
145
|
+
paths: [join(HOME, '.kiro', 'mcp.json')],
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'Zed',
|
|
149
|
+
parser: 'jsonEmbedded',
|
|
150
|
+
paths: buildPaths(
|
|
151
|
+
join(HOME, '.config', 'zed', 'settings.json'),
|
|
152
|
+
null,
|
|
153
|
+
join(linuxConfig('zed'), 'settings.json')
|
|
154
|
+
),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'Augment',
|
|
158
|
+
parser: 'json',
|
|
159
|
+
paths: [join(HOME, '.augment', 'mcp.json')],
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'Roo Code',
|
|
163
|
+
parser: 'json',
|
|
164
|
+
paths: [join(HOME, '.roo-code', 'mcp.json')],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'Project',
|
|
168
|
+
parser: 'json',
|
|
169
|
+
paths: [
|
|
170
|
+
join(process.cwd(), 'mcp.json'),
|
|
171
|
+
join(process.cwd(), '.mcp.json'),
|
|
172
|
+
join(process.cwd(), '.mcp', 'config.json'),
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
];
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List Command
|
|
3
|
+
* Displays a beautiful inventory of all detected MCP servers,
|
|
4
|
+
* their transport type, tool count, and config source.
|
|
5
|
+
*/
|
|
6
|
+
import Table from 'cli-table3';
|
|
7
|
+
import kleur from 'kleur';
|
|
8
|
+
import { inspectServerConfigForAauth } from '#core/services/security/aauthParser.js';
|
|
9
|
+
import { getAllServers, scanIdeConfigs } from './ConfigScanner.js';
|
|
10
|
+
import { TOOL_CLASSIFICATIONS } from './ToolClassifications.js';
|
|
11
|
+
import { S } from './symbols.js';
|
|
12
|
+
|
|
13
|
+
const TRANSPORT_LABELS = {
|
|
14
|
+
stdio: kleur.cyan('stdio'),
|
|
15
|
+
sse: kleur.magenta('sse'),
|
|
16
|
+
http: kleur.yellow('http'),
|
|
17
|
+
streamable: kleur.green('streamable'),
|
|
18
|
+
unknown: kleur.dim('unknown'),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Execute the list command — server inventory
|
|
23
|
+
* @param {object} options
|
|
24
|
+
* @param {string} [options.format] - Output format: terminal, json
|
|
25
|
+
* @returns {number} Exit code
|
|
26
|
+
*/
|
|
27
|
+
export function executeList(options = {}) {
|
|
28
|
+
const ideResults = scanIdeConfigs();
|
|
29
|
+
const servers = getAllServers(ideResults);
|
|
30
|
+
|
|
31
|
+
if (servers.length === 0) {
|
|
32
|
+
console.log(`\n ${kleur.yellow(S.warn)} No MCP servers found\n`);
|
|
33
|
+
console.log(kleur.dim(' Searched 15 IDEs. Install an MCP server to get started.'));
|
|
34
|
+
console.log('');
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (options.format === 'json') {
|
|
39
|
+
return renderJsonInventory(servers, ideResults);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return renderTerminalInventory(servers, ideResults);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render server inventory as a beautiful terminal table
|
|
47
|
+
*/
|
|
48
|
+
function renderTerminalInventory(servers, ideResults) {
|
|
49
|
+
const foundIdes = ideResults.filter((r) => r.found);
|
|
50
|
+
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(kleur.bold(' MCP Server Inventory'));
|
|
53
|
+
console.log(kleur.dim(` Found ${servers.length} servers across ${foundIdes.length} IDEs`));
|
|
54
|
+
console.log('');
|
|
55
|
+
|
|
56
|
+
const table = new Table({
|
|
57
|
+
head: ['Server', 'IDE', 'Transport', 'Tools', 'Capabilities'].map((h) => kleur.bold(h)),
|
|
58
|
+
chars: {
|
|
59
|
+
top: '─',
|
|
60
|
+
'top-mid': '┬',
|
|
61
|
+
'top-left': '┌',
|
|
62
|
+
'top-right': '┐',
|
|
63
|
+
bottom: '─',
|
|
64
|
+
'bottom-mid': '┴',
|
|
65
|
+
'bottom-left': '└',
|
|
66
|
+
'bottom-right': '┘',
|
|
67
|
+
left: '│',
|
|
68
|
+
'left-mid': '├',
|
|
69
|
+
mid: '─',
|
|
70
|
+
'mid-mid': '┼',
|
|
71
|
+
right: '│',
|
|
72
|
+
'right-mid': '┤',
|
|
73
|
+
middle: '│',
|
|
74
|
+
},
|
|
75
|
+
style: { head: [], border: [] },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
for (const server of servers) {
|
|
79
|
+
const transport = detectTransport(server.config);
|
|
80
|
+
const toolCount = getToolCount(server);
|
|
81
|
+
const capabilities = getServerCapabilities(server.name);
|
|
82
|
+
|
|
83
|
+
table.push([
|
|
84
|
+
kleur.white(server.name),
|
|
85
|
+
kleur.dim(server.ide),
|
|
86
|
+
TRANSPORT_LABELS[transport] || TRANSPORT_LABELS.unknown,
|
|
87
|
+
toolCount > 0 ? kleur.bold(String(toolCount)) : kleur.dim('?'),
|
|
88
|
+
capabilities || kleur.dim('—'),
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(table.toString());
|
|
93
|
+
console.log('');
|
|
94
|
+
|
|
95
|
+
renderAauthSummary(servers);
|
|
96
|
+
renderIdeSummary(ideResults);
|
|
97
|
+
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Print a single-line AAuth advertisement summary across all detected servers.
|
|
103
|
+
* Visibility-only: this only checks for AAuth-shaped fields in the static config
|
|
104
|
+
* (agent IDs, JWKS URLs, .well-known/aauth references). It does not connect.
|
|
105
|
+
*/
|
|
106
|
+
function renderAauthSummary(servers) {
|
|
107
|
+
let advertised = 0;
|
|
108
|
+
for (const server of servers) {
|
|
109
|
+
if (inspectServerConfigForAauth(server.config)) {
|
|
110
|
+
advertised += 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const remaining = servers.length - advertised;
|
|
114
|
+
if (servers.length === 0) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
console.log(kleur.bold(' AAuth Visibility'));
|
|
118
|
+
console.log(
|
|
119
|
+
` ${kleur.green(S.pass)} ${advertised} server${advertised === 1 ? '' : 's'} advertise AAuth · ${kleur.dim(`${remaining} do not`)}`
|
|
120
|
+
);
|
|
121
|
+
console.log(
|
|
122
|
+
kleur.dim(
|
|
123
|
+
' Observed only — mcp-shark does not verify AAuth identity. See https://www.aauth.dev'
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Render a summary of which IDEs were detected
|
|
131
|
+
*/
|
|
132
|
+
function renderIdeSummary(ideResults) {
|
|
133
|
+
const found = ideResults.filter((r) => r.found);
|
|
134
|
+
const notFound = ideResults.filter((r) => !r.found);
|
|
135
|
+
|
|
136
|
+
console.log(kleur.bold(' IDE Detection'));
|
|
137
|
+
for (const ide of found) {
|
|
138
|
+
console.log(` ${kleur.green(S.pass)} ${ide.name} (${ide.serverCount} servers)`);
|
|
139
|
+
}
|
|
140
|
+
if (notFound.length > 0) {
|
|
141
|
+
console.log(` ${kleur.dim(`${notFound.length} IDEs not installed`)}`);
|
|
142
|
+
}
|
|
143
|
+
console.log('');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Render inventory as JSON
|
|
148
|
+
*/
|
|
149
|
+
function renderJsonInventory(servers, ideResults) {
|
|
150
|
+
const output = {
|
|
151
|
+
total_servers: servers.length,
|
|
152
|
+
ides_found: ideResults.filter((r) => r.found).length,
|
|
153
|
+
servers: servers.map((s) => ({
|
|
154
|
+
name: s.name,
|
|
155
|
+
ide: s.ide,
|
|
156
|
+
config_path: s.configPath,
|
|
157
|
+
transport: detectTransport(s.config),
|
|
158
|
+
tool_count: getToolCount(s),
|
|
159
|
+
command: s.config?.command || null,
|
|
160
|
+
args_present: hasArgsPresent(s.config),
|
|
161
|
+
aauth: inspectServerConfigForAauth(s.config),
|
|
162
|
+
})),
|
|
163
|
+
};
|
|
164
|
+
console.log(JSON.stringify(output, null, 2));
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Detect transport type from server config
|
|
170
|
+
*/
|
|
171
|
+
function detectTransport(config) {
|
|
172
|
+
if (!config) {
|
|
173
|
+
return 'unknown';
|
|
174
|
+
}
|
|
175
|
+
if (config.command) {
|
|
176
|
+
return 'stdio';
|
|
177
|
+
}
|
|
178
|
+
if (config.url?.includes('/sse')) {
|
|
179
|
+
return 'sse';
|
|
180
|
+
}
|
|
181
|
+
if (config.url) {
|
|
182
|
+
return config.transport || 'http';
|
|
183
|
+
}
|
|
184
|
+
if (config.transport) {
|
|
185
|
+
return config.transport;
|
|
186
|
+
}
|
|
187
|
+
return 'unknown';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get number of tools for a server
|
|
192
|
+
*/
|
|
193
|
+
function getToolCount(server) {
|
|
194
|
+
if (Array.isArray(server.tools)) {
|
|
195
|
+
return server.tools.length;
|
|
196
|
+
}
|
|
197
|
+
if (server.tools && typeof server.tools === 'object') {
|
|
198
|
+
return Object.keys(server.tools).length;
|
|
199
|
+
}
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Whether server config includes non-empty args (boolean only for JSON output — avoids leaking secrets).
|
|
205
|
+
*/
|
|
206
|
+
export function hasArgsPresent(config) {
|
|
207
|
+
const args = config?.args;
|
|
208
|
+
if (args == null) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(args)) {
|
|
212
|
+
return args.length > 0;
|
|
213
|
+
}
|
|
214
|
+
if (typeof args === 'object') {
|
|
215
|
+
return Object.keys(args).length > 0;
|
|
216
|
+
}
|
|
217
|
+
return String(args).length > 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get known capabilities from the classification database
|
|
222
|
+
* (values are per-tool capability strings, not flat boolean flags)
|
|
223
|
+
*/
|
|
224
|
+
export function getServerCapabilities(serverName) {
|
|
225
|
+
const classification = TOOL_CLASSIFICATIONS[serverName];
|
|
226
|
+
if (!classification || typeof classification !== 'object') {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const capSet = new Set();
|
|
231
|
+
for (const cap of Object.values(classification)) {
|
|
232
|
+
if (typeof cap === 'string' && cap) {
|
|
233
|
+
capSet.add(cap);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const caps = [];
|
|
238
|
+
if (capSet.has('reads_secrets')) {
|
|
239
|
+
caps.push(kleur.red('secrets'));
|
|
240
|
+
}
|
|
241
|
+
if (capSet.has('writes_code')) {
|
|
242
|
+
caps.push(kleur.yellow('code'));
|
|
243
|
+
}
|
|
244
|
+
if (capSet.has('sends_external')) {
|
|
245
|
+
caps.push(kleur.magenta('network'));
|
|
246
|
+
}
|
|
247
|
+
if (capSet.has('modifies_infra')) {
|
|
248
|
+
caps.push(kleur.red('infra'));
|
|
249
|
+
}
|
|
250
|
+
if (capSet.has('ingests_untrusted')) {
|
|
251
|
+
caps.push(kleur.cyan('untrusted'));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return caps.length > 0 ? caps.join(', ') : null;
|
|
255
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock / Diff Commands
|
|
3
|
+
* Creates and verifies .mcp-shark.lock — SHA-256 hashes of tool definitions
|
|
4
|
+
* Detects rug pull attacks (Catalog §1.5)
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import kleur from 'kleur';
|
|
9
|
+
import { getAllServers, scanIdeConfigs } from './ConfigScanner.js';
|
|
10
|
+
import { computeDiff, countParameters, hashToolDefinition, renderDiff } from './LockDiffEngine.js';
|
|
11
|
+
import { S } from './symbols.js';
|
|
12
|
+
|
|
13
|
+
const LOCKFILE_NAME = '.mcp-shark.lock';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Execute the lock command — create or update lockfile
|
|
17
|
+
*/
|
|
18
|
+
export function executeLock(options = {}) {
|
|
19
|
+
const ideResults = scanIdeConfigs();
|
|
20
|
+
const servers = getAllServers(ideResults);
|
|
21
|
+
|
|
22
|
+
if (servers.length === 0) {
|
|
23
|
+
console.log(` ${kleur.yellow(S.warn)} No MCP servers found to lock`);
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const lockData = buildLockData(servers);
|
|
28
|
+
const lockfilePath = join(process.cwd(), LOCKFILE_NAME);
|
|
29
|
+
const content = JSON.stringify(lockData, null, 2);
|
|
30
|
+
|
|
31
|
+
writeFileSync(lockfilePath, content, 'utf-8');
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log(` ${kleur.green(S.pass)} Lockfile created: ${LOCKFILE_NAME}`);
|
|
34
|
+
console.log(` ${kleur.dim(`${Object.keys(lockData.servers).length} servers locked`)}`);
|
|
35
|
+
|
|
36
|
+
if (options.verify) {
|
|
37
|
+
return verifyLockfile(lockData);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log(kleur.dim(' Commit this file to detect future tool definition changes.'));
|
|
42
|
+
console.log(kleur.dim(' Verify: npx mcp-shark lock --verify'));
|
|
43
|
+
console.log('');
|
|
44
|
+
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Execute lock --verify — compare current state against lockfile
|
|
50
|
+
*/
|
|
51
|
+
export function executeLockVerify() {
|
|
52
|
+
const lockfilePath = join(process.cwd(), LOCKFILE_NAME);
|
|
53
|
+
|
|
54
|
+
if (!existsSync(lockfilePath)) {
|
|
55
|
+
console.log(` ${kleur.red(S.fail)} No ${LOCKFILE_NAME} found`);
|
|
56
|
+
console.log(kleur.dim(' Run: npx mcp-shark lock'));
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const lockData = JSON.parse(readFileSync(lockfilePath, 'utf-8'));
|
|
61
|
+
return verifyLockfile(lockData);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Execute the diff command — show changes since last lock
|
|
66
|
+
*/
|
|
67
|
+
export function executeDiff() {
|
|
68
|
+
const lockfilePath = join(process.cwd(), LOCKFILE_NAME);
|
|
69
|
+
|
|
70
|
+
if (!existsSync(lockfilePath)) {
|
|
71
|
+
console.log(` ${kleur.yellow(S.warn)} No ${LOCKFILE_NAME} found`);
|
|
72
|
+
console.log(kleur.dim(' Run: npx mcp-shark lock'));
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const lockData = JSON.parse(readFileSync(lockfilePath, 'utf-8'));
|
|
77
|
+
const ideResults = scanIdeConfigs();
|
|
78
|
+
const currentServers = getAllServers(ideResults);
|
|
79
|
+
|
|
80
|
+
const changes = computeDiff(lockData, currentServers);
|
|
81
|
+
renderDiff(changes);
|
|
82
|
+
|
|
83
|
+
return changes.length > 0 ? 1 : 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build lockfile data structure
|
|
88
|
+
*/
|
|
89
|
+
function buildLockData(servers) {
|
|
90
|
+
const now = new Date().toISOString();
|
|
91
|
+
const serverEntries = {};
|
|
92
|
+
|
|
93
|
+
for (const server of servers) {
|
|
94
|
+
const toolHashes = buildToolHashes(server, now);
|
|
95
|
+
|
|
96
|
+
serverEntries[server.name] = {
|
|
97
|
+
source: server.ide,
|
|
98
|
+
config_path: server.configPath,
|
|
99
|
+
tools: toolHashes,
|
|
100
|
+
tool_count: (Array.isArray(server.tools) ? server.tools : []).length,
|
|
101
|
+
locked_at: now,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
version: 1,
|
|
107
|
+
created: now,
|
|
108
|
+
updated: now,
|
|
109
|
+
shark_version: getSharkVersion(),
|
|
110
|
+
servers: serverEntries,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build tool hash entries for a server
|
|
116
|
+
*/
|
|
117
|
+
function buildToolHashes(server, now) {
|
|
118
|
+
const toolHashes = {};
|
|
119
|
+
const tools = Array.isArray(server.tools) ? server.tools : [];
|
|
120
|
+
|
|
121
|
+
for (const tool of tools) {
|
|
122
|
+
const toolObj = typeof tool === 'string' ? { name: tool } : tool;
|
|
123
|
+
const hash = hashToolDefinition(toolObj);
|
|
124
|
+
toolHashes[toolObj.name || 'unknown'] = {
|
|
125
|
+
hash: `sha256:${hash}`,
|
|
126
|
+
description_length: (toolObj.description || '').length,
|
|
127
|
+
parameter_count: countParameters(toolObj),
|
|
128
|
+
pinned_at: now,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return toolHashes;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Verify current state matches lockfile
|
|
137
|
+
*/
|
|
138
|
+
function verifyLockfile(lockData) {
|
|
139
|
+
const ideResults = scanIdeConfigs();
|
|
140
|
+
const currentServers = getAllServers(ideResults);
|
|
141
|
+
const changes = computeDiff(lockData, currentServers);
|
|
142
|
+
|
|
143
|
+
if (changes.length === 0) {
|
|
144
|
+
console.log(` ${kleur.green(S.pass)} All definitions match lockfile`);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log(` ${kleur.red(S.fail)} ${changes.length} changes detected`);
|
|
149
|
+
renderDiff(changes);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get shark version from package.json
|
|
155
|
+
*/
|
|
156
|
+
function getSharkVersion() {
|
|
157
|
+
try {
|
|
158
|
+
const pkgPath = join(import.meta.dirname, '..', '..', 'package.json');
|
|
159
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
160
|
+
return pkg.version;
|
|
161
|
+
} catch (_err) {
|
|
162
|
+
return '1.0.0';
|
|
163
|
+
}
|
|
164
|
+
}
|