@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,228 @@
1
+ import { Readable } from 'node:stream';
2
+ import { parse as parseJsonRpc } from 'jsonrpc-lite';
3
+ import { getSessionFromRequest } from '../server/internal/handlers/common.js';
4
+
5
+ /* ---------- helpers ---------- */
6
+
7
+ function toBuffer(body) {
8
+ if (body === undefined || body === null) {
9
+ return Buffer.alloc(0);
10
+ }
11
+
12
+ if (Buffer.isBuffer(body)) {
13
+ return body;
14
+ }
15
+
16
+ if (
17
+ typeof body === 'object' &&
18
+ body.type === 'Buffer' &&
19
+ Array.isArray(body.data)
20
+ ) {
21
+ return Buffer.from(body.data);
22
+ }
23
+
24
+ if (typeof body === 'string') {
25
+ return Buffer.from(body, 'utf8');
26
+ }
27
+
28
+ if (typeof body === 'object') {
29
+ return Buffer.from(JSON.stringify(body));
30
+ }
31
+
32
+ return Buffer.alloc(0);
33
+ }
34
+
35
+ async function readBody(req) {
36
+ if (req.body) {
37
+ return toBuffer(req.body);
38
+ }
39
+
40
+ const chunks = [];
41
+
42
+ for await (const chunk of req) {
43
+ chunks.push(chunk);
44
+ }
45
+
46
+ return Buffer.concat(chunks);
47
+ }
48
+
49
+ function captureResponse(res) {
50
+ const chunks = [];
51
+
52
+ const wrapped = new Proxy(res, {
53
+ get(target, prop, receiver) {
54
+ if (prop === 'write') {
55
+ return function (chunk, ...args) {
56
+ if (chunk) {
57
+ const buf = toBuffer(chunk);
58
+ chunks.push(buf);
59
+ }
60
+ return target.write(chunk, ...args);
61
+ };
62
+ }
63
+
64
+ if (prop === 'end') {
65
+ return function (chunk, ...args) {
66
+ if (chunk) {
67
+ const buf = toBuffer(chunk);
68
+ chunks.push(buf);
69
+ }
70
+ return target.end(chunk, ...args);
71
+ };
72
+ }
73
+
74
+ return Reflect.get(target, prop, receiver);
75
+ },
76
+ });
77
+
78
+ return {
79
+ res: wrapped,
80
+ getBody: () => {
81
+ return Buffer.concat(chunks);
82
+ },
83
+ };
84
+ }
85
+
86
+ function tryParseJsonRpc(buf) {
87
+ if (!buf || buf.length === 0) {
88
+ return null;
89
+ }
90
+
91
+ const text = buf.toString('utf8');
92
+
93
+ try {
94
+ const parsed = parseJsonRpc(text);
95
+ return parsed;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ function rebuildReq(req, buf) {
102
+ const r = new Readable({
103
+ read() {
104
+ this.push(buf);
105
+ this.push(null);
106
+ },
107
+ });
108
+
109
+ r.headers = req.headers;
110
+ r.method = req.method;
111
+ r.url = req.url;
112
+
113
+ return r;
114
+ }
115
+
116
+ function waitForResponseFinish(res) {
117
+ return new Promise(resolve => {
118
+ let done = false;
119
+
120
+ function finishOnce() {
121
+ if (!done) {
122
+ done = true;
123
+ resolve();
124
+ }
125
+ }
126
+
127
+ res.on('finish', finishOnce);
128
+ res.on('close', finishOnce);
129
+ });
130
+ }
131
+
132
+ /* ---------- main handler ---------- */
133
+
134
+ export async function withAuditRequestResponseHandler(
135
+ transport,
136
+ req,
137
+ res,
138
+ auditLogger
139
+ ) {
140
+ const reqBuf = await readBody(req);
141
+ const reqJsonRpc = tryParseJsonRpc(reqBuf);
142
+
143
+ // Extract session ID from request
144
+ // If no session ID exists, it's an initiation request
145
+ const sessionId = getSessionFromRequest(req);
146
+
147
+ // Extract request body as string
148
+ const reqBodyStr = reqBuf.toString('utf8');
149
+ const reqBodyJson = (() => {
150
+ try {
151
+ return JSON.parse(reqBodyStr);
152
+ } catch {
153
+ return null;
154
+ }
155
+ })();
156
+
157
+ // Log request packet to database
158
+ const requestResult = auditLogger.logRequestPacket({
159
+ method: req.method,
160
+ url: req.url,
161
+ headers: req.headers,
162
+ body: reqBodyJson || reqBodyStr,
163
+ userAgent: req.headers['user-agent'] || req.headers['User-Agent'] || null,
164
+ remoteAddress: req.socket?.remoteAddress || null,
165
+ sessionId: sessionId || null,
166
+ });
167
+
168
+ const { res: wrappedRes, getBody } = captureResponse(res);
169
+
170
+ const replayReq = req.body ? req : rebuildReq(req, reqBuf);
171
+
172
+ const parsedForTransport = reqJsonRpc
173
+ ? (() => {
174
+ try {
175
+ return JSON.parse(reqBuf.toString('utf8'));
176
+ } catch {
177
+ return undefined;
178
+ }
179
+ })()
180
+ : undefined;
181
+
182
+ // hand over to transport
183
+ if (!transport || typeof transport.handleRequest !== 'function') {
184
+ res.status(500).json({ error: 'Transport not available' });
185
+ return;
186
+ }
187
+ await transport.handleRequest(replayReq, wrappedRes, parsedForTransport);
188
+
189
+ // wait until response fully finished (important for SSE / streaming)
190
+ await waitForResponseFinish(wrappedRes);
191
+
192
+ const resBuf = getBody();
193
+
194
+ const resHeaders =
195
+ wrappedRes.getHeaders && typeof wrappedRes.getHeaders === 'function'
196
+ ? wrappedRes.getHeaders()
197
+ : {};
198
+
199
+ // Extract response body as string
200
+ const resBodyStr = resBuf.toString('utf8');
201
+ const resBodyJson = (() => {
202
+ try {
203
+ return JSON.parse(resBodyStr);
204
+ } catch {
205
+ return null;
206
+ }
207
+ })();
208
+
209
+ // Extract JSON-RPC ID from request for correlation
210
+ const jsonrpcId =
211
+ reqJsonRpc?.payload?.id !== undefined
212
+ ? String(reqJsonRpc.payload.id)
213
+ : null;
214
+
215
+ // Log response packet to database
216
+ // Use the same session ID from the request
217
+ auditLogger.logResponsePacket({
218
+ statusCode: wrappedRes.statusCode || 200,
219
+ headers: resHeaders,
220
+ body: resBodyJson || resBodyStr,
221
+ requestFrameNumber: requestResult?.frameNumber || null,
222
+ requestTimestampNs: requestResult?.timestampNs || null,
223
+ jsonrpcId,
224
+ sessionId: sessionId || null,
225
+ userAgent: req.headers['user-agent'] || req.headers['User-Agent'] || null,
226
+ remoteAddress: req.socket?.remoteAddress || null,
227
+ });
228
+ }
@@ -0,0 +1,15 @@
1
+ export class CompositeError extends Error {
2
+ constructor(name, message, error) {
3
+ super(name, message);
4
+ this.name = name;
5
+ this.error = error;
6
+ }
7
+ }
8
+
9
+ export function isError(error) {
10
+ return error instanceof CompositeError || error instanceof Error;
11
+ }
12
+
13
+ export function getErrors(results) {
14
+ return results.filter(result => isError(result));
15
+ }
@@ -0,0 +1,32 @@
1
+ import { normalizeConfig } from './config.js';
2
+ import { runExternalServer } from './single/run.js';
3
+ import { CompositeError, getErrors } from '../../common/error.js';
4
+ import { buildKv } from './kv.js';
5
+
6
+ export class RunAllExternalServersError extends CompositeError {
7
+ constructor(message, error, errors = []) {
8
+ super('RunAllExternalServersError', message, error);
9
+ this.errors = errors;
10
+ }
11
+ }
12
+
13
+ export async function runAllExternalServers(logger, parsedConfig) {
14
+ const configs = normalizeConfig(parsedConfig);
15
+ const results = await Promise.all(
16
+ Object.entries(configs).map(([name, config]) =>
17
+ runExternalServer({ logger, name, config })
18
+ )
19
+ );
20
+
21
+ const flattenedResults = results.flat();
22
+ const errors = getErrors(flattenedResults);
23
+ if (errors.length > 0) {
24
+ return new RunAllExternalServersError(
25
+ 'Errors occurred while running all external servers',
26
+ null,
27
+ errors
28
+ );
29
+ }
30
+
31
+ return buildKv(flattenedResults);
32
+ }
@@ -0,0 +1,59 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { CompositeError, isError } from '../../common/error.js';
3
+
4
+ const DEFAULT_TYPE = 'stdio';
5
+ export class ConfigError extends CompositeError {
6
+ constructor(message, error) {
7
+ super('ConfigError', message, error);
8
+ }
9
+ }
10
+
11
+ function parseConfig(configPath) {
12
+ try {
13
+ const conf = JSON.parse(readFileSync(configPath, 'utf-8'));
14
+ if (conf && typeof conf === 'object') {
15
+ return conf;
16
+ }
17
+ return new ConfigError(
18
+ 'Invalid config file',
19
+ new Error(
20
+ `Invalid config file: ${configPath} - ${JSON.stringify(conf, null, 2)}`
21
+ )
22
+ );
23
+ } catch (error) {
24
+ return new ConfigError(
25
+ 'Error parsing config',
26
+ new Error(`Error parsing config: ${configPath} - ${error.message}`)
27
+ );
28
+ }
29
+ }
30
+
31
+ export function normalizeConfig(configPath) {
32
+ const parsedConfigResult = parseConfig(configPath);
33
+ if (isError(parsedConfigResult)) {
34
+ return parsedConfigResult;
35
+ }
36
+ const out = new Map();
37
+ const { servers, mcpServers } = parsedConfigResult;
38
+ // Servers are the old format
39
+ if (servers) {
40
+ Object.entries(servers).forEach(([name, cfg]) => {
41
+ const type = cfg.type ?? DEFAULT_TYPE;
42
+ out.set(name, { type, ...cfg });
43
+ });
44
+ }
45
+ // MCP Servers are the new format
46
+ if (mcpServers) {
47
+ Object.entries(mcpServers).forEach(([name, cfg]) => {
48
+ // Cursor/Claude usually omit type; assume stdio if command is given
49
+ const type = cfg.type ?? DEFAULT_TYPE;
50
+ out.set(name, { type, ...cfg });
51
+ });
52
+ }
53
+
54
+ if (out.size === 0) {
55
+ return new ConfigError('No servers found in config');
56
+ }
57
+
58
+ return Object.fromEntries(out);
59
+ }
@@ -0,0 +1,102 @@
1
+ const kv = new Map();
2
+
3
+ function buildName(name, typeName) {
4
+ return `${name}.${typeName}`;
5
+ }
6
+
7
+ export function extractName(name) {
8
+ const [serverName, typeName] = name.split('.');
9
+ return { serverName, typeName };
10
+ }
11
+
12
+ export function buildKv(downstreamServers) {
13
+ for (const downstreamServer of downstreamServers) {
14
+ const {
15
+ name,
16
+ tools,
17
+ resources,
18
+ prompts,
19
+ callTool,
20
+ getPrompt,
21
+ readResource,
22
+ } = downstreamServer;
23
+
24
+ if (!kv.has(name)) {
25
+ const toolsMap = new Map();
26
+ const resourcesMap = new Map();
27
+ const promptsMap = new Map();
28
+
29
+ for (const tool of tools) {
30
+ toolsMap.set(tool.name, callTool);
31
+ }
32
+
33
+ for (const resource of resources) {
34
+ resourcesMap.set(resource.name, readResource);
35
+ }
36
+
37
+ for (const prompt of prompts) {
38
+ promptsMap.set(prompt.name, getPrompt);
39
+ }
40
+
41
+ kv.set(name, {
42
+ toolsMap,
43
+ resourcesMap,
44
+ promptsMap,
45
+ tools: tools.map(tool => {
46
+ return { ...tool, name: buildName(name, tool.name) };
47
+ }),
48
+ resources: resources.map(resource => {
49
+ return {
50
+ ...resource,
51
+ name: buildName(name, resource.name),
52
+ };
53
+ }),
54
+ prompts: prompts.map(prompt => {
55
+ return { ...prompt, name: buildName(name, prompt.name) };
56
+ }),
57
+ });
58
+ }
59
+ }
60
+
61
+ return kv;
62
+ }
63
+
64
+ export function getBy(database, calledName, action) {
65
+ const { serverName, typeName } = extractName(calledName);
66
+ if (!serverName || !typeName) {
67
+ return null;
68
+ }
69
+ const entry = database.get(serverName);
70
+ if (!entry) {
71
+ return null;
72
+ }
73
+
74
+ // Type-based lookup
75
+ if (action === 'getTools') {
76
+ return entry.toolsMap.get(typeName);
77
+ }
78
+ if (action === 'getResources') {
79
+ return entry.resourcesMap.get(typeName);
80
+ }
81
+ if (action === 'getPrompts') {
82
+ return entry.promptsMap.get(typeName);
83
+ }
84
+
85
+ // Action-based lookup
86
+ if (action === 'callTool') {
87
+ return entry.toolsMap.get(typeName);
88
+ }
89
+ if (action === 'readResource') {
90
+ return entry.resourcesMap.get(typeName);
91
+ }
92
+ if (action === 'getPrompt') {
93
+ return entry.promptsMap.get(typeName);
94
+ }
95
+ return null;
96
+ }
97
+
98
+ export function listAll(database, type) {
99
+ return Array.from(database.values())
100
+ .map(entry => entry[type])
101
+ .flat();
102
+ }
@@ -0,0 +1,35 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { CompositeError } from '../../../common/error.js';
3
+
4
+ const DEFAULT_VERSION = '1.0.0';
5
+ const DEFAULT_NAME = 'mcp-client';
6
+ const DEFAULT_CAPABILITIES = {};
7
+ export class ClientError extends CompositeError {
8
+ constructor(message, error) {
9
+ super('ClientError', message, error);
10
+ }
11
+ }
12
+
13
+ export async function createClient({
14
+ name = DEFAULT_NAME,
15
+ version = DEFAULT_VERSION,
16
+ capabilities = DEFAULT_CAPABILITIES,
17
+ transport,
18
+ }) {
19
+ const client = new Client({ name, version }, { capabilities });
20
+
21
+ try {
22
+ await client.connect(transport);
23
+ return client;
24
+ } catch (error) {
25
+ return new ClientError('Failed to connect to server', error);
26
+ }
27
+ }
28
+
29
+ export async function closeClient(client) {
30
+ try {
31
+ await client.close();
32
+ } catch (error) {
33
+ return new ClientError('Failed to close client', error);
34
+ }
35
+ }
@@ -0,0 +1,49 @@
1
+ import {
2
+ ListToolsResultSchema,
3
+ ListResourcesResultSchema,
4
+ ListPromptsResultSchema,
5
+ } from '@modelcontextprotocol/sdk/types.js';
6
+
7
+ import { CompositeError } from '../../../common/error.js';
8
+ const METHOD_NOT_FOUND_CODE = '-32601';
9
+
10
+ export class RequestError extends CompositeError {
11
+ constructor(message, error) {
12
+ super('RequestError', message, error);
13
+ }
14
+ }
15
+
16
+ function isMethodNotFound(error) {
17
+ return error?.code?.toString() === METHOD_NOT_FOUND_CODE;
18
+ }
19
+
20
+ async function getListOf(client, typeOfList, schema) {
21
+ const fetchedList = {
22
+ [typeOfList]: [],
23
+ };
24
+ try {
25
+ const result = await client.request(
26
+ { method: `${typeOfList}/list` },
27
+ schema
28
+ );
29
+ fetchedList[typeOfList] = result[typeOfList];
30
+ return fetchedList;
31
+ } catch (error) {
32
+ if (isMethodNotFound(error)) {
33
+ return fetchedList;
34
+ }
35
+ return new RequestError(`Failed to list ${typeOfList}`, error);
36
+ }
37
+ }
38
+
39
+ export function listTools(client) {
40
+ return getListOf(client, 'tools', ListToolsResultSchema);
41
+ }
42
+
43
+ export function listResources(client) {
44
+ return getListOf(client, 'resources', ListResourcesResultSchema);
45
+ }
46
+
47
+ export function listPrompts(client) {
48
+ return getListOf(client, 'prompts', ListPromptsResultSchema);
49
+ }
@@ -0,0 +1,75 @@
1
+ import { makeTransport } from './transport.js';
2
+ import { createClient } from './client.js';
3
+ import * as requests from './request.js';
4
+ import { isError, CompositeError, getErrors } from '../../../common/error.js';
5
+
6
+ export class RunError extends CompositeError {
7
+ constructor(message, error, errors = []) {
8
+ super('RunError', message, error);
9
+ this.errors = errors;
10
+ }
11
+ }
12
+
13
+ export async function runExternalServer({ logger, name, config }) {
14
+ logger.debug(
15
+ `Starting external server run for server ${name} with config:`,
16
+ config
17
+ );
18
+
19
+ // Create transport
20
+ const transport = makeTransport(config);
21
+ if (isError(transport)) {
22
+ return new RunError(
23
+ `Error creating transport for external server ${name}`,
24
+ transport.error
25
+ );
26
+ }
27
+
28
+ // Create client
29
+ const client = await createClient({ transport });
30
+ if (isError(client)) {
31
+ return new RunError(
32
+ `Error creating client for external server ${name}`,
33
+ client.error
34
+ );
35
+ }
36
+
37
+ // Run requests
38
+ const allResults = [
39
+ requests.listTools(client),
40
+ requests.listResources(client),
41
+ requests.listPrompts(client),
42
+ ];
43
+ const results = await Promise.allSettled(allResults);
44
+
45
+ // Check for errors
46
+ const errors = getErrors(results);
47
+ if (errors.length > 0) {
48
+ return new RunError(
49
+ `Errors occurred while running requests for external server ${name}`,
50
+ null,
51
+ errors
52
+ );
53
+ }
54
+
55
+ const [{ tools }, { resources }, { prompts }] = results.map(
56
+ result => result.value
57
+ );
58
+ return {
59
+ name,
60
+ client,
61
+ tools,
62
+ resources,
63
+ prompts,
64
+ callTool: args => client.callTool.bind(client)(args),
65
+ readResource: resourceUri => {
66
+ return client.readResource.bind(client)(resourceUri);
67
+ },
68
+ getPrompt: (promptName, args) => {
69
+ return client.getPrompt.bind(client)({
70
+ name: promptName,
71
+ arguments: args,
72
+ });
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,57 @@
1
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
2
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
+ import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
4
+
5
+ import { CompositeError } from '../../../common/error.js';
6
+
7
+ export class TransportError extends CompositeError {
8
+ constructor(message, error) {
9
+ super('TransportError', message, error);
10
+ }
11
+ }
12
+
13
+ export function makeTransport({
14
+ type,
15
+ url,
16
+ headers: configHeaders = {},
17
+ command,
18
+ args = [],
19
+ env: configEnv = {},
20
+ }) {
21
+ // Start with enhanced PATH
22
+ const env = {
23
+ ...process.env,
24
+ ...configEnv,
25
+ };
26
+
27
+ const requestInit = { headers: { ...configHeaders } };
28
+
29
+ switch (type) {
30
+ case 'stdio':
31
+ return new StdioClientTransport({ command, args, env });
32
+ case 'http':
33
+ case 'sse':
34
+ case 'streamable-http':
35
+ return new StreamableHTTPClientTransport(new URL(url), {
36
+ requestInit,
37
+ });
38
+ case 'ws':
39
+ case 'websocket':
40
+ return new WebSocketClientTransport(new URL(url));
41
+ default:
42
+ if (command) {
43
+ // fallback: assume stdio if only command is provided
44
+ return new StdioClientTransport({
45
+ command,
46
+ args,
47
+ env,
48
+ });
49
+ }
50
+ return new TransportError(
51
+ 'Unsupported server config',
52
+ new Error(
53
+ `Unsupported server config: ${JSON.stringify({ type, url, configHeaders, command, args })}`
54
+ )
55
+ );
56
+ }
57
+ }
@@ -0,0 +1,20 @@
1
+ export const SERVER_NAME = 'mcp-internal-server';
2
+ export const TRANSPORT_TYPE = 'http';
3
+
4
+ export function getSessionFromRequest(req) {
5
+ if (!req) {
6
+ return null;
7
+ }
8
+ if (req.sessionId) {
9
+ return req.sessionId;
10
+ }
11
+ if (req.get && typeof req.get === 'function') {
12
+ if (req.get('Mcp-Session-Id')) {
13
+ return req.get('Mcp-Session-Id');
14
+ }
15
+ if (req.get('X-MCP-Session-Id')) {
16
+ return req.get('X-MCP-Session-Id');
17
+ }
18
+ }
19
+ return null;
20
+ }
@@ -0,0 +1,7 @@
1
+ import { CompositeError } from '../../../common/error.js';
2
+
3
+ export class InternalServerError extends CompositeError {
4
+ constructor(message, error) {
5
+ super('InternalServerError', message, error);
6
+ }
7
+ }
@@ -0,0 +1,22 @@
1
+ import { getBy, extractName } from '../../external/kv.js';
2
+ import { InternalServerError } from './error.js';
3
+
4
+ export function createPromptsGetHandler(logger, mcpServers) {
5
+ return async req => {
6
+ const name = req.params.name;
7
+ const promptArgs = req?.params?.arguments || {};
8
+ logger.debug('Prompt get', name, promptArgs);
9
+
10
+ const { typeName } = extractName(name);
11
+
12
+ const getPrompt = getBy(mcpServers, name, 'getPrompt', promptArgs);
13
+ if (!getPrompt) {
14
+ throw new InternalServerError(`Prompt not found: ${name}`);
15
+ }
16
+
17
+ const result = await getPrompt(typeName, promptArgs);
18
+ logger.debug('Prompt get result', result);
19
+
20
+ return result;
21
+ };
22
+ }