@leanmcp/core 0.3.7 → 0.3.9

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.d.mts CHANGED
@@ -411,6 +411,7 @@ declare class MCPServer {
411
411
  private options;
412
412
  private initPromise;
413
413
  private autoDiscovered;
414
+ private manifestWatcher;
414
415
  constructor(options: MCPServerConstructorOptions);
415
416
  /**
416
417
  * Internal initialization - runs automatically in constructor
@@ -462,6 +463,14 @@ declare class MCPServer {
462
463
  * Register a service instance with decorated methods
463
464
  */
464
465
  registerService(instance: any): void;
466
+ /**
467
+ * Watch UI manifest for changes and reload resources dynamically
468
+ */
469
+ private watchUIManifest;
470
+ /**
471
+ * Reload UI manifest and update resource registrations
472
+ */
473
+ private reloadUIManifest;
465
474
  /**
466
475
  * Load UI manifest and auto-register resources for pre-built @UIApp components.
467
476
  * The manifest is generated by `leanmcp dev` or `leanmcp start` commands.
@@ -511,6 +520,10 @@ declare class MCPServer {
511
520
  } | undefined;
512
521
  } | undefined;
513
522
  }>;
523
+ /**
524
+ * Cleanup resources (call on server shutdown)
525
+ */
526
+ cleanup(): Promise<void>;
514
527
  }
515
528
  declare class MCPServerRuntime {
516
529
  private server;
package/dist/index.d.ts CHANGED
@@ -411,6 +411,7 @@ declare class MCPServer {
411
411
  private options;
412
412
  private initPromise;
413
413
  private autoDiscovered;
414
+ private manifestWatcher;
414
415
  constructor(options: MCPServerConstructorOptions);
415
416
  /**
416
417
  * Internal initialization - runs automatically in constructor
@@ -462,6 +463,14 @@ declare class MCPServer {
462
463
  * Register a service instance with decorated methods
463
464
  */
464
465
  registerService(instance: any): void;
466
+ /**
467
+ * Watch UI manifest for changes and reload resources dynamically
468
+ */
469
+ private watchUIManifest;
470
+ /**
471
+ * Reload UI manifest and update resource registrations
472
+ */
473
+ private reloadUIManifest;
465
474
  /**
466
475
  * Load UI manifest and auto-register resources for pre-built @UIApp components.
467
476
  * The manifest is generated by `leanmcp dev` or `leanmcp start` commands.
@@ -511,6 +520,10 @@ declare class MCPServer {
511
520
  } | undefined;
512
521
  } | undefined;
513
522
  }>;
523
+ /**
524
+ * Cleanup resources (call on server shutdown)
525
+ */
526
+ cleanup(): Promise<void>;
514
527
  }
515
528
  declare class MCPServerRuntime {
516
529
  private server;
package/dist/index.js CHANGED
@@ -812,8 +812,29 @@ async function createHTTPServer(serverInput, options) {
812
812
  }
813
813
  }
814
814
  }, "handleMCPRequestStateless");
815
- if (isDashboardEnabled) {
816
- app.get("/mcp", async (req, res) => {
815
+ app.get("/mcp", async (req, res) => {
816
+ const acceptHeader = req.headers["accept"] || "";
817
+ if (acceptHeader.includes("text/event-stream")) {
818
+ if (!isStateless) {
819
+ const sessionId = req.headers["mcp-session-id"];
820
+ if (sessionId && transports[sessionId]) {
821
+ const transport = transports[sessionId];
822
+ logger.info(`GET /mcp SSE request (session: ${sessionId.substring(0, 8)}...)`);
823
+ await transport.handleRequest(req, res);
824
+ return;
825
+ }
826
+ }
827
+ res.status(405).json({
828
+ jsonrpc: "2.0",
829
+ error: {
830
+ code: -32e3,
831
+ message: "SSE streaming not supported in stateless mode or invalid session"
832
+ },
833
+ id: null
834
+ });
835
+ return;
836
+ }
837
+ if (isDashboardEnabled) {
817
838
  try {
818
839
  const html = await fetchDashboard();
819
840
  res.setHeader("Content-Type", "text/html");
@@ -821,8 +842,12 @@ async function createHTTPServer(serverInput, options) {
821
842
  } catch (error) {
822
843
  res.status(500).send("<h1>Dashboard temporarily unavailable</h1><p>Please try again later.</p>");
823
844
  }
824
- });
825
- }
845
+ } else {
846
+ res.status(404).json({
847
+ error: "Dashboard disabled"
848
+ });
849
+ }
850
+ });
826
851
  if (isStateless) {
827
852
  app.post("/mcp", handleMCPRequestStateless);
828
853
  app.delete("/mcp", (_req, res) => {
@@ -965,6 +990,7 @@ var init_index = __esm({
965
990
  options;
966
991
  initPromise;
967
992
  autoDiscovered = false;
993
+ manifestWatcher = null;
968
994
  constructor(options) {
969
995
  this.options = options;
970
996
  this.logging = options.logging || false;
@@ -998,6 +1024,7 @@ var init_index = __esm({
998
1024
  await this.autoDiscoverServices(options.mcpDir, options.serviceFactories);
999
1025
  }
1000
1026
  await this.loadUIManifest();
1027
+ this.watchUIManifest();
1001
1028
  }
1002
1029
  /**
1003
1030
  * Wait for initialization to complete
@@ -1395,6 +1422,113 @@ var init_index = __esm({
1395
1422
  }
1396
1423
  }
1397
1424
  /**
1425
+ * Watch UI manifest for changes and reload resources dynamically
1426
+ */
1427
+ watchUIManifest() {
1428
+ try {
1429
+ const manifestPath = import_path.default.join(process.cwd(), "dist", "ui-manifest.json");
1430
+ if (!import_fs.default.existsSync(manifestPath)) {
1431
+ return;
1432
+ }
1433
+ if (this.logging) {
1434
+ this.logger.debug(`Watching UI manifest: ${manifestPath}`);
1435
+ }
1436
+ import("chokidar").then(({ default: chokidar }) => {
1437
+ this.manifestWatcher = chokidar.watch(manifestPath, {
1438
+ ignoreInitial: true,
1439
+ persistent: true,
1440
+ awaitWriteFinish: {
1441
+ stabilityThreshold: 100,
1442
+ pollInterval: 50
1443
+ }
1444
+ });
1445
+ this.manifestWatcher.on("change", async () => {
1446
+ if (this.logging) {
1447
+ this.logger.debug("UI manifest changed, reloading resources");
1448
+ }
1449
+ await this.reloadUIManifest();
1450
+ });
1451
+ this.manifestWatcher.on("error", (error) => {
1452
+ if (this.logging) {
1453
+ this.logger.warn(`Manifest watcher error: ${error.message}`);
1454
+ }
1455
+ });
1456
+ }).catch((error) => {
1457
+ if (this.logging) {
1458
+ this.logger.warn(`Failed to initialize manifest watcher: ${error.message}`);
1459
+ }
1460
+ });
1461
+ } catch (error) {
1462
+ if (this.logging) {
1463
+ this.logger.warn(`Failed to setup manifest watcher: ${error.message}`);
1464
+ }
1465
+ }
1466
+ }
1467
+ /**
1468
+ * Reload UI manifest and update resource registrations
1469
+ */
1470
+ async reloadUIManifest() {
1471
+ try {
1472
+ const manifestPath = import_path.default.join(process.cwd(), "dist", "ui-manifest.json");
1473
+ if (!import_fs.default.existsSync(manifestPath)) {
1474
+ const uiResourceUris = Array.from(this.resources.keys()).filter((uri) => uri.startsWith("ui://"));
1475
+ for (const uri of uiResourceUris) {
1476
+ this.resources.delete(uri);
1477
+ if (this.logging) {
1478
+ this.logger.debug(`Removed UI resource: ${uri}`);
1479
+ }
1480
+ }
1481
+ return;
1482
+ }
1483
+ const manifest = JSON.parse(import_fs.default.readFileSync(manifestPath, "utf-8"));
1484
+ const currentUIUris = new Set(Object.keys(manifest));
1485
+ const registeredUIUris = Array.from(this.resources.keys()).filter((uri) => uri.startsWith("ui://"));
1486
+ for (const uri of registeredUIUris) {
1487
+ if (!currentUIUris.has(uri)) {
1488
+ this.resources.delete(uri);
1489
+ if (this.logging) {
1490
+ this.logger.debug(`Removed UI resource: ${uri}`);
1491
+ }
1492
+ }
1493
+ }
1494
+ for (const [uri, htmlPath] of Object.entries(manifest)) {
1495
+ if (!import_fs.default.existsSync(htmlPath)) {
1496
+ if (this.logging) {
1497
+ this.logger.warn(`UI HTML file not found: ${htmlPath}`);
1498
+ }
1499
+ continue;
1500
+ }
1501
+ const wasRegistered = this.resources.has(uri);
1502
+ this.resources.set(uri, {
1503
+ uri,
1504
+ name: uri.replace("ui://", "").replace(/\//g, "-"),
1505
+ description: `Auto-generated UI resource from pre-built HTML`,
1506
+ mimeType: "text/html;profile=mcp-app",
1507
+ inputSchema: void 0,
1508
+ method: /* @__PURE__ */ __name(async () => {
1509
+ if (import_fs.default.existsSync(htmlPath)) {
1510
+ const html = import_fs.default.readFileSync(htmlPath, "utf-8");
1511
+ return {
1512
+ text: html
1513
+ };
1514
+ }
1515
+ throw new Error(`UI HTML file not found: ${htmlPath}`);
1516
+ }, "method"),
1517
+ instance: null,
1518
+ propertyKey: "getUI"
1519
+ });
1520
+ if (this.logging) {
1521
+ const action = wasRegistered ? "Updated" : "Registered";
1522
+ this.logger.debug(`${action} UI resource: ${uri}`);
1523
+ }
1524
+ }
1525
+ } catch (error) {
1526
+ if (this.logging) {
1527
+ this.logger.warn(`Failed to reload UI manifest: ${error.message}`);
1528
+ }
1529
+ }
1530
+ }
1531
+ /**
1398
1532
  * Load UI manifest and auto-register resources for pre-built @UIApp components.
1399
1533
  * The manifest is generated by `leanmcp dev` or `leanmcp start` commands.
1400
1534
  */
@@ -1449,6 +1583,15 @@ var init_index = __esm({
1449
1583
  this.server.waitForInit = () => this.waitForInit();
1450
1584
  return this.server;
1451
1585
  }
1586
+ /**
1587
+ * Cleanup resources (call on server shutdown)
1588
+ */
1589
+ async cleanup() {
1590
+ if (this.manifestWatcher) {
1591
+ await this.manifestWatcher.close();
1592
+ this.manifestWatcher = null;
1593
+ }
1594
+ }
1452
1595
  };
1453
1596
  MCPServerRuntime = class {
1454
1597
  static {
package/dist/index.mjs CHANGED
@@ -774,8 +774,29 @@ async function createHTTPServer(serverInput, options) {
774
774
  }
775
775
  }
776
776
  }, "handleMCPRequestStateless");
777
- if (isDashboardEnabled) {
778
- app.get("/mcp", async (req, res) => {
777
+ app.get("/mcp", async (req, res) => {
778
+ const acceptHeader = req.headers["accept"] || "";
779
+ if (acceptHeader.includes("text/event-stream")) {
780
+ if (!isStateless) {
781
+ const sessionId = req.headers["mcp-session-id"];
782
+ if (sessionId && transports[sessionId]) {
783
+ const transport = transports[sessionId];
784
+ logger.info(`GET /mcp SSE request (session: ${sessionId.substring(0, 8)}...)`);
785
+ await transport.handleRequest(req, res);
786
+ return;
787
+ }
788
+ }
789
+ res.status(405).json({
790
+ jsonrpc: "2.0",
791
+ error: {
792
+ code: -32e3,
793
+ message: "SSE streaming not supported in stateless mode or invalid session"
794
+ },
795
+ id: null
796
+ });
797
+ return;
798
+ }
799
+ if (isDashboardEnabled) {
779
800
  try {
780
801
  const html = await fetchDashboard();
781
802
  res.setHeader("Content-Type", "text/html");
@@ -783,8 +804,12 @@ async function createHTTPServer(serverInput, options) {
783
804
  } catch (error) {
784
805
  res.status(500).send("<h1>Dashboard temporarily unavailable</h1><p>Please try again later.</p>");
785
806
  }
786
- });
787
- }
807
+ } else {
808
+ res.status(404).json({
809
+ error: "Dashboard disabled"
810
+ });
811
+ }
812
+ });
788
813
  if (isStateless) {
789
814
  app.post("/mcp", handleMCPRequestStateless);
790
815
  app.delete("/mcp", (_req, res) => {
@@ -862,6 +887,7 @@ var MCPServer = class {
862
887
  options;
863
888
  initPromise;
864
889
  autoDiscovered = false;
890
+ manifestWatcher = null;
865
891
  constructor(options) {
866
892
  this.options = options;
867
893
  this.logging = options.logging || false;
@@ -895,6 +921,7 @@ var MCPServer = class {
895
921
  await this.autoDiscoverServices(options.mcpDir, options.serviceFactories);
896
922
  }
897
923
  await this.loadUIManifest();
924
+ this.watchUIManifest();
898
925
  }
899
926
  /**
900
927
  * Wait for initialization to complete
@@ -1292,6 +1319,113 @@ var MCPServer = class {
1292
1319
  }
1293
1320
  }
1294
1321
  /**
1322
+ * Watch UI manifest for changes and reload resources dynamically
1323
+ */
1324
+ watchUIManifest() {
1325
+ try {
1326
+ const manifestPath = path.join(process.cwd(), "dist", "ui-manifest.json");
1327
+ if (!fs.existsSync(manifestPath)) {
1328
+ return;
1329
+ }
1330
+ if (this.logging) {
1331
+ this.logger.debug(`Watching UI manifest: ${manifestPath}`);
1332
+ }
1333
+ import("chokidar").then(({ default: chokidar }) => {
1334
+ this.manifestWatcher = chokidar.watch(manifestPath, {
1335
+ ignoreInitial: true,
1336
+ persistent: true,
1337
+ awaitWriteFinish: {
1338
+ stabilityThreshold: 100,
1339
+ pollInterval: 50
1340
+ }
1341
+ });
1342
+ this.manifestWatcher.on("change", async () => {
1343
+ if (this.logging) {
1344
+ this.logger.debug("UI manifest changed, reloading resources");
1345
+ }
1346
+ await this.reloadUIManifest();
1347
+ });
1348
+ this.manifestWatcher.on("error", (error) => {
1349
+ if (this.logging) {
1350
+ this.logger.warn(`Manifest watcher error: ${error.message}`);
1351
+ }
1352
+ });
1353
+ }).catch((error) => {
1354
+ if (this.logging) {
1355
+ this.logger.warn(`Failed to initialize manifest watcher: ${error.message}`);
1356
+ }
1357
+ });
1358
+ } catch (error) {
1359
+ if (this.logging) {
1360
+ this.logger.warn(`Failed to setup manifest watcher: ${error.message}`);
1361
+ }
1362
+ }
1363
+ }
1364
+ /**
1365
+ * Reload UI manifest and update resource registrations
1366
+ */
1367
+ async reloadUIManifest() {
1368
+ try {
1369
+ const manifestPath = path.join(process.cwd(), "dist", "ui-manifest.json");
1370
+ if (!fs.existsSync(manifestPath)) {
1371
+ const uiResourceUris = Array.from(this.resources.keys()).filter((uri) => uri.startsWith("ui://"));
1372
+ for (const uri of uiResourceUris) {
1373
+ this.resources.delete(uri);
1374
+ if (this.logging) {
1375
+ this.logger.debug(`Removed UI resource: ${uri}`);
1376
+ }
1377
+ }
1378
+ return;
1379
+ }
1380
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1381
+ const currentUIUris = new Set(Object.keys(manifest));
1382
+ const registeredUIUris = Array.from(this.resources.keys()).filter((uri) => uri.startsWith("ui://"));
1383
+ for (const uri of registeredUIUris) {
1384
+ if (!currentUIUris.has(uri)) {
1385
+ this.resources.delete(uri);
1386
+ if (this.logging) {
1387
+ this.logger.debug(`Removed UI resource: ${uri}`);
1388
+ }
1389
+ }
1390
+ }
1391
+ for (const [uri, htmlPath] of Object.entries(manifest)) {
1392
+ if (!fs.existsSync(htmlPath)) {
1393
+ if (this.logging) {
1394
+ this.logger.warn(`UI HTML file not found: ${htmlPath}`);
1395
+ }
1396
+ continue;
1397
+ }
1398
+ const wasRegistered = this.resources.has(uri);
1399
+ this.resources.set(uri, {
1400
+ uri,
1401
+ name: uri.replace("ui://", "").replace(/\//g, "-"),
1402
+ description: `Auto-generated UI resource from pre-built HTML`,
1403
+ mimeType: "text/html;profile=mcp-app",
1404
+ inputSchema: void 0,
1405
+ method: /* @__PURE__ */ __name(async () => {
1406
+ if (fs.existsSync(htmlPath)) {
1407
+ const html = fs.readFileSync(htmlPath, "utf-8");
1408
+ return {
1409
+ text: html
1410
+ };
1411
+ }
1412
+ throw new Error(`UI HTML file not found: ${htmlPath}`);
1413
+ }, "method"),
1414
+ instance: null,
1415
+ propertyKey: "getUI"
1416
+ });
1417
+ if (this.logging) {
1418
+ const action = wasRegistered ? "Updated" : "Registered";
1419
+ this.logger.debug(`${action} UI resource: ${uri}`);
1420
+ }
1421
+ }
1422
+ } catch (error) {
1423
+ if (this.logging) {
1424
+ this.logger.warn(`Failed to reload UI manifest: ${error.message}`);
1425
+ }
1426
+ }
1427
+ }
1428
+ /**
1295
1429
  * Load UI manifest and auto-register resources for pre-built @UIApp components.
1296
1430
  * The manifest is generated by `leanmcp dev` or `leanmcp start` commands.
1297
1431
  */
@@ -1346,6 +1480,15 @@ var MCPServer = class {
1346
1480
  this.server.waitForInit = () => this.waitForInit();
1347
1481
  return this.server;
1348
1482
  }
1483
+ /**
1484
+ * Cleanup resources (call on server shutdown)
1485
+ */
1486
+ async cleanup() {
1487
+ if (this.manifestWatcher) {
1488
+ await this.manifestWatcher.close();
1489
+ this.manifestWatcher = null;
1490
+ }
1491
+ }
1349
1492
  };
1350
1493
  var MCPServerRuntime = class {
1351
1494
  static {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leanmcp/core",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Core library implementing decorators, reflection, and MCP runtime server",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -26,6 +26,7 @@
26
26
  "dependencies": {
27
27
  "@modelcontextprotocol/sdk": "^1.0.0",
28
28
  "ajv": "^8.12.0",
29
+ "chokidar": "^4.0.0",
29
30
  "dotenv": "^16.3.1",
30
31
  "reflect-metadata": "^0.2.1"
31
32
  },