@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/ui/server/setup.js
CHANGED
|
@@ -7,14 +7,15 @@ import { WebSocketServer } from 'ws';
|
|
|
7
7
|
|
|
8
8
|
import { DependencyContainer } from '#core';
|
|
9
9
|
import { getDatabaseFile, prepareAppDataSpaces } from '#core/configs/index.js';
|
|
10
|
+
import { Defaults } from '#core/constants/Defaults.js';
|
|
10
11
|
import { Server as ServerConstants } from '#core/constants/Server.js';
|
|
11
12
|
import { openDb } from '#core/db/init.js';
|
|
12
13
|
|
|
14
|
+
import { createAauthRoutes } from './routes/aauth.js';
|
|
13
15
|
import { createBackupRoutes } from './routes/backups/index.js';
|
|
14
16
|
import { createCompositeRoutes } from './routes/composite/index.js';
|
|
15
17
|
import { createConfigRoutes } from './routes/config.js';
|
|
16
18
|
import { createConversationsRoutes } from './routes/conversations.js';
|
|
17
|
-
import { createHelpRoutes } from './routes/help.js';
|
|
18
19
|
import { createLogsRoutes } from './routes/logs.js';
|
|
19
20
|
import { createPlaygroundRoutes } from './routes/playground.js';
|
|
20
21
|
import { createRequestsRoutes } from './routes/requests.js';
|
|
@@ -77,7 +78,6 @@ export function createUIServer() {
|
|
|
77
78
|
(logEntry) => broadcastLogUpdate(clients, logEntry),
|
|
78
79
|
cleanup
|
|
79
80
|
);
|
|
80
|
-
const helpRoutes = createHelpRoutes();
|
|
81
81
|
const playgroundRoutes = createPlaygroundRoutes(container);
|
|
82
82
|
const smartScanRoutes = createSmartScanRoutes(container);
|
|
83
83
|
const settingsRoutes = createSettingsRoutes(container);
|
|
@@ -98,6 +98,15 @@ export function createUIServer() {
|
|
|
98
98
|
|
|
99
99
|
app.get('/api/statistics', statisticsRoutes.getStatistics);
|
|
100
100
|
|
|
101
|
+
// AAuth visibility (observation only — no verification, no enforcement)
|
|
102
|
+
const aauthRoutes = createAauthRoutes(container);
|
|
103
|
+
app.get('/api/aauth/posture', aauthRoutes.getPosture);
|
|
104
|
+
app.get('/api/aauth/missions', aauthRoutes.getMissions);
|
|
105
|
+
app.get('/api/aauth/graph', aauthRoutes.getGraph);
|
|
106
|
+
app.get('/api/aauth/upstreams', aauthRoutes.getUpstreams);
|
|
107
|
+
app.get('/api/aauth/node/:category/:id', aauthRoutes.getNodePackets);
|
|
108
|
+
app.post('/api/aauth/self-test', aauthRoutes.runSelfTest);
|
|
109
|
+
|
|
101
110
|
app.get('/api/composite/logs', logsRoutes.getLogs);
|
|
102
111
|
app.post('/api/composite/logs/clear', logsRoutes.clearLogs);
|
|
103
112
|
app.get('/api/composite/logs/export', logsRoutes.exportLogs);
|
|
@@ -118,10 +127,6 @@ export function createUIServer() {
|
|
|
118
127
|
app.post('/api/composite/shutdown', compositeRoutes.shutdown);
|
|
119
128
|
app.get('/api/composite/servers', compositeRoutes.getServers);
|
|
120
129
|
|
|
121
|
-
app.get('/api/help/state', helpRoutes.getState);
|
|
122
|
-
app.post('/api/help/dismiss', helpRoutes.dismiss);
|
|
123
|
-
app.post('/api/help/reset', helpRoutes.reset);
|
|
124
|
-
|
|
125
130
|
app.post('/api/playground/proxy', playgroundRoutes.proxyRequest);
|
|
126
131
|
|
|
127
132
|
app.post('/api/smartscan/scans', smartScanRoutes.createScan);
|
|
@@ -147,6 +152,8 @@ export function createUIServer() {
|
|
|
147
152
|
app.get('/api/security/history', securityRoutes.getScanHistory);
|
|
148
153
|
app.post('/api/security/findings/clear', securityRoutes.clearFindings);
|
|
149
154
|
app.delete('/api/security/scan/:scanId', securityRoutes.deleteScanFindings);
|
|
155
|
+
app.get('/api/security/traffic-toxic-flows', securityRoutes.getTrafficToxicFlows);
|
|
156
|
+
app.post('/api/security/traffic-toxic-flows/replay', securityRoutes.replayTrafficToxicFlows);
|
|
150
157
|
|
|
151
158
|
// Security routes - YARA engine
|
|
152
159
|
app.get('/api/security/engine/status', securityRoutes.getEngineStatus);
|
|
@@ -203,5 +210,216 @@ export function createUIServer() {
|
|
|
203
210
|
|
|
204
211
|
const intervalId = setInterval(checkTimestampAndNotify, ServerConstants.PACKET_CHECK_INTERVAL_MS);
|
|
205
212
|
|
|
213
|
+
// Bring the proxy up automatically so a fresh `npm start` (including on
|
|
214
|
+
// a brand-new machine) gives the user a working setup without any clicks.
|
|
215
|
+
//
|
|
216
|
+
// 1. If ~/.mcp-shark/mcps.json already declares upstreams, start the
|
|
217
|
+
// proxy with it directly. This handles restarts, `testbed:up`, and
|
|
218
|
+
// any hand-edited config.
|
|
219
|
+
// 2. Otherwise, look for a real editor MCP config (Cursor, Windsurf,
|
|
220
|
+
// Codex). If one exists with actual upstreams (i.e. not just our
|
|
221
|
+
// own self-route from a previous run), run the full Setup flow
|
|
222
|
+
// against it: convert -> write mcps.json -> start proxy -> patch
|
|
223
|
+
// the editor config so the editor routes through us.
|
|
224
|
+
// 3. If neither path applies, the UI stays in monitoring-only mode and
|
|
225
|
+
// the Setup panel is still available for manual configuration.
|
|
226
|
+
autoBootstrapProxy({
|
|
227
|
+
container,
|
|
228
|
+
processState,
|
|
229
|
+
setMcpSharkProcess,
|
|
230
|
+
mcpSharkLogs,
|
|
231
|
+
clients,
|
|
232
|
+
logger,
|
|
233
|
+
}).catch((err) => {
|
|
234
|
+
logger?.warn(
|
|
235
|
+
{ error: err?.message },
|
|
236
|
+
'Auto-bootstrap of MCP proxy failed; user can run Setup manually'
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
206
240
|
return { server, cleanup, logger, wss };
|
|
207
241
|
}
|
|
242
|
+
|
|
243
|
+
async function autoBootstrapProxy(deps) {
|
|
244
|
+
const { container, logger } = deps;
|
|
245
|
+
const configService = container.getService('config');
|
|
246
|
+
const mcpsJsonPath = configService.getMcpConfigPath();
|
|
247
|
+
|
|
248
|
+
// Phase 1: existing healthy mcps.json wins.
|
|
249
|
+
if (await tryStartFromExistingMcpsJson({ ...deps, mcpsJsonPath })) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Phase 2: only auto-import on a *brand-new* install — i.e. no
|
|
254
|
+
// mcps.json on disk at all. If the user has an existing (but empty)
|
|
255
|
+
// mcps.json, that's an explicit "monitoring-only" choice and we
|
|
256
|
+
// respect it.
|
|
257
|
+
const mcpsJsonExists = configService.fileExists(mcpsJsonPath);
|
|
258
|
+
if (!mcpsJsonExists) {
|
|
259
|
+
if (await tryImportFromDetectedEditorConfig({ ...deps, mcpsJsonPath })) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
logger?.info(
|
|
265
|
+
{ mcpsJsonExists },
|
|
266
|
+
'No upstreams to auto-start; running in monitoring-only mode (Setup panel still available)'
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function tryStartFromExistingMcpsJson({
|
|
271
|
+
container,
|
|
272
|
+
processState,
|
|
273
|
+
setMcpSharkProcess,
|
|
274
|
+
mcpSharkLogs,
|
|
275
|
+
clients,
|
|
276
|
+
logger,
|
|
277
|
+
mcpsJsonPath,
|
|
278
|
+
}) {
|
|
279
|
+
const configService = container.getService('config');
|
|
280
|
+
const serverManagementService = container.getService('serverManagement');
|
|
281
|
+
|
|
282
|
+
if (!configService.fileExists(mcpsJsonPath)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const content = configService.readConfigFile(mcpsJsonPath);
|
|
287
|
+
const parsed = configService.tryParseJson(content, mcpsJsonPath);
|
|
288
|
+
if (!parsed) {
|
|
289
|
+
logger?.warn(
|
|
290
|
+
{ path: mcpsJsonPath },
|
|
291
|
+
'Existing mcps.json is not valid JSON; skipping auto-start'
|
|
292
|
+
);
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const upstreamCount =
|
|
297
|
+
Object.keys(parsed.servers || {}).length + Object.keys(parsed.mcpServers || {}).length;
|
|
298
|
+
if (upstreamCount === 0) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
logger?.info(
|
|
303
|
+
{ path: mcpsJsonPath, upstreams: upstreamCount },
|
|
304
|
+
'Auto-starting MCP proxy from existing mcps.json'
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
await serverManagementService.startServer({
|
|
308
|
+
configPath: mcpsJsonPath,
|
|
309
|
+
port: Defaults.DEFAULT_MCP_SERVER_PORT,
|
|
310
|
+
onLog: forwardLogEntry(mcpSharkLogs, clients),
|
|
311
|
+
onError: (err) => {
|
|
312
|
+
logger?.error({ error: err?.message, stack: err?.stack }, 'Auto-started MCP proxy crashed');
|
|
313
|
+
},
|
|
314
|
+
onReady: () => {
|
|
315
|
+
logger?.info('MCP proxy ready (auto-started from existing mcps.json)');
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
setMcpSharkProcess(processState, serverManagementService.getServerInstance());
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function tryImportFromDetectedEditorConfig({
|
|
324
|
+
container,
|
|
325
|
+
processState,
|
|
326
|
+
setMcpSharkProcess,
|
|
327
|
+
mcpSharkLogs,
|
|
328
|
+
clients,
|
|
329
|
+
logger,
|
|
330
|
+
mcpsJsonPath,
|
|
331
|
+
}) {
|
|
332
|
+
const configService = container.getService('config');
|
|
333
|
+
const serverManagementService = container.getService('serverManagement');
|
|
334
|
+
|
|
335
|
+
const detected = configService.detectConfigFiles();
|
|
336
|
+
const candidate = pickImportableEditorConfig(detected, configService, mcpsJsonPath, logger);
|
|
337
|
+
if (!candidate) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
logger?.info(
|
|
342
|
+
{ editor: candidate.editor, path: candidate.path, upstreams: candidate.upstreamCount },
|
|
343
|
+
'Auto-importing MCP servers from detected editor config'
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const result = await serverManagementService.setup({
|
|
347
|
+
filePath: candidate.path,
|
|
348
|
+
port: Defaults.DEFAULT_MCP_SERVER_PORT,
|
|
349
|
+
onLog: forwardLogEntry(mcpSharkLogs, clients),
|
|
350
|
+
onError: (err) => {
|
|
351
|
+
logger?.error({ error: err?.message, stack: err?.stack }, 'Auto-imported MCP proxy crashed');
|
|
352
|
+
},
|
|
353
|
+
onReady: () => {
|
|
354
|
+
logger?.info(
|
|
355
|
+
{ editor: candidate.editor },
|
|
356
|
+
'MCP proxy ready (auto-imported from editor config)'
|
|
357
|
+
);
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
if (!result?.success) {
|
|
362
|
+
logger?.warn(
|
|
363
|
+
{ editor: candidate.editor, error: result?.error },
|
|
364
|
+
'Auto-import setup did not complete successfully'
|
|
365
|
+
);
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
setMcpSharkProcess(processState, serverManagementService.getServerInstance());
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function pickImportableEditorConfig(detected, configService, mcpsJsonPath, logger) {
|
|
374
|
+
if (!Array.isArray(detected)) return null;
|
|
375
|
+
|
|
376
|
+
for (const entry of detected) {
|
|
377
|
+
if (!entry?.exists || !entry.path) continue;
|
|
378
|
+
|
|
379
|
+
// Skip our own proxy config — that's covered by phase 1.
|
|
380
|
+
try {
|
|
381
|
+
const a = configService.fileService.resolveFilePath(entry.path);
|
|
382
|
+
const b = configService.fileService.resolveFilePath(mcpsJsonPath);
|
|
383
|
+
if (a && b && a === b) continue;
|
|
384
|
+
} catch {
|
|
385
|
+
// best-effort comparison
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const content = configService.readConfigFile(entry.path);
|
|
389
|
+
if (!content) continue;
|
|
390
|
+
const parsed = configService.tryParseJson(content, entry.path);
|
|
391
|
+
if (!parsed) continue;
|
|
392
|
+
|
|
393
|
+
// If the editor config is already patched (only the mcp-shark
|
|
394
|
+
// self-route), we can't import anything useful from it. The user
|
|
395
|
+
// either configured this themselves (great — phase 1 handled it via
|
|
396
|
+
// mcps.json) or there is nothing to import yet.
|
|
397
|
+
if (configService.isConfigPatched(parsed)) {
|
|
398
|
+
logger?.debug?.(
|
|
399
|
+
{ editor: entry.editor, path: entry.path },
|
|
400
|
+
'Skipping already-patched editor config during auto-import'
|
|
401
|
+
);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const services = configService.extractServices(parsed) || [];
|
|
406
|
+
if (services.length === 0) continue;
|
|
407
|
+
|
|
408
|
+
return { ...entry, upstreamCount: services.length };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function forwardLogEntry(mcpSharkLogs, clients) {
|
|
415
|
+
return (logEntry) => {
|
|
416
|
+
mcpSharkLogs.push(logEntry);
|
|
417
|
+
for (const ws of clients) {
|
|
418
|
+
try {
|
|
419
|
+
ws.send(JSON.stringify({ type: 'log', payload: logEntry }));
|
|
420
|
+
} catch {
|
|
421
|
+
// best-effort; UI tolerates dropped log messages
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
}
|
|
@@ -72,5 +72,60 @@ export const components = {
|
|
|
72
72
|
results: { type: 'object' },
|
|
73
73
|
},
|
|
74
74
|
},
|
|
75
|
+
TrafficToxicFlowItem: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
risk: { type: 'string', example: 'HIGH' },
|
|
79
|
+
title: { type: 'string' },
|
|
80
|
+
source: { type: 'string' },
|
|
81
|
+
target: { type: 'string' },
|
|
82
|
+
scenario: { type: 'string' },
|
|
83
|
+
sourceIde: { type: 'string' },
|
|
84
|
+
targetIde: { type: 'string' },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
TrafficToxicFlowServerMeta: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
name: { type: 'string' },
|
|
91
|
+
toolCount: { type: 'integer' },
|
|
92
|
+
updatedAt: { type: 'integer' },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
TrafficToxicFlowSnapshot: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
success: { type: 'boolean', example: true },
|
|
99
|
+
toxicFlows: {
|
|
100
|
+
type: 'array',
|
|
101
|
+
items: { $ref: '#/components/schemas/TrafficToxicFlowItem' },
|
|
102
|
+
},
|
|
103
|
+
servers: {
|
|
104
|
+
type: 'array',
|
|
105
|
+
items: { $ref: '#/components/schemas/TrafficToxicFlowServerMeta' },
|
|
106
|
+
},
|
|
107
|
+
computedAt: { type: 'integer', nullable: true },
|
|
108
|
+
lastReplayPacketCount: { type: 'integer' },
|
|
109
|
+
note: { type: 'string' },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
TrafficToxicFlowReplayResponse: {
|
|
113
|
+
allOf: [
|
|
114
|
+
{ $ref: '#/components/schemas/TrafficToxicFlowSnapshot' },
|
|
115
|
+
{
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
replay: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties: {
|
|
121
|
+
packetRows: { type: 'integer' },
|
|
122
|
+
serverCount: { type: 'integer' },
|
|
123
|
+
flowCount: { type: 'integer' },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
75
130
|
},
|
|
76
131
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Analysis — traffic-derived toxic flow endpoints
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const securityTrafficFlowsPaths = {
|
|
6
|
+
'/api/security/traffic-toxic-flows': {
|
|
7
|
+
get: {
|
|
8
|
+
tags: ['Security'],
|
|
9
|
+
summary: 'Get toxic flows from proxy traffic',
|
|
10
|
+
description:
|
|
11
|
+
'Returns the latest cross-server toxic-flow heuristics built from tools/list responses observed on the HTTP proxy. In-memory model; use replay to rebuild from the packet database.',
|
|
12
|
+
responses: {
|
|
13
|
+
200: {
|
|
14
|
+
description: 'Snapshot with toxicFlows, servers, computedAt, note',
|
|
15
|
+
content: {
|
|
16
|
+
'application/json': {
|
|
17
|
+
schema: { $ref: '#/components/schemas/TrafficToxicFlowSnapshot' },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
503: {
|
|
22
|
+
description: 'Traffic toxic flow service unavailable',
|
|
23
|
+
content: {
|
|
24
|
+
'application/json': {
|
|
25
|
+
schema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
success: { type: 'boolean', example: false },
|
|
29
|
+
error: { type: 'string' },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
'/api/security/traffic-toxic-flows/replay': {
|
|
39
|
+
post: {
|
|
40
|
+
tags: ['Security'],
|
|
41
|
+
summary: 'Replay toxic flows from stored packets',
|
|
42
|
+
description:
|
|
43
|
+
'Scans stored HTTP response packets for tools/list-style JSON-RPC results and rebuilds the toxic-flow registry.',
|
|
44
|
+
responses: {
|
|
45
|
+
200: {
|
|
46
|
+
description: 'Snapshot plus replay stats (packetRows, serverCount, flowCount)',
|
|
47
|
+
content: {
|
|
48
|
+
'application/json': {
|
|
49
|
+
schema: { $ref: '#/components/schemas/TrafficToxicFlowReplayResponse' },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
503: {
|
|
54
|
+
description: 'Traffic toxic flow service unavailable',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -7,10 +7,10 @@ import { backupsPaths } from './paths/backups.js';
|
|
|
7
7
|
import { components } from './paths/components.js';
|
|
8
8
|
import { configPaths } from './paths/config.js';
|
|
9
9
|
import { conversationsPaths } from './paths/conversations.js';
|
|
10
|
-
import { helpPaths } from './paths/help.js';
|
|
11
10
|
import { logsPaths } from './paths/logs.js';
|
|
12
11
|
import { playgroundPaths } from './paths/playground.js';
|
|
13
12
|
import { requestsPaths } from './paths/requests.js';
|
|
13
|
+
import { securityTrafficFlowsPaths } from './paths/securityTrafficFlows.js';
|
|
14
14
|
import { serverManagementPaths } from './paths/serverManagement.js';
|
|
15
15
|
import { sessionsPaths } from './paths/sessions.js';
|
|
16
16
|
import { settingsPaths } from './paths/settings.js';
|
|
@@ -22,6 +22,7 @@ import { statisticsPaths } from './paths/statistics.js';
|
|
|
22
22
|
*/
|
|
23
23
|
export const paths = {
|
|
24
24
|
...requestsPaths,
|
|
25
|
+
...securityTrafficFlowsPaths,
|
|
25
26
|
...sessionsPaths,
|
|
26
27
|
...conversationsPaths,
|
|
27
28
|
...statisticsPaths,
|
|
@@ -29,7 +30,6 @@ export const paths = {
|
|
|
29
30
|
...configPaths,
|
|
30
31
|
...backupsPaths,
|
|
31
32
|
...serverManagementPaths,
|
|
32
|
-
...helpPaths,
|
|
33
33
|
...playgroundPaths,
|
|
34
34
|
...smartScanPaths,
|
|
35
35
|
...settingsPaths,
|
|
@@ -4,7 +4,7 @@ export const swaggerSpec = {
|
|
|
4
4
|
openapi: '3.0.0',
|
|
5
5
|
info: {
|
|
6
6
|
title: 'MCP Shark API',
|
|
7
|
-
version: '1.
|
|
7
|
+
version: '1.6.0',
|
|
8
8
|
description:
|
|
9
9
|
'API documentation for MCP Shark - A powerful monitoring and debugging tool for Model Context Protocol (MCP) servers. Provides deep visibility into every request and response.',
|
|
10
10
|
contact: {
|
|
@@ -27,10 +27,13 @@ export const swaggerSpec = {
|
|
|
27
27
|
{ name: 'Config', description: 'Configuration file management' },
|
|
28
28
|
{ name: 'Backups', description: 'Configuration backup management' },
|
|
29
29
|
{ name: 'Server Management', description: 'MCP Shark server lifecycle management' },
|
|
30
|
-
{ name: 'Help', description: 'Help and tour management' },
|
|
31
30
|
{ name: 'Playground', description: 'MCP playground for testing tools and resources' },
|
|
32
31
|
{ name: 'Smart Scan', description: 'AI-powered security analysis for MCP servers' },
|
|
33
32
|
{ name: 'Settings', description: 'Application settings' },
|
|
33
|
+
{
|
|
34
|
+
name: 'Security',
|
|
35
|
+
description: 'Local static analysis findings and traffic-derived toxic flows (HTTP proxy)',
|
|
36
|
+
},
|
|
34
37
|
],
|
|
35
38
|
paths,
|
|
36
39
|
components,
|
package/ui/server.js
CHANGED
|
@@ -29,7 +29,7 @@ export async function runUIServer() {
|
|
|
29
29
|
`Port ${port} is already in use. Please stop the existing server or use a different port.`
|
|
30
30
|
);
|
|
31
31
|
bootstrapLogger.error(
|
|
32
|
-
`\n❌ Port ${port} is already in use.\n Please stop the existing server or set MCP_SHARK_PORT environment variable to use a different port.\n`
|
|
32
|
+
`\n❌ Port ${port} is already in use.\n Please stop the existing server or set the UI_PORT (or MCP_SHARK_PORT) environment variable to use a different port.\n`
|
|
33
33
|
);
|
|
34
34
|
} else {
|
|
35
35
|
logger?.error({ error: error.message, stack: error.stack }, 'Server error');
|
package/ui/src/App.jsx
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { useEffect
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
2
|
import CompositeLogs from './CompositeLogs';
|
|
3
3
|
import CompositeSetup from './CompositeSetup';
|
|
4
|
-
import IntroTour from './IntroTour';
|
|
5
4
|
import Security from './Security';
|
|
6
5
|
import ShutdownPage from './ShutdownPage';
|
|
7
6
|
import SmartScan from './SmartScan';
|
|
8
7
|
import TabNavigation from './TabNavigation';
|
|
8
|
+
import AauthExplorerView from './components/AauthExplorer/AauthExplorerView';
|
|
9
9
|
import ActionMenu from './components/App/ActionMenu';
|
|
10
10
|
import TrafficTab from './components/App/TrafficTab';
|
|
11
11
|
import { useAppState } from './components/App/useAppState';
|
|
12
12
|
import McpPlayground from './components/McpPlayground';
|
|
13
|
-
import { tourSteps } from './config/tourSteps.jsx';
|
|
14
13
|
import { colors } from './theme';
|
|
15
14
|
import { fadeIn } from './utils/animations';
|
|
16
15
|
|
|
@@ -25,12 +24,9 @@ function App() {
|
|
|
25
24
|
setFilters,
|
|
26
25
|
stats,
|
|
27
26
|
firstRequestTime,
|
|
28
|
-
showTour,
|
|
29
|
-
setShowTour,
|
|
30
27
|
prevTabRef,
|
|
31
28
|
loadRequests,
|
|
32
29
|
} = useAppState();
|
|
33
|
-
const [tourKey, setTourKey] = useState(0);
|
|
34
30
|
|
|
35
31
|
useEffect(() => {
|
|
36
32
|
if (prevTabRef.current !== activeTab) {
|
|
@@ -51,54 +47,10 @@ function App() {
|
|
|
51
47
|
background: colors.bgPrimary,
|
|
52
48
|
}}
|
|
53
49
|
>
|
|
54
|
-
{
|
|
55
|
-
<IntroTour
|
|
56
|
-
key={tourKey}
|
|
57
|
-
steps={tourSteps}
|
|
58
|
-
onComplete={() => setShowTour(false)}
|
|
59
|
-
onSkip={() => setShowTour(false)}
|
|
60
|
-
onStepChange={(stepIndex) => {
|
|
61
|
-
const step = tourSteps[stepIndex];
|
|
62
|
-
if (step) {
|
|
63
|
-
if (
|
|
64
|
-
step.target === '[data-tour="setup-tab"]' ||
|
|
65
|
-
step.target === '[data-tour="detected-editors"]' ||
|
|
66
|
-
step.target === '[data-tour="select-file"]' ||
|
|
67
|
-
step.target === '[data-tour="start-button"]'
|
|
68
|
-
) {
|
|
69
|
-
if (activeTab !== 'setup') {
|
|
70
|
-
setActiveTab('setup');
|
|
71
|
-
}
|
|
72
|
-
} else if (step.target === '[data-tour="traffic-tab"]') {
|
|
73
|
-
if (activeTab !== 'traffic') {
|
|
74
|
-
setActiveTab('traffic');
|
|
75
|
-
}
|
|
76
|
-
} else if (step.target === '[data-tour="smart-scan-tab"]') {
|
|
77
|
-
if (activeTab !== 'smart-scan') {
|
|
78
|
-
setActiveTab('smart-scan');
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}}
|
|
83
|
-
/>
|
|
84
|
-
)}
|
|
85
|
-
<div style={{ position: 'relative' }} data-tour="tabs">
|
|
50
|
+
<div style={{ position: 'relative' }}>
|
|
86
51
|
<TabNavigation activeTab={activeTab} onTabChange={setActiveTab} />
|
|
87
52
|
</div>
|
|
88
|
-
<ActionMenu
|
|
89
|
-
onHelpClick={() => {
|
|
90
|
-
if (showTour) {
|
|
91
|
-
setShowTour(false);
|
|
92
|
-
setTourKey((prev) => prev + 1);
|
|
93
|
-
setTimeout(() => {
|
|
94
|
-
setShowTour(true);
|
|
95
|
-
}, 100);
|
|
96
|
-
} else {
|
|
97
|
-
setTourKey((prev) => prev + 1);
|
|
98
|
-
setShowTour(true);
|
|
99
|
-
}
|
|
100
|
-
}}
|
|
101
|
-
/>
|
|
53
|
+
<ActionMenu />
|
|
102
54
|
|
|
103
55
|
{activeTab === 'traffic' && (
|
|
104
56
|
<TrafficTab
|
|
@@ -166,6 +118,28 @@ function App() {
|
|
|
166
118
|
/>
|
|
167
119
|
</div>
|
|
168
120
|
)}
|
|
121
|
+
|
|
122
|
+
{activeTab === 'aauth-explorer' && (
|
|
123
|
+
<div
|
|
124
|
+
data-tab-content
|
|
125
|
+
style={{ flex: 1, overflow: 'hidden', width: '100%', height: '100%' }}
|
|
126
|
+
>
|
|
127
|
+
<AauthExplorerView
|
|
128
|
+
onOpenPacket={(frameNumber) => {
|
|
129
|
+
if (frameNumber == null) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const target = requests?.find?.((r) => r.frame_number === frameNumber);
|
|
133
|
+
if (target) {
|
|
134
|
+
setSelected(target);
|
|
135
|
+
} else {
|
|
136
|
+
setSelected({ frame_number: frameNumber });
|
|
137
|
+
}
|
|
138
|
+
setActiveTab('traffic');
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
169
143
|
</div>
|
|
170
144
|
);
|
|
171
145
|
}
|
package/ui/src/PacketFilters.jsx
CHANGED
|
@@ -3,6 +3,7 @@ import anime from 'animejs';
|
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import AlertModal from './components/AlertModal';
|
|
5
5
|
import ConfirmationModal from './components/ConfirmationModal';
|
|
6
|
+
import AAuthPostureFilter from './components/PacketFilters/AAuthPostureFilter';
|
|
6
7
|
import ExportControls from './components/PacketFilters/ExportControls';
|
|
7
8
|
import FilterInput from './components/PacketFilters/FilterInput';
|
|
8
9
|
import { colors, fonts } from './theme';
|
|
@@ -44,6 +45,15 @@ function RequestFilters({ filters, onFilterChange, stats, onClear }) {
|
|
|
44
45
|
if (filters.jsonrpcId) {
|
|
45
46
|
queryParams.append('jsonrpcId', filters.jsonrpcId);
|
|
46
47
|
}
|
|
48
|
+
if (filters.aauthPosture) {
|
|
49
|
+
queryParams.append('aauthPosture', filters.aauthPosture);
|
|
50
|
+
}
|
|
51
|
+
if (filters.aauthAgent) {
|
|
52
|
+
queryParams.append('aauthAgent', filters.aauthAgent);
|
|
53
|
+
}
|
|
54
|
+
if (filters.aauthMission) {
|
|
55
|
+
queryParams.append('aauthMission', filters.aauthMission);
|
|
56
|
+
}
|
|
47
57
|
queryParams.append('format', format);
|
|
48
58
|
|
|
49
59
|
const response = await fetch(`/api/requests/export?${queryParams}`);
|
|
@@ -67,7 +77,6 @@ function RequestFilters({ filters, onFilterChange, stats, onClear }) {
|
|
|
67
77
|
return (
|
|
68
78
|
<div
|
|
69
79
|
ref={filtersRef}
|
|
70
|
-
data-tour="filters"
|
|
71
80
|
style={{
|
|
72
81
|
padding: '12px 16px',
|
|
73
82
|
borderBottom: `1px solid ${colors.borderLight}`,
|
|
@@ -159,6 +168,27 @@ function RequestFilters({ filters, onFilterChange, stats, onClear }) {
|
|
|
159
168
|
style={{ width: '150px' }}
|
|
160
169
|
/>
|
|
161
170
|
|
|
171
|
+
<AAuthPostureFilter
|
|
172
|
+
value={filters.aauthPosture || null}
|
|
173
|
+
onChange={(posture) => onFilterChange({ ...filters, aauthPosture: posture })}
|
|
174
|
+
/>
|
|
175
|
+
|
|
176
|
+
<FilterInput
|
|
177
|
+
type="text"
|
|
178
|
+
placeholder="AAuth Agent (aauth:..)"
|
|
179
|
+
value={filters.aauthAgent || ''}
|
|
180
|
+
onChange={(e) => onFilterChange({ ...filters, aauthAgent: e.target.value || null })}
|
|
181
|
+
style={{ width: '200px' }}
|
|
182
|
+
/>
|
|
183
|
+
|
|
184
|
+
<FilterInput
|
|
185
|
+
type="text"
|
|
186
|
+
placeholder="AAuth Mission..."
|
|
187
|
+
value={filters.aauthMission || ''}
|
|
188
|
+
onChange={(e) => onFilterChange({ ...filters, aauthMission: e.target.value || null })}
|
|
189
|
+
style={{ width: '180px' }}
|
|
190
|
+
/>
|
|
191
|
+
|
|
162
192
|
<ExportControls stats={stats} onExport={handleExport} />
|
|
163
193
|
|
|
164
194
|
<button
|
package/ui/src/PacketList.jsx
CHANGED
|
@@ -38,11 +38,11 @@ function RequestList({ requests, selected, onSelect, firstRequestTime }) {
|
|
|
38
38
|
if (requests.length > prevRequestsLengthRef.current) {
|
|
39
39
|
const newRows = Array.from(rows).slice(prevRequestsLengthRef.current);
|
|
40
40
|
if (newRows.length > 0) {
|
|
41
|
-
staggerIn(newRows, { delay: 30, duration: 300 });
|
|
41
|
+
staggerIn(newRows, { delay: 30, duration: 300, fade: false });
|
|
42
42
|
}
|
|
43
43
|
} else {
|
|
44
44
|
// Animate all rows if the list was reset
|
|
45
|
-
staggerIn(rows, { delay: 20, duration: 300 });
|
|
45
|
+
staggerIn(rows, { delay: 20, duration: 300, fade: false });
|
|
46
46
|
}
|
|
47
47
|
prevRequestsLengthRef.current = requests.length;
|
|
48
48
|
}
|
package/ui/src/Security.jsx
CHANGED
|
@@ -30,6 +30,11 @@ function Security({ onNavigateToSmartScan, onNavigateToSetup }) {
|
|
|
30
30
|
runningServersCount,
|
|
31
31
|
// Scan complete state
|
|
32
32
|
scanComplete,
|
|
33
|
+
trafficToxicSnapshot,
|
|
34
|
+
trafficToxicLoading,
|
|
35
|
+
trafficToxicError,
|
|
36
|
+
loadTrafficToxicFlows,
|
|
37
|
+
replayTrafficToxicFlows,
|
|
33
38
|
// YARA rules
|
|
34
39
|
communityRules,
|
|
35
40
|
engineStatus,
|
|
@@ -103,6 +108,11 @@ function Security({ onNavigateToSmartScan, onNavigateToSetup }) {
|
|
|
103
108
|
showHistory={showHistory}
|
|
104
109
|
serversAvailable={runningServersCount > 0}
|
|
105
110
|
scanComplete={scanComplete}
|
|
111
|
+
trafficToxicSnapshot={trafficToxicSnapshot}
|
|
112
|
+
trafficToxicLoading={trafficToxicLoading}
|
|
113
|
+
trafficToxicError={trafficToxicError}
|
|
114
|
+
loadTrafficToxicFlows={loadTrafficToxicFlows}
|
|
115
|
+
replayTrafficToxicFlows={replayTrafficToxicFlows}
|
|
106
116
|
/>
|
|
107
117
|
)}
|
|
108
118
|
|