@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.
Files changed (212) hide show
  1. package/LICENSE +85 -0
  2. package/README.md +724 -0
  3. package/bin/mcp-shark.js +93 -0
  4. package/mcp-server/.editorconfig +15 -0
  5. package/mcp-server/.prettierignore +11 -0
  6. package/mcp-server/.prettierrc +12 -0
  7. package/mcp-server/README.md +280 -0
  8. package/mcp-server/commitlint.config.cjs +42 -0
  9. package/mcp-server/eslint.config.js +131 -0
  10. package/mcp-server/lib/auditor/audit.js +228 -0
  11. package/mcp-server/lib/common/error.js +15 -0
  12. package/mcp-server/lib/server/external/all.js +32 -0
  13. package/mcp-server/lib/server/external/config.js +59 -0
  14. package/mcp-server/lib/server/external/kv.js +102 -0
  15. package/mcp-server/lib/server/external/single/client.js +35 -0
  16. package/mcp-server/lib/server/external/single/request.js +49 -0
  17. package/mcp-server/lib/server/external/single/run.js +75 -0
  18. package/mcp-server/lib/server/external/single/transport.js +57 -0
  19. package/mcp-server/lib/server/internal/handlers/common.js +20 -0
  20. package/mcp-server/lib/server/internal/handlers/error.js +7 -0
  21. package/mcp-server/lib/server/internal/handlers/prompts-get.js +22 -0
  22. package/mcp-server/lib/server/internal/handlers/prompts-list.js +12 -0
  23. package/mcp-server/lib/server/internal/handlers/resources-list.js +12 -0
  24. package/mcp-server/lib/server/internal/handlers/resources-read.js +19 -0
  25. package/mcp-server/lib/server/internal/handlers/tools-call.js +37 -0
  26. package/mcp-server/lib/server/internal/handlers/tools-list.js +14 -0
  27. package/mcp-server/lib/server/internal/run.js +49 -0
  28. package/mcp-server/lib/server/internal/server.js +63 -0
  29. package/mcp-server/lib/server/internal/session.js +39 -0
  30. package/mcp-server/mcp-shark.js +72 -0
  31. package/mcp-server/package-lock.json +4784 -0
  32. package/mcp-server/package.json +30 -0
  33. package/package.json +103 -0
  34. package/ui/README.md +212 -0
  35. package/ui/index.html +16 -0
  36. package/ui/package-lock.json +3574 -0
  37. package/ui/package.json +12 -0
  38. package/ui/paths.js +282 -0
  39. package/ui/public/og-image.png +0 -0
  40. package/ui/server/routes/backups.js +251 -0
  41. package/ui/server/routes/composite.js +244 -0
  42. package/ui/server/routes/config.js +175 -0
  43. package/ui/server/routes/conversations.js +25 -0
  44. package/ui/server/routes/help.js +43 -0
  45. package/ui/server/routes/logs.js +32 -0
  46. package/ui/server/routes/playground.js +152 -0
  47. package/ui/server/routes/requests.js +235 -0
  48. package/ui/server/routes/sessions.js +27 -0
  49. package/ui/server/routes/smartscan/discover.js +117 -0
  50. package/ui/server/routes/smartscan/scans/clearCache.js +22 -0
  51. package/ui/server/routes/smartscan/scans/createBatchScans.js +123 -0
  52. package/ui/server/routes/smartscan/scans/createScan.js +42 -0
  53. package/ui/server/routes/smartscan/scans/getCachedResults.js +51 -0
  54. package/ui/server/routes/smartscan/scans/getScan.js +41 -0
  55. package/ui/server/routes/smartscan/scans/listScans.js +24 -0
  56. package/ui/server/routes/smartscan/scans.js +13 -0
  57. package/ui/server/routes/smartscan/token.js +56 -0
  58. package/ui/server/routes/smartscan/transport.js +53 -0
  59. package/ui/server/routes/smartscan.js +24 -0
  60. package/ui/server/routes/statistics.js +83 -0
  61. package/ui/server/utils/config-update.js +212 -0
  62. package/ui/server/utils/config.js +98 -0
  63. package/ui/server/utils/paths.js +23 -0
  64. package/ui/server/utils/port.js +28 -0
  65. package/ui/server/utils/process.js +80 -0
  66. package/ui/server/utils/scan-cache/all-results.js +180 -0
  67. package/ui/server/utils/scan-cache/directory.js +35 -0
  68. package/ui/server/utils/scan-cache/file-operations.js +104 -0
  69. package/ui/server/utils/scan-cache/hash.js +47 -0
  70. package/ui/server/utils/scan-cache/server-operations.js +80 -0
  71. package/ui/server/utils/scan-cache.js +12 -0
  72. package/ui/server/utils/serialization.js +13 -0
  73. package/ui/server/utils/smartscan-token.js +42 -0
  74. package/ui/server.js +199 -0
  75. package/ui/src/App.jsx +153 -0
  76. package/ui/src/CompositeLogs.jsx +164 -0
  77. package/ui/src/CompositeSetup.jsx +285 -0
  78. package/ui/src/HelpGuide/HelpGuideContent.jsx +118 -0
  79. package/ui/src/HelpGuide/HelpGuideFooter.jsx +58 -0
  80. package/ui/src/HelpGuide/HelpGuideHeader.jsx +56 -0
  81. package/ui/src/HelpGuide.jsx +65 -0
  82. package/ui/src/IntroTour.jsx +140 -0
  83. package/ui/src/LogDetail.jsx +122 -0
  84. package/ui/src/LogTable.jsx +242 -0
  85. package/ui/src/PacketDetail.jsx +190 -0
  86. package/ui/src/PacketFilters.jsx +222 -0
  87. package/ui/src/PacketList.jsx +183 -0
  88. package/ui/src/SmartScan.jsx +178 -0
  89. package/ui/src/TabNavigation.jsx +143 -0
  90. package/ui/src/components/App/HelpButton.jsx +64 -0
  91. package/ui/src/components/App/TrafficTab.jsx +69 -0
  92. package/ui/src/components/App/useAppState.js +163 -0
  93. package/ui/src/components/BackupList.jsx +192 -0
  94. package/ui/src/components/CollapsibleSection.jsx +82 -0
  95. package/ui/src/components/ConfigFileSection.jsx +84 -0
  96. package/ui/src/components/ConfigViewerModal.jsx +141 -0
  97. package/ui/src/components/ConfirmationModal.jsx +129 -0
  98. package/ui/src/components/DetailsTab/BodySection.jsx +27 -0
  99. package/ui/src/components/DetailsTab/CollapsibleRequestResponse.jsx +70 -0
  100. package/ui/src/components/DetailsTab/HeadersSection.jsx +25 -0
  101. package/ui/src/components/DetailsTab/InfoSection.jsx +28 -0
  102. package/ui/src/components/DetailsTab/NetworkInfoSection.jsx +63 -0
  103. package/ui/src/components/DetailsTab/ProtocolInfoSection.jsx +75 -0
  104. package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +46 -0
  105. package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +66 -0
  106. package/ui/src/components/DetailsTab.jsx +31 -0
  107. package/ui/src/components/DetectedPathsList.jsx +171 -0
  108. package/ui/src/components/FileInput.jsx +144 -0
  109. package/ui/src/components/GroupHeader.jsx +76 -0
  110. package/ui/src/components/GroupedByMcpView.jsx +103 -0
  111. package/ui/src/components/GroupedByServerView.jsx +134 -0
  112. package/ui/src/components/GroupedBySessionView.jsx +127 -0
  113. package/ui/src/components/GroupedViews.jsx +2 -0
  114. package/ui/src/components/HexTab.jsx +188 -0
  115. package/ui/src/components/LogsDisplay.jsx +93 -0
  116. package/ui/src/components/LogsToolbar.jsx +193 -0
  117. package/ui/src/components/McpPlayground/LoadingModal.jsx +113 -0
  118. package/ui/src/components/McpPlayground/PromptsSection/PromptCallPanel.jsx +125 -0
  119. package/ui/src/components/McpPlayground/PromptsSection/PromptItem.jsx +48 -0
  120. package/ui/src/components/McpPlayground/PromptsSection/PromptsList.jsx +45 -0
  121. package/ui/src/components/McpPlayground/PromptsSection.jsx +106 -0
  122. package/ui/src/components/McpPlayground/ResourcesSection/ResourceCallPanel.jsx +89 -0
  123. package/ui/src/components/McpPlayground/ResourcesSection/ResourceItem.jsx +59 -0
  124. package/ui/src/components/McpPlayground/ResourcesSection/ResourcesList.jsx +45 -0
  125. package/ui/src/components/McpPlayground/ResourcesSection.jsx +91 -0
  126. package/ui/src/components/McpPlayground/ToolsSection/ToolCallPanel.jsx +125 -0
  127. package/ui/src/components/McpPlayground/ToolsSection/ToolItem.jsx +48 -0
  128. package/ui/src/components/McpPlayground/ToolsSection/ToolsList.jsx +45 -0
  129. package/ui/src/components/McpPlayground/ToolsSection.jsx +107 -0
  130. package/ui/src/components/McpPlayground/common/EmptyState.jsx +17 -0
  131. package/ui/src/components/McpPlayground/common/ErrorState.jsx +17 -0
  132. package/ui/src/components/McpPlayground/common/LoadingState.jsx +17 -0
  133. package/ui/src/components/McpPlayground/useMcpPlayground.js +280 -0
  134. package/ui/src/components/McpPlayground.jsx +171 -0
  135. package/ui/src/components/MessageDisplay.jsx +28 -0
  136. package/ui/src/components/PacketDetailHeader.jsx +88 -0
  137. package/ui/src/components/PacketFilters/ExportControls.jsx +126 -0
  138. package/ui/src/components/PacketFilters/FilterInput.jsx +59 -0
  139. package/ui/src/components/RawTab.jsx +142 -0
  140. package/ui/src/components/RequestRow/OrphanedResponseRow.jsx +155 -0
  141. package/ui/src/components/RequestRow/RequestRowMain.jsx +240 -0
  142. package/ui/src/components/RequestRow/ResponseRow.jsx +158 -0
  143. package/ui/src/components/RequestRow.jsx +70 -0
  144. package/ui/src/components/ServerControl.jsx +133 -0
  145. package/ui/src/components/ServiceSelector.jsx +209 -0
  146. package/ui/src/components/SetupHeader.jsx +30 -0
  147. package/ui/src/components/SharkLogo.jsx +21 -0
  148. package/ui/src/components/SmartScan/AnalysisResult.jsx +64 -0
  149. package/ui/src/components/SmartScan/BatchResultsDisplay/BatchResultItem.jsx +215 -0
  150. package/ui/src/components/SmartScan/BatchResultsDisplay/BatchResultsHeader.jsx +94 -0
  151. package/ui/src/components/SmartScan/BatchResultsDisplay.jsx +26 -0
  152. package/ui/src/components/SmartScan/DebugInfoSection.jsx +53 -0
  153. package/ui/src/components/SmartScan/EmptyState.jsx +57 -0
  154. package/ui/src/components/SmartScan/ErrorDisplay.jsx +48 -0
  155. package/ui/src/components/SmartScan/ExpandableSection.jsx +93 -0
  156. package/ui/src/components/SmartScan/FindingsTable.jsx +257 -0
  157. package/ui/src/components/SmartScan/ListViewContent.jsx +75 -0
  158. package/ui/src/components/SmartScan/NotablePatternsSection.jsx +75 -0
  159. package/ui/src/components/SmartScan/OverallSummarySection.jsx +72 -0
  160. package/ui/src/components/SmartScan/RawDataSection.jsx +52 -0
  161. package/ui/src/components/SmartScan/RecommendationsSection.jsx +78 -0
  162. package/ui/src/components/SmartScan/ScanDetailHeader.jsx +92 -0
  163. package/ui/src/components/SmartScan/ScanDetailView.jsx +141 -0
  164. package/ui/src/components/SmartScan/ScanListView/ScanListHeader.jsx +49 -0
  165. package/ui/src/components/SmartScan/ScanListView/ScanListItem.jsx +201 -0
  166. package/ui/src/components/SmartScan/ScanListView.jsx +73 -0
  167. package/ui/src/components/SmartScan/ScanOverviewSection.jsx +123 -0
  168. package/ui/src/components/SmartScan/ScanResultsDisplay.jsx +35 -0
  169. package/ui/src/components/SmartScan/ScanViewContent.jsx +68 -0
  170. package/ui/src/components/SmartScan/ScanningProgress.jsx +47 -0
  171. package/ui/src/components/SmartScan/ServerInfoSection.jsx +43 -0
  172. package/ui/src/components/SmartScan/ServerSelectionRow.jsx +207 -0
  173. package/ui/src/components/SmartScan/SingleResultDisplay.jsx +269 -0
  174. package/ui/src/components/SmartScan/SmartScanControls.jsx +290 -0
  175. package/ui/src/components/SmartScan/SmartScanHeader.jsx +77 -0
  176. package/ui/src/components/SmartScan/ViewModeTabs.jsx +57 -0
  177. package/ui/src/components/SmartScan/hooks/useCacheManagement.js +34 -0
  178. package/ui/src/components/SmartScan/hooks/useMcpDiscovery.js +121 -0
  179. package/ui/src/components/SmartScan/hooks/useScanList.js +193 -0
  180. package/ui/src/components/SmartScan/hooks/useScanOperations.js +87 -0
  181. package/ui/src/components/SmartScan/hooks/useServerStatus.js +26 -0
  182. package/ui/src/components/SmartScan/hooks/useTokenManagement.js +53 -0
  183. package/ui/src/components/SmartScan/scanDataUtils.js +98 -0
  184. package/ui/src/components/SmartScan/useSmartScan.js +72 -0
  185. package/ui/src/components/SmartScan/utils.js +19 -0
  186. package/ui/src/components/SmartScanIcons.jsx +58 -0
  187. package/ui/src/components/TabNavigation/DesktopTabs.jsx +111 -0
  188. package/ui/src/components/TabNavigation/MobileDropdown.jsx +140 -0
  189. package/ui/src/components/TabNavigation.jsx +97 -0
  190. package/ui/src/components/TabNavigationIcons.jsx +40 -0
  191. package/ui/src/components/TableHeader.jsx +164 -0
  192. package/ui/src/components/TourOverlay.jsx +117 -0
  193. package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +117 -0
  194. package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +70 -0
  195. package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +45 -0
  196. package/ui/src/components/TourTooltip/useTooltipPosition.js +108 -0
  197. package/ui/src/components/TourTooltip.jsx +83 -0
  198. package/ui/src/components/ViewModeTabs.jsx +91 -0
  199. package/ui/src/components/WhatThisDoesSection.jsx +61 -0
  200. package/ui/src/config/tourSteps.jsx +141 -0
  201. package/ui/src/hooks/useAnimation.js +92 -0
  202. package/ui/src/hooks/useConfigManagement.js +124 -0
  203. package/ui/src/hooks/useServiceExtraction.js +51 -0
  204. package/ui/src/index.css +42 -0
  205. package/ui/src/main.jsx +10 -0
  206. package/ui/src/theme.js +65 -0
  207. package/ui/src/utils/animations.js +170 -0
  208. package/ui/src/utils/groupingUtils.js +93 -0
  209. package/ui/src/utils/hexUtils.js +24 -0
  210. package/ui/src/utils/mcpGroupingUtils.js +262 -0
  211. package/ui/src/utils/requestUtils.js +297 -0
  212. 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
+ });