@leanmcp/core 0.3.19 → 0.4.1

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,129 @@ 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(`Initialize request with session ${sessionId} - checking for session restoration...`);
990
+ if (httpOptions.sessionStore) {
991
+ const exists = await httpOptions.sessionStore.sessionExists(sessionId);
992
+ if (exists) {
993
+ logger.info(`Restoring session: ${sessionId}`);
994
+ transport = new StreamableHTTPServerTransport2({
995
+ sessionIdGenerator: /* @__PURE__ */ __name(() => sessionId, "sessionIdGenerator"),
996
+ onsessioninitialized: /* @__PURE__ */ __name((sid) => {
997
+ transports[sid] = transport;
998
+ logger.info(`Session restored: ${sid}`);
999
+ }, "onsessioninitialized")
1000
+ });
1001
+ transport.onclose = async () => {
1002
+ if (transport.sessionId) {
1003
+ delete transports[transport.sessionId];
1004
+ logger.debug(`Session cleaned up: ${transport.sessionId}`);
1005
+ if (httpOptions.sessionStore) {
1006
+ await httpOptions.sessionStore.deleteSession(transport.sessionId);
1007
+ }
1008
+ }
1009
+ };
1010
+ const freshServer = await serverFactory();
1011
+ if (freshServer && typeof freshServer.waitForInit === "function") {
1012
+ await freshServer.waitForInit();
1013
+ }
1014
+ await freshServer.connect(transport);
1015
+ } else {
1016
+ logger.info(`Session ${sessionId} not found in store, creating new session`);
1017
+ }
1018
+ }
1019
+ if (!transport) {
1020
+ transport = new StreamableHTTPServerTransport2({
1021
+ sessionIdGenerator: /* @__PURE__ */ __name(() => (0, import_node_crypto.randomUUID)(), "sessionIdGenerator"),
1022
+ onsessioninitialized: /* @__PURE__ */ __name(async (newSessionId) => {
1023
+ transports[newSessionId] = transport;
1024
+ logger.info(`Session initialized: ${newSessionId}`);
1025
+ if (httpOptions.sessionStore) {
1026
+ await httpOptions.sessionStore.createSession(newSessionId);
1027
+ }
1028
+ }, "onsessioninitialized")
1029
+ });
1030
+ transport.onclose = async () => {
1031
+ if (transport.sessionId) {
1032
+ delete transports[transport.sessionId];
1033
+ logger.debug(`Session cleaned up: ${transport.sessionId}`);
1034
+ if (httpOptions.sessionStore) {
1035
+ await httpOptions.sessionStore.deleteSession(transport.sessionId);
1036
+ }
1037
+ }
1038
+ };
1039
+ if (!mcpServer) {
1040
+ throw new Error("MCP server not initialized");
1041
+ }
1042
+ await mcpServer.connect(transport);
1043
+ }
1044
+ } else if (sessionId && !isInitializeRequest(req.body)) {
1045
+ logger.info(`Transport missing for session ${sessionId}, checking session store...`);
1046
+ if (httpOptions.sessionStore) {
1047
+ const exists = await httpOptions.sessionStore.sessionExists(sessionId);
1048
+ if (!exists) {
1049
+ res.status(404).json({
1050
+ jsonrpc: "2.0",
1051
+ error: {
1052
+ code: -32001,
1053
+ message: "Session not found"
1054
+ },
1055
+ id: req.body?.id || null
1056
+ });
1057
+ return;
1058
+ }
1059
+ logger.info(`Auto-restoring session: ${sessionId}`);
1060
+ transport = new StreamableHTTPServerTransport2({
1061
+ sessionIdGenerator: /* @__PURE__ */ __name(() => sessionId, "sessionIdGenerator"),
1062
+ onsessioninitialized: /* @__PURE__ */ __name((sid) => {
1063
+ transports[sid] = transport;
1064
+ logger.info(`Session auto-restored: ${sid}`);
1065
+ }, "onsessioninitialized")
1066
+ });
1067
+ transport.onclose = async () => {
1068
+ if (transport.sessionId) {
1069
+ delete transports[transport.sessionId];
1070
+ logger.debug(`Session cleaned up: ${transport.sessionId}`);
1071
+ if (httpOptions.sessionStore) {
1072
+ await httpOptions.sessionStore.deleteSession(transport.sessionId);
1073
+ }
1074
+ }
1075
+ };
1076
+ const freshServer = await serverFactory();
1077
+ if (freshServer && typeof freshServer.waitForInit === "function") {
1078
+ await freshServer.waitForInit();
1079
+ }
1080
+ await freshServer.connect(transport);
1081
+ } else {
1082
+ res.status(400).json({
1083
+ jsonrpc: "2.0",
1084
+ error: {
1085
+ code: -32e3,
1086
+ message: "Session expired (no session store configured)"
1087
+ },
1088
+ id: req.body?.id || null
1089
+ });
1090
+ return;
1091
+ }
817
1092
  } else if (!sessionId && isInitializeRequest(req.body)) {
818
1093
  logger.info("Creating new MCP session...");
819
- transport = new StreamableHTTPServerTransport({
1094
+ transport = new StreamableHTTPServerTransport2({
820
1095
  sessionIdGenerator: /* @__PURE__ */ __name(() => (0, import_node_crypto.randomUUID)(), "sessionIdGenerator"),
821
- onsessioninitialized: /* @__PURE__ */ __name((newSessionId) => {
1096
+ onsessioninitialized: /* @__PURE__ */ __name(async (newSessionId) => {
822
1097
  transports[newSessionId] = transport;
823
1098
  logger.info(`Session initialized: ${newSessionId}`);
1099
+ if (httpOptions.sessionStore) {
1100
+ await httpOptions.sessionStore.createSession(newSessionId);
1101
+ }
824
1102
  }, "onsessioninitialized")
825
1103
  });
826
- transport.onclose = () => {
1104
+ transport.onclose = async () => {
827
1105
  if (transport.sessionId) {
828
1106
  delete transports[transport.sessionId];
829
1107
  logger.debug(`Session cleaned up: ${transport.sessionId}`);
1108
+ if (httpOptions.sessionStore) {
1109
+ await httpOptions.sessionStore.deleteSession(transport.sessionId);
1110
+ }
830
1111
  }
831
1112
  };
832
1113
  if (!mcpServer) {
@@ -882,7 +1163,7 @@ async function createHTTPServer(serverInput, options) {
882
1163
  if (freshServer && typeof freshServer.waitForInit === "function") {
883
1164
  await freshServer.waitForInit();
884
1165
  }
885
- const transport = new StreamableHTTPServerTransport({
1166
+ const transport = new StreamableHTTPServerTransport2({
886
1167
  sessionIdGenerator: void 0
887
1168
  });
888
1169
  await freshServer.connect(transport);
@@ -1084,11 +1365,152 @@ var init_auth_helpers = __esm({
1084
1365
  }
1085
1366
  });
1086
1367
 
1368
+ // src/session-store.ts
1369
+ var init_session_store = __esm({
1370
+ "src/session-store.ts"() {
1371
+ "use strict";
1372
+ }
1373
+ });
1374
+
1375
+ // src/session-provider.ts
1376
+ var import_streamableHttp, LeanMCPSessionProvider;
1377
+ var init_session_provider = __esm({
1378
+ "src/session-provider.ts"() {
1379
+ "use strict";
1380
+ import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
1381
+ init_dynamodb_session_store();
1382
+ LeanMCPSessionProvider = class {
1383
+ static {
1384
+ __name(this, "LeanMCPSessionProvider");
1385
+ }
1386
+ transports = /* @__PURE__ */ new Map();
1387
+ sessionStore;
1388
+ constructor(options) {
1389
+ if (options?.sessionStore) {
1390
+ this.sessionStore = options.sessionStore;
1391
+ } else {
1392
+ this.sessionStore = new DynamoDBSessionStore({
1393
+ tableName: options?.tableName,
1394
+ region: options?.region,
1395
+ ttlSeconds: options?.ttlSeconds,
1396
+ logging: options?.logging
1397
+ });
1398
+ }
1399
+ }
1400
+ /**
1401
+ * Get transport from memory
1402
+ */
1403
+ get(sessionId) {
1404
+ return this.transports.get(sessionId);
1405
+ }
1406
+ /**
1407
+ * Check if session exists (memory or DynamoDB)
1408
+ */
1409
+ async has(sessionId) {
1410
+ if (this.transports.has(sessionId)) return true;
1411
+ return this.sessionStore.sessionExists(sessionId);
1412
+ }
1413
+ /**
1414
+ * Store transport and create session in DynamoDB
1415
+ */
1416
+ async set(sessionId, transport) {
1417
+ this.transports.set(sessionId, transport);
1418
+ await this.sessionStore.createSession(sessionId);
1419
+ }
1420
+ /**
1421
+ * Delete transport and session
1422
+ */
1423
+ async delete(sessionId) {
1424
+ this.transports.delete(sessionId);
1425
+ await this.sessionStore.deleteSession(sessionId);
1426
+ }
1427
+ /**
1428
+ * Get or recreate transport for a session
1429
+ * This is the key method for Lambda support - handles container recycling
1430
+ *
1431
+ * @param sessionId - Session ID to get or recreate
1432
+ * @param serverFactory - Factory function to create fresh MCP server instances
1433
+ * @param transportOptions - Optional callbacks for transport lifecycle events
1434
+ * @returns Transport instance or null if session doesn't exist
1435
+ */
1436
+ async getOrRecreate(sessionId, serverFactory, transportOptions) {
1437
+ const existing = this.transports.get(sessionId);
1438
+ if (existing) return existing;
1439
+ const exists = await this.sessionStore.sessionExists(sessionId);
1440
+ if (!exists) return null;
1441
+ const transport = new import_streamableHttp.StreamableHTTPServerTransport({
1442
+ sessionIdGenerator: /* @__PURE__ */ __name(() => sessionId, "sessionIdGenerator"),
1443
+ onsessioninitialized: /* @__PURE__ */ __name((sid) => {
1444
+ this.transports.set(sid, transport);
1445
+ transportOptions?.onsessioninitialized?.(sid);
1446
+ }, "onsessioninitialized")
1447
+ });
1448
+ transport.onclose = () => {
1449
+ this.transports.delete(sessionId);
1450
+ transportOptions?.onclose?.();
1451
+ };
1452
+ const server = await serverFactory();
1453
+ await server.connect(transport);
1454
+ return transport;
1455
+ }
1456
+ /**
1457
+ * Get session data from DynamoDB
1458
+ */
1459
+ async getSessionData(sessionId) {
1460
+ const session = await this.sessionStore.getSession(sessionId);
1461
+ return session?.data || null;
1462
+ }
1463
+ /**
1464
+ * Update session data in DynamoDB
1465
+ */
1466
+ async updateSessionData(sessionId, data) {
1467
+ await this.sessionStore.updateSession(sessionId, {
1468
+ data
1469
+ });
1470
+ }
1471
+ /**
1472
+ * Get number of in-memory transports
1473
+ */
1474
+ get size() {
1475
+ return this.transports.size;
1476
+ }
1477
+ /**
1478
+ * Get all session IDs in memory
1479
+ */
1480
+ keys() {
1481
+ return this.transports.keys();
1482
+ }
1483
+ /**
1484
+ * Get all transports in memory
1485
+ */
1486
+ values() {
1487
+ return this.transports.values();
1488
+ }
1489
+ /**
1490
+ * Iterate over all sessions in memory
1491
+ */
1492
+ entries() {
1493
+ return this.transports.entries();
1494
+ }
1495
+ /**
1496
+ * Clear all in-memory transports (does not affect DynamoDB)
1497
+ */
1498
+ clear() {
1499
+ this.transports.clear();
1500
+ }
1501
+ };
1502
+ }
1503
+ });
1504
+
1087
1505
  // src/index.ts
1088
1506
  var index_exports = {};
1089
1507
  __export(index_exports, {
1090
1508
  Auth: () => Auth,
1509
+ DEFAULT_TABLE_NAME: () => DEFAULT_TABLE_NAME,
1510
+ DEFAULT_TTL_SECONDS: () => DEFAULT_TTL_SECONDS,
1091
1511
  Deprecated: () => Deprecated,
1512
+ DynamoDBSessionStore: () => DynamoDBSessionStore,
1513
+ LeanMCPSessionProvider: () => LeanMCPSessionProvider,
1092
1514
  LogLevel: () => LogLevel,
1093
1515
  Logger: () => Logger,
1094
1516
  MCPServer: () => MCPServer,
@@ -1141,6 +1563,9 @@ var init_index = __esm({
1141
1563
  init_logger();
1142
1564
  init_validation();
1143
1565
  init_auth_helpers();
1566
+ init_session_store();
1567
+ init_session_provider();
1568
+ init_dynamodb_session_store();
1144
1569
  init_decorators();
1145
1570
  init_schema_generator();
1146
1571
  init_logger();
@@ -1454,14 +1879,14 @@ var init_index = __esm({
1454
1879
  /**
1455
1880
  * Auto-register all services from the mcp directory
1456
1881
  * Scans the directory recursively and registers all exported classes
1457
- *
1882
+ *
1458
1883
  * @param mcpDir - Path to the mcp directory containing service files
1459
1884
  * @param serviceFactories - Optional map of service class names to factory functions for dependency injection
1460
- *
1885
+ *
1461
1886
  * @example
1462
1887
  * // Auto-register services with no dependencies
1463
1888
  * await server.autoRegisterServices('./mcp');
1464
- *
1889
+ *
1465
1890
  * @example
1466
1891
  * // Auto-register with dependency injection
1467
1892
  * await server.autoRegisterServices('./mcp', {
@@ -1614,7 +2039,7 @@ var init_index = __esm({
1614
2039
  }
1615
2040
  /**
1616
2041
  * Watch UI manifest for changes and reload resources dynamically
1617
- *
2042
+ *
1618
2043
  * CRITICAL: Only for stateful mode. In stateless mode, each request
1619
2044
  * creates a fresh server that reads the manifest directly, making
1620
2045
  * watchers both unnecessary and a memory leak source.
@@ -2143,7 +2568,11 @@ init_index();
2143
2568
  // Annotate the CommonJS export names for ESM import in node:
2144
2569
  0 && (module.exports = {
2145
2570
  Auth,
2571
+ DEFAULT_TABLE_NAME,
2572
+ DEFAULT_TTL_SECONDS,
2146
2573
  Deprecated,
2574
+ DynamoDBSessionStore,
2575
+ LeanMCPSessionProvider,
2147
2576
  LogLevel,
2148
2577
  Logger,
2149
2578
  MCPServer,