@mcp-shark/mcp-shark 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +85 -0
- package/README.md +724 -0
- package/bin/mcp-shark.js +93 -0
- package/mcp-server/.editorconfig +15 -0
- package/mcp-server/.prettierignore +11 -0
- package/mcp-server/.prettierrc +12 -0
- package/mcp-server/README.md +280 -0
- package/mcp-server/commitlint.config.cjs +42 -0
- package/mcp-server/eslint.config.js +131 -0
- package/mcp-server/lib/auditor/audit.js +228 -0
- package/mcp-server/lib/common/error.js +15 -0
- package/mcp-server/lib/server/external/all.js +32 -0
- package/mcp-server/lib/server/external/config.js +59 -0
- package/mcp-server/lib/server/external/kv.js +102 -0
- package/mcp-server/lib/server/external/single/client.js +35 -0
- package/mcp-server/lib/server/external/single/request.js +49 -0
- package/mcp-server/lib/server/external/single/run.js +75 -0
- package/mcp-server/lib/server/external/single/transport.js +57 -0
- package/mcp-server/lib/server/internal/handlers/common.js +20 -0
- package/mcp-server/lib/server/internal/handlers/error.js +7 -0
- package/mcp-server/lib/server/internal/handlers/prompts-get.js +22 -0
- package/mcp-server/lib/server/internal/handlers/prompts-list.js +12 -0
- package/mcp-server/lib/server/internal/handlers/resources-list.js +12 -0
- package/mcp-server/lib/server/internal/handlers/resources-read.js +19 -0
- package/mcp-server/lib/server/internal/handlers/tools-call.js +37 -0
- package/mcp-server/lib/server/internal/handlers/tools-list.js +14 -0
- package/mcp-server/lib/server/internal/run.js +49 -0
- package/mcp-server/lib/server/internal/server.js +63 -0
- package/mcp-server/lib/server/internal/session.js +39 -0
- package/mcp-server/mcp-shark.js +72 -0
- package/mcp-server/package-lock.json +4784 -0
- package/mcp-server/package.json +30 -0
- package/package.json +103 -0
- package/ui/README.md +212 -0
- package/ui/index.html +16 -0
- package/ui/package-lock.json +3574 -0
- package/ui/package.json +12 -0
- package/ui/paths.js +282 -0
- package/ui/public/og-image.png +0 -0
- package/ui/server/routes/backups.js +251 -0
- package/ui/server/routes/composite.js +244 -0
- package/ui/server/routes/config.js +175 -0
- package/ui/server/routes/conversations.js +25 -0
- package/ui/server/routes/help.js +43 -0
- package/ui/server/routes/logs.js +32 -0
- package/ui/server/routes/playground.js +152 -0
- package/ui/server/routes/requests.js +235 -0
- package/ui/server/routes/sessions.js +27 -0
- package/ui/server/routes/smartscan/discover.js +117 -0
- package/ui/server/routes/smartscan/scans/clearCache.js +22 -0
- package/ui/server/routes/smartscan/scans/createBatchScans.js +123 -0
- package/ui/server/routes/smartscan/scans/createScan.js +42 -0
- package/ui/server/routes/smartscan/scans/getCachedResults.js +51 -0
- package/ui/server/routes/smartscan/scans/getScan.js +41 -0
- package/ui/server/routes/smartscan/scans/listScans.js +24 -0
- package/ui/server/routes/smartscan/scans.js +13 -0
- package/ui/server/routes/smartscan/token.js +56 -0
- package/ui/server/routes/smartscan/transport.js +53 -0
- package/ui/server/routes/smartscan.js +24 -0
- package/ui/server/routes/statistics.js +83 -0
- package/ui/server/utils/config-update.js +212 -0
- package/ui/server/utils/config.js +98 -0
- package/ui/server/utils/paths.js +23 -0
- package/ui/server/utils/port.js +28 -0
- package/ui/server/utils/process.js +80 -0
- package/ui/server/utils/scan-cache/all-results.js +180 -0
- package/ui/server/utils/scan-cache/directory.js +35 -0
- package/ui/server/utils/scan-cache/file-operations.js +104 -0
- package/ui/server/utils/scan-cache/hash.js +47 -0
- package/ui/server/utils/scan-cache/server-operations.js +80 -0
- package/ui/server/utils/scan-cache.js +12 -0
- package/ui/server/utils/serialization.js +13 -0
- package/ui/server/utils/smartscan-token.js +42 -0
- package/ui/server.js +199 -0
- package/ui/src/App.jsx +153 -0
- package/ui/src/CompositeLogs.jsx +164 -0
- package/ui/src/CompositeSetup.jsx +285 -0
- package/ui/src/HelpGuide/HelpGuideContent.jsx +118 -0
- package/ui/src/HelpGuide/HelpGuideFooter.jsx +58 -0
- package/ui/src/HelpGuide/HelpGuideHeader.jsx +56 -0
- package/ui/src/HelpGuide.jsx +65 -0
- package/ui/src/IntroTour.jsx +140 -0
- package/ui/src/LogDetail.jsx +122 -0
- package/ui/src/LogTable.jsx +242 -0
- package/ui/src/PacketDetail.jsx +190 -0
- package/ui/src/PacketFilters.jsx +222 -0
- package/ui/src/PacketList.jsx +183 -0
- package/ui/src/SmartScan.jsx +178 -0
- package/ui/src/TabNavigation.jsx +143 -0
- package/ui/src/components/App/HelpButton.jsx +64 -0
- package/ui/src/components/App/TrafficTab.jsx +69 -0
- package/ui/src/components/App/useAppState.js +163 -0
- package/ui/src/components/BackupList.jsx +192 -0
- package/ui/src/components/CollapsibleSection.jsx +82 -0
- package/ui/src/components/ConfigFileSection.jsx +84 -0
- package/ui/src/components/ConfigViewerModal.jsx +141 -0
- package/ui/src/components/ConfirmationModal.jsx +129 -0
- package/ui/src/components/DetailsTab/BodySection.jsx +27 -0
- package/ui/src/components/DetailsTab/CollapsibleRequestResponse.jsx +70 -0
- package/ui/src/components/DetailsTab/HeadersSection.jsx +25 -0
- package/ui/src/components/DetailsTab/InfoSection.jsx +28 -0
- package/ui/src/components/DetailsTab/NetworkInfoSection.jsx +63 -0
- package/ui/src/components/DetailsTab/ProtocolInfoSection.jsx +75 -0
- package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +46 -0
- package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +66 -0
- package/ui/src/components/DetailsTab.jsx +31 -0
- package/ui/src/components/DetectedPathsList.jsx +171 -0
- package/ui/src/components/FileInput.jsx +144 -0
- package/ui/src/components/GroupHeader.jsx +76 -0
- package/ui/src/components/GroupedByMcpView.jsx +103 -0
- package/ui/src/components/GroupedByServerView.jsx +134 -0
- package/ui/src/components/GroupedBySessionView.jsx +127 -0
- package/ui/src/components/GroupedViews.jsx +2 -0
- package/ui/src/components/HexTab.jsx +188 -0
- package/ui/src/components/LogsDisplay.jsx +93 -0
- package/ui/src/components/LogsToolbar.jsx +193 -0
- package/ui/src/components/McpPlayground/LoadingModal.jsx +113 -0
- package/ui/src/components/McpPlayground/PromptsSection/PromptCallPanel.jsx +125 -0
- package/ui/src/components/McpPlayground/PromptsSection/PromptItem.jsx +48 -0
- package/ui/src/components/McpPlayground/PromptsSection/PromptsList.jsx +45 -0
- package/ui/src/components/McpPlayground/PromptsSection.jsx +106 -0
- package/ui/src/components/McpPlayground/ResourcesSection/ResourceCallPanel.jsx +89 -0
- package/ui/src/components/McpPlayground/ResourcesSection/ResourceItem.jsx +59 -0
- package/ui/src/components/McpPlayground/ResourcesSection/ResourcesList.jsx +45 -0
- package/ui/src/components/McpPlayground/ResourcesSection.jsx +91 -0
- package/ui/src/components/McpPlayground/ToolsSection/ToolCallPanel.jsx +125 -0
- package/ui/src/components/McpPlayground/ToolsSection/ToolItem.jsx +48 -0
- package/ui/src/components/McpPlayground/ToolsSection/ToolsList.jsx +45 -0
- package/ui/src/components/McpPlayground/ToolsSection.jsx +107 -0
- package/ui/src/components/McpPlayground/common/EmptyState.jsx +17 -0
- package/ui/src/components/McpPlayground/common/ErrorState.jsx +17 -0
- package/ui/src/components/McpPlayground/common/LoadingState.jsx +17 -0
- package/ui/src/components/McpPlayground/useMcpPlayground.js +280 -0
- package/ui/src/components/McpPlayground.jsx +171 -0
- package/ui/src/components/MessageDisplay.jsx +28 -0
- package/ui/src/components/PacketDetailHeader.jsx +88 -0
- package/ui/src/components/PacketFilters/ExportControls.jsx +126 -0
- package/ui/src/components/PacketFilters/FilterInput.jsx +59 -0
- package/ui/src/components/RawTab.jsx +142 -0
- package/ui/src/components/RequestRow/OrphanedResponseRow.jsx +155 -0
- package/ui/src/components/RequestRow/RequestRowMain.jsx +240 -0
- package/ui/src/components/RequestRow/ResponseRow.jsx +158 -0
- package/ui/src/components/RequestRow.jsx +70 -0
- package/ui/src/components/ServerControl.jsx +133 -0
- package/ui/src/components/ServiceSelector.jsx +209 -0
- package/ui/src/components/SetupHeader.jsx +30 -0
- package/ui/src/components/SharkLogo.jsx +21 -0
- package/ui/src/components/SmartScan/AnalysisResult.jsx +64 -0
- package/ui/src/components/SmartScan/BatchResultsDisplay/BatchResultItem.jsx +215 -0
- package/ui/src/components/SmartScan/BatchResultsDisplay/BatchResultsHeader.jsx +94 -0
- package/ui/src/components/SmartScan/BatchResultsDisplay.jsx +26 -0
- package/ui/src/components/SmartScan/DebugInfoSection.jsx +53 -0
- package/ui/src/components/SmartScan/EmptyState.jsx +57 -0
- package/ui/src/components/SmartScan/ErrorDisplay.jsx +48 -0
- package/ui/src/components/SmartScan/ExpandableSection.jsx +93 -0
- package/ui/src/components/SmartScan/FindingsTable.jsx +257 -0
- package/ui/src/components/SmartScan/ListViewContent.jsx +75 -0
- package/ui/src/components/SmartScan/NotablePatternsSection.jsx +75 -0
- package/ui/src/components/SmartScan/OverallSummarySection.jsx +72 -0
- package/ui/src/components/SmartScan/RawDataSection.jsx +52 -0
- package/ui/src/components/SmartScan/RecommendationsSection.jsx +78 -0
- package/ui/src/components/SmartScan/ScanDetailHeader.jsx +92 -0
- package/ui/src/components/SmartScan/ScanDetailView.jsx +141 -0
- package/ui/src/components/SmartScan/ScanListView/ScanListHeader.jsx +49 -0
- package/ui/src/components/SmartScan/ScanListView/ScanListItem.jsx +201 -0
- package/ui/src/components/SmartScan/ScanListView.jsx +73 -0
- package/ui/src/components/SmartScan/ScanOverviewSection.jsx +123 -0
- package/ui/src/components/SmartScan/ScanResultsDisplay.jsx +35 -0
- package/ui/src/components/SmartScan/ScanViewContent.jsx +68 -0
- package/ui/src/components/SmartScan/ScanningProgress.jsx +47 -0
- package/ui/src/components/SmartScan/ServerInfoSection.jsx +43 -0
- package/ui/src/components/SmartScan/ServerSelectionRow.jsx +207 -0
- package/ui/src/components/SmartScan/SingleResultDisplay.jsx +269 -0
- package/ui/src/components/SmartScan/SmartScanControls.jsx +290 -0
- package/ui/src/components/SmartScan/SmartScanHeader.jsx +77 -0
- package/ui/src/components/SmartScan/ViewModeTabs.jsx +57 -0
- package/ui/src/components/SmartScan/hooks/useCacheManagement.js +34 -0
- package/ui/src/components/SmartScan/hooks/useMcpDiscovery.js +121 -0
- package/ui/src/components/SmartScan/hooks/useScanList.js +193 -0
- package/ui/src/components/SmartScan/hooks/useScanOperations.js +87 -0
- package/ui/src/components/SmartScan/hooks/useServerStatus.js +26 -0
- package/ui/src/components/SmartScan/hooks/useTokenManagement.js +53 -0
- package/ui/src/components/SmartScan/scanDataUtils.js +98 -0
- package/ui/src/components/SmartScan/useSmartScan.js +72 -0
- package/ui/src/components/SmartScan/utils.js +19 -0
- package/ui/src/components/SmartScanIcons.jsx +58 -0
- package/ui/src/components/TabNavigation/DesktopTabs.jsx +111 -0
- package/ui/src/components/TabNavigation/MobileDropdown.jsx +140 -0
- package/ui/src/components/TabNavigation.jsx +97 -0
- package/ui/src/components/TabNavigationIcons.jsx +40 -0
- package/ui/src/components/TableHeader.jsx +164 -0
- package/ui/src/components/TourOverlay.jsx +117 -0
- package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +117 -0
- package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +70 -0
- package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +45 -0
- package/ui/src/components/TourTooltip/useTooltipPosition.js +108 -0
- package/ui/src/components/TourTooltip.jsx +83 -0
- package/ui/src/components/ViewModeTabs.jsx +91 -0
- package/ui/src/components/WhatThisDoesSection.jsx +61 -0
- package/ui/src/config/tourSteps.jsx +141 -0
- package/ui/src/hooks/useAnimation.js +92 -0
- package/ui/src/hooks/useConfigManagement.js +124 -0
- package/ui/src/hooks/useServiceExtraction.js +51 -0
- package/ui/src/index.css +42 -0
- package/ui/src/main.jsx +10 -0
- package/ui/src/theme.js +65 -0
- package/ui/src/utils/animations.js +170 -0
- package/ui/src/utils/groupingUtils.js +93 -0
- package/ui/src/utils/hexUtils.js +24 -0
- package/ui/src/utils/mcpGroupingUtils.js +262 -0
- package/ui/src/utils/requestUtils.js +297 -0
- package/ui/vite.config.js +18 -0
|
@@ -0,0 +1,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,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
|
+
}
|