@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
@@ -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
+ }