@leanmcp/core 0.3.18 → 0.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/dist/index.js CHANGED
@@ -482,6 +482,164 @@ var init_validation = __esm({
482
482
  }
483
483
  });
484
484
 
485
+ // src/dynamodb-session-store.ts
486
+ var dynamodb_session_store_exports = {};
487
+ __export(dynamodb_session_store_exports, {
488
+ DEFAULT_TABLE_NAME: () => DEFAULT_TABLE_NAME,
489
+ DEFAULT_TTL_SECONDS: () => DEFAULT_TTL_SECONDS,
490
+ DynamoDBSessionStore: () => DynamoDBSessionStore
491
+ });
492
+ var import_client_dynamodb, import_lib_dynamodb, DEFAULT_TABLE_NAME, DEFAULT_TTL_SECONDS, DynamoDBSessionStore;
493
+ var init_dynamodb_session_store = __esm({
494
+ "src/dynamodb-session-store.ts"() {
495
+ "use strict";
496
+ import_client_dynamodb = require("@aws-sdk/client-dynamodb");
497
+ import_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
498
+ init_logger();
499
+ DEFAULT_TABLE_NAME = "leanmcp-sessions";
500
+ DEFAULT_TTL_SECONDS = 86400;
501
+ DynamoDBSessionStore = class {
502
+ static {
503
+ __name(this, "DynamoDBSessionStore");
504
+ }
505
+ client;
506
+ tableName;
507
+ ttlSeconds;
508
+ logger;
509
+ constructor(options) {
510
+ this.tableName = options?.tableName || process.env.DYNAMODB_TABLE_NAME || DEFAULT_TABLE_NAME;
511
+ this.ttlSeconds = options?.ttlSeconds || DEFAULT_TTL_SECONDS;
512
+ this.logger = new Logger({
513
+ level: options?.logging ? LogLevel.INFO : LogLevel.NONE,
514
+ prefix: "DynamoDBSessionStore"
515
+ });
516
+ const dynamoClient = new import_client_dynamodb.DynamoDBClient({
517
+ region: options?.region || process.env.AWS_REGION || "us-east-1"
518
+ });
519
+ this.client = import_lib_dynamodb.DynamoDBDocumentClient.from(dynamoClient);
520
+ this.logger.info(`Initialized with table: ${this.tableName}, TTL: ${this.ttlSeconds}s`);
521
+ }
522
+ /**
523
+ * Check if a session exists in DynamoDB
524
+ */
525
+ async sessionExists(sessionId) {
526
+ try {
527
+ const result = await this.client.send(new import_lib_dynamodb.GetCommand({
528
+ TableName: this.tableName,
529
+ Key: {
530
+ sessionId
531
+ },
532
+ ProjectionExpression: "sessionId"
533
+ }));
534
+ const exists = !!result.Item;
535
+ this.logger.debug(`Session ${sessionId} exists: ${exists}`);
536
+ return exists;
537
+ } catch (error) {
538
+ this.logger.error(`Error checking session existence: ${error.message}`);
539
+ return false;
540
+ }
541
+ }
542
+ /**
543
+ * Create a new session in DynamoDB
544
+ */
545
+ async createSession(sessionId, data) {
546
+ try {
547
+ const now = /* @__PURE__ */ new Date();
548
+ const ttl = Math.floor(Date.now() / 1e3) + this.ttlSeconds;
549
+ await this.client.send(new import_lib_dynamodb.PutCommand({
550
+ TableName: this.tableName,
551
+ Item: {
552
+ sessionId,
553
+ createdAt: now.toISOString(),
554
+ updatedAt: now.toISOString(),
555
+ ttl,
556
+ data: data || {}
557
+ }
558
+ }));
559
+ this.logger.info(`Created session: ${sessionId} (TTL: ${new Date(ttl * 1e3).toISOString()})`);
560
+ } catch (error) {
561
+ this.logger.error(`Error creating session ${sessionId}: ${error.message}`);
562
+ throw new Error(`Failed to create session: ${error.message}`);
563
+ }
564
+ }
565
+ /**
566
+ * Get session data from DynamoDB
567
+ */
568
+ async getSession(sessionId) {
569
+ try {
570
+ const result = await this.client.send(new import_lib_dynamodb.GetCommand({
571
+ TableName: this.tableName,
572
+ Key: {
573
+ sessionId
574
+ }
575
+ }));
576
+ if (!result.Item) {
577
+ this.logger.debug(`Session ${sessionId} not found`);
578
+ return null;
579
+ }
580
+ const sessionData = {
581
+ sessionId: result.Item.sessionId,
582
+ createdAt: new Date(result.Item.createdAt),
583
+ updatedAt: new Date(result.Item.updatedAt),
584
+ ttl: result.Item.ttl,
585
+ data: result.Item.data
586
+ };
587
+ this.logger.debug(`Retrieved session: ${sessionId}`);
588
+ return sessionData;
589
+ } catch (error) {
590
+ this.logger.error(`Error getting session ${sessionId}: ${error.message}`);
591
+ return null;
592
+ }
593
+ }
594
+ /**
595
+ * Update session data in DynamoDB
596
+ * Automatically refreshes TTL on each update
597
+ */
598
+ async updateSession(sessionId, updates) {
599
+ try {
600
+ const ttl = Math.floor(Date.now() / 1e3) + this.ttlSeconds;
601
+ await this.client.send(new import_lib_dynamodb.UpdateCommand({
602
+ TableName: this.tableName,
603
+ Key: {
604
+ sessionId
605
+ },
606
+ UpdateExpression: "SET updatedAt = :now, #data = :data, #ttl = :ttl",
607
+ ExpressionAttributeNames: {
608
+ "#data": "data",
609
+ "#ttl": "ttl"
610
+ },
611
+ ExpressionAttributeValues: {
612
+ ":now": (/* @__PURE__ */ new Date()).toISOString(),
613
+ ":data": updates.data || {},
614
+ ":ttl": ttl
615
+ }
616
+ }));
617
+ this.logger.debug(`Updated session: ${sessionId}`);
618
+ } catch (error) {
619
+ this.logger.error(`Error updating session ${sessionId}: ${error.message}`);
620
+ throw new Error(`Failed to update session: ${error.message}`);
621
+ }
622
+ }
623
+ /**
624
+ * Delete a session from DynamoDB
625
+ */
626
+ async deleteSession(sessionId) {
627
+ try {
628
+ await this.client.send(new import_lib_dynamodb.DeleteCommand({
629
+ TableName: this.tableName,
630
+ Key: {
631
+ sessionId
632
+ }
633
+ }));
634
+ this.logger.info(`Deleted session: ${sessionId}`);
635
+ } catch (error) {
636
+ this.logger.error(`Error deleting session ${sessionId}: ${error.message}`);
637
+ }
638
+ }
639
+ };
640
+ }
641
+ });
642
+
485
643
  // src/http-server.ts
486
644
  function isInitializeRequest(body) {
487
645
  return body && body.method === "initialize";
@@ -558,7 +716,7 @@ async function createHTTPServer(serverInput, options) {
558
716
  auth: serverOptions.auth
559
717
  };
560
718
  }
561
- const [express, { StreamableHTTPServerTransport }, cors] = await Promise.all([
719
+ const [express, { StreamableHTTPServerTransport: StreamableHTTPServerTransport2 }, cors] = await Promise.all([
562
720
  // @ts-ignore
563
721
  import("express").catch(() => {
564
722
  throw new Error("Express not found. Install with: npm install express @types/express");
@@ -673,6 +831,19 @@ async function createHTTPServer(serverInput, options) {
673
831
  app.use(express.json());
674
832
  const isStateless = httpOptions.stateless !== false;
675
833
  console.log(`Starting LeanMCP HTTP Server (${isStateless ? "STATELESS" : "STATEFUL"})...`);
834
+ if (!isStateless && !httpOptions.sessionStore) {
835
+ if (process.env.LEANMCP_LAMBDA === "true") {
836
+ try {
837
+ const { DynamoDBSessionStore: DynamoDBSessionStore2 } = await Promise.resolve().then(() => (init_dynamodb_session_store(), dynamodb_session_store_exports));
838
+ httpOptions.sessionStore = new DynamoDBSessionStore2({
839
+ logging: httpOptions.logging
840
+ });
841
+ logger.info("Auto-configured DynamoDB session store for LeanMCP Lambda");
842
+ } catch (e) {
843
+ logger.warn(`Running on LeanMCP Lambda but failed to initialize DynamoDB session store: ${e.message}`);
844
+ }
845
+ }
846
+ }
676
847
  const DASHBOARD_URL = process.env.DASHBOARD_URL || "https://s3-dashboard-build.s3.us-west-2.amazonaws.com/out/index.html";
677
848
  let cachedDashboard = null;
678
849
  let cacheTimestamp = 0;
@@ -814,19 +985,72 @@ async function createHTTPServer(serverInput, options) {
814
985
  if (sessionId && transports[sessionId]) {
815
986
  transport = transports[sessionId];
816
987
  logger.debug(`Reusing session: ${sessionId}`);
988
+ } else if (sessionId && !isInitializeRequest(req.body)) {
989
+ logger.info(`Transport missing for session ${sessionId}, checking session store...`);
990
+ if (httpOptions.sessionStore) {
991
+ const exists = await httpOptions.sessionStore.sessionExists(sessionId);
992
+ if (!exists) {
993
+ res.status(404).json({
994
+ jsonrpc: "2.0",
995
+ error: {
996
+ code: -32001,
997
+ message: "Session not found"
998
+ },
999
+ id: req.body?.id || null
1000
+ });
1001
+ return;
1002
+ }
1003
+ transport = new StreamableHTTPServerTransport2({
1004
+ sessionIdGenerator: /* @__PURE__ */ __name(() => sessionId, "sessionIdGenerator"),
1005
+ onsessioninitialized: /* @__PURE__ */ __name((sid) => {
1006
+ transports[sid] = transport;
1007
+ logger.info(`Transport recreated for session: ${sid}`);
1008
+ }, "onsessioninitialized")
1009
+ });
1010
+ transport.onclose = async () => {
1011
+ if (transport.sessionId) {
1012
+ delete transports[transport.sessionId];
1013
+ logger.debug(`Session cleaned up: ${transport.sessionId}`);
1014
+ if (httpOptions.sessionStore) {
1015
+ await httpOptions.sessionStore.deleteSession(transport.sessionId);
1016
+ }
1017
+ }
1018
+ };
1019
+ const freshServer = await serverFactory();
1020
+ if (freshServer && typeof freshServer.waitForInit === "function") {
1021
+ await freshServer.waitForInit();
1022
+ }
1023
+ await freshServer.connect(transport);
1024
+ } else {
1025
+ res.status(400).json({
1026
+ jsonrpc: "2.0",
1027
+ error: {
1028
+ code: -32e3,
1029
+ message: "Session expired (no session store configured)"
1030
+ },
1031
+ id: req.body?.id || null
1032
+ });
1033
+ return;
1034
+ }
817
1035
  } else if (!sessionId && isInitializeRequest(req.body)) {
818
1036
  logger.info("Creating new MCP session...");
819
- transport = new StreamableHTTPServerTransport({
1037
+ transport = new StreamableHTTPServerTransport2({
820
1038
  sessionIdGenerator: /* @__PURE__ */ __name(() => (0, import_node_crypto.randomUUID)(), "sessionIdGenerator"),
821
- onsessioninitialized: /* @__PURE__ */ __name((newSessionId) => {
1039
+ onsessioninitialized: /* @__PURE__ */ __name(async (newSessionId) => {
822
1040
  transports[newSessionId] = transport;
823
1041
  logger.info(`Session initialized: ${newSessionId}`);
1042
+ if (httpOptions.sessionStore) {
1043
+ await httpOptions.sessionStore.createSession(newSessionId);
1044
+ }
824
1045
  }, "onsessioninitialized")
825
1046
  });
826
- transport.onclose = () => {
1047
+ transport.onclose = async () => {
827
1048
  if (transport.sessionId) {
828
1049
  delete transports[transport.sessionId];
829
1050
  logger.debug(`Session cleaned up: ${transport.sessionId}`);
1051
+ if (httpOptions.sessionStore) {
1052
+ await httpOptions.sessionStore.deleteSession(transport.sessionId);
1053
+ }
830
1054
  }
831
1055
  };
832
1056
  if (!mcpServer) {
@@ -882,7 +1106,7 @@ async function createHTTPServer(serverInput, options) {
882
1106
  if (freshServer && typeof freshServer.waitForInit === "function") {
883
1107
  await freshServer.waitForInit();
884
1108
  }
885
- const transport = new StreamableHTTPServerTransport({
1109
+ const transport = new StreamableHTTPServerTransport2({
886
1110
  sessionIdGenerator: void 0
887
1111
  });
888
1112
  await freshServer.connect(transport);
@@ -1084,11 +1308,152 @@ var init_auth_helpers = __esm({
1084
1308
  }
1085
1309
  });
1086
1310
 
1311
+ // src/session-store.ts
1312
+ var init_session_store = __esm({
1313
+ "src/session-store.ts"() {
1314
+ "use strict";
1315
+ }
1316
+ });
1317
+
1318
+ // src/session-provider.ts
1319
+ var import_streamableHttp, LeanMCPSessionProvider;
1320
+ var init_session_provider = __esm({
1321
+ "src/session-provider.ts"() {
1322
+ "use strict";
1323
+ import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
1324
+ init_dynamodb_session_store();
1325
+ LeanMCPSessionProvider = class {
1326
+ static {
1327
+ __name(this, "LeanMCPSessionProvider");
1328
+ }
1329
+ transports = /* @__PURE__ */ new Map();
1330
+ sessionStore;
1331
+ constructor(options) {
1332
+ if (options?.sessionStore) {
1333
+ this.sessionStore = options.sessionStore;
1334
+ } else {
1335
+ this.sessionStore = new DynamoDBSessionStore({
1336
+ tableName: options?.tableName,
1337
+ region: options?.region,
1338
+ ttlSeconds: options?.ttlSeconds,
1339
+ logging: options?.logging
1340
+ });
1341
+ }
1342
+ }
1343
+ /**
1344
+ * Get transport from memory
1345
+ */
1346
+ get(sessionId) {
1347
+ return this.transports.get(sessionId);
1348
+ }
1349
+ /**
1350
+ * Check if session exists (memory or DynamoDB)
1351
+ */
1352
+ async has(sessionId) {
1353
+ if (this.transports.has(sessionId)) return true;
1354
+ return this.sessionStore.sessionExists(sessionId);
1355
+ }
1356
+ /**
1357
+ * Store transport and create session in DynamoDB
1358
+ */
1359
+ async set(sessionId, transport) {
1360
+ this.transports.set(sessionId, transport);
1361
+ await this.sessionStore.createSession(sessionId);
1362
+ }
1363
+ /**
1364
+ * Delete transport and session
1365
+ */
1366
+ async delete(sessionId) {
1367
+ this.transports.delete(sessionId);
1368
+ await this.sessionStore.deleteSession(sessionId);
1369
+ }
1370
+ /**
1371
+ * Get or recreate transport for a session
1372
+ * This is the key method for Lambda support - handles container recycling
1373
+ *
1374
+ * @param sessionId - Session ID to get or recreate
1375
+ * @param serverFactory - Factory function to create fresh MCP server instances
1376
+ * @param transportOptions - Optional callbacks for transport lifecycle events
1377
+ * @returns Transport instance or null if session doesn't exist
1378
+ */
1379
+ async getOrRecreate(sessionId, serverFactory, transportOptions) {
1380
+ const existing = this.transports.get(sessionId);
1381
+ if (existing) return existing;
1382
+ const exists = await this.sessionStore.sessionExists(sessionId);
1383
+ if (!exists) return null;
1384
+ const transport = new import_streamableHttp.StreamableHTTPServerTransport({
1385
+ sessionIdGenerator: /* @__PURE__ */ __name(() => sessionId, "sessionIdGenerator"),
1386
+ onsessioninitialized: /* @__PURE__ */ __name((sid) => {
1387
+ this.transports.set(sid, transport);
1388
+ transportOptions?.onsessioninitialized?.(sid);
1389
+ }, "onsessioninitialized")
1390
+ });
1391
+ transport.onclose = () => {
1392
+ this.transports.delete(sessionId);
1393
+ transportOptions?.onclose?.();
1394
+ };
1395
+ const server = await serverFactory();
1396
+ await server.connect(transport);
1397
+ return transport;
1398
+ }
1399
+ /**
1400
+ * Get session data from DynamoDB
1401
+ */
1402
+ async getSessionData(sessionId) {
1403
+ const session = await this.sessionStore.getSession(sessionId);
1404
+ return session?.data || null;
1405
+ }
1406
+ /**
1407
+ * Update session data in DynamoDB
1408
+ */
1409
+ async updateSessionData(sessionId, data) {
1410
+ await this.sessionStore.updateSession(sessionId, {
1411
+ data
1412
+ });
1413
+ }
1414
+ /**
1415
+ * Get number of in-memory transports
1416
+ */
1417
+ get size() {
1418
+ return this.transports.size;
1419
+ }
1420
+ /**
1421
+ * Get all session IDs in memory
1422
+ */
1423
+ keys() {
1424
+ return this.transports.keys();
1425
+ }
1426
+ /**
1427
+ * Get all transports in memory
1428
+ */
1429
+ values() {
1430
+ return this.transports.values();
1431
+ }
1432
+ /**
1433
+ * Iterate over all sessions in memory
1434
+ */
1435
+ entries() {
1436
+ return this.transports.entries();
1437
+ }
1438
+ /**
1439
+ * Clear all in-memory transports (does not affect DynamoDB)
1440
+ */
1441
+ clear() {
1442
+ this.transports.clear();
1443
+ }
1444
+ };
1445
+ }
1446
+ });
1447
+
1087
1448
  // src/index.ts
1088
1449
  var index_exports = {};
1089
1450
  __export(index_exports, {
1090
1451
  Auth: () => Auth,
1452
+ DEFAULT_TABLE_NAME: () => DEFAULT_TABLE_NAME,
1453
+ DEFAULT_TTL_SECONDS: () => DEFAULT_TTL_SECONDS,
1091
1454
  Deprecated: () => Deprecated,
1455
+ DynamoDBSessionStore: () => DynamoDBSessionStore,
1456
+ LeanMCPSessionProvider: () => LeanMCPSessionProvider,
1092
1457
  LogLevel: () => LogLevel,
1093
1458
  Logger: () => Logger,
1094
1459
  MCPServer: () => MCPServer,
@@ -1141,6 +1506,9 @@ var init_index = __esm({
1141
1506
  init_logger();
1142
1507
  init_validation();
1143
1508
  init_auth_helpers();
1509
+ init_session_store();
1510
+ init_session_provider();
1511
+ init_dynamodb_session_store();
1144
1512
  init_decorators();
1145
1513
  init_schema_generator();
1146
1514
  init_logger();
@@ -2143,7 +2511,11 @@ init_index();
2143
2511
  // Annotate the CommonJS export names for ESM import in node:
2144
2512
  0 && (module.exports = {
2145
2513
  Auth,
2514
+ DEFAULT_TABLE_NAME,
2515
+ DEFAULT_TTL_SECONDS,
2146
2516
  Deprecated,
2517
+ DynamoDBSessionStore,
2518
+ LeanMCPSessionProvider,
2147
2519
  LogLevel,
2148
2520
  Logger,
2149
2521
  MCPServer,