@mcp-shark/mcp-shark 1.5.4 → 1.5.5

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 (188) hide show
  1. package/README.md +32 -96
  2. package/bin/mcp-shark.js +1 -1
  3. package/core/configs/codex.js +68 -0
  4. package/core/configs/environment.js +51 -0
  5. package/{lib/common → core}/configs/index.js +16 -1
  6. package/core/constants/Defaults.js +15 -0
  7. package/core/constants/HttpStatus.js +14 -0
  8. package/core/constants/Server.js +20 -0
  9. package/core/constants/StatusCodes.js +25 -0
  10. package/core/constants/index.js +7 -0
  11. package/core/container/DependencyContainer.js +179 -0
  12. package/core/db/init.js +33 -0
  13. package/core/index.js +10 -0
  14. package/{mcp-server/lib/common/error.js → core/libraries/ErrorLibrary.js} +4 -0
  15. package/core/libraries/LoggerLibrary.js +91 -0
  16. package/core/libraries/SerializationLibrary.js +32 -0
  17. package/core/libraries/bootstrap-logger.js +19 -0
  18. package/core/libraries/errors/ApplicationError.js +97 -0
  19. package/core/libraries/index.js +17 -0
  20. package/{mcp-server/lib → core/mcp-server}/auditor/audit.js +77 -53
  21. package/core/mcp-server/index.js +192 -0
  22. package/{mcp-server/lib → core/mcp-server}/server/external/all.js +1 -1
  23. package/core/mcp-server/server/external/config.js +75 -0
  24. package/{mcp-server/lib → core/mcp-server}/server/external/single/client.js +1 -1
  25. package/{mcp-server/lib → core/mcp-server}/server/external/single/request.js +1 -1
  26. package/{mcp-server/lib → core/mcp-server}/server/external/single/run.js +20 -11
  27. package/{mcp-server/lib → core/mcp-server}/server/external/single/transport.js +1 -1
  28. package/{mcp-server/lib → core/mcp-server}/server/internal/handlers/error.js +1 -1
  29. package/core/mcp-server/server/internal/handlers/prompts-get.js +28 -0
  30. package/core/mcp-server/server/internal/handlers/prompts-list.js +21 -0
  31. package/core/mcp-server/server/internal/handlers/resources-list.js +21 -0
  32. package/core/mcp-server/server/internal/handlers/resources-read.js +28 -0
  33. package/core/mcp-server/server/internal/handlers/tools-call.js +44 -0
  34. package/core/mcp-server/server/internal/handlers/tools-list.js +23 -0
  35. package/core/mcp-server/server/internal/run.js +53 -0
  36. package/{mcp-server/lib → core/mcp-server}/server/internal/server.js +11 -1
  37. package/core/models/ConversationFilters.js +31 -0
  38. package/core/models/ExportFormat.js +8 -0
  39. package/core/models/RequestFilters.js +43 -0
  40. package/core/models/SessionFilters.js +23 -0
  41. package/core/models/index.js +8 -0
  42. package/core/repositories/AuditRepository.js +233 -0
  43. package/core/repositories/ConversationRepository.js +182 -0
  44. package/core/repositories/PacketRepository.js +237 -0
  45. package/core/repositories/SchemaRepository.js +107 -0
  46. package/core/repositories/SessionRepository.js +59 -0
  47. package/core/repositories/StatisticsRepository.js +54 -0
  48. package/core/repositories/index.js +10 -0
  49. package/core/services/AuditService.js +144 -0
  50. package/core/services/BackupService.js +222 -0
  51. package/core/services/ConfigDetectionService.js +89 -0
  52. package/core/services/ConfigFileService.js +210 -0
  53. package/core/services/ConfigPatchingService.js +137 -0
  54. package/core/services/ConfigService.js +250 -0
  55. package/core/services/ConfigTransformService.js +178 -0
  56. package/core/services/ConversationService.js +19 -0
  57. package/core/services/ExportService.js +117 -0
  58. package/core/services/LogService.js +64 -0
  59. package/core/services/McpClientService.js +235 -0
  60. package/core/services/McpDiscoveryService.js +107 -0
  61. package/core/services/RequestService.js +56 -0
  62. package/core/services/ScanCacheService.js +242 -0
  63. package/core/services/ScanService.js +167 -0
  64. package/core/services/ServerManagementService.js +206 -0
  65. package/core/services/SessionService.js +34 -0
  66. package/core/services/SettingsService.js +163 -0
  67. package/core/services/StatisticsService.js +64 -0
  68. package/core/services/TokenService.js +94 -0
  69. package/core/services/index.js +25 -0
  70. package/core/services/parsers/ConfigParserFactory.js +113 -0
  71. package/core/services/parsers/JsonConfigParser.js +66 -0
  72. package/core/services/parsers/LegacyJsonConfigParser.js +71 -0
  73. package/core/services/parsers/TomlConfigParser.js +87 -0
  74. package/core/services/parsers/index.js +4 -0
  75. package/{ui/server → core}/utils/scan-cache/directory.js +1 -1
  76. package/core/utils/validation.js +77 -0
  77. package/package.json +14 -11
  78. package/ui/dist/assets/index-CArYxKxS.js +35 -0
  79. package/ui/dist/index.html +1 -1
  80. package/ui/server/controllers/BackupController.js +129 -0
  81. package/ui/server/controllers/ConfigController.js +92 -0
  82. package/ui/server/controllers/ConversationController.js +41 -0
  83. package/ui/server/controllers/LogController.js +44 -0
  84. package/ui/server/controllers/McpClientController.js +60 -0
  85. package/ui/server/controllers/McpDiscoveryController.js +44 -0
  86. package/ui/server/controllers/RequestController.js +129 -0
  87. package/ui/server/controllers/ScanController.js +122 -0
  88. package/ui/server/controllers/ServerManagementController.js +134 -0
  89. package/ui/server/controllers/SessionController.js +57 -0
  90. package/ui/server/controllers/SettingsController.js +24 -0
  91. package/ui/server/controllers/StatisticsController.js +54 -0
  92. package/ui/server/controllers/TokenController.js +58 -0
  93. package/ui/server/controllers/index.js +17 -0
  94. package/ui/server/routes/backups/index.js +15 -9
  95. package/ui/server/routes/composite/index.js +62 -32
  96. package/ui/server/routes/composite/servers.js +20 -15
  97. package/ui/server/routes/config.js +13 -172
  98. package/ui/server/routes/conversations.js +9 -19
  99. package/ui/server/routes/help.js +4 -3
  100. package/ui/server/routes/logs.js +14 -26
  101. package/ui/server/routes/playground.js +11 -174
  102. package/ui/server/routes/requests.js +12 -232
  103. package/ui/server/routes/sessions.js +10 -21
  104. package/ui/server/routes/settings.js +10 -192
  105. package/ui/server/routes/smartscan.js +26 -15
  106. package/ui/server/routes/statistics.js +8 -79
  107. package/ui/server/setup.js +162 -0
  108. package/ui/server/swagger/paths/backups.js +151 -0
  109. package/ui/server/swagger/paths/components.js +76 -0
  110. package/ui/server/swagger/paths/config.js +117 -0
  111. package/ui/server/swagger/paths/conversations.js +29 -0
  112. package/ui/server/swagger/paths/help.js +82 -0
  113. package/ui/server/swagger/paths/logs.js +87 -0
  114. package/ui/server/swagger/paths/playground.js +49 -0
  115. package/ui/server/swagger/paths/requests.js +178 -0
  116. package/ui/server/swagger/paths/serverManagement.js +169 -0
  117. package/ui/server/swagger/paths/sessions.js +61 -0
  118. package/ui/server/swagger/paths/settings.js +31 -0
  119. package/ui/server/swagger/paths/smartScan/discovery.js +97 -0
  120. package/ui/server/swagger/paths/smartScan/index.js +13 -0
  121. package/ui/server/swagger/paths/smartScan/scans.js +151 -0
  122. package/ui/server/swagger/paths/smartScan/token.js +71 -0
  123. package/ui/server/swagger/paths/statistics.js +40 -0
  124. package/ui/server/swagger/paths.js +38 -0
  125. package/ui/server/swagger/swagger.js +37 -0
  126. package/ui/server/utils/cleanup.js +99 -0
  127. package/ui/server/utils/config.js +18 -96
  128. package/ui/server/utils/errorHandler.js +43 -0
  129. package/ui/server/utils/logger.js +2 -2
  130. package/ui/server/utils/paths.js +27 -30
  131. package/ui/server/utils/port.js +21 -21
  132. package/ui/server/utils/process.js +18 -10
  133. package/ui/server/utils/processState.js +17 -0
  134. package/ui/server/utils/signals.js +34 -0
  135. package/ui/server/websocket/broadcast.js +33 -0
  136. package/ui/server/websocket/handler.js +52 -0
  137. package/ui/server.js +51 -230
  138. package/ui/src/App.jsx +2 -0
  139. package/ui/src/CompositeSetup.jsx +23 -9
  140. package/ui/src/PacketFilters.jsx +17 -3
  141. package/ui/src/components/AlertModal.jsx +116 -0
  142. package/ui/src/components/App/ApiDocsButton.jsx +57 -0
  143. package/ui/src/components/App/useAppState.js +43 -1
  144. package/ui/src/components/BackupList.jsx +27 -3
  145. package/ui/src/utils/requestPairing.js +35 -36
  146. package/ui/src/utils/requestUtils.js +1 -0
  147. package/lib/common/db/init.js +0 -132
  148. package/lib/common/db/logger.js +0 -349
  149. package/lib/common/db/query.js +0 -403
  150. package/lib/common/logger.js +0 -90
  151. package/mcp-server/index.js +0 -138
  152. package/mcp-server/lib/server/external/config.js +0 -57
  153. package/mcp-server/lib/server/internal/handlers/prompts-get.js +0 -20
  154. package/mcp-server/lib/server/internal/handlers/prompts-list.js +0 -13
  155. package/mcp-server/lib/server/internal/handlers/resources-list.js +0 -13
  156. package/mcp-server/lib/server/internal/handlers/resources-read.js +0 -20
  157. package/mcp-server/lib/server/internal/handlers/tools-call.js +0 -35
  158. package/mcp-server/lib/server/internal/handlers/tools-list.js +0 -15
  159. package/mcp-server/lib/server/internal/run.js +0 -37
  160. package/mcp-server/mcp-shark.js +0 -22
  161. package/ui/dist/assets/index-CFHeMNwd.js +0 -35
  162. package/ui/server/routes/backups/deleteBackup.js +0 -54
  163. package/ui/server/routes/backups/listBackups.js +0 -75
  164. package/ui/server/routes/backups/restoreBackup.js +0 -83
  165. package/ui/server/routes/backups/viewBackup.js +0 -47
  166. package/ui/server/routes/composite/setup.js +0 -129
  167. package/ui/server/routes/composite/status.js +0 -7
  168. package/ui/server/routes/composite/stop.js +0 -39
  169. package/ui/server/routes/composite/utils.js +0 -45
  170. package/ui/server/routes/smartscan/discover.js +0 -118
  171. package/ui/server/routes/smartscan/scans/clearCache.js +0 -23
  172. package/ui/server/routes/smartscan/scans/createBatchScans.js +0 -124
  173. package/ui/server/routes/smartscan/scans/createScan.js +0 -43
  174. package/ui/server/routes/smartscan/scans/getCachedResults.js +0 -52
  175. package/ui/server/routes/smartscan/scans/getScan.js +0 -42
  176. package/ui/server/routes/smartscan/scans/listScans.js +0 -25
  177. package/ui/server/routes/smartscan/scans.js +0 -13
  178. package/ui/server/routes/smartscan/token.js +0 -57
  179. package/ui/server/utils/config-update.js +0 -240
  180. package/ui/server/utils/scan-cache/all-results.js +0 -197
  181. package/ui/server/utils/scan-cache/file-operations.js +0 -107
  182. package/ui/server/utils/scan-cache/hash.js +0 -47
  183. package/ui/server/utils/scan-cache/server-operations.js +0 -85
  184. package/ui/server/utils/scan-cache.js +0 -12
  185. package/ui/server/utils/smartscan-token.js +0 -43
  186. /package/{mcp-server/lib → core/mcp-server}/server/external/kv.js +0 -0
  187. /package/{mcp-server/lib → core/mcp-server}/server/internal/handlers/common.js +0 -0
  188. /package/{mcp-server/lib → core/mcp-server}/server/internal/session.js +0 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Service for configuration transformations
3
+ * Handles converting, filtering, and updating config structures
4
+ */
5
+ export class ConfigTransformService {
6
+ constructor(configParserFactory) {
7
+ this.parserFactory = configParserFactory;
8
+ }
9
+
10
+ /**
11
+ * Convert MCP servers format to servers format
12
+ * Normalizes config first, then converts mcpServers to servers
13
+ */
14
+ convertMcpServersToServers(config) {
15
+ // Normalize config to ensure consistent format
16
+ const normalized = this.parserFactory.normalizeToInternalFormat(config);
17
+ if (!normalized) {
18
+ return { servers: {} };
19
+ }
20
+
21
+ const converted = { servers: {} };
22
+
23
+ // Handle normalized servers (legacy format)
24
+ if (normalized.servers) {
25
+ converted.servers = { ...normalized.servers };
26
+ }
27
+
28
+ // Convert mcpServers to servers format
29
+ if (normalized.mcpServers) {
30
+ for (const [name, cfg] of Object.entries(normalized.mcpServers)) {
31
+ const type = cfg.type || (cfg.url ? 'http' : cfg.command ? 'stdio' : 'stdio');
32
+ converted.servers[name] = { type, ...cfg };
33
+ }
34
+ }
35
+
36
+ return converted;
37
+ }
38
+
39
+ /**
40
+ * Extract services from config
41
+ */
42
+ extractServices(config) {
43
+ const { mcpServers, servers } = config;
44
+ const servicesMap = new Map();
45
+
46
+ if (servers) {
47
+ for (const [name, cfg] of Object.entries(servers)) {
48
+ const type = cfg.type || (cfg.url ? 'http' : cfg.command ? 'stdio' : 'stdio');
49
+ servicesMap.set(name, {
50
+ name,
51
+ type,
52
+ url: cfg.url || null,
53
+ command: cfg.command || null,
54
+ args: cfg.args || null,
55
+ });
56
+ }
57
+ }
58
+
59
+ if (mcpServers) {
60
+ for (const [name, cfg] of Object.entries(mcpServers)) {
61
+ if (!servicesMap.has(name)) {
62
+ const type = cfg.type || (cfg.url ? 'http' : cfg.command ? 'stdio' : 'stdio');
63
+ servicesMap.set(name, {
64
+ name,
65
+ type,
66
+ url: cfg.url || null,
67
+ command: cfg.command || null,
68
+ args: cfg.args || null,
69
+ });
70
+ }
71
+ }
72
+ }
73
+
74
+ return Array.from(servicesMap.values());
75
+ }
76
+
77
+ /**
78
+ * Filter servers from config by selected service names
79
+ */
80
+ filterServers(config, selectedServices) {
81
+ if (!selectedServices || !Array.isArray(selectedServices) || selectedServices.length === 0) {
82
+ return config;
83
+ }
84
+
85
+ const filtered = { servers: {} };
86
+ for (const serviceName of selectedServices) {
87
+ if (config.servers?.[serviceName]) {
88
+ filtered.servers[serviceName] = config.servers[serviceName];
89
+ }
90
+ }
91
+
92
+ return filtered;
93
+ }
94
+
95
+ /**
96
+ * Update config to use MCP Shark HTTP endpoints
97
+ */
98
+ updateConfigForMcpShark(originalConfig) {
99
+ const [serverObject, serverType] = this._getServerObject(originalConfig);
100
+ const updatedConfig = { ...originalConfig };
101
+
102
+ if (serverObject) {
103
+ const updatedServers = {};
104
+ for (const [name, _cfg] of Object.entries(serverObject)) {
105
+ updatedServers[name] = {
106
+ type: 'http',
107
+ url: `http://localhost:9851/mcp/${encodeURIComponent(name)}`,
108
+ };
109
+ }
110
+ updatedConfig[serverType] = updatedServers;
111
+ }
112
+
113
+ return updatedConfig;
114
+ }
115
+
116
+ /**
117
+ * Get selected service names from config
118
+ */
119
+ getSelectedServiceNames(originalConfig, selectedServices) {
120
+ if (selectedServices && Array.isArray(selectedServices) && selectedServices.length > 0) {
121
+ return new Set(selectedServices);
122
+ }
123
+
124
+ const selectedServiceNames = new Set();
125
+ const hasMcpServers =
126
+ originalConfig.mcpServers && typeof originalConfig.mcpServers === 'object';
127
+ const hasServers = originalConfig.servers && typeof originalConfig.servers === 'object';
128
+
129
+ if (hasMcpServers) {
130
+ for (const name of Object.keys(originalConfig.mcpServers)) {
131
+ selectedServiceNames.add(name);
132
+ }
133
+ } else if (hasServers) {
134
+ for (const name of Object.keys(originalConfig.servers)) {
135
+ selectedServiceNames.add(name);
136
+ }
137
+ }
138
+
139
+ return selectedServiceNames;
140
+ }
141
+
142
+ /**
143
+ * Check if config file is patched by mcp-shark
144
+ * A patched config has servers with URLs pointing to localhost:9851/mcp/
145
+ */
146
+ isConfigPatched(config) {
147
+ if (!config || typeof config !== 'object') {
148
+ return false;
149
+ }
150
+
151
+ const servers = config.servers || config.mcpServers || {};
152
+ for (const [_name, cfg] of Object.entries(servers)) {
153
+ if (cfg && typeof cfg === 'object' && cfg.url) {
154
+ if (cfg.url.includes('localhost:9851/mcp/') || cfg.url.includes('127.0.0.1:9851/mcp/')) {
155
+ return true;
156
+ }
157
+ }
158
+ }
159
+
160
+ return false;
161
+ }
162
+
163
+ _getServerObject(originalConfig) {
164
+ const hasMcpServers =
165
+ originalConfig.mcpServers && typeof originalConfig.mcpServers === 'object';
166
+ const hasServers = originalConfig.servers && typeof originalConfig.servers === 'object';
167
+
168
+ if (hasMcpServers) {
169
+ return [originalConfig.mcpServers, 'mcpServers'];
170
+ }
171
+
172
+ if (hasServers) {
173
+ return [originalConfig.servers, 'servers'];
174
+ }
175
+
176
+ return [null, null];
177
+ }
178
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Service for conversation-related business logic
3
+ * HTTP-agnostic: accepts models, returns models
4
+ */
5
+ export class ConversationService {
6
+ constructor(conversationRepository) {
7
+ this.conversationRepository = conversationRepository;
8
+ }
9
+
10
+ /**
11
+ * Get conversations with filters
12
+ * @param {ConversationFilters} filters - Typed filter model
13
+ * @returns {Array} Array of conversation objects (raw from repository)
14
+ */
15
+ getConversations(filters) {
16
+ const repoFilters = filters.toRepositoryFilters();
17
+ return this.conversationRepository.queryConversations(repoFilters);
18
+ }
19
+ }
@@ -0,0 +1,117 @@
1
+ import { ExportFormat } from '../models/ExportFormat.js';
2
+
3
+ /**
4
+ * Service for exporting data in various formats
5
+ * Handles formatting business logic for CSV, TXT, and JSON exports
6
+ */
7
+ export class ExportService {
8
+ /**
9
+ * Format requests as CSV
10
+ * @param {Array} requests - Array of request objects
11
+ * @returns {{content: string, contentType: string, extension: string}}
12
+ */
13
+ formatAsCsv(requests) {
14
+ const headers = [
15
+ 'Frame',
16
+ 'Time',
17
+ 'Source',
18
+ 'Destination',
19
+ 'Protocol',
20
+ 'Length',
21
+ 'Method',
22
+ 'Status',
23
+ 'JSON-RPC Method',
24
+ 'Session ID',
25
+ 'Server Name',
26
+ ];
27
+ const rows = requests.map((req) => [
28
+ req.frame_number || '',
29
+ req.timestamp_iso || '',
30
+ req.request?.host || '',
31
+ req.request?.host || '',
32
+ 'HTTP',
33
+ req.length || '',
34
+ req.request?.method || '',
35
+ req.response?.status_code || '',
36
+ req.jsonrpc_method || '',
37
+ req.session_id || '',
38
+ req.server_name || '',
39
+ ]);
40
+
41
+ const content = [
42
+ headers.join(','),
43
+ ...rows.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(',')),
44
+ ].join('\n');
45
+
46
+ return { content, contentType: 'text/csv', extension: 'csv' };
47
+ }
48
+
49
+ /**
50
+ * Format requests as TXT
51
+ * @param {Array} requests - Array of request objects
52
+ * @returns {{content: string, contentType: string, extension: string}}
53
+ */
54
+ formatAsTxt(requests) {
55
+ const content = requests
56
+ .map((req, idx) => {
57
+ const lines = [
58
+ `=== Request/Response #${idx + 1} (Frame ${req.frame_number || 'N/A'}) ===`,
59
+ `Time: ${req.timestamp_iso || 'N/A'}`,
60
+ `Session ID: ${req.session_id || 'N/A'}`,
61
+ `Server: ${req.server_name || 'N/A'}`,
62
+ `Direction: ${req.direction || 'N/A'}`,
63
+ `Method: ${req.request?.method || 'N/A'}`,
64
+ `Status: ${req.response?.status_code || 'N/A'}`,
65
+ `JSON-RPC Method: ${req.jsonrpc_method || 'N/A'}`,
66
+ `JSON-RPC ID: ${req.jsonrpc_id || 'N/A'}`,
67
+ `Length: ${req.length || 0} bytes`,
68
+ '',
69
+ 'Request:',
70
+ JSON.stringify(req.request || {}, null, 2),
71
+ '',
72
+ 'Response:',
73
+ JSON.stringify(req.response || {}, null, 2),
74
+ '',
75
+ '---',
76
+ '',
77
+ ];
78
+ return lines.join('\n');
79
+ })
80
+ .join('\n');
81
+
82
+ return { content, contentType: 'text/plain', extension: 'txt' };
83
+ }
84
+
85
+ /**
86
+ * Format requests as JSON
87
+ * @param {Array} requests - Array of request objects
88
+ * @param {Object} serializationLib - Serialization library for BigInt handling
89
+ * @returns {{content: string, contentType: string, extension: string}}
90
+ */
91
+ formatAsJson(requests, serializationLib) {
92
+ const serialized = serializationLib.serializeBigInt(requests);
93
+ return {
94
+ content: JSON.stringify(serialized, null, 2),
95
+ contentType: 'application/json',
96
+ extension: 'json',
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Export requests in the specified format
102
+ * @param {Array} requests - Array of request objects
103
+ * @param {string} format - Export format (csv, txt, json)
104
+ * @param {Object} serializationLib - Serialization library for BigInt handling (required for JSON)
105
+ * @returns {{content: string, contentType: string, extension: string}}
106
+ */
107
+ exportRequests(requests, format, serializationLib) {
108
+ if (format === ExportFormat.CSV) {
109
+ return this.formatAsCsv(requests);
110
+ }
111
+ if (format === ExportFormat.TXT) {
112
+ return this.formatAsTxt(requests);
113
+ }
114
+ // Default to JSON
115
+ return this.formatAsJson(requests, serializationLib);
116
+ }
117
+ }
@@ -0,0 +1,64 @@
1
+ import { Defaults } from '#core/constants/Defaults.js';
2
+
3
+ /**
4
+ * Service for log management
5
+ * Handles log storage, retrieval, and export
6
+ */
7
+ export class LogService {
8
+ constructor(logger) {
9
+ this.logger = logger;
10
+ this.logs = [];
11
+ }
12
+
13
+ /**
14
+ * Initialize with log array (for compatibility with existing code)
15
+ */
16
+ initialize(logsArray) {
17
+ this.logs = logsArray;
18
+ }
19
+
20
+ /**
21
+ * Get logs with filters
22
+ */
23
+ getLogs(filters = {}) {
24
+ const { limit = Defaults.DEFAULT_LIMIT, offset = 0 } = filters;
25
+ return [...this.logs].reverse().slice(offset, offset + limit);
26
+ }
27
+
28
+ /**
29
+ * Clear all logs
30
+ */
31
+ clearLogs() {
32
+ this.logs.length = 0;
33
+ return { success: true, message: 'Logs cleared' };
34
+ }
35
+
36
+ /**
37
+ * Export logs in text format
38
+ */
39
+ exportLogs() {
40
+ const logsText = this.logs
41
+ .map((log) => `[${log.timestamp}] [${log.type.toUpperCase()}] ${log.line}`)
42
+ .join('\n');
43
+
44
+ return logsText;
45
+ }
46
+
47
+ /**
48
+ * Add log entry
49
+ */
50
+ addLog(logEntry) {
51
+ this.logs.push(logEntry);
52
+ if (this.logs.length > Defaults.MAX_LOG_LINES) {
53
+ this.logs.shift();
54
+ }
55
+ return logEntry;
56
+ }
57
+
58
+ /**
59
+ * Get all logs (for internal use)
60
+ */
61
+ getAllLogs() {
62
+ return this.logs;
63
+ }
64
+ }
@@ -0,0 +1,235 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
+ import { Server as ServerConstants } from '#core/constants/Server.js';
4
+ import { ValidationError } from '#core/libraries/index.js';
5
+
6
+ /**
7
+ * Service for MCP client management (Playground)
8
+ * Handles client connections and method execution
9
+ */
10
+ export class McpClientService {
11
+ constructor(logger) {
12
+ this.logger = logger;
13
+ this.clientSessions = new Map();
14
+ this.mcpServerBaseUrl = 'http://localhost:9851/mcp';
15
+ this.cleanupIntervalId = null;
16
+ this.startCleanupJob();
17
+ }
18
+
19
+ /**
20
+ * Get session key
21
+ */
22
+ getSessionKey(serverName, sessionId) {
23
+ return `${serverName}:${sessionId}`;
24
+ }
25
+
26
+ /**
27
+ * Get or create client for a session and server
28
+ */
29
+ async getOrCreateClient(serverName, sessionId) {
30
+ const sessionKey = this.getSessionKey(serverName, sessionId);
31
+ if (this.clientSessions.has(sessionKey)) {
32
+ return this.clientSessions.get(sessionKey);
33
+ }
34
+
35
+ if (!serverName) {
36
+ throw new ValidationError('Server name is required');
37
+ }
38
+
39
+ const mcpServerUrl = `${this.mcpServerBaseUrl}/${encodeURIComponent(serverName)}`;
40
+
41
+ const client = new Client(
42
+ { name: 'mcp-shark-playground', version: '1.0.0' },
43
+ {
44
+ capabilities: {
45
+ tools: {},
46
+ resources: {},
47
+ prompts: {},
48
+ },
49
+ }
50
+ );
51
+
52
+ const transport = new StreamableHTTPClientTransport(new URL(mcpServerUrl));
53
+ await client.connect(transport);
54
+
55
+ const clientWrapper = {
56
+ client,
57
+ transport,
58
+ createdAt: Date.now(),
59
+ lastAccessed: Date.now(),
60
+ close: async () => {
61
+ await client.close();
62
+ transport.close?.();
63
+ },
64
+ };
65
+
66
+ this.clientSessions.set(sessionKey, clientWrapper);
67
+ return clientWrapper;
68
+ }
69
+
70
+ /**
71
+ * Update last accessed time for a session
72
+ */
73
+ updateLastAccessed(serverName, sessionId) {
74
+ const sessionKey = this.getSessionKey(serverName, sessionId);
75
+ const clientWrapper = this.clientSessions.get(sessionKey);
76
+ if (clientWrapper) {
77
+ clientWrapper.lastAccessed = Date.now();
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Execute MCP method
83
+ */
84
+ async executeMethod(client, method, params) {
85
+ switch (method) {
86
+ case 'tools/list':
87
+ return await client.listTools();
88
+ case 'tools/call':
89
+ if (!params?.name) {
90
+ throw new ValidationError('Tool name is required');
91
+ }
92
+ return await client.callTool({
93
+ name: params.name,
94
+ arguments: params.arguments || {},
95
+ });
96
+ case 'prompts/list':
97
+ return await client.listPrompts();
98
+ case 'prompts/get':
99
+ if (!params?.name) {
100
+ throw new ValidationError('Prompt name is required');
101
+ }
102
+ return await client.getPrompt({
103
+ name: params.name,
104
+ arguments: params.arguments || {},
105
+ });
106
+ case 'resources/list':
107
+ return await client.listResources();
108
+ case 'resources/read':
109
+ if (!params?.uri) {
110
+ throw new ValidationError('Resource URI is required');
111
+ }
112
+ return await client.readResource({ uri: params.uri });
113
+ default:
114
+ throw new ValidationError(`Unsupported method: ${method}`);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Close client connection
120
+ */
121
+ async closeClient(serverName, sessionId) {
122
+ const sessionKey = this.getSessionKey(serverName, sessionId);
123
+ if (this.clientSessions.has(sessionKey)) {
124
+ const clientWrapper = this.clientSessions.get(sessionKey);
125
+ await clientWrapper.close();
126
+ this.clientSessions.delete(sessionKey);
127
+ return true;
128
+ }
129
+ return false;
130
+ }
131
+
132
+ /**
133
+ * Cleanup session (close all clients for a session)
134
+ */
135
+ async cleanupSession(sessionId) {
136
+ const keysToDelete = [];
137
+ for (const [key, clientWrapper] of this.clientSessions.entries()) {
138
+ if (key.endsWith(`:${sessionId}`)) {
139
+ await clientWrapper.close();
140
+ keysToDelete.push(key);
141
+ }
142
+ }
143
+ for (const key of keysToDelete) {
144
+ this.clientSessions.delete(key);
145
+ }
146
+ return keysToDelete.length;
147
+ }
148
+
149
+ /**
150
+ * Cleanup stale client sessions based on timeout
151
+ */
152
+ async cleanupStaleSessions() {
153
+ const now = Date.now();
154
+ const staleThreshold = now - ServerConstants.MCP_CLIENT_SESSION_TIMEOUT_MS;
155
+ const keysToDelete = [];
156
+
157
+ for (const [key, clientWrapper] of this.clientSessions.entries()) {
158
+ if (clientWrapper.lastAccessed < staleThreshold) {
159
+ try {
160
+ await clientWrapper.close();
161
+ keysToDelete.push(key);
162
+ } catch (error) {
163
+ this.logger?.error(
164
+ { error: error.message, sessionKey: key },
165
+ 'Error closing stale MCP client session'
166
+ );
167
+ }
168
+ }
169
+ }
170
+
171
+ for (const key of keysToDelete) {
172
+ this.clientSessions.delete(key);
173
+ }
174
+
175
+ if (keysToDelete.length > 0) {
176
+ this.logger?.info({ count: keysToDelete.length }, 'Cleaned up stale MCP client sessions');
177
+ }
178
+
179
+ return keysToDelete.length;
180
+ }
181
+
182
+ /**
183
+ * Start periodic cleanup job for stale sessions
184
+ */
185
+ startCleanupJob() {
186
+ if (this.cleanupIntervalId) {
187
+ return;
188
+ }
189
+
190
+ // Run cleanup every 5 minutes
191
+ this.cleanupIntervalId = setInterval(
192
+ () => {
193
+ this.cleanupStaleSessions().catch((error) => {
194
+ this.logger?.error({ error: error.message }, 'Error in MCP client session cleanup job');
195
+ });
196
+ },
197
+ 5 * 60 * 1000
198
+ );
199
+ }
200
+
201
+ /**
202
+ * Stop cleanup job
203
+ */
204
+ stopCleanupJob() {
205
+ if (this.cleanupIntervalId) {
206
+ clearInterval(this.cleanupIntervalId);
207
+ this.cleanupIntervalId = null;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Cleanup all client sessions (for shutdown)
213
+ */
214
+ async cleanupAll() {
215
+ this.stopCleanupJob();
216
+ const keysToDelete = Array.from(this.clientSessions.keys());
217
+
218
+ for (const key of keysToDelete) {
219
+ try {
220
+ const clientWrapper = this.clientSessions.get(key);
221
+ if (clientWrapper) {
222
+ await clientWrapper.close();
223
+ }
224
+ } catch (error) {
225
+ this.logger?.error(
226
+ { error: error.message, sessionKey: key },
227
+ 'Error closing MCP client session during cleanup'
228
+ );
229
+ }
230
+ this.clientSessions.delete(key);
231
+ }
232
+
233
+ return keysToDelete.length;
234
+ }
235
+ }