mcp-use 1.10.6 → 1.11.0-canary.11
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/README.md +1 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/{chunk-KIWNNI6F.js → chunk-2NE5H4KG.js} +16 -2
- package/dist/{chunk-D5WOXLJ2.js → chunk-4QWS5ME6.js} +171 -11
- package/dist/chunk-7P2EMREO.js +101 -0
- package/dist/chunk-7PSUUT4A.js +1055 -0
- package/dist/{chunk-44DFBJUL.js → chunk-BOCIQYWG.js} +196 -496
- package/dist/chunk-BPZJIV4V.js +1873 -0
- package/dist/{chunk-EEUJZMOP.js → chunk-CVKKDXI3.js} +1 -1
- package/dist/chunk-DPK5NHDR.js +12 -0
- package/dist/{chunk-FDKY2O5P.js → chunk-EHCLF3JO.js} +443 -969
- package/dist/{chunk-34R6SIER.js → chunk-FRUZDWXH.js} +1 -1
- package/dist/chunk-GXNAXUDI.js +0 -0
- package/dist/{chunk-JH3ZOGLI.js → chunk-J3WTIYVV.js} +2 -2
- package/dist/{chunk-CPG2WZUL.js → chunk-JRGQRPTN.js} +1 -1
- package/dist/chunk-MFSO5PUW.js +1049 -0
- package/dist/chunk-SQSJ5NKY.js +1055 -0
- package/dist/chunk-T4EDAWDS.js +2638 -0
- package/dist/chunk-UWWLWLS2.js +62 -0
- package/dist/{chunk-BLWPCOUZ.js → chunk-V4YUDB7N.js} +3 -8
- package/dist/index.cjs +5065 -4608
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -1061
- package/dist/{langfuse-N5Y5BSXK.js → langfuse-74RGPTAH.js} +2 -2
- package/dist/notifications-FLGIFS56.js +9 -0
- package/dist/src/adapters/base.d.ts +44 -0
- package/dist/src/adapters/base.d.ts.map +1 -1
- package/dist/src/adapters/index.cjs +1346 -0
- package/dist/src/adapters/index.js +11 -0
- package/dist/src/adapters/langchain_adapter.d.ts +12 -1
- package/dist/src/adapters/langchain_adapter.d.ts.map +1 -1
- package/dist/src/agents/index.cjs +3141 -159
- package/dist/src/agents/index.d.ts +2 -0
- package/dist/src/agents/index.d.ts.map +1 -1
- package/dist/src/agents/index.js +12 -8
- package/dist/src/agents/mcp_agent.d.ts +59 -37
- package/dist/src/agents/mcp_agent.d.ts.map +1 -1
- package/dist/src/agents/remote.d.ts +25 -0
- package/dist/src/agents/remote.d.ts.map +1 -1
- package/dist/src/agents/types.d.ts +76 -0
- package/dist/src/agents/types.d.ts.map +1 -1
- package/dist/src/agents/utils/index.d.ts +1 -0
- package/dist/src/agents/utils/index.d.ts.map +1 -1
- package/dist/src/agents/utils/llm_provider.d.ts +53 -0
- package/dist/src/agents/utils/llm_provider.d.ts.map +1 -0
- package/dist/src/browser.cjs +1856 -423
- package/dist/src/browser.d.ts +1 -2
- package/dist/src/browser.d.ts.map +1 -1
- package/dist/src/browser.js +30 -19
- package/dist/src/client/base.d.ts +1 -0
- package/dist/src/client/base.d.ts.map +1 -1
- package/dist/src/client/browser.d.ts +2 -2
- package/dist/src/client/browser.d.ts.map +1 -1
- package/dist/src/client/prompts.cjs +1 -1
- package/dist/src/client/prompts.js +5 -4
- package/dist/src/client.cjs +3788 -0
- package/dist/src/client.d.ts +2 -0
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +23 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/connectors/base.d.ts +8 -0
- package/dist/src/connectors/base.d.ts.map +1 -1
- package/dist/src/connectors/index.d.ts +0 -1
- package/dist/src/connectors/index.d.ts.map +1 -1
- package/dist/src/managers/server_manager.d.ts.map +1 -1
- package/dist/src/managers/tools/connect_mcp_server.d.ts.map +1 -1
- package/dist/src/react/index.cjs +259 -298
- package/dist/src/react/index.js +9 -8
- package/dist/src/react/types.d.ts +42 -4
- package/dist/src/react/types.d.ts.map +1 -1
- package/dist/src/react/useMcp.d.ts.map +1 -1
- package/dist/src/react/useWidget.d.ts +11 -7
- package/dist/src/react/useWidget.d.ts.map +1 -1
- package/dist/src/react/widget-types.d.ts +6 -2
- package/dist/src/react/widget-types.d.ts.map +1 -1
- package/dist/src/server/endpoints/mount-mcp.d.ts.map +1 -1
- package/dist/src/server/index.cjs +1288 -140
- package/dist/src/server/index.d.ts +2 -0
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/server/index.js +1183 -102
- package/dist/src/server/mcp-server.d.ts +5 -1
- package/dist/src/server/mcp-server.d.ts.map +1 -1
- package/dist/src/server/notifications/index.d.ts +1 -1
- package/dist/src/server/notifications/index.d.ts.map +1 -1
- package/dist/src/server/notifications/notification-registration.d.ts +51 -0
- package/dist/src/server/notifications/notification-registration.d.ts.map +1 -1
- package/dist/src/server/sessions/index.d.ts +3 -1
- package/dist/src/server/sessions/index.d.ts.map +1 -1
- package/dist/src/server/sessions/session-manager.d.ts +36 -19
- package/dist/src/server/sessions/session-manager.d.ts.map +1 -1
- package/dist/src/server/sessions/stores/filesystem.d.ts +121 -0
- package/dist/src/server/sessions/stores/filesystem.d.ts.map +1 -0
- package/dist/src/server/sessions/stores/index.d.ts +94 -0
- package/dist/src/server/sessions/stores/index.d.ts.map +1 -0
- package/dist/src/server/sessions/stores/memory.d.ts +82 -0
- package/dist/src/server/sessions/stores/memory.d.ts.map +1 -0
- package/dist/src/server/sessions/stores/redis.d.ts +164 -0
- package/dist/src/server/sessions/stores/redis.d.ts.map +1 -0
- package/dist/src/server/sessions/streams/index.d.ts +77 -0
- package/dist/src/server/sessions/streams/index.d.ts.map +1 -0
- package/dist/src/server/sessions/streams/memory.d.ts +76 -0
- package/dist/src/server/sessions/streams/memory.d.ts.map +1 -0
- package/dist/src/server/sessions/streams/redis.d.ts +146 -0
- package/dist/src/server/sessions/streams/redis.d.ts.map +1 -0
- package/dist/src/server/types/common.d.ts +105 -28
- package/dist/src/server/types/common.d.ts.map +1 -1
- package/dist/src/server/types/resource.d.ts +16 -0
- package/dist/src/server/types/resource.d.ts.map +1 -1
- package/dist/src/server/types/widget.d.ts +21 -2
- package/dist/src/server/types/widget.d.ts.map +1 -1
- package/dist/src/server/utils/response-helpers.d.ts +12 -6
- package/dist/src/server/utils/response-helpers.d.ts.map +1 -1
- package/dist/src/server/widgets/index.d.ts +1 -1
- package/dist/src/server/widgets/index.d.ts.map +1 -1
- package/dist/src/server/widgets/mount-widgets-dev.d.ts.map +1 -1
- package/dist/src/server/widgets/setup-widget-routes.d.ts.map +1 -1
- package/dist/src/server/widgets/ui-resource-registration.d.ts.map +1 -1
- package/dist/src/server/widgets/widget-helpers.d.ts +22 -0
- package/dist/src/server/widgets/widget-helpers.d.ts.map +1 -1
- package/dist/src/server/widgets/widget-types.d.ts +2 -0
- package/dist/src/server/widgets/widget-types.d.ts.map +1 -1
- package/dist/src/session.d.ts +16 -2
- package/dist/src/session.d.ts.map +1 -1
- package/dist/src/task_managers/index.d.ts +10 -1
- package/dist/src/task_managers/index.d.ts.map +1 -1
- package/dist/src/task_managers/sse.d.ts +34 -1
- package/dist/src/task_managers/sse.d.ts.map +1 -1
- package/dist/src/task_managers/streamable_http.d.ts +8 -2
- package/dist/src/task_managers/streamable_http.d.ts.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.d.ts.map +1 -1
- package/dist/{tool-execution-helpers-4X6A63AS.js → tool-execution-helpers-3BYYGIA5.js} +3 -3
- package/dist/tsup.config.d.ts.map +1 -1
- package/package.json +47 -14
- package/dist/src/connectors/websocket.d.ts +0 -38
- package/dist/src/connectors/websocket.d.ts.map +0 -1
- package/dist/src/task_managers/websocket.d.ts +0 -18
- package/dist/src/task_managers/websocket.d.ts.map +0 -1
- /package/dist/{chunk-EW4MJSHA.js → chunk-LGDFGYRL.js} +0 -0
package/dist/src/server/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRequest,
|
|
3
|
+
sendNotificationToAll,
|
|
4
|
+
sendNotificationToSession
|
|
5
|
+
} from "../../chunk-UWWLWLS2.js";
|
|
1
6
|
import {
|
|
2
7
|
getRequestContext,
|
|
3
8
|
hasRequestContext,
|
|
@@ -7,7 +12,7 @@ import {
|
|
|
7
12
|
createEnhancedContext,
|
|
8
13
|
findSessionContext,
|
|
9
14
|
isValidLogLevel
|
|
10
|
-
} from "../../chunk-
|
|
15
|
+
} from "../../chunk-CVKKDXI3.js";
|
|
11
16
|
import {
|
|
12
17
|
convertToolResultToResourceResult
|
|
13
18
|
} from "../../chunk-362PI25Z.js";
|
|
@@ -25,8 +30,8 @@ import {
|
|
|
25
30
|
getPackageVersion,
|
|
26
31
|
isDeno,
|
|
27
32
|
pathHelpers
|
|
28
|
-
} from "../../chunk-
|
|
29
|
-
import "../../chunk-
|
|
33
|
+
} from "../../chunk-J3WTIYVV.js";
|
|
34
|
+
import "../../chunk-FRUZDWXH.js";
|
|
30
35
|
import {
|
|
31
36
|
__name
|
|
32
37
|
} from "../../chunk-3GQAWCBQ.js";
|
|
@@ -314,17 +319,23 @@ function binary(base64Data, mimeType) {
|
|
|
314
319
|
}
|
|
315
320
|
__name(binary, "binary");
|
|
316
321
|
function widget(config) {
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
322
|
+
const props = config.props || config.data || {};
|
|
323
|
+
const { output, message } = config;
|
|
324
|
+
const finalContent = message ? [{ type: "text", text: message }] : Array.isArray(output?.content) && output.content.length > 0 ? output.content : [{ type: "text", text: "" }];
|
|
325
|
+
const meta = {
|
|
326
|
+
...output?._meta || {},
|
|
327
|
+
"mcp-use/props": props
|
|
328
|
+
};
|
|
329
|
+
const result = {
|
|
330
|
+
content: finalContent,
|
|
331
|
+
_meta: meta
|
|
327
332
|
};
|
|
333
|
+
if (output?.structuredContent) {
|
|
334
|
+
result.structuredContent = output.structuredContent;
|
|
335
|
+
} else if (Object.keys(props).length > 0) {
|
|
336
|
+
result.structuredContent = props;
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
328
339
|
}
|
|
329
340
|
__name(widget, "widget");
|
|
330
341
|
function mix(...results) {
|
|
@@ -985,7 +996,7 @@ function processWidgetHtml(html2, widgetName, baseUrl) {
|
|
|
985
996
|
}
|
|
986
997
|
__name(processWidgetHtml, "processWidgetHtml");
|
|
987
998
|
function createWidgetRegistration(widgetName, metadata, html2, serverConfig, isDev = false) {
|
|
988
|
-
const props = metadata.inputs || {};
|
|
999
|
+
const props = metadata.props || metadata.inputs || metadata.schema || {};
|
|
989
1000
|
const description = metadata.description || `Widget: ${widgetName}`;
|
|
990
1001
|
const title = metadata.title || widgetName;
|
|
991
1002
|
const exposeAsTool = metadata.exposeAsTool !== void 0 ? metadata.exposeAsTool : true;
|
|
@@ -1127,6 +1138,33 @@ function setupPublicRoutes(app, useDistDirectory = false) {
|
|
|
1127
1138
|
});
|
|
1128
1139
|
}
|
|
1129
1140
|
__name(setupPublicRoutes, "setupPublicRoutes");
|
|
1141
|
+
function setupFaviconRoute(app, faviconPath, useDistDirectory = false) {
|
|
1142
|
+
if (!faviconPath) {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
app.get("/favicon.ico", async (c) => {
|
|
1146
|
+
const basePath = useDistDirectory ? "dist/public" : "public";
|
|
1147
|
+
const fullPath = pathHelpers.join(getCwd(), basePath, faviconPath);
|
|
1148
|
+
try {
|
|
1149
|
+
if (await fsHelpers.existsSync(fullPath)) {
|
|
1150
|
+
const content = await fsHelpers.readFile(fullPath);
|
|
1151
|
+
const contentType = getContentType(faviconPath);
|
|
1152
|
+
return new Response(content, {
|
|
1153
|
+
status: 200,
|
|
1154
|
+
headers: {
|
|
1155
|
+
"Content-Type": contentType,
|
|
1156
|
+
"Cache-Control": "public, max-age=31536000"
|
|
1157
|
+
// Cache for 1 year
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
return c.notFound();
|
|
1162
|
+
} catch {
|
|
1163
|
+
return c.notFound();
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
__name(setupFaviconRoute, "setupFaviconRoute");
|
|
1130
1168
|
|
|
1131
1169
|
// src/server/widgets/mount-widgets-dev.ts
|
|
1132
1170
|
var TMP_MCP_USE_DIR = ".mcp-use";
|
|
@@ -1176,6 +1214,19 @@ async function mountWidgetsDev(app, serverConfig, registerWidget, options) {
|
|
|
1176
1214
|
return;
|
|
1177
1215
|
}
|
|
1178
1216
|
const tempDir = pathHelpers.join(getCwd(), TMP_MCP_USE_DIR);
|
|
1217
|
+
try {
|
|
1218
|
+
await fs.access(tempDir);
|
|
1219
|
+
const currentWidgetNames = new Set(entries.map((e) => e.name));
|
|
1220
|
+
const existingDirs = await fs.readdir(tempDir, { withFileTypes: true });
|
|
1221
|
+
for (const dirent of existingDirs) {
|
|
1222
|
+
if (dirent.isDirectory() && !currentWidgetNames.has(dirent.name)) {
|
|
1223
|
+
const staleDir = pathHelpers.join(tempDir, dirent.name);
|
|
1224
|
+
await fs.rm(staleDir, { recursive: true, force: true });
|
|
1225
|
+
console.log(`[WIDGETS] Cleaned up stale widget: ${dirent.name}`);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
} catch {
|
|
1229
|
+
}
|
|
1179
1230
|
await fs.mkdir(tempDir, { recursive: true }).catch(() => {
|
|
1180
1231
|
});
|
|
1181
1232
|
let createServer;
|
|
@@ -1213,10 +1264,13 @@ async function mountWidgetsDev(app, serverConfig, registerWidget, options) {
|
|
|
1213
1264
|
await fs.mkdir(widgetTempDir, { recursive: true });
|
|
1214
1265
|
const resourcesPath = pathHelpers.join(getCwd(), resourcesDir);
|
|
1215
1266
|
const relativeResourcesPath = pathHelpers.relative(widgetTempDir, resourcesPath).replace(/\\/g, "/");
|
|
1267
|
+
const mcpUsePath = pathHelpers.join(getCwd(), "node_modules", "mcp-use");
|
|
1268
|
+
const relativeMcpUsePath = pathHelpers.relative(widgetTempDir, mcpUsePath).replace(/\\/g, "/");
|
|
1216
1269
|
const cssContent = `@import "tailwindcss";
|
|
1217
1270
|
|
|
1218
|
-
/* Configure Tailwind to scan the resources directory */
|
|
1271
|
+
/* Configure Tailwind to scan the resources directory and mcp-use package */
|
|
1219
1272
|
@source "${relativeResourcesPath}";
|
|
1273
|
+
@source "${relativeMcpUsePath}/**/*.{ts,tsx,js,jsx}";
|
|
1220
1274
|
`;
|
|
1221
1275
|
await fs.writeFile(
|
|
1222
1276
|
pathHelpers.join(widgetTempDir, "styles.css"),
|
|
@@ -1239,7 +1293,8 @@ if (container && Component) {
|
|
|
1239
1293
|
<head>
|
|
1240
1294
|
<meta charset="UTF-8" />
|
|
1241
1295
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
1242
|
-
<title>${widget2.name} Widget</title
|
|
1296
|
+
<title>${widget2.name} Widget</title>${serverConfig.favicon ? `
|
|
1297
|
+
<link rel="icon" href="/mcp-use/public/${serverConfig.favicon}" />` : ""}
|
|
1243
1298
|
</head>
|
|
1244
1299
|
<body>
|
|
1245
1300
|
<div id="widget-root"></div>
|
|
@@ -1283,6 +1338,50 @@ if (container && Component) {
|
|
|
1283
1338
|
const resourcesPath = pathHelpers.join(getCwd(), resourcesDir);
|
|
1284
1339
|
server.watcher.add(resourcesPath);
|
|
1285
1340
|
console.log(`[WIDGETS] Watching resources directory: ${resourcesPath}`);
|
|
1341
|
+
server.watcher.on("unlink", async (filePath) => {
|
|
1342
|
+
const relativePath = pathHelpers.relative(resourcesPath, filePath);
|
|
1343
|
+
if ((relativePath.endsWith(".tsx") || relativePath.endsWith(".ts")) && !relativePath.includes("/")) {
|
|
1344
|
+
const widgetName = relativePath.replace(/\.tsx?$/, "");
|
|
1345
|
+
const widgetDir = pathHelpers.join(tempDir, widgetName);
|
|
1346
|
+
try {
|
|
1347
|
+
await fs.access(widgetDir);
|
|
1348
|
+
await fs.rm(widgetDir, { recursive: true, force: true });
|
|
1349
|
+
console.log(
|
|
1350
|
+
`[WIDGETS] Cleaned up stale widget (file removed): ${widgetName}`
|
|
1351
|
+
);
|
|
1352
|
+
} catch {
|
|
1353
|
+
}
|
|
1354
|
+
} else if (relativePath.endsWith("widget.tsx")) {
|
|
1355
|
+
const parts = relativePath.split("/");
|
|
1356
|
+
if (parts.length === 2) {
|
|
1357
|
+
const widgetName = parts[0];
|
|
1358
|
+
const widgetDir = pathHelpers.join(tempDir, widgetName);
|
|
1359
|
+
try {
|
|
1360
|
+
await fs.access(widgetDir);
|
|
1361
|
+
await fs.rm(widgetDir, { recursive: true, force: true });
|
|
1362
|
+
console.log(
|
|
1363
|
+
`[WIDGETS] Cleaned up stale widget (file removed): ${widgetName}`
|
|
1364
|
+
);
|
|
1365
|
+
} catch {
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
server.watcher.on("unlinkDir", async (dirPath) => {
|
|
1371
|
+
const relativePath = pathHelpers.relative(resourcesPath, dirPath);
|
|
1372
|
+
if (relativePath && !relativePath.includes("/")) {
|
|
1373
|
+
const widgetName = relativePath;
|
|
1374
|
+
const widgetDir = pathHelpers.join(tempDir, widgetName);
|
|
1375
|
+
try {
|
|
1376
|
+
await fs.access(widgetDir);
|
|
1377
|
+
await fs.rm(widgetDir, { recursive: true, force: true });
|
|
1378
|
+
console.log(
|
|
1379
|
+
`[WIDGETS] Cleaned up stale widget (directory removed): ${widgetName}`
|
|
1380
|
+
);
|
|
1381
|
+
} catch {
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1286
1385
|
}
|
|
1287
1386
|
};
|
|
1288
1387
|
const nodeStubsPlugin = {
|
|
@@ -1395,6 +1494,7 @@ export default PostHog;
|
|
|
1395
1494
|
);
|
|
1396
1495
|
app.use(`${baseRoute}/*`, viteMiddleware);
|
|
1397
1496
|
setupPublicRoutes(app, false);
|
|
1497
|
+
setupFaviconRoute(app, serverConfig.favicon, false);
|
|
1398
1498
|
app.use(`${baseRoute}/*`, async (c) => {
|
|
1399
1499
|
const url = new URL(c.req.url);
|
|
1400
1500
|
const isAsset = url.pathname.match(
|
|
@@ -1602,6 +1702,7 @@ function setupWidgetRoutes(app, serverConfig) {
|
|
|
1602
1702
|
}
|
|
1603
1703
|
});
|
|
1604
1704
|
setupPublicRoutes(app, true);
|
|
1705
|
+
setupFaviconRoute(app, serverConfig.favicon, true);
|
|
1605
1706
|
}
|
|
1606
1707
|
__name(setupWidgetRoutes, "setupWidgetRoutes");
|
|
1607
1708
|
|
|
@@ -1736,18 +1837,30 @@ function uiResourceRegistration(server, definition) {
|
|
|
1736
1837
|
);
|
|
1737
1838
|
const uniqueToolMetadata = {
|
|
1738
1839
|
...toolMetadata,
|
|
1739
|
-
"openai/outputTemplate": uniqueUri
|
|
1840
|
+
"openai/outputTemplate": uniqueUri,
|
|
1841
|
+
"mcp-use/props": params
|
|
1842
|
+
// Pass params as widget props
|
|
1740
1843
|
};
|
|
1844
|
+
let toolOutputResult;
|
|
1845
|
+
if (definition.toolOutput) {
|
|
1846
|
+
toolOutputResult = typeof definition.toolOutput === "function" ? definition.toolOutput(params) : definition.toolOutput;
|
|
1847
|
+
} else {
|
|
1848
|
+
toolOutputResult = {
|
|
1849
|
+
content: [
|
|
1850
|
+
{
|
|
1851
|
+
type: "text",
|
|
1852
|
+
text: `Displaying ${displayName}`
|
|
1853
|
+
}
|
|
1854
|
+
]
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
const content = toolOutputResult.content || [
|
|
1858
|
+
{ type: "text", text: `Displaying ${displayName}` }
|
|
1859
|
+
];
|
|
1741
1860
|
return {
|
|
1742
1861
|
_meta: uniqueToolMetadata,
|
|
1743
|
-
content
|
|
1744
|
-
|
|
1745
|
-
type: "text",
|
|
1746
|
-
text: `Displaying ${displayName}`
|
|
1747
|
-
}
|
|
1748
|
-
],
|
|
1749
|
-
// structuredContent will be injected as window.openai.toolOutput by Apps SDK
|
|
1750
|
-
structuredContent: params
|
|
1862
|
+
content,
|
|
1863
|
+
structuredContent: toolOutputResult.structuredContent
|
|
1751
1864
|
};
|
|
1752
1865
|
}
|
|
1753
1866
|
return {
|
|
@@ -1773,7 +1886,8 @@ async function mountWidgets(server, options) {
|
|
|
1773
1886
|
serverBaseUrl: server.serverBaseUrl || `http://${server.serverHost}:${server.serverPort || 3e3}`,
|
|
1774
1887
|
serverPort: server.serverPort || 3e3,
|
|
1775
1888
|
cspUrls: getCSPUrls(),
|
|
1776
|
-
buildId: server.buildId
|
|
1889
|
+
buildId: server.buildId,
|
|
1890
|
+
favicon: server.favicon
|
|
1777
1891
|
};
|
|
1778
1892
|
const registerWidget = /* @__PURE__ */ __name((widgetDef) => {
|
|
1779
1893
|
server.uiResource(widgetDef);
|
|
@@ -2091,7 +2205,7 @@ function registerResource(resourceDefinition, callback) {
|
|
|
2091
2205
|
const explicitMimeType = resourceDefinition.mimeType;
|
|
2092
2206
|
const wrappedCallback = /* @__PURE__ */ __name(async () => {
|
|
2093
2207
|
const { getRequestContext: getRequestContext2, runWithContext: runWithContext2 } = await import("../../context-storage-NA4MHWOZ.js");
|
|
2094
|
-
const { findSessionContext: findSessionContext2 } = await import("../../tool-execution-helpers-
|
|
2208
|
+
const { findSessionContext: findSessionContext2 } = await import("../../tool-execution-helpers-3BYYGIA5.js");
|
|
2095
2209
|
const initialRequestContext = getRequestContext2();
|
|
2096
2210
|
const sessions = this.sessions || /* @__PURE__ */ new Map();
|
|
2097
2211
|
const { requestContext } = findSessionContext2(
|
|
@@ -2169,7 +2283,7 @@ function registerResourceTemplate(resourceTemplateDefinition, callback) {
|
|
|
2169
2283
|
async (uri) => {
|
|
2170
2284
|
const params = this.parseTemplateUri(uriTemplate, uri.toString());
|
|
2171
2285
|
const { getRequestContext: getRequestContext2, runWithContext: runWithContext2 } = await import("../../context-storage-NA4MHWOZ.js");
|
|
2172
|
-
const { findSessionContext: findSessionContext2 } = await import("../../tool-execution-helpers-
|
|
2286
|
+
const { findSessionContext: findSessionContext2 } = await import("../../tool-execution-helpers-3BYYGIA5.js");
|
|
2173
2287
|
const initialRequestContext = getRequestContext2();
|
|
2174
2288
|
const sessions = this.sessions || /* @__PURE__ */ new Map();
|
|
2175
2289
|
const { requestContext } = findSessionContext2(
|
|
@@ -2224,7 +2338,7 @@ function registerPrompt(promptDefinition, callback) {
|
|
|
2224
2338
|
}
|
|
2225
2339
|
const wrappedCallback = /* @__PURE__ */ __name(async (params, extra) => {
|
|
2226
2340
|
const { getRequestContext: getRequestContext2, runWithContext: runWithContext2 } = await import("../../context-storage-NA4MHWOZ.js");
|
|
2227
|
-
const { findSessionContext: findSessionContext2 } = await import("../../tool-execution-helpers-
|
|
2341
|
+
const { findSessionContext: findSessionContext2 } = await import("../../tool-execution-helpers-3BYYGIA5.js");
|
|
2228
2342
|
const initialRequestContext = getRequestContext2();
|
|
2229
2343
|
const sessions = this.sessions || /* @__PURE__ */ new Map();
|
|
2230
2344
|
const { requestContext } = findSessionContext2(
|
|
@@ -2261,25 +2375,6 @@ function registerPrompt(promptDefinition, callback) {
|
|
|
2261
2375
|
}
|
|
2262
2376
|
__name(registerPrompt, "registerPrompt");
|
|
2263
2377
|
|
|
2264
|
-
// src/server/utils/jsonrpc-helpers.ts
|
|
2265
|
-
function createNotification(method, params) {
|
|
2266
|
-
return {
|
|
2267
|
-
jsonrpc: "2.0",
|
|
2268
|
-
method,
|
|
2269
|
-
...params && { params }
|
|
2270
|
-
};
|
|
2271
|
-
}
|
|
2272
|
-
__name(createNotification, "createNotification");
|
|
2273
|
-
function createRequest(id, method, params) {
|
|
2274
|
-
return {
|
|
2275
|
-
jsonrpc: "2.0",
|
|
2276
|
-
id,
|
|
2277
|
-
method,
|
|
2278
|
-
...params && { params }
|
|
2279
|
-
};
|
|
2280
|
-
}
|
|
2281
|
-
__name(createRequest, "createRequest");
|
|
2282
|
-
|
|
2283
2378
|
// src/server/roots/roots-registration.ts
|
|
2284
2379
|
function onRootsChanged(callback) {
|
|
2285
2380
|
this.onRootsChangedCallback = callback;
|
|
@@ -2441,40 +2536,6 @@ async function requestLogger(c, next) {
|
|
|
2441
2536
|
}
|
|
2442
2537
|
__name(requestLogger, "requestLogger");
|
|
2443
2538
|
|
|
2444
|
-
// src/server/sessions/notifications.ts
|
|
2445
|
-
async function sendNotificationToAll(sessions, method, params) {
|
|
2446
|
-
const notification = createNotification(method, params);
|
|
2447
|
-
for (const [sessionId, session] of sessions.entries()) {
|
|
2448
|
-
try {
|
|
2449
|
-
await session.transport.send(notification);
|
|
2450
|
-
} catch (error2) {
|
|
2451
|
-
console.warn(
|
|
2452
|
-
`[MCP] Failed to send notification to session ${sessionId}:`,
|
|
2453
|
-
error2
|
|
2454
|
-
);
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
}
|
|
2458
|
-
__name(sendNotificationToAll, "sendNotificationToAll");
|
|
2459
|
-
async function sendNotificationToSession(sessions, sessionId, method, params) {
|
|
2460
|
-
const session = sessions.get(sessionId);
|
|
2461
|
-
if (!session) {
|
|
2462
|
-
return false;
|
|
2463
|
-
}
|
|
2464
|
-
const notification = createNotification(method, params);
|
|
2465
|
-
try {
|
|
2466
|
-
await session.transport.send(notification);
|
|
2467
|
-
return true;
|
|
2468
|
-
} catch (error2) {
|
|
2469
|
-
console.warn(
|
|
2470
|
-
`[MCP] Failed to send notification to session ${sessionId}:`,
|
|
2471
|
-
error2
|
|
2472
|
-
);
|
|
2473
|
-
return false;
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
__name(sendNotificationToSession, "sendNotificationToSession");
|
|
2477
|
-
|
|
2478
2539
|
// src/server/notifications/notification-registration.ts
|
|
2479
2540
|
function getActiveSessions() {
|
|
2480
2541
|
return Array.from(this.sessions.keys());
|
|
@@ -2493,9 +2554,30 @@ async function sendNotificationToSession2(sessionId, method, params) {
|
|
|
2493
2554
|
);
|
|
2494
2555
|
}
|
|
2495
2556
|
__name(sendNotificationToSession2, "sendNotificationToSession");
|
|
2557
|
+
async function sendToolsListChanged() {
|
|
2558
|
+
await sendNotificationToAll(
|
|
2559
|
+
this.sessions,
|
|
2560
|
+
"notifications/tools/list_changed"
|
|
2561
|
+
);
|
|
2562
|
+
}
|
|
2563
|
+
__name(sendToolsListChanged, "sendToolsListChanged");
|
|
2564
|
+
async function sendResourcesListChanged() {
|
|
2565
|
+
await sendNotificationToAll(
|
|
2566
|
+
this.sessions,
|
|
2567
|
+
"notifications/resources/list_changed"
|
|
2568
|
+
);
|
|
2569
|
+
}
|
|
2570
|
+
__name(sendResourcesListChanged, "sendResourcesListChanged");
|
|
2571
|
+
async function sendPromptsListChanged() {
|
|
2572
|
+
await sendNotificationToAll(
|
|
2573
|
+
this.sessions,
|
|
2574
|
+
"notifications/prompts/list_changed"
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
__name(sendPromptsListChanged, "sendPromptsListChanged");
|
|
2496
2578
|
|
|
2497
2579
|
// src/server/sessions/session-manager.ts
|
|
2498
|
-
function startIdleCleanup(sessions, idleTimeoutMs, mcpServerInstance) {
|
|
2580
|
+
function startIdleCleanup(sessions, idleTimeoutMs, transports, mcpServerInstance) {
|
|
2499
2581
|
if (idleTimeoutMs <= 0) {
|
|
2500
2582
|
return void 0;
|
|
2501
2583
|
}
|
|
@@ -2512,37 +2594,895 @@ function startIdleCleanup(sessions, idleTimeoutMs, mcpServerInstance) {
|
|
|
2512
2594
|
`[MCP] Cleaning up ${expiredSessions.length} idle session(s)`
|
|
2513
2595
|
);
|
|
2514
2596
|
for (const sessionId of expiredSessions) {
|
|
2597
|
+
const transport = transports?.get(sessionId);
|
|
2598
|
+
if (transport?.close) {
|
|
2599
|
+
Promise.resolve(transport.close()).catch((e) => {
|
|
2600
|
+
console.warn(
|
|
2601
|
+
`[MCP] Error closing transport for session ${sessionId}:`,
|
|
2602
|
+
e
|
|
2603
|
+
);
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
transports?.delete(sessionId);
|
|
2515
2607
|
sessions.delete(sessionId);
|
|
2516
2608
|
mcpServerInstance?.cleanupSessionSubscriptions?.(sessionId);
|
|
2609
|
+
console.log(
|
|
2610
|
+
`[MCP] Cleaned up resource subscriptions for session ${sessionId}`
|
|
2611
|
+
);
|
|
2517
2612
|
}
|
|
2518
2613
|
}
|
|
2519
2614
|
}, 6e4);
|
|
2520
2615
|
}
|
|
2521
2616
|
__name(startIdleCleanup, "startIdleCleanup");
|
|
2522
2617
|
|
|
2618
|
+
// src/server/sessions/stores/memory.ts
|
|
2619
|
+
var InMemorySessionStore = class {
|
|
2620
|
+
static {
|
|
2621
|
+
__name(this, "InMemorySessionStore");
|
|
2622
|
+
}
|
|
2623
|
+
/**
|
|
2624
|
+
* Internal map storing session metadata
|
|
2625
|
+
* Key: sessionId, Value: SessionMetadata
|
|
2626
|
+
*/
|
|
2627
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2628
|
+
/**
|
|
2629
|
+
* Retrieve session metadata by ID
|
|
2630
|
+
*/
|
|
2631
|
+
async get(sessionId) {
|
|
2632
|
+
const data = this.sessions.get(sessionId);
|
|
2633
|
+
return data ?? null;
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Store or update session metadata
|
|
2637
|
+
*/
|
|
2638
|
+
async set(sessionId, data) {
|
|
2639
|
+
this.sessions.set(sessionId, data);
|
|
2640
|
+
}
|
|
2641
|
+
/**
|
|
2642
|
+
* Delete session metadata
|
|
2643
|
+
*/
|
|
2644
|
+
async delete(sessionId) {
|
|
2645
|
+
this.sessions.delete(sessionId);
|
|
2646
|
+
}
|
|
2647
|
+
/**
|
|
2648
|
+
* Check if session exists
|
|
2649
|
+
*/
|
|
2650
|
+
async has(sessionId) {
|
|
2651
|
+
return this.sessions.has(sessionId);
|
|
2652
|
+
}
|
|
2653
|
+
/**
|
|
2654
|
+
* List all session IDs
|
|
2655
|
+
*/
|
|
2656
|
+
async keys() {
|
|
2657
|
+
return Array.from(this.sessions.keys());
|
|
2658
|
+
}
|
|
2659
|
+
/**
|
|
2660
|
+
* Store session metadata with TTL (time-to-live)
|
|
2661
|
+
*
|
|
2662
|
+
* Note: In-memory implementation uses setTimeout for TTL.
|
|
2663
|
+
* For production TTL support, use Redis or another store with native TTL.
|
|
2664
|
+
*/
|
|
2665
|
+
async setWithTTL(sessionId, data, ttlMs) {
|
|
2666
|
+
this.sessions.set(sessionId, data);
|
|
2667
|
+
setTimeout(() => {
|
|
2668
|
+
this.sessions.delete(sessionId);
|
|
2669
|
+
console.log(`[MCP] Session ${sessionId} expired after ${ttlMs}ms`);
|
|
2670
|
+
}, ttlMs);
|
|
2671
|
+
}
|
|
2672
|
+
/**
|
|
2673
|
+
* Get the number of active sessions
|
|
2674
|
+
* Useful for monitoring and debugging
|
|
2675
|
+
*/
|
|
2676
|
+
get size() {
|
|
2677
|
+
return this.sessions.size;
|
|
2678
|
+
}
|
|
2679
|
+
/**
|
|
2680
|
+
* Clear all sessions
|
|
2681
|
+
* Useful for testing and manual cleanup
|
|
2682
|
+
*/
|
|
2683
|
+
async clear() {
|
|
2684
|
+
this.sessions.clear();
|
|
2685
|
+
}
|
|
2686
|
+
};
|
|
2687
|
+
|
|
2688
|
+
// src/server/sessions/stores/redis.ts
|
|
2689
|
+
var RedisSessionStore = class {
|
|
2690
|
+
static {
|
|
2691
|
+
__name(this, "RedisSessionStore");
|
|
2692
|
+
}
|
|
2693
|
+
client;
|
|
2694
|
+
prefix;
|
|
2695
|
+
defaultTTL;
|
|
2696
|
+
constructor(config) {
|
|
2697
|
+
this.client = config.client;
|
|
2698
|
+
this.prefix = config.prefix ?? "mcp:session:";
|
|
2699
|
+
this.defaultTTL = config.defaultTTL ?? 3600;
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Get full Redis key for a session ID
|
|
2703
|
+
*/
|
|
2704
|
+
getKey(sessionId) {
|
|
2705
|
+
return `${this.prefix}${sessionId}`;
|
|
2706
|
+
}
|
|
2707
|
+
/**
|
|
2708
|
+
* Retrieve session metadata by ID
|
|
2709
|
+
*/
|
|
2710
|
+
async get(sessionId) {
|
|
2711
|
+
try {
|
|
2712
|
+
const key = this.getKey(sessionId);
|
|
2713
|
+
const data = await this.client.get(key);
|
|
2714
|
+
if (!data) {
|
|
2715
|
+
return null;
|
|
2716
|
+
}
|
|
2717
|
+
return JSON.parse(data);
|
|
2718
|
+
} catch (error2) {
|
|
2719
|
+
console.error(
|
|
2720
|
+
`[RedisSessionStore] Error getting session ${sessionId}:`,
|
|
2721
|
+
error2
|
|
2722
|
+
);
|
|
2723
|
+
return null;
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
/**
|
|
2727
|
+
* Store or update session metadata
|
|
2728
|
+
*/
|
|
2729
|
+
async set(sessionId, data) {
|
|
2730
|
+
try {
|
|
2731
|
+
const key = this.getKey(sessionId);
|
|
2732
|
+
const value = JSON.stringify(data);
|
|
2733
|
+
if (this.client.setEx) {
|
|
2734
|
+
await this.client.setEx(key, this.defaultTTL, value);
|
|
2735
|
+
} else if (this.client.setex) {
|
|
2736
|
+
await this.client.setex(key, this.defaultTTL, value);
|
|
2737
|
+
} else {
|
|
2738
|
+
await this.client.set(key, value, { EX: this.defaultTTL });
|
|
2739
|
+
}
|
|
2740
|
+
} catch (error2) {
|
|
2741
|
+
console.error(
|
|
2742
|
+
`[RedisSessionStore] Error setting session ${sessionId}:`,
|
|
2743
|
+
error2
|
|
2744
|
+
);
|
|
2745
|
+
throw error2;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
/**
|
|
2749
|
+
* Delete a session
|
|
2750
|
+
*/
|
|
2751
|
+
async delete(sessionId) {
|
|
2752
|
+
try {
|
|
2753
|
+
const key = this.getKey(sessionId);
|
|
2754
|
+
await this.client.del(key);
|
|
2755
|
+
} catch (error2) {
|
|
2756
|
+
console.error(
|
|
2757
|
+
`[RedisSessionStore] Error deleting session ${sessionId}:`,
|
|
2758
|
+
error2
|
|
2759
|
+
);
|
|
2760
|
+
throw error2;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Check if a session exists
|
|
2765
|
+
*/
|
|
2766
|
+
async has(sessionId) {
|
|
2767
|
+
try {
|
|
2768
|
+
const key = this.getKey(sessionId);
|
|
2769
|
+
const exists = await this.client.exists(key);
|
|
2770
|
+
return exists === 1;
|
|
2771
|
+
} catch (error2) {
|
|
2772
|
+
console.error(
|
|
2773
|
+
`[RedisSessionStore] Error checking session ${sessionId}:`,
|
|
2774
|
+
error2
|
|
2775
|
+
);
|
|
2776
|
+
return false;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* List all session IDs
|
|
2781
|
+
*
|
|
2782
|
+
* WARNING: Uses KEYS command which blocks Redis. For production systems with
|
|
2783
|
+
* many sessions, consider using SCAN instead or maintaining a separate SET of
|
|
2784
|
+
* active session IDs.
|
|
2785
|
+
*/
|
|
2786
|
+
async keys() {
|
|
2787
|
+
try {
|
|
2788
|
+
const pattern = `${this.prefix}*`;
|
|
2789
|
+
const keys = await this.client.keys(pattern);
|
|
2790
|
+
return keys.map((key) => key.substring(this.prefix.length));
|
|
2791
|
+
} catch (error2) {
|
|
2792
|
+
console.error("[RedisSessionStore] Error listing session keys:", error2);
|
|
2793
|
+
return [];
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
/**
|
|
2797
|
+
* Store session metadata with custom TTL (time-to-live)
|
|
2798
|
+
*/
|
|
2799
|
+
async setWithTTL(sessionId, data, ttlMs) {
|
|
2800
|
+
try {
|
|
2801
|
+
const key = this.getKey(sessionId);
|
|
2802
|
+
const value = JSON.stringify(data);
|
|
2803
|
+
const ttlSeconds = Math.ceil(ttlMs / 1e3);
|
|
2804
|
+
if (this.client.setEx) {
|
|
2805
|
+
await this.client.setEx(key, ttlSeconds, value);
|
|
2806
|
+
} else if (this.client.setex) {
|
|
2807
|
+
await this.client.setex(key, ttlSeconds, value);
|
|
2808
|
+
} else {
|
|
2809
|
+
await this.client.set(key, value, { EX: ttlSeconds });
|
|
2810
|
+
}
|
|
2811
|
+
} catch (error2) {
|
|
2812
|
+
console.error(
|
|
2813
|
+
`[RedisSessionStore] Error setting session ${sessionId} with TTL:`,
|
|
2814
|
+
error2
|
|
2815
|
+
);
|
|
2816
|
+
throw error2;
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
/**
|
|
2820
|
+
* Close Redis connection
|
|
2821
|
+
* Should be called when shutting down the server
|
|
2822
|
+
*/
|
|
2823
|
+
async close() {
|
|
2824
|
+
try {
|
|
2825
|
+
await this.client.quit();
|
|
2826
|
+
} catch (error2) {
|
|
2827
|
+
console.error(
|
|
2828
|
+
"[RedisSessionStore] Error closing Redis connection:",
|
|
2829
|
+
error2
|
|
2830
|
+
);
|
|
2831
|
+
throw error2;
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
/**
|
|
2835
|
+
* Clear all sessions (useful for testing)
|
|
2836
|
+
* WARNING: This will delete all sessions with the configured prefix
|
|
2837
|
+
*
|
|
2838
|
+
* NOTE: Uses KEYS command which blocks Redis. This is acceptable for testing
|
|
2839
|
+
* but should be avoided in production with large datasets.
|
|
2840
|
+
*/
|
|
2841
|
+
async clear() {
|
|
2842
|
+
try {
|
|
2843
|
+
const pattern = `${this.prefix}*`;
|
|
2844
|
+
const keys = await this.client.keys(pattern);
|
|
2845
|
+
if (keys.length > 0) {
|
|
2846
|
+
await this.client.del(keys);
|
|
2847
|
+
}
|
|
2848
|
+
} catch (error2) {
|
|
2849
|
+
console.error("[RedisSessionStore] Error clearing sessions:", error2);
|
|
2850
|
+
throw error2;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
};
|
|
2854
|
+
|
|
2855
|
+
// src/server/sessions/stores/filesystem.ts
|
|
2856
|
+
import { mkdir, writeFile, rename, unlink } from "fs/promises";
|
|
2857
|
+
import { join, dirname } from "path";
|
|
2858
|
+
import { existsSync, readFileSync } from "fs";
|
|
2859
|
+
var FileSystemSessionStore = class {
|
|
2860
|
+
static {
|
|
2861
|
+
__name(this, "FileSystemSessionStore");
|
|
2862
|
+
}
|
|
2863
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2864
|
+
filePath;
|
|
2865
|
+
debounceMs;
|
|
2866
|
+
maxAgeMs;
|
|
2867
|
+
saveTimer = null;
|
|
2868
|
+
saving = false;
|
|
2869
|
+
pendingSave = false;
|
|
2870
|
+
constructor(config = {}) {
|
|
2871
|
+
this.filePath = config.path ?? join(process.cwd(), ".mcp-use", "sessions.json");
|
|
2872
|
+
this.debounceMs = config.debounceMs ?? 100;
|
|
2873
|
+
this.maxAgeMs = config.maxAgeMs ?? 24 * 60 * 60 * 1e3;
|
|
2874
|
+
this.loadSessionsSync();
|
|
2875
|
+
}
|
|
2876
|
+
/**
|
|
2877
|
+
* Load sessions from file synchronously during construction
|
|
2878
|
+
* This ensures sessions are available immediately when the server starts
|
|
2879
|
+
*/
|
|
2880
|
+
loadSessionsSync() {
|
|
2881
|
+
try {
|
|
2882
|
+
if (!existsSync(this.filePath)) {
|
|
2883
|
+
console.log(
|
|
2884
|
+
`[FileSystemSessionStore] No session file found at ${this.filePath}, starting fresh`
|
|
2885
|
+
);
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
const data = readFileSync(this.filePath, "utf-8");
|
|
2889
|
+
const parsed = JSON.parse(data);
|
|
2890
|
+
const now = Date.now();
|
|
2891
|
+
let loadedCount = 0;
|
|
2892
|
+
let expiredCount = 0;
|
|
2893
|
+
for (const [sessionId, metadata] of Object.entries(parsed)) {
|
|
2894
|
+
const sessionMetadata = metadata;
|
|
2895
|
+
const age = now - sessionMetadata.lastAccessedAt;
|
|
2896
|
+
if (age > this.maxAgeMs) {
|
|
2897
|
+
expiredCount++;
|
|
2898
|
+
continue;
|
|
2899
|
+
}
|
|
2900
|
+
this.sessions.set(sessionId, sessionMetadata);
|
|
2901
|
+
loadedCount++;
|
|
2902
|
+
}
|
|
2903
|
+
console.log(
|
|
2904
|
+
`[FileSystemSessionStore] Loaded ${loadedCount} session(s) from ${this.filePath}` + (expiredCount > 0 ? ` (cleaned up ${expiredCount} expired)` : "")
|
|
2905
|
+
);
|
|
2906
|
+
} catch (error2) {
|
|
2907
|
+
if (error2.code === "ENOENT") {
|
|
2908
|
+
console.log(`[FileSystemSessionStore] No existing sessions file`);
|
|
2909
|
+
} else if (error2 instanceof SyntaxError) {
|
|
2910
|
+
console.warn(
|
|
2911
|
+
`[FileSystemSessionStore] Corrupted session file, starting fresh:`,
|
|
2912
|
+
error2.message
|
|
2913
|
+
);
|
|
2914
|
+
} else {
|
|
2915
|
+
console.warn(
|
|
2916
|
+
`[FileSystemSessionStore] Error loading sessions, starting fresh:`,
|
|
2917
|
+
error2.message
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Retrieve session metadata by ID
|
|
2924
|
+
*/
|
|
2925
|
+
async get(sessionId) {
|
|
2926
|
+
const data = this.sessions.get(sessionId);
|
|
2927
|
+
return data ?? null;
|
|
2928
|
+
}
|
|
2929
|
+
/**
|
|
2930
|
+
* Store or update session metadata
|
|
2931
|
+
* Uses debouncing to batch rapid consecutive writes
|
|
2932
|
+
*/
|
|
2933
|
+
async set(sessionId, data) {
|
|
2934
|
+
this.sessions.set(sessionId, data);
|
|
2935
|
+
await this.scheduleSave();
|
|
2936
|
+
}
|
|
2937
|
+
/**
|
|
2938
|
+
* Delete session metadata
|
|
2939
|
+
*/
|
|
2940
|
+
async delete(sessionId) {
|
|
2941
|
+
this.sessions.delete(sessionId);
|
|
2942
|
+
await this.scheduleSave();
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* Check if session exists
|
|
2946
|
+
*/
|
|
2947
|
+
async has(sessionId) {
|
|
2948
|
+
return this.sessions.has(sessionId);
|
|
2949
|
+
}
|
|
2950
|
+
/**
|
|
2951
|
+
* List all session IDs
|
|
2952
|
+
*/
|
|
2953
|
+
async keys() {
|
|
2954
|
+
return Array.from(this.sessions.keys());
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* Store session metadata with TTL
|
|
2958
|
+
* Note: TTL is enforced on load, not with timers (simple implementation)
|
|
2959
|
+
*/
|
|
2960
|
+
async setWithTTL(sessionId, data, ttlMs) {
|
|
2961
|
+
const metadataWithExpiry = {
|
|
2962
|
+
...data,
|
|
2963
|
+
lastAccessedAt: Date.now()
|
|
2964
|
+
};
|
|
2965
|
+
this.sessions.set(sessionId, metadataWithExpiry);
|
|
2966
|
+
await this.scheduleSave();
|
|
2967
|
+
setTimeout(() => {
|
|
2968
|
+
this.sessions.delete(sessionId);
|
|
2969
|
+
this.scheduleSave();
|
|
2970
|
+
}, ttlMs);
|
|
2971
|
+
}
|
|
2972
|
+
/**
|
|
2973
|
+
* Get the number of active sessions
|
|
2974
|
+
*/
|
|
2975
|
+
get size() {
|
|
2976
|
+
return this.sessions.size;
|
|
2977
|
+
}
|
|
2978
|
+
/**
|
|
2979
|
+
* Clear all sessions
|
|
2980
|
+
*/
|
|
2981
|
+
async clear() {
|
|
2982
|
+
this.sessions.clear();
|
|
2983
|
+
await this.scheduleSave();
|
|
2984
|
+
}
|
|
2985
|
+
/**
|
|
2986
|
+
* Schedule a save operation with debouncing
|
|
2987
|
+
* Prevents excessive disk I/O from rapid consecutive writes
|
|
2988
|
+
*/
|
|
2989
|
+
async scheduleSave() {
|
|
2990
|
+
if (this.saving) {
|
|
2991
|
+
this.pendingSave = true;
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
if (this.saveTimer) {
|
|
2995
|
+
clearTimeout(this.saveTimer);
|
|
2996
|
+
}
|
|
2997
|
+
this.saveTimer = setTimeout(() => {
|
|
2998
|
+
this.performSave();
|
|
2999
|
+
}, this.debounceMs);
|
|
3000
|
+
}
|
|
3001
|
+
/**
|
|
3002
|
+
* Perform the actual save operation with atomic writes
|
|
3003
|
+
* Uses write-to-temp-then-rename pattern to prevent corruption
|
|
3004
|
+
*/
|
|
3005
|
+
async performSave() {
|
|
3006
|
+
this.saveTimer = null;
|
|
3007
|
+
this.saving = true;
|
|
3008
|
+
this.pendingSave = false;
|
|
3009
|
+
try {
|
|
3010
|
+
const dir = dirname(this.filePath);
|
|
3011
|
+
await mkdir(dir, { recursive: true });
|
|
3012
|
+
const data = {};
|
|
3013
|
+
for (const [sessionId, metadata] of Array.from(this.sessions.entries())) {
|
|
3014
|
+
data[sessionId] = metadata;
|
|
3015
|
+
}
|
|
3016
|
+
const tempPath = `${this.filePath}.tmp`;
|
|
3017
|
+
await writeFile(tempPath, JSON.stringify(data, null, 2), "utf-8");
|
|
3018
|
+
await rename(tempPath, this.filePath);
|
|
3019
|
+
console.debug(
|
|
3020
|
+
`[FileSystemSessionStore] Saved ${this.sessions.size} session(s) to ${this.filePath}`
|
|
3021
|
+
);
|
|
3022
|
+
} catch (error2) {
|
|
3023
|
+
console.error(
|
|
3024
|
+
`[FileSystemSessionStore] Error saving sessions:`,
|
|
3025
|
+
error2.message
|
|
3026
|
+
);
|
|
3027
|
+
try {
|
|
3028
|
+
const tempPath = `${this.filePath}.tmp`;
|
|
3029
|
+
if (existsSync(tempPath)) {
|
|
3030
|
+
await unlink(tempPath);
|
|
3031
|
+
}
|
|
3032
|
+
} catch {
|
|
3033
|
+
}
|
|
3034
|
+
} finally {
|
|
3035
|
+
this.saving = false;
|
|
3036
|
+
if (this.pendingSave) {
|
|
3037
|
+
await this.scheduleSave();
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
/**
|
|
3042
|
+
* Force an immediate save (bypasses debouncing)
|
|
3043
|
+
* Useful for ensuring persistence before process exit
|
|
3044
|
+
*/
|
|
3045
|
+
async flush() {
|
|
3046
|
+
if (this.saveTimer) {
|
|
3047
|
+
clearTimeout(this.saveTimer);
|
|
3048
|
+
this.saveTimer = null;
|
|
3049
|
+
}
|
|
3050
|
+
await this.performSave();
|
|
3051
|
+
}
|
|
3052
|
+
};
|
|
3053
|
+
|
|
3054
|
+
// src/server/sessions/streams/memory.ts
|
|
3055
|
+
var InMemoryStreamManager = class {
|
|
3056
|
+
static {
|
|
3057
|
+
__name(this, "InMemoryStreamManager");
|
|
3058
|
+
}
|
|
3059
|
+
/**
|
|
3060
|
+
* Map of active SSE stream controllers
|
|
3061
|
+
* Key: sessionId, Value: ReadableStreamDefaultController
|
|
3062
|
+
*/
|
|
3063
|
+
streams = /* @__PURE__ */ new Map();
|
|
3064
|
+
/**
|
|
3065
|
+
* Text encoder for converting strings to Uint8Array
|
|
3066
|
+
*/
|
|
3067
|
+
textEncoder = new TextEncoder();
|
|
3068
|
+
/**
|
|
3069
|
+
* Register an active SSE stream controller
|
|
3070
|
+
*/
|
|
3071
|
+
async create(sessionId, controller) {
|
|
3072
|
+
this.streams.set(sessionId, controller);
|
|
3073
|
+
}
|
|
3074
|
+
/**
|
|
3075
|
+
* Send data to active SSE streams
|
|
3076
|
+
*
|
|
3077
|
+
* Directly enqueues data to in-memory controllers.
|
|
3078
|
+
* For distributed deployments, use RedisStreamManager instead.
|
|
3079
|
+
*/
|
|
3080
|
+
async send(sessionIds, data) {
|
|
3081
|
+
const encoded = this.textEncoder.encode(data);
|
|
3082
|
+
if (!sessionIds) {
|
|
3083
|
+
for (const [_id, controller] of this.streams.entries()) {
|
|
3084
|
+
try {
|
|
3085
|
+
controller.enqueue(encoded);
|
|
3086
|
+
} catch (error2) {
|
|
3087
|
+
console.warn(
|
|
3088
|
+
`[InMemoryStreamManager] Failed to send to session ${_id}:`,
|
|
3089
|
+
error2
|
|
3090
|
+
);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
} else {
|
|
3094
|
+
for (const sessionId of sessionIds) {
|
|
3095
|
+
const controller = this.streams.get(sessionId);
|
|
3096
|
+
if (controller) {
|
|
3097
|
+
try {
|
|
3098
|
+
controller.enqueue(encoded);
|
|
3099
|
+
} catch (error2) {
|
|
3100
|
+
console.warn(
|
|
3101
|
+
`[InMemoryStreamManager] Failed to send to session ${sessionId}:`,
|
|
3102
|
+
error2
|
|
3103
|
+
);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
/**
|
|
3110
|
+
* Remove an active SSE stream
|
|
3111
|
+
*/
|
|
3112
|
+
async delete(sessionId) {
|
|
3113
|
+
const controller = this.streams.get(sessionId);
|
|
3114
|
+
if (controller) {
|
|
3115
|
+
try {
|
|
3116
|
+
controller.close();
|
|
3117
|
+
} catch (error2) {
|
|
3118
|
+
console.debug(
|
|
3119
|
+
`[InMemoryStreamManager] Controller already closed for session ${sessionId}`
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
this.streams.delete(sessionId);
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
/**
|
|
3126
|
+
* Check if an active stream exists
|
|
3127
|
+
*/
|
|
3128
|
+
async has(sessionId) {
|
|
3129
|
+
return this.streams.has(sessionId);
|
|
3130
|
+
}
|
|
3131
|
+
/**
|
|
3132
|
+
* Close all active streams
|
|
3133
|
+
*/
|
|
3134
|
+
async close() {
|
|
3135
|
+
for (const [sessionId, controller] of this.streams.entries()) {
|
|
3136
|
+
try {
|
|
3137
|
+
controller.close();
|
|
3138
|
+
} catch (error2) {
|
|
3139
|
+
console.debug(
|
|
3140
|
+
`[InMemoryStreamManager] Error closing stream for ${sessionId}:`,
|
|
3141
|
+
error2
|
|
3142
|
+
);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
this.streams.clear();
|
|
3146
|
+
}
|
|
3147
|
+
/**
|
|
3148
|
+
* Get the number of active streams
|
|
3149
|
+
* Useful for monitoring
|
|
3150
|
+
*/
|
|
3151
|
+
get size() {
|
|
3152
|
+
return this.streams.size;
|
|
3153
|
+
}
|
|
3154
|
+
};
|
|
3155
|
+
|
|
3156
|
+
// src/server/sessions/streams/redis.ts
|
|
3157
|
+
var RedisStreamManager = class {
|
|
3158
|
+
static {
|
|
3159
|
+
__name(this, "RedisStreamManager");
|
|
3160
|
+
}
|
|
3161
|
+
pubSubClient;
|
|
3162
|
+
client;
|
|
3163
|
+
prefix;
|
|
3164
|
+
heartbeatInterval;
|
|
3165
|
+
textEncoder = new TextEncoder();
|
|
3166
|
+
/**
|
|
3167
|
+
* Map of local controllers (only on this server instance)
|
|
3168
|
+
* Key: sessionId, Value: controller
|
|
3169
|
+
*/
|
|
3170
|
+
localControllers = /* @__PURE__ */ new Map();
|
|
3171
|
+
/**
|
|
3172
|
+
* Map of heartbeat intervals for keeping sessions alive
|
|
3173
|
+
* Key: sessionId, Value: interval timer
|
|
3174
|
+
*/
|
|
3175
|
+
heartbeats = /* @__PURE__ */ new Map();
|
|
3176
|
+
constructor(config) {
|
|
3177
|
+
this.pubSubClient = config.pubSubClient;
|
|
3178
|
+
this.client = config.client;
|
|
3179
|
+
this.prefix = config.prefix ?? "mcp:stream:";
|
|
3180
|
+
this.heartbeatInterval = config.heartbeatInterval ?? 10;
|
|
3181
|
+
}
|
|
3182
|
+
/**
|
|
3183
|
+
* Get the Redis channel name for a session
|
|
3184
|
+
*/
|
|
3185
|
+
getChannel(sessionId) {
|
|
3186
|
+
return `${this.prefix}${sessionId}`;
|
|
3187
|
+
}
|
|
3188
|
+
/**
|
|
3189
|
+
* Get the Redis key for tracking active sessions
|
|
3190
|
+
*/
|
|
3191
|
+
getAvailableKey(sessionId) {
|
|
3192
|
+
return `available:${this.prefix}${sessionId}`;
|
|
3193
|
+
}
|
|
3194
|
+
/**
|
|
3195
|
+
* Get the Redis key for the active sessions SET
|
|
3196
|
+
*/
|
|
3197
|
+
getActiveSessionsKey() {
|
|
3198
|
+
return `${this.prefix}active`;
|
|
3199
|
+
}
|
|
3200
|
+
/**
|
|
3201
|
+
* Register an active SSE stream and subscribe to Redis channel
|
|
3202
|
+
*/
|
|
3203
|
+
async create(sessionId, controller) {
|
|
3204
|
+
try {
|
|
3205
|
+
this.localControllers.set(sessionId, controller);
|
|
3206
|
+
const availableKey = this.getAvailableKey(sessionId);
|
|
3207
|
+
await this.client.set(availableKey, "active");
|
|
3208
|
+
if (this.client.expire) {
|
|
3209
|
+
await this.client.expire(availableKey, this.heartbeatInterval * 2);
|
|
3210
|
+
}
|
|
3211
|
+
const activeSessionsKey = this.getActiveSessionsKey();
|
|
3212
|
+
if (this.client.sAdd) {
|
|
3213
|
+
await this.client.sAdd(activeSessionsKey, sessionId);
|
|
3214
|
+
if (this.client.expire) {
|
|
3215
|
+
await this.client.expire(
|
|
3216
|
+
activeSessionsKey,
|
|
3217
|
+
this.heartbeatInterval * 2
|
|
3218
|
+
);
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
const heartbeat = setInterval(async () => {
|
|
3222
|
+
try {
|
|
3223
|
+
if (this.client.expire) {
|
|
3224
|
+
await this.client.expire(availableKey, this.heartbeatInterval * 2);
|
|
3225
|
+
const activeSessionsKey2 = this.getActiveSessionsKey();
|
|
3226
|
+
await this.client.expire(
|
|
3227
|
+
activeSessionsKey2,
|
|
3228
|
+
this.heartbeatInterval * 2
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
} catch (error2) {
|
|
3232
|
+
console.warn(
|
|
3233
|
+
`[RedisStreamManager] Heartbeat failed for session ${sessionId}:`,
|
|
3234
|
+
error2
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
}, this.heartbeatInterval * 1e3);
|
|
3238
|
+
this.heartbeats.set(sessionId, heartbeat);
|
|
3239
|
+
const channel = this.getChannel(sessionId);
|
|
3240
|
+
if (!this.pubSubClient.subscribe) {
|
|
3241
|
+
throw new Error(
|
|
3242
|
+
"[RedisStreamManager] Redis client does not support subscribe method"
|
|
3243
|
+
);
|
|
3244
|
+
}
|
|
3245
|
+
await this.pubSubClient.subscribe(channel, (message) => {
|
|
3246
|
+
const localController = this.localControllers.get(sessionId);
|
|
3247
|
+
if (localController) {
|
|
3248
|
+
try {
|
|
3249
|
+
localController.enqueue(this.textEncoder.encode(message));
|
|
3250
|
+
} catch (error2) {
|
|
3251
|
+
console.warn(
|
|
3252
|
+
`[RedisStreamManager] Failed to enqueue message for ${sessionId}:`,
|
|
3253
|
+
error2
|
|
3254
|
+
);
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
});
|
|
3258
|
+
const deleteChannel = `delete:${this.getChannel(sessionId)}`;
|
|
3259
|
+
await this.pubSubClient.subscribe(deleteChannel, async () => {
|
|
3260
|
+
await this.delete(sessionId);
|
|
3261
|
+
});
|
|
3262
|
+
console.log(
|
|
3263
|
+
`[RedisStreamManager] Created stream for session ${sessionId}`
|
|
3264
|
+
);
|
|
3265
|
+
} catch (error2) {
|
|
3266
|
+
console.error(
|
|
3267
|
+
`[RedisStreamManager] Error creating stream for ${sessionId}:`,
|
|
3268
|
+
error2
|
|
3269
|
+
);
|
|
3270
|
+
throw error2;
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
/**
|
|
3274
|
+
* Send data to sessions via Redis Pub/Sub
|
|
3275
|
+
*
|
|
3276
|
+
* This works across distributed servers - any server with an active
|
|
3277
|
+
* SSE connection for the target session will receive and forward the message.
|
|
3278
|
+
*
|
|
3279
|
+
* Note: Uses the regular client (not pubSubClient) for publishing.
|
|
3280
|
+
* In node-redis v5+, clients in subscriber mode cannot publish.
|
|
3281
|
+
*/
|
|
3282
|
+
async send(sessionIds, data) {
|
|
3283
|
+
try {
|
|
3284
|
+
if (!sessionIds) {
|
|
3285
|
+
const activeSessionsKey = this.getActiveSessionsKey();
|
|
3286
|
+
if (this.client.sMembers) {
|
|
3287
|
+
const sessionIds2 = await this.client.sMembers(activeSessionsKey);
|
|
3288
|
+
for (const sessionId of sessionIds2) {
|
|
3289
|
+
const channel = this.getChannel(sessionId);
|
|
3290
|
+
if (!this.client.publish) {
|
|
3291
|
+
throw new Error(
|
|
3292
|
+
"[RedisStreamManager] Redis client does not support publish method"
|
|
3293
|
+
);
|
|
3294
|
+
}
|
|
3295
|
+
await this.client.publish(channel, data);
|
|
3296
|
+
}
|
|
3297
|
+
} else {
|
|
3298
|
+
const pattern = `available:${this.prefix}*`;
|
|
3299
|
+
const keys = await this.client.keys(pattern);
|
|
3300
|
+
for (const key of keys) {
|
|
3301
|
+
const sessionId = key.replace(`available:${this.prefix}`, "");
|
|
3302
|
+
const channel = this.getChannel(sessionId);
|
|
3303
|
+
if (!this.client.publish) {
|
|
3304
|
+
throw new Error(
|
|
3305
|
+
"[RedisStreamManager] Redis client does not support publish method"
|
|
3306
|
+
);
|
|
3307
|
+
}
|
|
3308
|
+
await this.client.publish(channel, data);
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
} else {
|
|
3312
|
+
for (const sessionId of sessionIds) {
|
|
3313
|
+
const channel = this.getChannel(sessionId);
|
|
3314
|
+
if (!this.client.publish) {
|
|
3315
|
+
throw new Error(
|
|
3316
|
+
"[RedisStreamManager] Redis client does not support publish method"
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
3319
|
+
await this.client.publish(channel, data);
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
} catch (error2) {
|
|
3323
|
+
console.error(`[RedisStreamManager] Error sending to sessions:`, error2);
|
|
3324
|
+
throw error2;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
/**
|
|
3328
|
+
* Remove an active SSE stream
|
|
3329
|
+
*/
|
|
3330
|
+
async delete(sessionId) {
|
|
3331
|
+
try {
|
|
3332
|
+
const heartbeat = this.heartbeats.get(sessionId);
|
|
3333
|
+
if (heartbeat) {
|
|
3334
|
+
clearInterval(heartbeat);
|
|
3335
|
+
this.heartbeats.delete(sessionId);
|
|
3336
|
+
}
|
|
3337
|
+
const channel = this.getChannel(sessionId);
|
|
3338
|
+
const deleteChannel = `delete:${channel}`;
|
|
3339
|
+
if (!this.pubSubClient.unsubscribe) {
|
|
3340
|
+
throw new Error(
|
|
3341
|
+
"[RedisStreamManager] Redis client does not support unsubscribe method"
|
|
3342
|
+
);
|
|
3343
|
+
}
|
|
3344
|
+
await this.pubSubClient.unsubscribe(channel);
|
|
3345
|
+
await this.pubSubClient.unsubscribe(deleteChannel);
|
|
3346
|
+
if (!this.client.publish) {
|
|
3347
|
+
throw new Error(
|
|
3348
|
+
"[RedisStreamManager] Redis client does not support publish method"
|
|
3349
|
+
);
|
|
3350
|
+
}
|
|
3351
|
+
await this.client.publish(deleteChannel, "");
|
|
3352
|
+
await this.client.del(this.getAvailableKey(sessionId));
|
|
3353
|
+
const activeSessionsKey = this.getActiveSessionsKey();
|
|
3354
|
+
if (this.client.sRem) {
|
|
3355
|
+
await this.client.sRem(activeSessionsKey, sessionId);
|
|
3356
|
+
}
|
|
3357
|
+
const controller = this.localControllers.get(sessionId);
|
|
3358
|
+
if (controller) {
|
|
3359
|
+
try {
|
|
3360
|
+
controller.close();
|
|
3361
|
+
} catch (error2) {
|
|
3362
|
+
console.debug(
|
|
3363
|
+
`[RedisStreamManager] Controller already closed for ${sessionId}`
|
|
3364
|
+
);
|
|
3365
|
+
}
|
|
3366
|
+
this.localControllers.delete(sessionId);
|
|
3367
|
+
}
|
|
3368
|
+
console.log(
|
|
3369
|
+
`[RedisStreamManager] Deleted stream for session ${sessionId}`
|
|
3370
|
+
);
|
|
3371
|
+
} catch (error2) {
|
|
3372
|
+
console.error(
|
|
3373
|
+
`[RedisStreamManager] Error deleting stream for ${sessionId}:`,
|
|
3374
|
+
error2
|
|
3375
|
+
);
|
|
3376
|
+
throw error2;
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Check if a session has an active stream (on ANY server)
|
|
3381
|
+
*/
|
|
3382
|
+
async has(sessionId) {
|
|
3383
|
+
try {
|
|
3384
|
+
const availableKey = this.getAvailableKey(sessionId);
|
|
3385
|
+
const exists = await this.client.exists(availableKey);
|
|
3386
|
+
return exists === 1;
|
|
3387
|
+
} catch (error2) {
|
|
3388
|
+
console.error(
|
|
3389
|
+
`[RedisStreamManager] Error checking session ${sessionId}:`,
|
|
3390
|
+
error2
|
|
3391
|
+
);
|
|
3392
|
+
return false;
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
/**
|
|
3396
|
+
* Close all connections and cleanup
|
|
3397
|
+
*/
|
|
3398
|
+
async close() {
|
|
3399
|
+
try {
|
|
3400
|
+
for (const heartbeat of this.heartbeats.values()) {
|
|
3401
|
+
clearInterval(heartbeat);
|
|
3402
|
+
}
|
|
3403
|
+
this.heartbeats.clear();
|
|
3404
|
+
const activeSessionsKey = this.getActiveSessionsKey();
|
|
3405
|
+
const sessionIdsToCleanup = Array.from(this.localControllers.keys());
|
|
3406
|
+
for (const sessionId of sessionIdsToCleanup) {
|
|
3407
|
+
await this.client.del(this.getAvailableKey(sessionId));
|
|
3408
|
+
if (this.client.sRem) {
|
|
3409
|
+
await this.client.sRem(activeSessionsKey, sessionId);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
for (const controller of this.localControllers.values()) {
|
|
3413
|
+
try {
|
|
3414
|
+
controller.close();
|
|
3415
|
+
} catch (error2) {
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
this.localControllers.clear();
|
|
3419
|
+
console.log(`[RedisStreamManager] Closed all streams`);
|
|
3420
|
+
} catch (error2) {
|
|
3421
|
+
console.error(`[RedisStreamManager] Error during close:`, error2);
|
|
3422
|
+
throw error2;
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
/**
|
|
3426
|
+
* Get count of active local streams on this server instance
|
|
3427
|
+
*/
|
|
3428
|
+
get localSize() {
|
|
3429
|
+
return this.localControllers.size;
|
|
3430
|
+
}
|
|
3431
|
+
};
|
|
3432
|
+
|
|
2523
3433
|
// src/server/endpoints/mount-mcp.ts
|
|
3434
|
+
import { join as join2 } from "path";
|
|
2524
3435
|
async function mountMcp(app, mcpServerInstance, sessions, config, isProductionMode2) {
|
|
2525
3436
|
const { FetchStreamableHTTPServerTransport } = await import("@mcp-use/modelcontextprotocol-sdk/experimental/fetch-streamable-http/index.js");
|
|
2526
3437
|
const idleTimeoutMs = config.sessionIdleTimeoutMs ?? 3e5;
|
|
3438
|
+
const sessionStore = config.sessionStore ?? (isProductionMode2 ? new InMemorySessionStore() : new FileSystemSessionStore({
|
|
3439
|
+
path: join2(process.cwd(), ".mcp-use", "sessions.json")
|
|
3440
|
+
}));
|
|
3441
|
+
const streamManager = config.streamManager ?? new InMemoryStreamManager();
|
|
2527
3442
|
const transports = /* @__PURE__ */ new Map();
|
|
3443
|
+
if (config.autoCreateSessionOnInvalidId !== void 0) {
|
|
3444
|
+
console.warn(
|
|
3445
|
+
"[MCP] WARNING: 'autoCreateSessionOnInvalidId' is deprecated and will be removed in a future version.\nThe MCP specification requires clients to send a new InitializeRequest when receiving a 404 for stale sessions.\nModern MCP clients handle this correctly. For session persistence across restarts, use the 'sessionStore' option.\nSee: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management"
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
2528
3448
|
let idleCleanupInterval;
|
|
2529
3449
|
if (!config.stateless && idleTimeoutMs > 0) {
|
|
2530
3450
|
idleCleanupInterval = startIdleCleanup(
|
|
2531
3451
|
sessions,
|
|
2532
3452
|
idleTimeoutMs,
|
|
3453
|
+
transports,
|
|
2533
3454
|
mcpServerInstance
|
|
2534
3455
|
);
|
|
2535
3456
|
}
|
|
2536
3457
|
const handleRequest = /* @__PURE__ */ __name(async (c) => {
|
|
2537
|
-
|
|
3458
|
+
const acceptHeader = c.req.header("Accept") || c.req.header("accept") || "";
|
|
3459
|
+
const clientSupportsSSE = acceptHeader.includes("text/event-stream");
|
|
3460
|
+
const useStatelessMode = config.stateless || !clientSupportsSSE;
|
|
3461
|
+
if (useStatelessMode) {
|
|
2538
3462
|
const server = mcpServerInstance.getServerForSession();
|
|
2539
3463
|
const transport = new FetchStreamableHTTPServerTransport({
|
|
2540
|
-
sessionIdGenerator: void 0
|
|
3464
|
+
sessionIdGenerator: void 0,
|
|
2541
3465
|
// No session tracking
|
|
3466
|
+
// Enable plain JSON responses ONLY if client doesn't support SSE
|
|
3467
|
+
// This allows k6/curl to work while maintaining SSE format for compatible clients
|
|
3468
|
+
enableJsonResponse: !clientSupportsSSE
|
|
2542
3469
|
});
|
|
2543
3470
|
try {
|
|
2544
3471
|
await server.connect(transport);
|
|
2545
|
-
|
|
3472
|
+
const request = c.req.raw;
|
|
3473
|
+
if (!clientSupportsSSE) {
|
|
3474
|
+
const modifiedRequest = new Request(request.url, {
|
|
3475
|
+
method: request.method,
|
|
3476
|
+
headers: {
|
|
3477
|
+
...Object.fromEntries(request.headers.entries()),
|
|
3478
|
+
Accept: "application/json, text/event-stream"
|
|
3479
|
+
},
|
|
3480
|
+
body: request.body,
|
|
3481
|
+
...request.body && { duplex: "half" }
|
|
3482
|
+
});
|
|
3483
|
+
return await transport.handleRequest(modifiedRequest);
|
|
3484
|
+
}
|
|
3485
|
+
return await transport.handleRequest(request);
|
|
2546
3486
|
} catch (error2) {
|
|
2547
3487
|
console.error("[MCP] Stateless request error:", error2);
|
|
2548
3488
|
transport.close();
|
|
@@ -2551,41 +3491,166 @@ async function mountMcp(app, mcpServerInstance, sessions, config, isProductionMo
|
|
|
2551
3491
|
}
|
|
2552
3492
|
} else {
|
|
2553
3493
|
const sessionId = c.req.header("mcp-session-id");
|
|
3494
|
+
if (c.req.method === "HEAD") {
|
|
3495
|
+
if (sessionId && await sessionStore.has(sessionId)) {
|
|
3496
|
+
const session = await sessionStore.get(sessionId);
|
|
3497
|
+
if (session) {
|
|
3498
|
+
session.lastAccessedAt = Date.now();
|
|
3499
|
+
await sessionStore.set(sessionId, session);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
return new Response(null, { status: 200 });
|
|
3503
|
+
}
|
|
3504
|
+
if (sessionId && await sessionStore.has(sessionId) && !transports.has(sessionId)) {
|
|
3505
|
+
console.log(
|
|
3506
|
+
`[MCP] Session metadata found but transport lost (likely hot reload): ${sessionId} - recreating transport`
|
|
3507
|
+
);
|
|
3508
|
+
const server2 = mcpServerInstance.getServerForSession();
|
|
3509
|
+
const transport2 = new FetchStreamableHTTPServerTransport({
|
|
3510
|
+
sessionIdGenerator: /* @__PURE__ */ __name(() => sessionId, "sessionIdGenerator"),
|
|
3511
|
+
// Reuse existing session ID
|
|
3512
|
+
onsessioninitialized: /* @__PURE__ */ __name(async (sid) => {
|
|
3513
|
+
console.log(`[MCP] Session reconnected: ${sid}`);
|
|
3514
|
+
transports.set(sid, transport2);
|
|
3515
|
+
const metadata = await sessionStore.get(sid);
|
|
3516
|
+
const sessionData = {
|
|
3517
|
+
transport: transport2,
|
|
3518
|
+
server: server2,
|
|
3519
|
+
lastAccessedAt: Date.now(),
|
|
3520
|
+
context: c,
|
|
3521
|
+
honoContext: c,
|
|
3522
|
+
...metadata || {}
|
|
3523
|
+
};
|
|
3524
|
+
sessions.set(sid, sessionData);
|
|
3525
|
+
server2.server.oninitialized = async () => {
|
|
3526
|
+
const clientCapabilities = server2.server.getClientCapabilities();
|
|
3527
|
+
const clientInfo = server2.server.getClientInfo?.() || {};
|
|
3528
|
+
const protocolVersion = server2.server.getProtocolVersion?.() || "unknown";
|
|
3529
|
+
const metadata2 = await sessionStore.get(sid);
|
|
3530
|
+
if (metadata2) {
|
|
3531
|
+
metadata2.clientCapabilities = clientCapabilities;
|
|
3532
|
+
metadata2.clientInfo = clientInfo;
|
|
3533
|
+
metadata2.protocolVersion = String(protocolVersion);
|
|
3534
|
+
await sessionStore.set(sid, metadata2);
|
|
3535
|
+
}
|
|
3536
|
+
const sessionData2 = sessions.get(sid);
|
|
3537
|
+
if (sessionData2) {
|
|
3538
|
+
sessionData2.clientCapabilities = clientCapabilities;
|
|
3539
|
+
}
|
|
3540
|
+
Telemetry.getInstance().trackServerInitialize({
|
|
3541
|
+
protocolVersion: String(protocolVersion),
|
|
3542
|
+
clientInfo: clientInfo || {},
|
|
3543
|
+
clientCapabilities: clientCapabilities || {},
|
|
3544
|
+
sessionId: sid
|
|
3545
|
+
}).catch(
|
|
3546
|
+
(e) => console.debug(`Failed to track server initialize: ${e}`)
|
|
3547
|
+
);
|
|
3548
|
+
if (!isProductionMode2) {
|
|
3549
|
+
console.log(
|
|
3550
|
+
`[MCP] Development mode: Sending list_changed notifications to reconnected session ${sid}`
|
|
3551
|
+
);
|
|
3552
|
+
try {
|
|
3553
|
+
const { sendNotificationToSession: sendNotificationToSession3 } = await import("../../notifications-FLGIFS56.js");
|
|
3554
|
+
await sendNotificationToSession3(
|
|
3555
|
+
sessions,
|
|
3556
|
+
sid,
|
|
3557
|
+
"notifications/tools/list_changed"
|
|
3558
|
+
);
|
|
3559
|
+
await sendNotificationToSession3(
|
|
3560
|
+
sessions,
|
|
3561
|
+
sid,
|
|
3562
|
+
"notifications/resources/list_changed"
|
|
3563
|
+
);
|
|
3564
|
+
await sendNotificationToSession3(
|
|
3565
|
+
sessions,
|
|
3566
|
+
sid,
|
|
3567
|
+
"notifications/prompts/list_changed"
|
|
3568
|
+
);
|
|
3569
|
+
} catch (err) {
|
|
3570
|
+
console.debug(
|
|
3571
|
+
`[MCP] Failed to send list_changed notification:`,
|
|
3572
|
+
err
|
|
3573
|
+
);
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
};
|
|
3577
|
+
}, "onsessioninitialized"),
|
|
3578
|
+
onsessionclosed: /* @__PURE__ */ __name(async (sid) => {
|
|
3579
|
+
console.log(`[MCP] Session closed: ${sid}`);
|
|
3580
|
+
transports.delete(sid);
|
|
3581
|
+
await streamManager.delete(sid);
|
|
3582
|
+
await sessionStore.delete(sid);
|
|
3583
|
+
sessions.delete(sid);
|
|
3584
|
+
mcpServerInstance.cleanupSessionSubscriptions?.(sid);
|
|
3585
|
+
}, "onsessionclosed")
|
|
3586
|
+
});
|
|
3587
|
+
await server2.connect(transport2);
|
|
3588
|
+
return transport2.handleRequest(c.req.raw);
|
|
3589
|
+
}
|
|
3590
|
+
if (sessionId && !await sessionStore.has(sessionId)) {
|
|
3591
|
+
console.log(
|
|
3592
|
+
`[MCP] Session not found: ${sessionId} - returning 404 (client should re-initialize)`
|
|
3593
|
+
);
|
|
3594
|
+
return c.json(
|
|
3595
|
+
{
|
|
3596
|
+
jsonrpc: "2.0",
|
|
3597
|
+
error: { code: -32001, message: "Session not found" },
|
|
3598
|
+
id: null
|
|
3599
|
+
},
|
|
3600
|
+
404
|
|
3601
|
+
);
|
|
3602
|
+
}
|
|
2554
3603
|
if (sessionId && transports.has(sessionId)) {
|
|
2555
3604
|
const transport2 = transports.get(sessionId);
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
3605
|
+
const metadata = await sessionStore.get(sessionId);
|
|
3606
|
+
if (metadata) {
|
|
3607
|
+
metadata.lastAccessedAt = Date.now();
|
|
3608
|
+
await sessionStore.set(sessionId, metadata);
|
|
3609
|
+
}
|
|
3610
|
+
const sessionData = sessions.get(sessionId);
|
|
3611
|
+
if (sessionData) {
|
|
3612
|
+
sessionData.lastAccessedAt = Date.now();
|
|
3613
|
+
sessionData.context = c;
|
|
3614
|
+
sessionData.honoContext = c;
|
|
2561
3615
|
}
|
|
2562
3616
|
return transport2.handleRequest(c.req.raw);
|
|
2563
3617
|
}
|
|
2564
3618
|
const server = mcpServerInstance.getServerForSession();
|
|
2565
3619
|
const transport = new FetchStreamableHTTPServerTransport({
|
|
2566
3620
|
sessionIdGenerator: /* @__PURE__ */ __name(() => generateUUID(), "sessionIdGenerator"),
|
|
2567
|
-
onsessioninitialized: /* @__PURE__ */ __name((sid) => {
|
|
3621
|
+
onsessioninitialized: /* @__PURE__ */ __name(async (sid) => {
|
|
2568
3622
|
console.log(`[MCP] Session initialized: ${sid}`);
|
|
2569
3623
|
transports.set(sid, transport);
|
|
2570
|
-
|
|
3624
|
+
const sessionData = {
|
|
2571
3625
|
transport,
|
|
2572
3626
|
server,
|
|
2573
3627
|
lastAccessedAt: Date.now(),
|
|
2574
3628
|
context: c,
|
|
2575
3629
|
honoContext: c
|
|
3630
|
+
};
|
|
3631
|
+
sessions.set(sid, sessionData);
|
|
3632
|
+
await sessionStore.set(sid, {
|
|
3633
|
+
lastAccessedAt: Date.now()
|
|
2576
3634
|
});
|
|
2577
|
-
server.server.oninitialized = () => {
|
|
3635
|
+
server.server.oninitialized = async () => {
|
|
2578
3636
|
const clientCapabilities = server.server.getClientCapabilities();
|
|
2579
3637
|
const clientInfo = server.server.getClientInfo?.() || {};
|
|
2580
3638
|
const protocolVersion = server.server.getProtocolVersion?.() || "unknown";
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
3639
|
+
const metadata = await sessionStore.get(sid);
|
|
3640
|
+
if (metadata) {
|
|
3641
|
+
metadata.clientCapabilities = clientCapabilities;
|
|
3642
|
+
metadata.clientInfo = clientInfo;
|
|
3643
|
+
metadata.protocolVersion = String(protocolVersion);
|
|
3644
|
+
await sessionStore.set(sid, metadata);
|
|
2584
3645
|
console.log(
|
|
2585
3646
|
`[MCP] Captured client capabilities for session ${sid}:`,
|
|
2586
|
-
Object.keys(clientCapabilities)
|
|
3647
|
+
clientCapabilities ? Object.keys(clientCapabilities) : "none"
|
|
2587
3648
|
);
|
|
2588
3649
|
}
|
|
3650
|
+
const sessionData2 = sessions.get(sid);
|
|
3651
|
+
if (sessionData2) {
|
|
3652
|
+
sessionData2.clientCapabilities = clientCapabilities;
|
|
3653
|
+
}
|
|
2589
3654
|
Telemetry.getInstance().trackServerInitialize({
|
|
2590
3655
|
protocolVersion: String(protocolVersion),
|
|
2591
3656
|
clientInfo: clientInfo || {},
|
|
@@ -2596,9 +3661,11 @@ async function mountMcp(app, mcpServerInstance, sessions, config, isProductionMo
|
|
|
2596
3661
|
);
|
|
2597
3662
|
};
|
|
2598
3663
|
}, "onsessioninitialized"),
|
|
2599
|
-
onsessionclosed: /* @__PURE__ */ __name((sid) => {
|
|
3664
|
+
onsessionclosed: /* @__PURE__ */ __name(async (sid) => {
|
|
2600
3665
|
console.log(`[MCP] Session closed: ${sid}`);
|
|
2601
3666
|
transports.delete(sid);
|
|
3667
|
+
await streamManager.delete(sid);
|
|
3668
|
+
await sessionStore.delete(sid);
|
|
2602
3669
|
sessions.delete(sid);
|
|
2603
3670
|
mcpServerInstance.cleanupSessionSubscriptions?.(sid);
|
|
2604
3671
|
}, "onsessionclosed")
|
|
@@ -2608,7 +3675,7 @@ async function mountMcp(app, mcpServerInstance, sessions, config, isProductionMo
|
|
|
2608
3675
|
}
|
|
2609
3676
|
}, "handleRequest");
|
|
2610
3677
|
for (const endpoint of ["/mcp", "/sse"]) {
|
|
2611
|
-
app.on(["GET", "POST", "DELETE"], endpoint, handleRequest);
|
|
3678
|
+
app.on(["GET", "POST", "DELETE", "HEAD"], endpoint, handleRequest);
|
|
2612
3679
|
}
|
|
2613
3680
|
console.log(
|
|
2614
3681
|
`[MCP] Server mounted at /mcp and /sse (${config.stateless ? "stateless" : "stateful"} mode)`
|
|
@@ -2920,6 +3987,7 @@ var MCPServerClass = class {
|
|
|
2920
3987
|
serverPort;
|
|
2921
3988
|
serverHost;
|
|
2922
3989
|
serverBaseUrl;
|
|
3990
|
+
favicon;
|
|
2923
3991
|
registeredTools = [];
|
|
2924
3992
|
registeredPrompts = [];
|
|
2925
3993
|
registeredResources = [];
|
|
@@ -2985,6 +4053,7 @@ var MCPServerClass = class {
|
|
|
2985
4053
|
}
|
|
2986
4054
|
this.serverHost = config.host || "localhost";
|
|
2987
4055
|
this.serverBaseUrl = config.baseUrl;
|
|
4056
|
+
this.favicon = config.favicon;
|
|
2988
4057
|
this.nativeServer = new OfficialMcpServer(
|
|
2989
4058
|
{
|
|
2990
4059
|
name: config.name,
|
|
@@ -3049,7 +4118,10 @@ var MCPServerClass = class {
|
|
|
3049
4118
|
"openai/widgetAccessible": widgetConfig.widgetAccessible ?? true,
|
|
3050
4119
|
"openai/resultCanProduceWidget": widgetConfig.resultCanProduceWidget ?? true
|
|
3051
4120
|
};
|
|
3052
|
-
result._meta =
|
|
4121
|
+
result._meta = {
|
|
4122
|
+
...result._meta || {},
|
|
4123
|
+
...responseMeta
|
|
4124
|
+
};
|
|
3053
4125
|
if (result.content?.[0]?.type === "text" && !result.content[0].text) {
|
|
3054
4126
|
result.content[0].text = `Displaying ${widgetName}`;
|
|
3055
4127
|
}
|
|
@@ -3452,6 +4524,9 @@ var MCPServerClass = class {
|
|
|
3452
4524
|
getActiveSessions = getActiveSessions;
|
|
3453
4525
|
sendNotification = sendNotification;
|
|
3454
4526
|
sendNotificationToSession = sendNotificationToSession2;
|
|
4527
|
+
sendToolsListChanged = sendToolsListChanged;
|
|
4528
|
+
sendResourcesListChanged = sendResourcesListChanged;
|
|
4529
|
+
sendPromptsListChanged = sendPromptsListChanged;
|
|
3455
4530
|
/**
|
|
3456
4531
|
* Notify subscribed clients that a resource has been updated
|
|
3457
4532
|
*
|
|
@@ -3704,7 +4779,8 @@ function createMCPServer(name, config = {}) {
|
|
|
3704
4779
|
allowedOrigins: config.allowedOrigins,
|
|
3705
4780
|
sessionIdleTimeoutMs: config.sessionIdleTimeoutMs,
|
|
3706
4781
|
autoCreateSessionOnInvalidId: config.autoCreateSessionOnInvalidId,
|
|
3707
|
-
oauth: config.oauth
|
|
4782
|
+
oauth: config.oauth,
|
|
4783
|
+
favicon: config.favicon
|
|
3708
4784
|
});
|
|
3709
4785
|
return instance;
|
|
3710
4786
|
}
|
|
@@ -4306,7 +5382,12 @@ function requireAnyScope(needed) {
|
|
|
4306
5382
|
}
|
|
4307
5383
|
__name(requireAnyScope, "requireAnyScope");
|
|
4308
5384
|
export {
|
|
5385
|
+
FileSystemSessionStore,
|
|
5386
|
+
InMemorySessionStore,
|
|
5387
|
+
InMemoryStreamManager,
|
|
4309
5388
|
MCPServer,
|
|
5389
|
+
RedisSessionStore,
|
|
5390
|
+
RedisStreamManager,
|
|
4310
5391
|
VERSION,
|
|
4311
5392
|
adaptConnectMiddleware,
|
|
4312
5393
|
adaptMiddleware,
|