@mcp-shark/mcp-shark 1.4.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.
- package/LICENSE +85 -0
- package/README.md +724 -0
- package/bin/mcp-shark.js +93 -0
- package/mcp-server/.editorconfig +15 -0
- package/mcp-server/.prettierignore +11 -0
- package/mcp-server/.prettierrc +12 -0
- package/mcp-server/README.md +280 -0
- package/mcp-server/commitlint.config.cjs +42 -0
- package/mcp-server/eslint.config.js +131 -0
- package/mcp-server/lib/auditor/audit.js +228 -0
- package/mcp-server/lib/common/error.js +15 -0
- package/mcp-server/lib/server/external/all.js +32 -0
- package/mcp-server/lib/server/external/config.js +59 -0
- package/mcp-server/lib/server/external/kv.js +102 -0
- package/mcp-server/lib/server/external/single/client.js +35 -0
- package/mcp-server/lib/server/external/single/request.js +49 -0
- package/mcp-server/lib/server/external/single/run.js +75 -0
- package/mcp-server/lib/server/external/single/transport.js +57 -0
- package/mcp-server/lib/server/internal/handlers/common.js +20 -0
- package/mcp-server/lib/server/internal/handlers/error.js +7 -0
- package/mcp-server/lib/server/internal/handlers/prompts-get.js +22 -0
- package/mcp-server/lib/server/internal/handlers/prompts-list.js +12 -0
- package/mcp-server/lib/server/internal/handlers/resources-list.js +12 -0
- package/mcp-server/lib/server/internal/handlers/resources-read.js +19 -0
- package/mcp-server/lib/server/internal/handlers/tools-call.js +37 -0
- package/mcp-server/lib/server/internal/handlers/tools-list.js +14 -0
- package/mcp-server/lib/server/internal/run.js +49 -0
- package/mcp-server/lib/server/internal/server.js +63 -0
- package/mcp-server/lib/server/internal/session.js +39 -0
- package/mcp-server/mcp-shark.js +72 -0
- package/mcp-server/package-lock.json +4784 -0
- package/mcp-server/package.json +30 -0
- package/package.json +103 -0
- package/ui/README.md +212 -0
- package/ui/index.html +16 -0
- package/ui/package-lock.json +3574 -0
- package/ui/package.json +12 -0
- package/ui/paths.js +282 -0
- package/ui/public/og-image.png +0 -0
- package/ui/server/routes/backups.js +251 -0
- package/ui/server/routes/composite.js +244 -0
- package/ui/server/routes/config.js +175 -0
- package/ui/server/routes/conversations.js +25 -0
- package/ui/server/routes/help.js +43 -0
- package/ui/server/routes/logs.js +32 -0
- package/ui/server/routes/playground.js +152 -0
- package/ui/server/routes/requests.js +235 -0
- package/ui/server/routes/sessions.js +27 -0
- package/ui/server/routes/smartscan/discover.js +117 -0
- package/ui/server/routes/smartscan/scans/clearCache.js +22 -0
- package/ui/server/routes/smartscan/scans/createBatchScans.js +123 -0
- package/ui/server/routes/smartscan/scans/createScan.js +42 -0
- package/ui/server/routes/smartscan/scans/getCachedResults.js +51 -0
- package/ui/server/routes/smartscan/scans/getScan.js +41 -0
- package/ui/server/routes/smartscan/scans/listScans.js +24 -0
- package/ui/server/routes/smartscan/scans.js +13 -0
- package/ui/server/routes/smartscan/token.js +56 -0
- package/ui/server/routes/smartscan/transport.js +53 -0
- package/ui/server/routes/smartscan.js +24 -0
- package/ui/server/routes/statistics.js +83 -0
- package/ui/server/utils/config-update.js +212 -0
- package/ui/server/utils/config.js +98 -0
- package/ui/server/utils/paths.js +23 -0
- package/ui/server/utils/port.js +28 -0
- package/ui/server/utils/process.js +80 -0
- package/ui/server/utils/scan-cache/all-results.js +180 -0
- package/ui/server/utils/scan-cache/directory.js +35 -0
- package/ui/server/utils/scan-cache/file-operations.js +104 -0
- package/ui/server/utils/scan-cache/hash.js +47 -0
- package/ui/server/utils/scan-cache/server-operations.js +80 -0
- package/ui/server/utils/scan-cache.js +12 -0
- package/ui/server/utils/serialization.js +13 -0
- package/ui/server/utils/smartscan-token.js +42 -0
- package/ui/server.js +199 -0
- package/ui/src/App.jsx +153 -0
- package/ui/src/CompositeLogs.jsx +164 -0
- package/ui/src/CompositeSetup.jsx +285 -0
- package/ui/src/HelpGuide/HelpGuideContent.jsx +118 -0
- package/ui/src/HelpGuide/HelpGuideFooter.jsx +58 -0
- package/ui/src/HelpGuide/HelpGuideHeader.jsx +56 -0
- package/ui/src/HelpGuide.jsx +65 -0
- package/ui/src/IntroTour.jsx +140 -0
- package/ui/src/LogDetail.jsx +122 -0
- package/ui/src/LogTable.jsx +242 -0
- package/ui/src/PacketDetail.jsx +190 -0
- package/ui/src/PacketFilters.jsx +222 -0
- package/ui/src/PacketList.jsx +183 -0
- package/ui/src/SmartScan.jsx +178 -0
- package/ui/src/TabNavigation.jsx +143 -0
- package/ui/src/components/App/HelpButton.jsx +64 -0
- package/ui/src/components/App/TrafficTab.jsx +69 -0
- package/ui/src/components/App/useAppState.js +163 -0
- package/ui/src/components/BackupList.jsx +192 -0
- package/ui/src/components/CollapsibleSection.jsx +82 -0
- package/ui/src/components/ConfigFileSection.jsx +84 -0
- package/ui/src/components/ConfigViewerModal.jsx +141 -0
- package/ui/src/components/ConfirmationModal.jsx +129 -0
- package/ui/src/components/DetailsTab/BodySection.jsx +27 -0
- package/ui/src/components/DetailsTab/CollapsibleRequestResponse.jsx +70 -0
- package/ui/src/components/DetailsTab/HeadersSection.jsx +25 -0
- package/ui/src/components/DetailsTab/InfoSection.jsx +28 -0
- package/ui/src/components/DetailsTab/NetworkInfoSection.jsx +63 -0
- package/ui/src/components/DetailsTab/ProtocolInfoSection.jsx +75 -0
- package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +46 -0
- package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +66 -0
- package/ui/src/components/DetailsTab.jsx +31 -0
- package/ui/src/components/DetectedPathsList.jsx +171 -0
- package/ui/src/components/FileInput.jsx +144 -0
- package/ui/src/components/GroupHeader.jsx +76 -0
- package/ui/src/components/GroupedByMcpView.jsx +103 -0
- package/ui/src/components/GroupedByServerView.jsx +134 -0
- package/ui/src/components/GroupedBySessionView.jsx +127 -0
- package/ui/src/components/GroupedViews.jsx +2 -0
- package/ui/src/components/HexTab.jsx +188 -0
- package/ui/src/components/LogsDisplay.jsx +93 -0
- package/ui/src/components/LogsToolbar.jsx +193 -0
- package/ui/src/components/McpPlayground/LoadingModal.jsx +113 -0
- package/ui/src/components/McpPlayground/PromptsSection/PromptCallPanel.jsx +125 -0
- package/ui/src/components/McpPlayground/PromptsSection/PromptItem.jsx +48 -0
- package/ui/src/components/McpPlayground/PromptsSection/PromptsList.jsx +45 -0
- package/ui/src/components/McpPlayground/PromptsSection.jsx +106 -0
- package/ui/src/components/McpPlayground/ResourcesSection/ResourceCallPanel.jsx +89 -0
- package/ui/src/components/McpPlayground/ResourcesSection/ResourceItem.jsx +59 -0
- package/ui/src/components/McpPlayground/ResourcesSection/ResourcesList.jsx +45 -0
- package/ui/src/components/McpPlayground/ResourcesSection.jsx +91 -0
- package/ui/src/components/McpPlayground/ToolsSection/ToolCallPanel.jsx +125 -0
- package/ui/src/components/McpPlayground/ToolsSection/ToolItem.jsx +48 -0
- package/ui/src/components/McpPlayground/ToolsSection/ToolsList.jsx +45 -0
- package/ui/src/components/McpPlayground/ToolsSection.jsx +107 -0
- package/ui/src/components/McpPlayground/common/EmptyState.jsx +17 -0
- package/ui/src/components/McpPlayground/common/ErrorState.jsx +17 -0
- package/ui/src/components/McpPlayground/common/LoadingState.jsx +17 -0
- package/ui/src/components/McpPlayground/useMcpPlayground.js +280 -0
- package/ui/src/components/McpPlayground.jsx +171 -0
- package/ui/src/components/MessageDisplay.jsx +28 -0
- package/ui/src/components/PacketDetailHeader.jsx +88 -0
- package/ui/src/components/PacketFilters/ExportControls.jsx +126 -0
- package/ui/src/components/PacketFilters/FilterInput.jsx +59 -0
- package/ui/src/components/RawTab.jsx +142 -0
- package/ui/src/components/RequestRow/OrphanedResponseRow.jsx +155 -0
- package/ui/src/components/RequestRow/RequestRowMain.jsx +240 -0
- package/ui/src/components/RequestRow/ResponseRow.jsx +158 -0
- package/ui/src/components/RequestRow.jsx +70 -0
- package/ui/src/components/ServerControl.jsx +133 -0
- package/ui/src/components/ServiceSelector.jsx +209 -0
- package/ui/src/components/SetupHeader.jsx +30 -0
- package/ui/src/components/SharkLogo.jsx +21 -0
- package/ui/src/components/SmartScan/AnalysisResult.jsx +64 -0
- package/ui/src/components/SmartScan/BatchResultsDisplay/BatchResultItem.jsx +215 -0
- package/ui/src/components/SmartScan/BatchResultsDisplay/BatchResultsHeader.jsx +94 -0
- package/ui/src/components/SmartScan/BatchResultsDisplay.jsx +26 -0
- package/ui/src/components/SmartScan/DebugInfoSection.jsx +53 -0
- package/ui/src/components/SmartScan/EmptyState.jsx +57 -0
- package/ui/src/components/SmartScan/ErrorDisplay.jsx +48 -0
- package/ui/src/components/SmartScan/ExpandableSection.jsx +93 -0
- package/ui/src/components/SmartScan/FindingsTable.jsx +257 -0
- package/ui/src/components/SmartScan/ListViewContent.jsx +75 -0
- package/ui/src/components/SmartScan/NotablePatternsSection.jsx +75 -0
- package/ui/src/components/SmartScan/OverallSummarySection.jsx +72 -0
- package/ui/src/components/SmartScan/RawDataSection.jsx +52 -0
- package/ui/src/components/SmartScan/RecommendationsSection.jsx +78 -0
- package/ui/src/components/SmartScan/ScanDetailHeader.jsx +92 -0
- package/ui/src/components/SmartScan/ScanDetailView.jsx +141 -0
- package/ui/src/components/SmartScan/ScanListView/ScanListHeader.jsx +49 -0
- package/ui/src/components/SmartScan/ScanListView/ScanListItem.jsx +201 -0
- package/ui/src/components/SmartScan/ScanListView.jsx +73 -0
- package/ui/src/components/SmartScan/ScanOverviewSection.jsx +123 -0
- package/ui/src/components/SmartScan/ScanResultsDisplay.jsx +35 -0
- package/ui/src/components/SmartScan/ScanViewContent.jsx +68 -0
- package/ui/src/components/SmartScan/ScanningProgress.jsx +47 -0
- package/ui/src/components/SmartScan/ServerInfoSection.jsx +43 -0
- package/ui/src/components/SmartScan/ServerSelectionRow.jsx +207 -0
- package/ui/src/components/SmartScan/SingleResultDisplay.jsx +269 -0
- package/ui/src/components/SmartScan/SmartScanControls.jsx +290 -0
- package/ui/src/components/SmartScan/SmartScanHeader.jsx +77 -0
- package/ui/src/components/SmartScan/ViewModeTabs.jsx +57 -0
- package/ui/src/components/SmartScan/hooks/useCacheManagement.js +34 -0
- package/ui/src/components/SmartScan/hooks/useMcpDiscovery.js +121 -0
- package/ui/src/components/SmartScan/hooks/useScanList.js +193 -0
- package/ui/src/components/SmartScan/hooks/useScanOperations.js +87 -0
- package/ui/src/components/SmartScan/hooks/useServerStatus.js +26 -0
- package/ui/src/components/SmartScan/hooks/useTokenManagement.js +53 -0
- package/ui/src/components/SmartScan/scanDataUtils.js +98 -0
- package/ui/src/components/SmartScan/useSmartScan.js +72 -0
- package/ui/src/components/SmartScan/utils.js +19 -0
- package/ui/src/components/SmartScanIcons.jsx +58 -0
- package/ui/src/components/TabNavigation/DesktopTabs.jsx +111 -0
- package/ui/src/components/TabNavigation/MobileDropdown.jsx +140 -0
- package/ui/src/components/TabNavigation.jsx +97 -0
- package/ui/src/components/TabNavigationIcons.jsx +40 -0
- package/ui/src/components/TableHeader.jsx +164 -0
- package/ui/src/components/TourOverlay.jsx +117 -0
- package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +117 -0
- package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +70 -0
- package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +45 -0
- package/ui/src/components/TourTooltip/useTooltipPosition.js +108 -0
- package/ui/src/components/TourTooltip.jsx +83 -0
- package/ui/src/components/ViewModeTabs.jsx +91 -0
- package/ui/src/components/WhatThisDoesSection.jsx +61 -0
- package/ui/src/config/tourSteps.jsx +141 -0
- package/ui/src/hooks/useAnimation.js +92 -0
- package/ui/src/hooks/useConfigManagement.js +124 -0
- package/ui/src/hooks/useServiceExtraction.js +51 -0
- package/ui/src/index.css +42 -0
- package/ui/src/main.jsx +10 -0
- package/ui/src/theme.js +65 -0
- package/ui/src/utils/animations.js +170 -0
- package/ui/src/utils/groupingUtils.js +93 -0
- package/ui/src/utils/hexUtils.js +24 -0
- package/ui/src/utils/mcpGroupingUtils.js +262 -0
- package/ui/src/utils/requestUtils.js +297 -0
- package/ui/vite.config.js +18 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { extractServerName } from './requestUtils.js';
|
|
2
|
+
|
|
3
|
+
export function groupByServerAndSession(requests) {
|
|
4
|
+
const serverGroups = new Map();
|
|
5
|
+
|
|
6
|
+
requests.forEach((request) => {
|
|
7
|
+
const sessionId = request.session_id || '__NO_SESSION__';
|
|
8
|
+
const serverName = extractServerName(request);
|
|
9
|
+
|
|
10
|
+
if (!serverGroups.has(serverName)) {
|
|
11
|
+
serverGroups.set(serverName, new Map());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const sessionGroups = serverGroups.get(serverName);
|
|
15
|
+
if (!sessionGroups.has(sessionId)) {
|
|
16
|
+
sessionGroups.set(sessionId, []);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
sessionGroups.get(sessionId).push(request);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return Array.from(serverGroups.entries())
|
|
23
|
+
.map(([serverName, sessionGroups]) => {
|
|
24
|
+
const sessions = Array.from(sessionGroups.entries())
|
|
25
|
+
.map(([sessionId, groupRequests]) => ({
|
|
26
|
+
sessionId: sessionId === '__NO_SESSION__' ? null : sessionId,
|
|
27
|
+
requests: groupRequests.sort(
|
|
28
|
+
(a, b) => new Date(a.timestamp_iso) - new Date(b.timestamp_iso)
|
|
29
|
+
),
|
|
30
|
+
}))
|
|
31
|
+
.sort((a, b) => {
|
|
32
|
+
const aTime = a.requests[0]?.timestamp_iso || '';
|
|
33
|
+
const bTime = b.requests[0]?.timestamp_iso || '';
|
|
34
|
+
return new Date(aTime) - new Date(bTime);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
serverName: serverName === '__UNKNOWN_SERVER__' ? null : serverName,
|
|
39
|
+
sessions,
|
|
40
|
+
};
|
|
41
|
+
})
|
|
42
|
+
.sort((a, b) => {
|
|
43
|
+
const aTime = a.sessions[0]?.requests[0]?.timestamp_iso || '';
|
|
44
|
+
const bTime = b.sessions[0]?.requests[0]?.timestamp_iso || '';
|
|
45
|
+
return new Date(bTime) - new Date(aTime);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function groupBySessionAndServer(requests) {
|
|
50
|
+
const sessionGroups = new Map();
|
|
51
|
+
|
|
52
|
+
requests.forEach((request) => {
|
|
53
|
+
const sessionId = request.session_id || '__NO_SESSION__';
|
|
54
|
+
const serverName = extractServerName(request);
|
|
55
|
+
|
|
56
|
+
if (!sessionGroups.has(sessionId)) {
|
|
57
|
+
sessionGroups.set(sessionId, new Map());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const serverGroups = sessionGroups.get(sessionId);
|
|
61
|
+
if (!serverGroups.has(serverName)) {
|
|
62
|
+
serverGroups.set(serverName, []);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
serverGroups.get(serverName).push(request);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return Array.from(sessionGroups.entries())
|
|
69
|
+
.map(([sessionId, serverGroups]) => {
|
|
70
|
+
const servers = Array.from(serverGroups.entries())
|
|
71
|
+
.map(([serverName, groupRequests]) => ({
|
|
72
|
+
serverName: serverName === '__UNKNOWN_SERVER__' ? null : serverName,
|
|
73
|
+
requests: groupRequests.sort(
|
|
74
|
+
(a, b) => new Date(a.timestamp_iso) - new Date(b.timestamp_iso)
|
|
75
|
+
),
|
|
76
|
+
}))
|
|
77
|
+
.sort((a, b) => {
|
|
78
|
+
const aTime = a.requests[0]?.timestamp_iso || '';
|
|
79
|
+
const bTime = b.requests[0]?.timestamp_iso || '';
|
|
80
|
+
return new Date(aTime) - new Date(bTime);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
sessionId: sessionId === '__NO_SESSION__' ? null : sessionId,
|
|
85
|
+
servers,
|
|
86
|
+
};
|
|
87
|
+
})
|
|
88
|
+
.sort((a, b) => {
|
|
89
|
+
const aTime = a.servers[0]?.requests[0]?.timestamp_iso || '';
|
|
90
|
+
const bTime = b.servers[0]?.requests[0]?.timestamp_iso || '';
|
|
91
|
+
return new Date(bTime) - new Date(aTime);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function generateHexDump(text) {
|
|
2
|
+
if (!text) return [];
|
|
3
|
+
const bytes = new TextEncoder().encode(text);
|
|
4
|
+
const lines = [];
|
|
5
|
+
for (let i = 0; i < bytes.length; i += 16) {
|
|
6
|
+
const chunk = bytes.slice(i, i + 16);
|
|
7
|
+
const hex = Array.from(chunk)
|
|
8
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
9
|
+
.join(' ');
|
|
10
|
+
const ascii = Array.from(chunk)
|
|
11
|
+
.map((b) => (b >= 32 && b < 127 ? String.fromCharCode(b) : '.'))
|
|
12
|
+
.join('');
|
|
13
|
+
const offset = i.toString(16).padStart(8, '0');
|
|
14
|
+
lines.push({ offset, hex, ascii });
|
|
15
|
+
}
|
|
16
|
+
return lines;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createFullRequestText(headers, bodyRaw) {
|
|
20
|
+
const headersText = Object.entries(headers)
|
|
21
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
22
|
+
.join('\r\n');
|
|
23
|
+
return headersText + (bodyRaw ? '\r\n\r\n' + bodyRaw : '');
|
|
24
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { pairRequestsWithResponses } from './requestUtils.js';
|
|
2
|
+
import {
|
|
3
|
+
IconRefresh,
|
|
4
|
+
IconTool,
|
|
5
|
+
IconDatabase,
|
|
6
|
+
IconMessage,
|
|
7
|
+
IconBell,
|
|
8
|
+
IconUser,
|
|
9
|
+
IconPackage,
|
|
10
|
+
} from '@tabler/icons-react';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* MCP Method Categories based on the protocol specification
|
|
14
|
+
* Reference: https://modelcontextprotocol.io/docs/learn/architecture
|
|
15
|
+
*/
|
|
16
|
+
export const MCP_METHOD_CATEGORIES = {
|
|
17
|
+
LIFECYCLE: 'lifecycle',
|
|
18
|
+
TOOLS: 'tools',
|
|
19
|
+
RESOURCES: 'resources',
|
|
20
|
+
PROMPTS: 'prompts',
|
|
21
|
+
NOTIFICATIONS: 'notifications',
|
|
22
|
+
CLIENT_FEATURES: 'client-features',
|
|
23
|
+
OTHER: 'other',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Categorize an MCP method into its protocol category
|
|
28
|
+
*/
|
|
29
|
+
export function categorizeMcpMethod(method) {
|
|
30
|
+
if (!method) return MCP_METHOD_CATEGORIES.OTHER;
|
|
31
|
+
|
|
32
|
+
// Lifecycle methods
|
|
33
|
+
if (method === 'initialize' || method === 'notifications/initialized') {
|
|
34
|
+
return MCP_METHOD_CATEGORIES.LIFECYCLE;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Tools methods
|
|
38
|
+
if (method.startsWith('tools/')) {
|
|
39
|
+
return MCP_METHOD_CATEGORIES.TOOLS;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Resources methods
|
|
43
|
+
if (method.startsWith('resources/')) {
|
|
44
|
+
return MCP_METHOD_CATEGORIES.RESOURCES;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Prompts methods
|
|
48
|
+
if (method.startsWith('prompts/')) {
|
|
49
|
+
return MCP_METHOD_CATEGORIES.PROMPTS;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Notifications (no response expected)
|
|
53
|
+
if (method.startsWith('notifications/')) {
|
|
54
|
+
return MCP_METHOD_CATEGORIES.NOTIFICATIONS;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Client features
|
|
58
|
+
if (
|
|
59
|
+
method.startsWith('elicitation/') ||
|
|
60
|
+
method.startsWith('sampling/') ||
|
|
61
|
+
method.startsWith('logging/')
|
|
62
|
+
) {
|
|
63
|
+
return MCP_METHOD_CATEGORIES.CLIENT_FEATURES;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return MCP_METHOD_CATEGORIES.OTHER;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get a human-readable label for an MCP method category
|
|
71
|
+
*/
|
|
72
|
+
export function getCategoryLabel(category) {
|
|
73
|
+
const labels = {
|
|
74
|
+
[MCP_METHOD_CATEGORIES.LIFECYCLE]: 'Lifecycle',
|
|
75
|
+
[MCP_METHOD_CATEGORIES.TOOLS]: 'Tools',
|
|
76
|
+
[MCP_METHOD_CATEGORIES.RESOURCES]: 'Resources',
|
|
77
|
+
[MCP_METHOD_CATEGORIES.PROMPTS]: 'Prompts',
|
|
78
|
+
[MCP_METHOD_CATEGORIES.NOTIFICATIONS]: 'Notifications',
|
|
79
|
+
[MCP_METHOD_CATEGORIES.CLIENT_FEATURES]: 'Client Features',
|
|
80
|
+
[MCP_METHOD_CATEGORIES.OTHER]: 'Other',
|
|
81
|
+
};
|
|
82
|
+
return labels[category] || 'Unknown';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get Tabler icon component for category (for visual grouping)
|
|
87
|
+
*/
|
|
88
|
+
export function getCategoryIconComponent(category) {
|
|
89
|
+
const iconMap = {
|
|
90
|
+
[MCP_METHOD_CATEGORIES.LIFECYCLE]: IconRefresh,
|
|
91
|
+
[MCP_METHOD_CATEGORIES.TOOLS]: IconTool,
|
|
92
|
+
[MCP_METHOD_CATEGORIES.RESOURCES]: IconDatabase,
|
|
93
|
+
[MCP_METHOD_CATEGORIES.PROMPTS]: IconMessage,
|
|
94
|
+
[MCP_METHOD_CATEGORIES.NOTIFICATIONS]: IconBell,
|
|
95
|
+
[MCP_METHOD_CATEGORIES.CLIENT_FEATURES]: IconUser,
|
|
96
|
+
[MCP_METHOD_CATEGORIES.OTHER]: IconPackage,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return iconMap[category] || IconPackage;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Import getJsonRpcMethod from requestUtils instead of duplicating
|
|
103
|
+
import { getJsonRpcMethod } from './requestUtils.js';
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Group requests by MCP session and method category
|
|
107
|
+
* This provides a view that shows the flow of MCP operations organized by protocol category
|
|
108
|
+
*/
|
|
109
|
+
export function groupByMcpSessionAndCategory(requests) {
|
|
110
|
+
const pairs = pairRequestsWithResponses(requests);
|
|
111
|
+
const sessionGroups = new Map();
|
|
112
|
+
|
|
113
|
+
pairs.forEach((pair) => {
|
|
114
|
+
const request = pair.request || pair.response;
|
|
115
|
+
if (!request) return;
|
|
116
|
+
|
|
117
|
+
const sessionId = request.session_id || '__NO_SESSION__';
|
|
118
|
+
const method = getJsonRpcMethod(request);
|
|
119
|
+
const category = categorizeMcpMethod(method || '');
|
|
120
|
+
|
|
121
|
+
if (!sessionGroups.has(sessionId)) {
|
|
122
|
+
sessionGroups.set(sessionId, {
|
|
123
|
+
sessionId: sessionId === '__NO_SESSION__' ? null : sessionId,
|
|
124
|
+
categories: new Map(),
|
|
125
|
+
firstTimestamp: request.timestamp_iso,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const session = sessionGroups.get(sessionId);
|
|
130
|
+
if (!session.categories.has(category)) {
|
|
131
|
+
session.categories.set(category, []);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
session.categories.get(category).push(pair);
|
|
135
|
+
|
|
136
|
+
// Update first timestamp if earlier
|
|
137
|
+
if (new Date(request.timestamp_iso) < new Date(session.firstTimestamp)) {
|
|
138
|
+
session.firstTimestamp = request.timestamp_iso;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return Array.from(sessionGroups.entries())
|
|
143
|
+
.map(([sessionId, session]) => ({
|
|
144
|
+
sessionId: session.sessionId,
|
|
145
|
+
firstTimestamp: session.firstTimestamp,
|
|
146
|
+
categories: Array.from(session.categories.entries())
|
|
147
|
+
.map(([category, pairs]) => ({
|
|
148
|
+
category,
|
|
149
|
+
label: getCategoryLabel(category),
|
|
150
|
+
pairs: pairs.sort((a, b) => {
|
|
151
|
+
const aTime = (a.request || a.response)?.timestamp_iso || '';
|
|
152
|
+
const bTime = (b.request || b.response)?.timestamp_iso || '';
|
|
153
|
+
return new Date(aTime) - new Date(bTime);
|
|
154
|
+
}),
|
|
155
|
+
}))
|
|
156
|
+
.sort((a, b) => {
|
|
157
|
+
// Order: lifecycle first, then tools, resources, prompts, notifications, client features, other
|
|
158
|
+
const order = [
|
|
159
|
+
MCP_METHOD_CATEGORIES.LIFECYCLE,
|
|
160
|
+
MCP_METHOD_CATEGORIES.TOOLS,
|
|
161
|
+
MCP_METHOD_CATEGORIES.RESOURCES,
|
|
162
|
+
MCP_METHOD_CATEGORIES.PROMPTS,
|
|
163
|
+
MCP_METHOD_CATEGORIES.NOTIFICATIONS,
|
|
164
|
+
MCP_METHOD_CATEGORIES.CLIENT_FEATURES,
|
|
165
|
+
MCP_METHOD_CATEGORIES.OTHER,
|
|
166
|
+
];
|
|
167
|
+
const aIndex = order.indexOf(a.category);
|
|
168
|
+
const bIndex = order.indexOf(b.category);
|
|
169
|
+
return aIndex - bIndex;
|
|
170
|
+
}),
|
|
171
|
+
}))
|
|
172
|
+
.sort((a, b) => new Date(b.firstTimestamp) - new Date(a.firstTimestamp));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Group requests by MCP method category (across all sessions)
|
|
177
|
+
* Useful for seeing all tool calls, all resource reads, etc.
|
|
178
|
+
*/
|
|
179
|
+
export function groupByMcpCategory(requests) {
|
|
180
|
+
const pairs = pairRequestsWithResponses(requests);
|
|
181
|
+
const categoryGroups = new Map();
|
|
182
|
+
|
|
183
|
+
pairs.forEach((pair) => {
|
|
184
|
+
const request = pair.request || pair.response;
|
|
185
|
+
if (!request) return;
|
|
186
|
+
|
|
187
|
+
const method = getJsonRpcMethod(request);
|
|
188
|
+
const category = categorizeMcpMethod(method || '');
|
|
189
|
+
|
|
190
|
+
if (!categoryGroups.has(category)) {
|
|
191
|
+
categoryGroups.set(category, []);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
categoryGroups.get(category).push(pair);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return Array.from(categoryGroups.entries())
|
|
198
|
+
.map(([category, pairs]) => ({
|
|
199
|
+
category,
|
|
200
|
+
label: getCategoryLabel(category),
|
|
201
|
+
pairs: pairs.sort((a, b) => {
|
|
202
|
+
const aTime = (a.request || a.response)?.timestamp_iso || '';
|
|
203
|
+
const bTime = (b.request || b.response)?.timestamp_iso || '';
|
|
204
|
+
return new Date(bTime) - new Date(aTime);
|
|
205
|
+
}),
|
|
206
|
+
}))
|
|
207
|
+
.sort((a, b) => {
|
|
208
|
+
// Order: lifecycle first, then tools, resources, prompts, notifications, client features, other
|
|
209
|
+
const order = [
|
|
210
|
+
MCP_METHOD_CATEGORIES.LIFECYCLE,
|
|
211
|
+
MCP_METHOD_CATEGORIES.TOOLS,
|
|
212
|
+
MCP_METHOD_CATEGORIES.RESOURCES,
|
|
213
|
+
MCP_METHOD_CATEGORIES.PROMPTS,
|
|
214
|
+
MCP_METHOD_CATEGORIES.NOTIFICATIONS,
|
|
215
|
+
MCP_METHOD_CATEGORIES.CLIENT_FEATURES,
|
|
216
|
+
MCP_METHOD_CATEGORIES.OTHER,
|
|
217
|
+
];
|
|
218
|
+
const aIndex = order.indexOf(a.category);
|
|
219
|
+
const bIndex = order.indexOf(b.category);
|
|
220
|
+
return aIndex - bIndex;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get a short description of what an MCP method does
|
|
226
|
+
* Based on the protocol specification
|
|
227
|
+
*/
|
|
228
|
+
export function getMethodDescription(method) {
|
|
229
|
+
if (!method) return 'Unknown operation';
|
|
230
|
+
|
|
231
|
+
const descriptions = {
|
|
232
|
+
// Lifecycle
|
|
233
|
+
initialize: 'Initialize MCP connection and negotiate capabilities',
|
|
234
|
+
'notifications/initialized': 'Client ready notification after initialization',
|
|
235
|
+
|
|
236
|
+
// Tools
|
|
237
|
+
'tools/list': 'Discover available tools from server',
|
|
238
|
+
'tools/call': 'Execute a tool with arguments',
|
|
239
|
+
|
|
240
|
+
// Resources
|
|
241
|
+
'resources/list': 'List available direct resources',
|
|
242
|
+
'resources/templates/list': 'Discover resource templates',
|
|
243
|
+
'resources/read': 'Retrieve resource contents',
|
|
244
|
+
'resources/subscribe': 'Monitor resource changes',
|
|
245
|
+
|
|
246
|
+
// Prompts
|
|
247
|
+
'prompts/list': 'Discover available prompts',
|
|
248
|
+
'prompts/get': 'Retrieve prompt details',
|
|
249
|
+
|
|
250
|
+
// Notifications
|
|
251
|
+
'notifications/tools/list_changed': 'Server notifies client that tool list changed',
|
|
252
|
+
'notifications/resources/list_changed': 'Server notifies client that resource list changed',
|
|
253
|
+
'notifications/prompts/list_changed': 'Server notifies client that prompt list changed',
|
|
254
|
+
|
|
255
|
+
// Client features
|
|
256
|
+
'elicitation/request': 'Server requests information from user',
|
|
257
|
+
'sampling/complete': 'Server requests LLM completion from client',
|
|
258
|
+
'logging/message': 'Server sends log message to client',
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
return descriptions[method] || `${method} operation`;
|
|
262
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
export function extractServerName(request) {
|
|
2
|
+
if (request.body_json) {
|
|
3
|
+
try {
|
|
4
|
+
const body =
|
|
5
|
+
typeof request.body_json === 'string' ? JSON.parse(request.body_json) : request.body_json;
|
|
6
|
+
if (body.params && body.params.name) {
|
|
7
|
+
const fullName = body.params.name;
|
|
8
|
+
return fullName.includes('.') ? fullName.split('.')[0] : fullName;
|
|
9
|
+
}
|
|
10
|
+
} catch (e) {
|
|
11
|
+
// Failed to parse JSON, try body_raw
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (request.body_raw) {
|
|
16
|
+
try {
|
|
17
|
+
const body =
|
|
18
|
+
typeof request.body_raw === 'string' ? JSON.parse(request.body_raw) : request.body_raw;
|
|
19
|
+
if (body.params && body.params.name) {
|
|
20
|
+
const fullName = body.params.name;
|
|
21
|
+
return fullName.includes('.') ? fullName.split('.')[0] : fullName;
|
|
22
|
+
}
|
|
23
|
+
} catch (e) {
|
|
24
|
+
// Failed to parse
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (request.host) {
|
|
29
|
+
return request.host;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return '__UNKNOWN_SERVER__';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function formatRelativeTime(timestampISO, firstTime) {
|
|
36
|
+
if (!firstTime) return '0.000000';
|
|
37
|
+
const diff = new Date(timestampISO) - new Date(firstTime);
|
|
38
|
+
return (diff / 1000).toFixed(6);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function formatDateTime(timestampISO) {
|
|
42
|
+
if (!timestampISO) return '-';
|
|
43
|
+
try {
|
|
44
|
+
const date = new Date(timestampISO);
|
|
45
|
+
return date.toLocaleString('en-US', {
|
|
46
|
+
year: 'numeric',
|
|
47
|
+
month: '2-digit',
|
|
48
|
+
day: '2-digit',
|
|
49
|
+
hour: '2-digit',
|
|
50
|
+
minute: '2-digit',
|
|
51
|
+
second: '2-digit',
|
|
52
|
+
hour12: false,
|
|
53
|
+
});
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return timestampISO;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getSourceDest(request) {
|
|
60
|
+
if (request.direction === 'request') {
|
|
61
|
+
return {
|
|
62
|
+
source: request.remote_address || 'Client',
|
|
63
|
+
dest: request.host || 'Server',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
source: request.host || 'Server',
|
|
68
|
+
dest: request.remote_address || 'Client',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getEndpoint(request) {
|
|
73
|
+
if (request.direction === 'request') {
|
|
74
|
+
if (request.body_json) {
|
|
75
|
+
try {
|
|
76
|
+
const body =
|
|
77
|
+
typeof request.body_json === 'string' ? JSON.parse(request.body_json) : request.body_json;
|
|
78
|
+
if (body && typeof body === 'object' && body.method) {
|
|
79
|
+
return body.method;
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {
|
|
82
|
+
// Failed to parse JSON, try body_raw
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (request.body_raw) {
|
|
86
|
+
try {
|
|
87
|
+
const body =
|
|
88
|
+
typeof request.body_raw === 'string' ? JSON.parse(request.body_raw) : request.body_raw;
|
|
89
|
+
if (body && typeof body === 'object' && body.method) {
|
|
90
|
+
return body.method;
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// Failed to parse
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (request.jsonrpc_method) {
|
|
97
|
+
return request.jsonrpc_method;
|
|
98
|
+
}
|
|
99
|
+
if (request.url) {
|
|
100
|
+
try {
|
|
101
|
+
const url = new URL(request.url);
|
|
102
|
+
return url.pathname + (url.search || '');
|
|
103
|
+
} catch (e) {
|
|
104
|
+
const url = request.url;
|
|
105
|
+
const match = url.match(/^https?:\/\/[^\/]+(\/.*)$/);
|
|
106
|
+
return match ? match[1] : url;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return '-';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getInfo(request) {
|
|
114
|
+
if (request.direction === 'request') {
|
|
115
|
+
// Use getEndpoint to get the method/endpoint (it already handles extraction from body)
|
|
116
|
+
const endpoint = getEndpoint(request);
|
|
117
|
+
|
|
118
|
+
// Get HTTP method if available
|
|
119
|
+
const httpMethod = request.method || '';
|
|
120
|
+
|
|
121
|
+
// Get URL if available
|
|
122
|
+
const url = request.url || '';
|
|
123
|
+
|
|
124
|
+
// Build info string - prioritize endpoint (JSON-RPC method), then HTTP method + URL, then just method
|
|
125
|
+
if (endpoint && endpoint !== '-') {
|
|
126
|
+
// If we have both HTTP method and endpoint, show both
|
|
127
|
+
if (httpMethod && url) {
|
|
128
|
+
return `${httpMethod} ${endpoint}`;
|
|
129
|
+
} else if (httpMethod) {
|
|
130
|
+
return `${httpMethod} ${endpoint}`;
|
|
131
|
+
}
|
|
132
|
+
return endpoint;
|
|
133
|
+
} else if (httpMethod && url) {
|
|
134
|
+
return `${httpMethod} ${url}`;
|
|
135
|
+
} else if (httpMethod) {
|
|
136
|
+
return httpMethod;
|
|
137
|
+
} else if (url) {
|
|
138
|
+
return url;
|
|
139
|
+
}
|
|
140
|
+
return 'Request';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// For responses
|
|
144
|
+
const status = request.status_code || '';
|
|
145
|
+
|
|
146
|
+
// Try to get JSON-RPC method if available
|
|
147
|
+
const rpcMethod = request.jsonrpc_method || getJsonRpcMethod(request);
|
|
148
|
+
|
|
149
|
+
if (status && rpcMethod) {
|
|
150
|
+
return `${status} ${rpcMethod}`;
|
|
151
|
+
} else if (status) {
|
|
152
|
+
return `Status: ${status}`;
|
|
153
|
+
} else if (rpcMethod) {
|
|
154
|
+
return rpcMethod;
|
|
155
|
+
}
|
|
156
|
+
return 'Response';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function getRequestColor(request) {
|
|
160
|
+
if (request.direction === 'request') {
|
|
161
|
+
return '#faf9f7';
|
|
162
|
+
}
|
|
163
|
+
if (request.status_code >= 400) {
|
|
164
|
+
return '#fef0f0';
|
|
165
|
+
}
|
|
166
|
+
if (request.status_code >= 300) {
|
|
167
|
+
return '#fff8e8';
|
|
168
|
+
}
|
|
169
|
+
return '#f0f8f0';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Helper function to extract JSON-RPC method from a request or response
|
|
173
|
+
export function getJsonRpcMethod(req) {
|
|
174
|
+
// First check the jsonrpc_method field (most reliable)
|
|
175
|
+
if (req.jsonrpc_method) {
|
|
176
|
+
return req.jsonrpc_method;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// For requests, try to extract from body
|
|
180
|
+
if (req.direction === 'request') {
|
|
181
|
+
if (req.body_json) {
|
|
182
|
+
try {
|
|
183
|
+
const body = typeof req.body_json === 'string' ? JSON.parse(req.body_json) : req.body_json;
|
|
184
|
+
if (body && typeof body === 'object' && body.method) {
|
|
185
|
+
return body.method;
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
// Failed to parse
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (req.body_raw) {
|
|
192
|
+
try {
|
|
193
|
+
const body = typeof req.body_raw === 'string' ? JSON.parse(req.body_raw) : req.body_raw;
|
|
194
|
+
if (body && typeof body === 'object' && body.method) {
|
|
195
|
+
return body.method;
|
|
196
|
+
}
|
|
197
|
+
} catch (e) {
|
|
198
|
+
// Failed to parse
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// For responses, try to extract from body if available
|
|
204
|
+
if (req.direction === 'response' && req.body_json) {
|
|
205
|
+
try {
|
|
206
|
+
const body = typeof req.body_json === 'string' ? JSON.parse(req.body_json) : req.body_json;
|
|
207
|
+
// Responses don't have a method field, but we can check if it's an error response
|
|
208
|
+
// For now, we'll rely on jsonrpc_method field
|
|
209
|
+
} catch (e) {
|
|
210
|
+
// Failed to parse
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function pairRequestsWithResponses(requests) {
|
|
218
|
+
const pairs = [];
|
|
219
|
+
const processed = new Set();
|
|
220
|
+
|
|
221
|
+
// Helper function to check if two requests match (same session, JSON-RPC method, and optionally jsonrpc_id)
|
|
222
|
+
const matches = (req, resp) => {
|
|
223
|
+
// Session ID must match (or both null for initiation)
|
|
224
|
+
const sessionMatch = req.session_id === resp.session_id;
|
|
225
|
+
if (!sessionMatch) return false;
|
|
226
|
+
|
|
227
|
+
// JSON-RPC Method must match
|
|
228
|
+
const reqMethod = getJsonRpcMethod(req);
|
|
229
|
+
const respMethod = getJsonRpcMethod(resp);
|
|
230
|
+
|
|
231
|
+
// Both must have a method, and they must match
|
|
232
|
+
if (!reqMethod || !respMethod) {
|
|
233
|
+
// If either doesn't have a method, we can't match by method
|
|
234
|
+
// Fall back to JSON-RPC ID matching only
|
|
235
|
+
if (req.jsonrpc_id && resp.jsonrpc_id) {
|
|
236
|
+
return req.jsonrpc_id === resp.jsonrpc_id;
|
|
237
|
+
}
|
|
238
|
+
// If no method and no ID, we can't match reliably
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const methodMatch = reqMethod === respMethod;
|
|
243
|
+
if (!methodMatch) return false;
|
|
244
|
+
|
|
245
|
+
// If JSON-RPC ID exists, it must match (for more precise pairing)
|
|
246
|
+
if (req.jsonrpc_id && resp.jsonrpc_id) {
|
|
247
|
+
return req.jsonrpc_id === resp.jsonrpc_id;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// If no JSON-RPC ID, match by session and method only
|
|
251
|
+
return true;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
requests.forEach((request) => {
|
|
255
|
+
if (processed.has(request.frame_number)) return;
|
|
256
|
+
|
|
257
|
+
if (request.direction === 'request') {
|
|
258
|
+
// Find matching response - must match session, endpoint, and optionally jsonrpc_id
|
|
259
|
+
const response = requests.find(
|
|
260
|
+
(r) =>
|
|
261
|
+
r.direction === 'response' &&
|
|
262
|
+
!processed.has(r.frame_number) &&
|
|
263
|
+
matches(request, r) &&
|
|
264
|
+
r.frame_number > request.frame_number
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
if (response) {
|
|
268
|
+
pairs.push({ request, response, frame_number: request.frame_number });
|
|
269
|
+
processed.add(request.frame_number);
|
|
270
|
+
processed.add(response.frame_number);
|
|
271
|
+
} else {
|
|
272
|
+
// Request without response
|
|
273
|
+
pairs.push({ request, response: null, frame_number: request.frame_number });
|
|
274
|
+
processed.add(request.frame_number);
|
|
275
|
+
}
|
|
276
|
+
} else if (request.direction === 'response') {
|
|
277
|
+
// Find matching request - must match session, endpoint, and optionally jsonrpc_id
|
|
278
|
+
const matchingRequest = requests.find(
|
|
279
|
+
(r) =>
|
|
280
|
+
r.direction === 'request' &&
|
|
281
|
+
!processed.has(r.frame_number) &&
|
|
282
|
+
matches(r, request) &&
|
|
283
|
+
r.frame_number < request.frame_number
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
if (!matchingRequest) {
|
|
287
|
+
// Response without request (orphaned)
|
|
288
|
+
pairs.push({ request: null, response: request, frame_number: request.frame_number });
|
|
289
|
+
processed.add(request.frame_number);
|
|
290
|
+
}
|
|
291
|
+
// If matching request exists, it will be handled when we iterate over it
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Sort by frame number (descending - latest first)
|
|
296
|
+
return pairs.sort((a, b) => b.frame_number - a.frame_number);
|
|
297
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [react()],
|
|
6
|
+
server: {
|
|
7
|
+
proxy: {
|
|
8
|
+
'/api': {
|
|
9
|
+
target: 'http://localhost:9853',
|
|
10
|
+
changeOrigin: true,
|
|
11
|
+
},
|
|
12
|
+
'/ws': {
|
|
13
|
+
target: 'ws://localhost:9853',
|
|
14
|
+
ws: true,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|