@mcp-shark/mcp-shark 1.5.13 → 1.6.0

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 (100) hide show
  1. package/README.md +396 -59
  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 +224 -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/agentic-security-2026.json +180 -0
  26. package/core/cli/data/rule-packs/general-security.json +173 -0
  27. package/core/cli/data/rule-packs/owasp-mcp-2026.json +244 -0
  28. package/core/cli/data/rule-packs/toxic-flow-heuristics.json +21 -0
  29. package/core/cli/data/rule-sources.json +5 -0
  30. package/core/cli/data/secret-patterns.json +18 -0
  31. package/core/cli/data/tool-classifications.json +111 -0
  32. package/core/cli/data/toxic-flow-rules.json +47 -0
  33. package/core/cli/index.js +23 -0
  34. package/core/cli/output/Banner.js +52 -0
  35. package/core/cli/output/Formatter.js +183 -0
  36. package/core/cli/output/JsonFormatter.js +106 -0
  37. package/core/cli/output/index.js +16 -0
  38. package/core/cli/secureRegistryFetch.js +157 -0
  39. package/core/cli/symbols.js +16 -0
  40. package/core/container/DependencyContainer.js +4 -1
  41. package/core/repositories/PacketRepository.js +16 -0
  42. package/core/services/AuditService.js +2 -0
  43. package/core/services/security/StaticRulesService.js +69 -13
  44. package/core/services/security/TrafficAnalysisService.js +19 -1
  45. package/core/services/security/TrafficToxicFlowService.js +154 -0
  46. package/core/services/security/index.js +2 -1
  47. package/core/services/security/rules/index.js +25 -59
  48. package/core/services/security/rules/scans/configPermissions.js +91 -0
  49. package/core/services/security/rules/scans/duplicateToolNames.js +85 -0
  50. package/core/services/security/rules/scans/insecureTransport.js +148 -0
  51. package/core/services/security/rules/scans/missingContainment.js +123 -0
  52. package/core/services/security/rules/scans/shellEnvInjection.js +101 -0
  53. package/core/services/security/rules/scans/unsafeDefaults.js +99 -0
  54. package/core/services/security/toolsListFromTrafficParser.js +70 -0
  55. package/core/tui/App.js +144 -0
  56. package/core/tui/FindingsPanel.js +115 -0
  57. package/core/tui/FixPanel.js +132 -0
  58. package/core/tui/Header.js +51 -0
  59. package/core/tui/HelpBar.js +42 -0
  60. package/core/tui/ServersPanel.js +109 -0
  61. package/core/tui/ToxicFlowsPanel.js +100 -0
  62. package/core/tui/h.js +8 -0
  63. package/core/tui/index.js +11 -0
  64. package/core/tui/render.js +22 -0
  65. package/package.json +24 -16
  66. package/ui/dist/assets/{index-CiCSDYf-.js → index-Buah4fNS.js} +32 -32
  67. package/ui/dist/index.html +1 -1
  68. package/ui/server/controllers/RequestController.js +9 -1
  69. package/ui/server/controllers/SecurityFindingsController.js +46 -1
  70. package/ui/server/routes/requests.js +8 -1
  71. package/ui/server/routes/security.js +5 -1
  72. package/ui/server/setup.js +2 -0
  73. package/ui/server/swagger/paths/components.js +55 -0
  74. package/ui/server/swagger/paths/securityTrafficFlows.js +59 -0
  75. package/ui/server/swagger/paths.js +2 -0
  76. package/ui/server/swagger/swagger.js +5 -1
  77. package/ui/src/Security.jsx +10 -0
  78. package/ui/src/components/Security/ScannerContent.jsx +30 -1
  79. package/ui/src/components/Security/TrafficToxicFlowsPanel.jsx +253 -0
  80. package/ui/src/components/Security/securityApi.js +15 -0
  81. package/ui/src/components/Security/useSecurity.js +60 -3
  82. package/core/services/security/rules/scans/agentic01GoalHijack.js +0 -130
  83. package/core/services/security/rules/scans/agentic02ToolMisuse.js +0 -129
  84. package/core/services/security/rules/scans/agentic03IdentityAbuse.js +0 -130
  85. package/core/services/security/rules/scans/agentic04SupplyChain.js +0 -130
  86. package/core/services/security/rules/scans/agentic06MemoryPoisoning.js +0 -130
  87. package/core/services/security/rules/scans/agentic07InsecureCommunication.js +0 -135
  88. package/core/services/security/rules/scans/agentic08CascadingFailures.js +0 -135
  89. package/core/services/security/rules/scans/agentic09TrustExploitation.js +0 -135
  90. package/core/services/security/rules/scans/agentic10RogueAgent.js +0 -130
  91. package/core/services/security/rules/scans/hardcodedSecrets.js +0 -130
  92. package/core/services/security/rules/scans/mcp01TokenMismanagement.js +0 -127
  93. package/core/services/security/rules/scans/mcp02ScopeCreep.js +0 -130
  94. package/core/services/security/rules/scans/mcp03ToolPoisoning.js +0 -132
  95. package/core/services/security/rules/scans/mcp04SupplyChain.js +0 -131
  96. package/core/services/security/rules/scans/mcp06PromptInjection.js +0 -200
  97. package/core/services/security/rules/scans/mcp07InsufficientAuth.js +0 -130
  98. package/core/services/security/rules/scans/mcp08LackAudit.js +0 -129
  99. package/core/services/security/rules/scans/mcp09ShadowServers.js +0 -129
  100. package/core/services/security/rules/scans/mcp10ContextInjection.js +0 -130
@@ -7,7 +7,7 @@
7
7
  <title>MCP Shark</title>
8
8
  <link rel="icon" type="image/png" href="/og-image.png" />
9
9
  <link rel="apple-touch-icon" href="/og-image.png" />
10
- <script type="module" crossorigin src="/assets/index-CiCSDYf-.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-Buah4fNS.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-Cc-IUa83.css">
12
12
  </head>
13
13
  <body>
@@ -8,11 +8,18 @@ import { RequestFilters } from '#core/models/RequestFilters.js';
8
8
  */
9
9
 
10
10
  export class RequestController {
11
- constructor(requestService, exportService, serializationLib, logger) {
11
+ constructor(
12
+ requestService,
13
+ exportService,
14
+ serializationLib,
15
+ logger,
16
+ trafficToxicFlowService = null
17
+ ) {
12
18
  this.requestService = requestService;
13
19
  this.exportService = exportService;
14
20
  this.serializationLib = serializationLib;
15
21
  this.logger = logger;
22
+ this.trafficToxicFlowService = trafficToxicFlowService;
16
23
  }
17
24
 
18
25
  /**
@@ -90,6 +97,7 @@ export class RequestController {
90
97
  clearRequests(_req, res) {
91
98
  try {
92
99
  const result = this.requestService.clearRequests();
100
+ this.trafficToxicFlowService?.clear();
93
101
  res.json({
94
102
  success: true,
95
103
  message: `Cleared ${result.clearedTables.length} table(s): ${result.clearedTables.join(', ')}. All captured traffic has been cleared.`,
@@ -5,10 +5,16 @@ import { handleError, handleValidationError } from '../utils/errorHandler.js';
5
5
  * Controller for Security Detection and Findings HTTP endpoints
6
6
  */
7
7
  export class SecurityFindingsController {
8
- constructor(securityDetectionService, serverManagementService, logger) {
8
+ constructor(
9
+ securityDetectionService,
10
+ serverManagementService,
11
+ logger,
12
+ trafficToxicFlowService = null
13
+ ) {
9
14
  this.securityService = securityDetectionService;
10
15
  this.serverManagementService = serverManagementService;
11
16
  this.logger = logger;
17
+ this.trafficToxicFlowService = trafficToxicFlowService;
12
18
  }
13
19
 
14
20
  /**
@@ -198,6 +204,7 @@ export class SecurityFindingsController {
198
204
  clearFindings = (_req, res) => {
199
205
  try {
200
206
  const deletedCount = this.securityService.clearAllFindings();
207
+ this.trafficToxicFlowService?.clear();
201
208
  return res.json({
202
209
  success: true,
203
210
  message: `Cleared ${deletedCount} finding${deletedCount !== 1 ? 's' : ''}`,
@@ -229,4 +236,42 @@ export class SecurityFindingsController {
229
236
  handleError(error, res, this.logger, 'Error deleting scan findings');
230
237
  }
231
238
  };
239
+
240
+ /**
241
+ * Cross-server toxic flows from observed tools/list traffic (live registry + last recompute).
242
+ */
243
+ getTrafficToxicFlows = (_req, res) => {
244
+ try {
245
+ if (!this.trafficToxicFlowService) {
246
+ return res.status(StatusCodes.SERVICE_UNAVAILABLE).json({
247
+ success: false,
248
+ error: 'Traffic toxic flow service is not available',
249
+ });
250
+ }
251
+ return res.json(this.trafficToxicFlowService.getSnapshot());
252
+ } catch (error) {
253
+ handleError(error, res, this.logger, 'Error getting traffic toxic flows');
254
+ }
255
+ };
256
+
257
+ /**
258
+ * Rebuild toxic-flow model from stored response packets (batch replay).
259
+ */
260
+ replayTrafficToxicFlows = (_req, res) => {
261
+ try {
262
+ if (!this.trafficToxicFlowService) {
263
+ return res.status(StatusCodes.SERVICE_UNAVAILABLE).json({
264
+ success: false,
265
+ error: 'Traffic toxic flow service is not available',
266
+ });
267
+ }
268
+ const stats = this.trafficToxicFlowService.rebuildFromDatabase();
269
+ return res.json({
270
+ ...this.trafficToxicFlowService.getSnapshot(),
271
+ replay: stats,
272
+ });
273
+ } catch (error) {
274
+ handleError(error, res, this.logger, 'Error replaying traffic toxic flows');
275
+ }
276
+ };
232
277
  }
@@ -5,8 +5,15 @@ export function createRequestsRoutes(container) {
5
5
  const exportService = container.getService('export');
6
6
  const serializationLib = container.getLibrary('serialization');
7
7
  const logger = container.getLibrary('logger');
8
+ const trafficToxicFlowService = container.getService('trafficToxicFlow');
8
9
 
9
- const controller = new RequestController(requestService, exportService, serializationLib, logger);
10
+ const controller = new RequestController(
11
+ requestService,
12
+ exportService,
13
+ serializationLib,
14
+ logger,
15
+ trafficToxicFlowService
16
+ );
10
17
 
11
18
  const router = {};
12
19
 
@@ -13,10 +13,12 @@ export function createSecurityRoutes(container) {
13
13
  const yaraEngineService = container.getService('yaraEngine');
14
14
  const logger = container.getLibrary('logger');
15
15
 
16
+ const trafficToxicFlowService = container.getService('trafficToxicFlow');
16
17
  const findingsController = new SecurityFindingsController(
17
18
  securityDetectionService,
18
19
  serverManagementService,
19
- logger
20
+ logger,
21
+ trafficToxicFlowService
20
22
  );
21
23
 
22
24
  const sourcesController = new RuleSourcesController(
@@ -40,6 +42,8 @@ export function createSecurityRoutes(container) {
40
42
  router.getScanHistory = findingsController.getScanHistory;
41
43
  router.clearFindings = findingsController.clearFindings;
42
44
  router.deleteScanFindings = findingsController.deleteScanFindings;
45
+ router.getTrafficToxicFlows = findingsController.getTrafficToxicFlows;
46
+ router.replayTrafficToxicFlows = findingsController.replayTrafficToxicFlows;
43
47
 
44
48
  // YARA engine (RuleSourcesController)
45
49
  router.getEngineStatus = sourcesController.getEngineStatus;
@@ -147,6 +147,8 @@ export function createUIServer() {
147
147
  app.get('/api/security/history', securityRoutes.getScanHistory);
148
148
  app.post('/api/security/findings/clear', securityRoutes.clearFindings);
149
149
  app.delete('/api/security/scan/:scanId', securityRoutes.deleteScanFindings);
150
+ app.get('/api/security/traffic-toxic-flows', securityRoutes.getTrafficToxicFlows);
151
+ app.post('/api/security/traffic-toxic-flows/replay', securityRoutes.replayTrafficToxicFlows);
150
152
 
151
153
  // Security routes - YARA engine
152
154
  app.get('/api/security/engine/status', securityRoutes.getEngineStatus);
@@ -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
+ };
@@ -11,6 +11,7 @@ import { helpPaths } from './paths/help.js';
11
11
  import { logsPaths } from './paths/logs.js';
12
12
  import { playgroundPaths } from './paths/playground.js';
13
13
  import { requestsPaths } from './paths/requests.js';
14
+ import { securityTrafficFlowsPaths } from './paths/securityTrafficFlows.js';
14
15
  import { serverManagementPaths } from './paths/serverManagement.js';
15
16
  import { sessionsPaths } from './paths/sessions.js';
16
17
  import { settingsPaths } from './paths/settings.js';
@@ -22,6 +23,7 @@ import { statisticsPaths } from './paths/statistics.js';
22
23
  */
23
24
  export const paths = {
24
25
  ...requestsPaths,
26
+ ...securityTrafficFlowsPaths,
25
27
  ...sessionsPaths,
26
28
  ...conversationsPaths,
27
29
  ...statisticsPaths,
@@ -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.5.4',
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: {
@@ -31,6 +31,10 @@ export const swaggerSpec = {
31
31
  { name: 'Playground', description: 'MCP playground for testing tools and resources' },
32
32
  { name: 'Smart Scan', description: 'AI-powered security analysis for MCP servers' },
33
33
  { name: 'Settings', description: 'Application settings' },
34
+ {
35
+ name: 'Security',
36
+ description: 'Local static analysis findings and traffic-derived toxic flows (HTTP proxy)',
37
+ },
34
38
  ],
35
39
  paths,
36
40
  components,
@@ -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
 
@@ -7,7 +7,7 @@ import {
7
7
  IconSparkles,
8
8
  IconTool,
9
9
  } from '@tabler/icons-react';
10
- import { useState } from 'react';
10
+ import { useEffect, useState } from 'react';
11
11
  import { colors, fonts } from '../../theme';
12
12
  import CategoryView from './CategoryView/index.js';
13
13
  import ErrorDisplay from './ErrorDisplay.jsx';
@@ -18,6 +18,10 @@ import ScanningProgress from './ScanningProgress.jsx';
18
18
  import SecurityCharts from './SecurityCharts/index.js';
19
19
  import SecuritySummary from './SecuritySummary.jsx';
20
20
  import TargetView from './TargetView/index.js';
21
+ import TrafficToxicFlowsPanel from './TrafficToxicFlowsPanel.jsx';
22
+
23
+ /** Keep proxy toxic-flow snapshot aligned when traffic/findings are cleared on other tabs. */
24
+ const TRAFFIC_TOXIC_POLL_MS = 20_000;
21
25
 
22
26
  const VIEW_MODES = [
23
27
  { id: 'dashboard', label: 'Dashboard', icon: IconChartBar },
@@ -162,11 +166,26 @@ export default function ScannerContent({
162
166
  showHistory,
163
167
  serversAvailable,
164
168
  scanComplete,
169
+ trafficToxicSnapshot,
170
+ trafficToxicLoading,
171
+ trafficToxicError,
172
+ loadTrafficToxicFlows,
173
+ replayTrafficToxicFlows,
165
174
  }) {
166
175
  const [viewMode, setViewMode] = useState('dashboard');
167
176
  const hasFindings = findings && findings.length > 0;
168
177
  const showEmpty = !error && !scanning && !hasFindings && !showHistory;
169
178
 
179
+ useEffect(() => {
180
+ if (error || scanning) {
181
+ return undefined;
182
+ }
183
+ const id = setInterval(() => {
184
+ loadTrafficToxicFlows();
185
+ }, TRAFFIC_TOXIC_POLL_MS);
186
+ return () => clearInterval(id);
187
+ }, [error, scanning, loadTrafficToxicFlows]);
188
+
170
189
  return (
171
190
  <div
172
191
  style={{
@@ -184,6 +203,16 @@ export default function ScannerContent({
184
203
  <StaticAnalysisBanner onNavigateToSmartScan={onNavigateToSmartScan} />
185
204
  )}
186
205
 
206
+ {!error && !scanning && (
207
+ <TrafficToxicFlowsPanel
208
+ snapshot={trafficToxicSnapshot}
209
+ loading={trafficToxicLoading}
210
+ error={trafficToxicError}
211
+ onRefresh={loadTrafficToxicFlows}
212
+ onReplay={replayTrafficToxicFlows}
213
+ />
214
+ )}
215
+
187
216
  {/* History View */}
188
217
  {showHistory && !scanning && (
189
218
  <ScanHistory
@@ -0,0 +1,253 @@
1
+ import { IconDatabase, IconGitBranch, IconRefresh } from '@tabler/icons-react';
2
+ import { colors, fonts } from '../../theme';
3
+
4
+ function riskColor(risk) {
5
+ const r = (risk || '').toUpperCase();
6
+ if (r === 'HIGH') {
7
+ return colors.error;
8
+ }
9
+ if (r === 'MEDIUM') {
10
+ return colors.warning;
11
+ }
12
+ return colors.textSecondary;
13
+ }
14
+
15
+ export default function TrafficToxicFlowsPanel({ snapshot, loading, error, onRefresh, onReplay }) {
16
+ const flows = snapshot?.toxicFlows ?? [];
17
+ const servers = snapshot?.servers ?? [];
18
+ const replay = snapshot?.replay;
19
+
20
+ return (
21
+ <div
22
+ style={{
23
+ marginBottom: '20px',
24
+ padding: '14px 16px',
25
+ background: colors.bgSecondary,
26
+ border: `1px solid ${colors.borderLight}`,
27
+ borderRadius: '8px',
28
+ }}
29
+ >
30
+ <div
31
+ style={{
32
+ display: 'flex',
33
+ alignItems: 'flex-start',
34
+ justifyContent: 'space-between',
35
+ gap: '12px',
36
+ flexWrap: 'wrap',
37
+ marginBottom: '10px',
38
+ }}
39
+ >
40
+ <div style={{ display: 'flex', alignItems: 'center', gap: '10px', minWidth: 0 }}>
41
+ <IconGitBranch
42
+ size={18}
43
+ stroke={1.5}
44
+ style={{ color: colors.textMuted, flexShrink: 0 }}
45
+ />
46
+ <div>
47
+ <div
48
+ style={{
49
+ fontSize: '13px',
50
+ fontWeight: 600,
51
+ color: colors.textPrimary,
52
+ fontFamily: fonts.body,
53
+ }}
54
+ >
55
+ Toxic flows (proxy traffic)
56
+ </div>
57
+ <div
58
+ style={{
59
+ fontSize: '11px',
60
+ color: colors.textSecondary,
61
+ fontFamily: fonts.body,
62
+ marginTop: '2px',
63
+ maxWidth: '720px',
64
+ lineHeight: 1.45,
65
+ }}
66
+ >
67
+ Heuristic cross-server pairs from <strong>tools/list</strong> responses seen through
68
+ the HTTP proxy. Run MCP clients through mcp-shark, trigger tool discovery, then
69
+ refresh or replay from the packet database. Clearing findings or all captured traffic
70
+ resets this in-memory model.
71
+ </div>
72
+ </div>
73
+ </div>
74
+ <div style={{ display: 'flex', gap: '8px', flexShrink: 0 }}>
75
+ <button
76
+ type="button"
77
+ disabled={loading}
78
+ onClick={() => onRefresh()}
79
+ style={{
80
+ display: 'flex',
81
+ alignItems: 'center',
82
+ gap: '6px',
83
+ padding: '6px 12px',
84
+ fontSize: '11px',
85
+ fontWeight: 500,
86
+ fontFamily: fonts.body,
87
+ background: colors.buttonSecondary,
88
+ border: `1px solid ${colors.borderLight}`,
89
+ borderRadius: '4px',
90
+ cursor: loading ? 'wait' : 'pointer',
91
+ color: colors.textPrimary,
92
+ }}
93
+ >
94
+ <IconRefresh size={14} stroke={1.5} />
95
+ Refresh
96
+ </button>
97
+ <button
98
+ type="button"
99
+ disabled={loading}
100
+ onClick={() => onReplay()}
101
+ style={{
102
+ display: 'flex',
103
+ alignItems: 'center',
104
+ gap: '6px',
105
+ padding: '6px 12px',
106
+ fontSize: '11px',
107
+ fontWeight: 500,
108
+ fontFamily: fonts.body,
109
+ background: colors.buttonPrimary,
110
+ border: 'none',
111
+ borderRadius: '4px',
112
+ cursor: loading ? 'wait' : 'pointer',
113
+ color: colors.textInverse,
114
+ }}
115
+ >
116
+ <IconDatabase size={14} stroke={1.5} />
117
+ Replay from DB
118
+ </button>
119
+ </div>
120
+ </div>
121
+
122
+ {error && (
123
+ <div
124
+ style={{
125
+ fontSize: '12px',
126
+ color: colors.error,
127
+ background: colors.errorBg,
128
+ padding: '8px 10px',
129
+ borderRadius: '4px',
130
+ marginBottom: '10px',
131
+ fontFamily: fonts.body,
132
+ }}
133
+ >
134
+ {error}
135
+ </div>
136
+ )}
137
+
138
+ {loading && !snapshot && !error && (
139
+ <div style={{ fontSize: '12px', color: colors.textSecondary, fontFamily: fonts.body }}>
140
+ Loading…
141
+ </div>
142
+ )}
143
+
144
+ {snapshot && !error && (
145
+ <div style={{ fontSize: '11px', color: colors.textSecondary, fontFamily: fonts.body }}>
146
+ {servers.length > 0 ? (
147
+ <span>
148
+ <strong style={{ color: colors.textPrimary }}>{servers.length}</strong> server
149
+ {servers.length !== 1 ? 's' : ''} with tool metadata
150
+ {snapshot.computedAt
151
+ ? ` · updated ${new Date(snapshot.computedAt).toLocaleString()}`
152
+ : ''}
153
+ {replay != null
154
+ ? ` · replay scanned ${replay.packetRows} packet row${replay.packetRows !== 1 ? 's' : ''}`
155
+ : ''}
156
+ </span>
157
+ ) : (
158
+ <span>
159
+ No <code style={{ fontFamily: fonts.mono }}>tools/list</code> traffic captured yet.
160
+ Use the proxy, open a client that lists tools, then Refresh or Replay from DB.
161
+ </span>
162
+ )}
163
+ </div>
164
+ )}
165
+
166
+ {flows.length > 0 && (
167
+ <ul
168
+ style={{
169
+ listStyle: 'none',
170
+ margin: '12px 0 0',
171
+ padding: 0,
172
+ display: 'flex',
173
+ flexDirection: 'column',
174
+ gap: '10px',
175
+ }}
176
+ >
177
+ {flows.map((flow, idx) => (
178
+ <li
179
+ key={`${flow.source}-${flow.target}-${flow.title}-${idx}`}
180
+ style={{
181
+ padding: '10px 12px',
182
+ background: colors.bgCard,
183
+ border: `1px solid ${colors.borderLight}`,
184
+ borderRadius: '6px',
185
+ }}
186
+ >
187
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
188
+ <span
189
+ style={{
190
+ fontSize: '10px',
191
+ fontWeight: 700,
192
+ letterSpacing: '0.04em',
193
+ color: riskColor(flow.risk),
194
+ fontFamily: fonts.body,
195
+ }}
196
+ >
197
+ {(flow.risk || '').toUpperCase()}
198
+ </span>
199
+ <span
200
+ style={{
201
+ fontSize: '12px',
202
+ fontWeight: 600,
203
+ color: colors.textPrimary,
204
+ fontFamily: fonts.body,
205
+ }}
206
+ >
207
+ {flow.title}
208
+ </span>
209
+ </div>
210
+ <div
211
+ style={{
212
+ fontSize: '11px',
213
+ color: colors.accentBlue,
214
+ fontFamily: fonts.mono,
215
+ marginTop: '4px',
216
+ }}
217
+ >
218
+ {flow.source} → {flow.target}
219
+ </div>
220
+ {flow.scenario && (
221
+ <div
222
+ style={{
223
+ fontSize: '11px',
224
+ color: colors.textSecondary,
225
+ fontFamily: fonts.body,
226
+ marginTop: '6px',
227
+ lineHeight: 1.45,
228
+ }}
229
+ >
230
+ {flow.scenario}
231
+ </div>
232
+ )}
233
+ </li>
234
+ ))}
235
+ </ul>
236
+ )}
237
+
238
+ {snapshot && servers.length > 0 && (
239
+ <div
240
+ style={{
241
+ marginTop: '12px',
242
+ fontSize: '10px',
243
+ color: colors.textTertiary,
244
+ fontFamily: fonts.body,
245
+ lineHeight: 1.4,
246
+ }}
247
+ >
248
+ {snapshot.note}
249
+ </div>
250
+ )}
251
+ </div>
252
+ );
253
+ }
@@ -66,6 +66,21 @@ export async function postClearFindings() {
66
66
  return response.json();
67
67
  }
68
68
 
69
+ /** Cross-server toxic flows from observed tools/list proxy traffic */
70
+ export async function fetchTrafficToxicFlows() {
71
+ const response = await fetch('/api/security/traffic-toxic-flows');
72
+ return response.json();
73
+ }
74
+
75
+ /** Rebuild toxic-flow model from stored response packets */
76
+ export async function postReplayTrafficToxicFlows() {
77
+ const response = await fetch('/api/security/traffic-toxic-flows/replay', {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ });
81
+ return response.json();
82
+ }
83
+
69
84
  export async function fetchCommunityRules() {
70
85
  const response = await fetch('/api/security/community-rules');
71
86
  return response.json();