@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,107 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { createTransport } from '#ui/server/routes/smartscan/transport.js';
3
+
4
+ /**
5
+ * Service for MCP server discovery
6
+ * Handles discovering MCP servers and their capabilities
7
+ */
8
+ export class McpDiscoveryService {
9
+ constructor(configService, logger) {
10
+ this.configService = configService;
11
+ this.logger = logger;
12
+ }
13
+
14
+ /**
15
+ * Discover a single MCP server
16
+ */
17
+ async discoverServer(serverName, serverConfig) {
18
+ const transport = createTransport(serverConfig, serverName);
19
+ const client = new Client(
20
+ { name: 'mcp-shark-smart-scan', version: '1.0.0' },
21
+ {
22
+ capabilities: {
23
+ tools: {},
24
+ resources: {},
25
+ prompts: {},
26
+ },
27
+ }
28
+ );
29
+
30
+ try {
31
+ await client.connect(transport);
32
+
33
+ const [toolsResult, resourcesResult, promptsResult] = await Promise.allSettled([
34
+ client.listTools(),
35
+ client.listResources(),
36
+ client.listPrompts(),
37
+ ]);
38
+
39
+ const tools = toolsResult.status === 'fulfilled' ? toolsResult.value?.tools || [] : [];
40
+ const resources =
41
+ resourcesResult.status === 'fulfilled' ? resourcesResult.value?.resources || [] : [];
42
+ const prompts =
43
+ promptsResult.status === 'fulfilled' ? promptsResult.value?.prompts || [] : [];
44
+
45
+ await client.close();
46
+ if (transport.close) {
47
+ await transport.close();
48
+ }
49
+
50
+ return {
51
+ name: serverName,
52
+ tools,
53
+ resources,
54
+ prompts,
55
+ };
56
+ } catch (error) {
57
+ try {
58
+ await client.close();
59
+ if (transport.close) {
60
+ await transport.close();
61
+ }
62
+ } catch (_closeError) {
63
+ // Ignore close errors
64
+ }
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Discover all MCP servers from config
71
+ */
72
+ async discoverAllServers() {
73
+ const config = this.configService.readMcpConfig();
74
+ if (!config) {
75
+ return { success: false, error: 'MCP config file not found' };
76
+ }
77
+
78
+ const convertedConfig = this.configService.convertMcpServersToServers(config);
79
+ const servers = convertedConfig.servers || {};
80
+
81
+ if (Object.keys(servers).length === 0) {
82
+ return { success: false, error: 'No servers found in config' };
83
+ }
84
+
85
+ const discoveryPromises = Object.entries(servers).map(async ([serverName, serverConfig]) => {
86
+ try {
87
+ return await this.discoverServer(serverName, serverConfig);
88
+ } catch (error) {
89
+ this.logger?.error({ serverName, error: error.message }, 'Error discovering server');
90
+ return {
91
+ name: serverName,
92
+ tools: [],
93
+ resources: [],
94
+ prompts: [],
95
+ error: error.message,
96
+ };
97
+ }
98
+ });
99
+
100
+ const discoveredServers = await Promise.all(discoveryPromises);
101
+
102
+ return {
103
+ success: true,
104
+ servers: discoveredServers,
105
+ };
106
+ }
107
+ }
@@ -0,0 +1,56 @@
1
+ import { Defaults } from '../constants/Defaults.js';
2
+ /**
3
+ * Service for request-related business logic
4
+ * Uses repositories for data access
5
+ * HTTP-agnostic: accepts models, returns models
6
+ */
7
+ import { RequestFilters } from '../models/RequestFilters.js';
8
+
9
+ export class RequestService {
10
+ constructor(packetRepository) {
11
+ this.packetRepository = packetRepository;
12
+ }
13
+
14
+ /**
15
+ * Get requests with filters
16
+ * @param {RequestFilters} filters - Typed filter model
17
+ * @returns {Array} Array of packet objects (raw from repository)
18
+ */
19
+ getRequests(filters) {
20
+ const repoFilters = filters.toRepositoryFilters();
21
+ return this.packetRepository.queryRequests(repoFilters);
22
+ }
23
+
24
+ /**
25
+ * Get request by frame number
26
+ * @param {number} frameNumber - Frame number
27
+ * @returns {Object|null} Packet object or null if not found
28
+ */
29
+ getRequest(frameNumber) {
30
+ const parsedFrameNumber = Number.parseInt(frameNumber);
31
+ return this.packetRepository.getByFrameNumber(parsedFrameNumber);
32
+ }
33
+
34
+ /**
35
+ * Clear all requests
36
+ * @returns {Object} Result with clearedTables array
37
+ */
38
+ clearRequests() {
39
+ return this.packetRepository.clearAll();
40
+ }
41
+
42
+ /**
43
+ * Get requests for export (no limit)
44
+ * @param {RequestFilters} filters - Typed filter model
45
+ * @returns {Array} Array of packet objects
46
+ */
47
+ getRequestsForExport(filters) {
48
+ const exportFilters = new RequestFilters({
49
+ ...filters,
50
+ limit: Defaults.EXPORT_LIMIT,
51
+ offset: Defaults.DEFAULT_OFFSET,
52
+ });
53
+ const repoFilters = exportFilters.toRepositoryFilters();
54
+ return this.packetRepository.queryRequests(repoFilters);
55
+ }
56
+ }
@@ -0,0 +1,242 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { Defaults } from '#core/constants/Defaults.js';
5
+ import {
6
+ ensureScanResultsDirectory,
7
+ getScanResultFilePath,
8
+ getScanResultsDirectory,
9
+ } from '#core/utils/scan-cache/directory.js';
10
+
11
+ /**
12
+ * Service for Smart Scan cache operations
13
+ * Handles caching, retrieval, and management of scan results
14
+ */
15
+ export class ScanCacheService {
16
+ constructor(logger) {
17
+ this.logger = logger;
18
+ }
19
+
20
+ /**
21
+ * Compute SHA-256 hash of MCP server data
22
+ */
23
+ computeMcpHash(serverData) {
24
+ const normalized = {
25
+ name: serverData.name || '',
26
+ tools: (serverData.tools || [])
27
+ .map((tool) => ({
28
+ name: tool.name || '',
29
+ description: tool.description || '',
30
+ inputSchema: tool.inputSchema || tool.input_schema || null,
31
+ outputSchema: tool.outputSchema || tool.output_schema || null,
32
+ }))
33
+ .sort((a, b) => (a.name || '').localeCompare(b.name || '')),
34
+ resources: (serverData.resources || [])
35
+ .map((resource) => ({
36
+ uri: resource.uri || '',
37
+ name: resource.name || '',
38
+ description: resource.description || '',
39
+ mimeType: resource.mimeType || resource.mime_type || null,
40
+ }))
41
+ .sort((a, b) => (a.uri || '').localeCompare(b.uri || '')),
42
+ prompts: (serverData.prompts || [])
43
+ .map((prompt) => ({
44
+ name: prompt.name || '',
45
+ description: prompt.description || '',
46
+ arguments: (prompt.arguments || []).sort((a, b) => {
47
+ const aName = (a.name || '').toString();
48
+ const bName = (b.name || '').toString();
49
+ return aName.localeCompare(bName);
50
+ }),
51
+ }))
52
+ .sort((a, b) => (a.name || '').localeCompare(b.name || '')),
53
+ };
54
+
55
+ const jsonString = JSON.stringify(normalized);
56
+ return createHash('sha256').update(jsonString).digest('hex');
57
+ }
58
+
59
+ /**
60
+ * Get cached scan result by hash
61
+ */
62
+ getCachedScanResult(hash) {
63
+ try {
64
+ const filePath = getScanResultFilePath(hash);
65
+
66
+ if (!existsSync(filePath)) {
67
+ return null;
68
+ }
69
+
70
+ const fileContent = readFileSync(filePath, 'utf8');
71
+ const data = JSON.parse(fileContent);
72
+
73
+ return {
74
+ ...data.scanData,
75
+ cached: true,
76
+ cachedAt: data.createdAt,
77
+ updatedAt: data.updatedAt,
78
+ serverName: data.serverName,
79
+ };
80
+ } catch (error) {
81
+ this.logger?.error({ hash, error: error.message }, 'Error getting cached scan result');
82
+ return null;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Get created timestamp from existing file or use default
88
+ */
89
+ _getCreatedAt(filePath, defaultTime) {
90
+ if (!existsSync(filePath)) {
91
+ return defaultTime;
92
+ }
93
+ try {
94
+ const existingContent = readFileSync(filePath, 'utf8');
95
+ const existingData = JSON.parse(existingContent);
96
+ return existingData.createdAt || defaultTime;
97
+ } catch (_e) {
98
+ return defaultTime;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Store scan result in cache
104
+ */
105
+ storeScanResult(serverName, hash, scanData) {
106
+ try {
107
+ const filePath = getScanResultFilePath(hash);
108
+ const now = Date.now();
109
+ const createdAt = this._getCreatedAt(filePath, now);
110
+
111
+ const dataToStore = {
112
+ serverName,
113
+ hash,
114
+ scanData,
115
+ createdAt,
116
+ updatedAt: now,
117
+ };
118
+
119
+ writeFileSync(filePath, JSON.stringify(dataToStore, null, 2), 'utf8');
120
+ return true;
121
+ } catch (error) {
122
+ this.logger?.error({ serverName, hash, error: error.message }, 'Error storing scan result');
123
+ return false;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Get all cached scan results
129
+ */
130
+ getAllCachedScanResults() {
131
+ try {
132
+ const scanResultsDir = getScanResultsDirectory();
133
+ if (!existsSync(scanResultsDir)) {
134
+ return [];
135
+ }
136
+
137
+ const files = readdirSync(scanResultsDir).filter((f) => f.endsWith('.json'));
138
+ const results = [];
139
+
140
+ for (const file of files) {
141
+ try {
142
+ const filePath = join(scanResultsDir, file);
143
+ const fileContent = readFileSync(filePath, 'utf8');
144
+ const data = JSON.parse(fileContent);
145
+
146
+ const scanData = data.scanData || data;
147
+ const scanId = scanData.id || scanData.scan_id || data.hash || file.replace('.json', '');
148
+ const serverName = data.serverName || 'Unknown Server';
149
+
150
+ results.push({
151
+ id: scanId,
152
+ scan_id: scanId,
153
+ server: { name: serverName },
154
+ server_name: serverName,
155
+ serverName: serverName,
156
+ status: 'completed',
157
+ risk_level: scanData.overall_risk_level || scanData.risk_level || 'unknown',
158
+ overall_risk_level: scanData.overall_risk_level || scanData.risk_level || 'unknown',
159
+ created_at: data.createdAt || data.created_at || scanData.created_at,
160
+ updated_at: data.updatedAt || data.updated_at || scanData.updated_at,
161
+ cached: true,
162
+ hash: data.hash || file.replace('.json', ''),
163
+ data: scanData,
164
+ result: scanData,
165
+ });
166
+ } catch (error) {
167
+ this.logger?.warn({ file, error: error.message }, 'Error reading scan result file');
168
+ }
169
+ }
170
+
171
+ results.sort((a, b) => {
172
+ const aTime = a.updated_at || a.created_at || 0;
173
+ const bTime = b.updated_at || b.created_at || 0;
174
+ return bTime - aTime;
175
+ });
176
+
177
+ return results;
178
+ } catch (error) {
179
+ this.logger?.error({ error: error.message }, 'Error getting all cached scan results');
180
+ return [];
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Clear all cached scan results
186
+ */
187
+ clearAllScanResults() {
188
+ try {
189
+ const scanResultsDir = ensureScanResultsDirectory();
190
+ const files = readdirSync(scanResultsDir).filter((f) => f.endsWith('.json'));
191
+
192
+ const deletedCount = files.reduce((count, file) => {
193
+ try {
194
+ const filePath = join(scanResultsDir, file);
195
+ unlinkSync(filePath);
196
+ return count + 1;
197
+ } catch (error) {
198
+ this.logger?.warn({ file, error: error.message }, 'Error deleting scan result file');
199
+ return count;
200
+ }
201
+ }, 0);
202
+
203
+ return deletedCount;
204
+ } catch (error) {
205
+ this.logger?.error({ error: error.message }, 'Error clearing all scan results');
206
+ return 0;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Clear old scan results
212
+ */
213
+ clearOldScanResults(maxAgeMs = Defaults.SCAN_RESULTS_MAX_AGE_MS) {
214
+ try {
215
+ const scanResultsDir = ensureScanResultsDirectory();
216
+ const files = readdirSync(scanResultsDir).filter((f) => f.endsWith('.json'));
217
+ const cutoffTime = Date.now() - maxAgeMs;
218
+
219
+ const deletedCount = files.reduce((count, file) => {
220
+ try {
221
+ const filePath = join(scanResultsDir, file);
222
+ const fileContent = readFileSync(filePath, 'utf8');
223
+ const data = JSON.parse(fileContent);
224
+
225
+ if (data.updatedAt && data.updatedAt < cutoffTime) {
226
+ unlinkSync(filePath);
227
+ return count + 1;
228
+ }
229
+ return count;
230
+ } catch (error) {
231
+ this.logger?.warn({ file, error: error.message }, 'Error processing scan result file');
232
+ return count;
233
+ }
234
+ }, 0);
235
+
236
+ return deletedCount;
237
+ } catch (error) {
238
+ this.logger?.error({ error: error.message }, 'Error clearing old scan results');
239
+ return 0;
240
+ }
241
+ }
242
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Service for Smart Scan API operations
3
+ * Handles communication with external Smart Scan API
4
+ */
5
+ export class ScanService {
6
+ constructor(scanCacheService, logger) {
7
+ this.scanCacheService = scanCacheService;
8
+ this.logger = logger;
9
+ this.apiBaseUrl = 'https://smart.mcpshark.sh';
10
+ }
11
+
12
+ /**
13
+ * Create scan for a single server
14
+ */
15
+ async createScan(scanData, apiToken) {
16
+ const response = await fetch(`${this.apiBaseUrl}/api/scans`, {
17
+ method: 'POST',
18
+ headers: {
19
+ 'Content-Type': 'application/json',
20
+ Accept: 'application/json',
21
+ Authorization: `Bearer ${apiToken}`,
22
+ },
23
+ body: JSON.stringify(scanData),
24
+ });
25
+
26
+ const data = await response.json();
27
+ return {
28
+ success: response.ok,
29
+ status: response.status,
30
+ data: response.ok ? data : null,
31
+ error: response.ok ? null : data.error || data.message || 'Unknown error',
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Create scans for multiple servers
37
+ */
38
+ async createBatchScans(servers, apiToken) {
39
+ const scanPromises = servers.map(async (serverData) => {
40
+ const hash = this.scanCacheService.computeMcpHash(serverData);
41
+ const cachedResult = this.scanCacheService.getCachedScanResult(hash);
42
+
43
+ if (cachedResult) {
44
+ this.logger?.info({ serverName: serverData.name }, 'Using cached scan result');
45
+ return {
46
+ serverName: serverData.name,
47
+ success: true,
48
+ status: 200,
49
+ data: cachedResult,
50
+ error: null,
51
+ cached: true,
52
+ };
53
+ }
54
+
55
+ const scanData = {
56
+ server: {
57
+ name: serverData.name || 'unknown',
58
+ description: serverData.description || null,
59
+ },
60
+ tools: (serverData.tools || []).map((tool) => {
61
+ const toolData = {
62
+ name: tool.name,
63
+ description: tool.description || null,
64
+ };
65
+ if (tool.inputSchema && typeof tool.inputSchema === 'object') {
66
+ toolData.input_schema = tool.inputSchema;
67
+ }
68
+ if (tool.outputSchema && typeof tool.outputSchema === 'object') {
69
+ toolData.output_schema = tool.outputSchema;
70
+ }
71
+ return toolData;
72
+ }),
73
+ resources: (serverData.resources || []).map((resource) => ({
74
+ uri: resource.uri,
75
+ name: resource.name || null,
76
+ description: resource.description || null,
77
+ mimeType: resource.mimeType || null,
78
+ })),
79
+ prompts: (serverData.prompts || []).map((prompt) => ({
80
+ name: prompt.name,
81
+ description: prompt.description || null,
82
+ arguments: prompt.arguments || [],
83
+ })),
84
+ };
85
+
86
+ try {
87
+ const result = await this.createScan(scanData, apiToken);
88
+
89
+ const batchResult = {
90
+ serverName: serverData.name,
91
+ success: result.success,
92
+ status: result.status,
93
+ data: result.data,
94
+ error: result.error,
95
+ cached: false,
96
+ };
97
+
98
+ if (result.success && result.data) {
99
+ this.scanCacheService.storeScanResult(serverData.name, hash, result.data);
100
+ this.logger?.info({ serverName: serverData.name }, 'Stored scan result in cache');
101
+ }
102
+
103
+ return batchResult;
104
+ } catch (error) {
105
+ return {
106
+ serverName: serverData.name,
107
+ success: false,
108
+ status: 500,
109
+ data: null,
110
+ error: error.message,
111
+ cached: false,
112
+ };
113
+ }
114
+ });
115
+
116
+ return Promise.all(scanPromises);
117
+ }
118
+
119
+ /**
120
+ * Get scan by ID
121
+ */
122
+ async getScan(scanId, apiToken) {
123
+ const response = await fetch(`${this.apiBaseUrl}/api/scans/${scanId}`, {
124
+ method: 'GET',
125
+ headers: {
126
+ Accept: 'application/json',
127
+ Authorization: `Bearer ${apiToken}`,
128
+ },
129
+ });
130
+
131
+ const data = await response.json();
132
+ return {
133
+ success: response.ok,
134
+ status: response.status,
135
+ data: response.ok ? data : null,
136
+ error: response.ok ? null : data.error || data.message || 'Unknown error',
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Get cached results for servers
142
+ */
143
+ getCachedResults(servers) {
144
+ return servers.map((serverData) => {
145
+ const hash = this.scanCacheService.computeMcpHash(serverData);
146
+ const cachedResult = this.scanCacheService.getCachedScanResult(hash);
147
+
148
+ if (cachedResult) {
149
+ return {
150
+ serverName: serverData.name,
151
+ success: true,
152
+ data: cachedResult,
153
+ cached: true,
154
+ hash,
155
+ };
156
+ }
157
+
158
+ return {
159
+ serverName: serverData.name,
160
+ success: false,
161
+ data: null,
162
+ cached: false,
163
+ hash,
164
+ };
165
+ });
166
+ }
167
+ }