@leanmcp/core 0.3.2 → 0.3.4

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
@@ -63,7 +63,7 @@ function Resource(options = {}) {
63
63
  return (target, propertyKey, descriptor) => {
64
64
  const resourceName = String(propertyKey);
65
65
  const className = target.constructor.name.toLowerCase().replace("service", "");
66
- const resourceUri = `${className}://${resourceName}`;
66
+ const resourceUri = options.uri ?? `ui://${className}/${resourceName}`;
67
67
  Reflect.defineMetadata("resource:uri", resourceUri, descriptor.value);
68
68
  Reflect.defineMetadata("resource:name", resourceName, descriptor.value);
69
69
  Reflect.defineMetadata("resource:description", options.description || "", descriptor.value);
@@ -312,7 +312,7 @@ var init_schema_generator = __esm({
312
312
  });
313
313
 
314
314
  // src/logger.ts
315
- var LogLevel, Logger, defaultLogger;
315
+ var LogLevel, COLORS, levelStyles, Logger, defaultLogger;
316
316
  var init_logger = __esm({
317
317
  "src/logger.ts"() {
318
318
  "use strict";
@@ -324,6 +324,35 @@ var init_logger = __esm({
324
324
  LogLevel2[LogLevel2["NONE"] = 4] = "NONE";
325
325
  return LogLevel2;
326
326
  })({});
327
+ COLORS = {
328
+ reset: "\x1B[0m",
329
+ gray: "\x1B[38;5;244m",
330
+ blue: "\x1B[1;34m",
331
+ amber: "\x1B[38;5;214m",
332
+ red: "\x1B[1;31m"
333
+ };
334
+ levelStyles = {
335
+ [0]: {
336
+ label: "DEBUG",
337
+ color: COLORS.gray
338
+ },
339
+ [1]: {
340
+ label: "INFO",
341
+ color: COLORS.blue
342
+ },
343
+ [2]: {
344
+ label: "WARN",
345
+ color: COLORS.amber
346
+ },
347
+ [3]: {
348
+ label: "ERROR",
349
+ color: COLORS.red
350
+ },
351
+ [4]: {
352
+ label: "NONE",
353
+ color: COLORS.gray
354
+ }
355
+ };
327
356
  Logger = class {
328
357
  static {
329
358
  __name(this, "Logger");
@@ -331,38 +360,61 @@ var init_logger = __esm({
331
360
  level;
332
361
  prefix;
333
362
  timestamps;
363
+ colorize;
364
+ context;
365
+ handlers;
334
366
  constructor(options = {}) {
335
367
  this.level = options.level ?? 1;
336
368
  this.prefix = options.prefix ?? "";
337
369
  this.timestamps = options.timestamps ?? true;
370
+ this.colorize = options.colorize ?? true;
371
+ this.context = options.context;
372
+ this.handlers = options.handlers ?? [];
338
373
  }
339
- format(level, message, ...args) {
374
+ format(level, message) {
375
+ const style = levelStyles[level];
340
376
  const timestamp = this.timestamps ? `[${(/* @__PURE__ */ new Date()).toISOString()}]` : "";
341
377
  const prefix = this.prefix ? `[${this.prefix}]` : "";
342
- return `${timestamp}${prefix}[${level}] ${message}`;
378
+ const context = this.context ? `[${this.context}]` : "";
379
+ const label = `[${style.label}]`;
380
+ const parts = `${timestamp}${prefix}${context}${label} ${message}`;
381
+ if (!this.colorize) return parts;
382
+ return `${style.color}${parts}${COLORS.reset}`;
343
383
  }
344
384
  shouldLog(level) {
345
- return level >= this.level;
385
+ return level >= this.level && this.level !== 4;
386
+ }
387
+ emit(level, message, consoleFn, ...args) {
388
+ if (!this.shouldLog(level)) return;
389
+ const payload = {
390
+ level,
391
+ levelLabel: levelStyles[level].label,
392
+ message,
393
+ args,
394
+ prefix: this.prefix,
395
+ context: this.context,
396
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
397
+ };
398
+ consoleFn(this.format(level, message), ...args);
399
+ this.handlers.forEach((handler) => {
400
+ try {
401
+ handler(payload);
402
+ } catch (err) {
403
+ console.debug("Logger handler error", err);
404
+ }
405
+ });
346
406
  }
347
407
  debug(message, ...args) {
348
- if (this.shouldLog(0)) {
349
- console.debug(this.format("DEBUG", message), ...args);
350
- }
408
+ this.emit(0, message, console.debug, ...args);
351
409
  }
352
410
  info(message, ...args) {
353
- if (this.shouldLog(1)) {
354
- console.info(this.format("INFO", message), ...args);
355
- }
411
+ this.emit(1, message, console.info, ...args);
356
412
  }
357
413
  warn(message, ...args) {
358
- if (this.shouldLog(2)) {
359
- console.warn(this.format("WARN", message), ...args);
360
- }
414
+ this.emit(2, message, console.warn, ...args);
361
415
  }
362
416
  error(message, ...args) {
363
- if (this.shouldLog(3)) {
364
- console.error(this.format("ERROR", message), ...args);
365
- }
417
+ this.emit(3, message, console.error, ...args);
366
418
  }
367
419
  setLevel(level) {
368
420
  this.level = level;
@@ -448,7 +500,8 @@ async function createHTTPServer(serverInput, options) {
448
500
  port: serverOptions.port,
449
501
  cors: serverOptions.cors,
450
502
  logging: serverOptions.logging,
451
- sessionTimeout: serverOptions.sessionTimeout
503
+ sessionTimeout: serverOptions.sessionTimeout,
504
+ stateless: serverOptions.stateless
452
505
  };
453
506
  }
454
507
  const [express, { StreamableHTTPServerTransport }, cors] = await Promise.all([
@@ -473,12 +526,18 @@ async function createHTTPServer(serverInput, options) {
473
526
  prefix: "HTTP"
474
527
  });
475
528
  const logPrimary = /* @__PURE__ */ __name((message) => {
476
- console.log(message);
477
- logger.info?.(message);
529
+ if (httpOptions.logging) {
530
+ logger.info?.(message);
531
+ } else {
532
+ console.log(message);
533
+ }
478
534
  }, "logPrimary");
479
535
  const warnPrimary = /* @__PURE__ */ __name((message) => {
480
- console.warn(message);
481
- logger.warn?.(message);
536
+ if (httpOptions.logging) {
537
+ logger.warn?.(message);
538
+ } else {
539
+ console.warn(message);
540
+ }
482
541
  }, "warnPrimary");
483
542
  const startServerWithPortRetry = /* @__PURE__ */ __name(async () => {
484
543
  const maxAttempts = 20;
@@ -515,7 +574,7 @@ async function createHTTPServer(serverInput, options) {
515
574
  }, "startServerWithPortRetry");
516
575
  if (cors && httpOptions.cors) {
517
576
  const corsOptions = typeof httpOptions.cors === "object" ? {
518
- origin: httpOptions.cors.origin || false,
577
+ origin: httpOptions.cors.origin || "*",
519
578
  methods: [
520
579
  "GET",
521
580
  "POST",
@@ -533,21 +592,41 @@ async function createHTTPServer(serverInput, options) {
533
592
  ],
534
593
  credentials: httpOptions.cors.credentials ?? false,
535
594
  maxAge: 86400
536
- } : false;
537
- if (corsOptions) {
538
- app.use(cors.default(corsOptions));
539
- }
595
+ } : {
596
+ // When cors: true, use permissive defaults for development
597
+ origin: "*",
598
+ methods: [
599
+ "GET",
600
+ "POST",
601
+ "DELETE",
602
+ "OPTIONS"
603
+ ],
604
+ allowedHeaders: [
605
+ "Content-Type",
606
+ "mcp-session-id",
607
+ "mcp-protocol-version",
608
+ "Authorization"
609
+ ],
610
+ exposedHeaders: [
611
+ "mcp-session-id"
612
+ ],
613
+ credentials: false,
614
+ maxAge: 86400
615
+ };
616
+ app.use(cors.default(corsOptions));
540
617
  }
541
618
  app.use(express.json());
542
- logPrimary("Starting LeanMCP HTTP Server...");
619
+ const isStateless = httpOptions.stateless !== false;
620
+ console.log(`Starting LeanMCP HTTP Server (${isStateless ? "STATELESS" : "STATEFUL"})...`);
543
621
  app.get("/health", (req, res) => {
544
622
  res.json({
545
623
  status: "ok",
546
- activeSessions: Object.keys(transports).length,
624
+ mode: isStateless ? "stateless" : "stateful",
625
+ activeSessions: isStateless ? 0 : Object.keys(transports).length,
547
626
  uptime: process.uptime()
548
627
  });
549
628
  });
550
- const handleMCPRequest = /* @__PURE__ */ __name(async (req, res) => {
629
+ const handleMCPRequestStateful = /* @__PURE__ */ __name(async (req, res) => {
551
630
  const sessionId = req.headers["mcp-session-id"];
552
631
  let transport;
553
632
  const method = req.body?.method || "unknown";
@@ -610,9 +689,68 @@ async function createHTTPServer(serverInput, options) {
610
689
  });
611
690
  }
612
691
  }
613
- }, "handleMCPRequest");
614
- app.post("/mcp", handleMCPRequest);
615
- app.delete("/mcp", handleMCPRequest);
692
+ }, "handleMCPRequestStateful");
693
+ const handleMCPRequestStateless = /* @__PURE__ */ __name(async (req, res) => {
694
+ const method = req.body?.method || "unknown";
695
+ const params = req.body?.params;
696
+ let logMessage = `${req.method} /mcp - ${method}`;
697
+ if (params?.name) logMessage += ` [${params.name}]`;
698
+ else if (params?.uri) logMessage += ` [${params.uri}]`;
699
+ logger.info(logMessage);
700
+ try {
701
+ const freshServer = await serverFactory();
702
+ if (freshServer && typeof freshServer.waitForInit === "function") {
703
+ await freshServer.waitForInit();
704
+ }
705
+ const transport = new StreamableHTTPServerTransport({
706
+ sessionIdGenerator: void 0
707
+ });
708
+ await freshServer.connect(transport);
709
+ await transport.handleRequest(req, res, req.body);
710
+ res.on("close", () => {
711
+ transport.close();
712
+ freshServer.close();
713
+ });
714
+ } catch (error) {
715
+ logger.error("Error handling MCP request:", error);
716
+ if (!res.headersSent) {
717
+ res.status(500).json({
718
+ jsonrpc: "2.0",
719
+ error: {
720
+ code: -32603,
721
+ message: "Internal server error"
722
+ },
723
+ id: null
724
+ });
725
+ }
726
+ }
727
+ }, "handleMCPRequestStateless");
728
+ if (isStateless) {
729
+ app.post("/mcp", handleMCPRequestStateless);
730
+ app.get("/mcp", (_req, res) => {
731
+ res.status(405).json({
732
+ jsonrpc: "2.0",
733
+ error: {
734
+ code: -32e3,
735
+ message: "Method not allowed (stateless mode)"
736
+ },
737
+ id: null
738
+ });
739
+ });
740
+ app.delete("/mcp", (_req, res) => {
741
+ res.status(405).json({
742
+ jsonrpc: "2.0",
743
+ error: {
744
+ code: -32e3,
745
+ message: "Method not allowed (stateless mode)"
746
+ },
747
+ id: null
748
+ });
749
+ });
750
+ } else {
751
+ app.post("/mcp", handleMCPRequestStateful);
752
+ app.delete("/mcp", handleMCPRequestStateful);
753
+ }
616
754
  return new Promise(async (resolve, reject) => {
617
755
  let activeListener;
618
756
  try {
@@ -624,9 +762,9 @@ async function createHTTPServer(serverInput, options) {
624
762
  activeListener = listener;
625
763
  process.env.PORT = String(port);
626
764
  listener.port = port;
627
- logPrimary(`Server running on http://localhost:${port}`);
628
- logPrimary(`MCP endpoint: http://localhost:${port}/mcp`);
629
- logPrimary(`Health check: http://localhost:${port}/health`);
765
+ console.log(`Server running on http://localhost:${port}`);
766
+ console.log(`MCP endpoint: http://localhost:${port}/mcp`);
767
+ console.log(`Health check: http://localhost:${port}/health`);
630
768
  resolve({
631
769
  listener,
632
770
  port
@@ -767,6 +905,7 @@ var init_index = __esm({
767
905
  if (options.autoDiscover !== false) {
768
906
  await this.autoDiscoverServices(options.mcpDir, options.serviceFactories);
769
907
  }
908
+ await this.loadUIManifest();
770
909
  }
771
910
  /**
772
911
  * Wait for initialization to complete
@@ -848,14 +987,18 @@ var init_index = __esm({
848
987
  this.server.setRequestHandler(import_types.ListToolsRequestSchema, async () => {
849
988
  const tools = [];
850
989
  for (const [name, tool] of this.tools.entries()) {
851
- tools.push({
990
+ const toolDef = {
852
991
  name,
853
992
  description: tool.description,
854
993
  inputSchema: tool.inputSchema || {
855
994
  type: "object",
856
995
  properties: {}
857
996
  }
858
- });
997
+ };
998
+ if (tool._meta && Object.keys(tool._meta).length > 0) {
999
+ toolDef._meta = tool._meta;
1000
+ }
1001
+ tools.push(toolDef);
859
1002
  }
860
1003
  return {
861
1004
  tools
@@ -886,7 +1029,7 @@ var init_index = __esm({
886
1029
  } else {
887
1030
  formattedResult = String(result);
888
1031
  }
889
- return {
1032
+ const response = {
890
1033
  content: [
891
1034
  {
892
1035
  type: "text",
@@ -894,6 +1037,10 @@ var init_index = __esm({
894
1037
  }
895
1038
  ]
896
1039
  };
1040
+ if (tool._meta && Object.keys(tool._meta).length > 0) {
1041
+ response._meta = tool._meta;
1042
+ }
1043
+ return response;
897
1044
  } catch (error) {
898
1045
  return {
899
1046
  content: [
@@ -932,12 +1079,20 @@ var init_index = __esm({
932
1079
  }
933
1080
  try {
934
1081
  const result = await resource.method.call(resource.instance);
1082
+ let text;
1083
+ if (typeof result === "string") {
1084
+ text = result;
1085
+ } else if (result && typeof result === "object" && "text" in result) {
1086
+ text = result.text;
1087
+ } else {
1088
+ text = JSON.stringify(result, null, 2);
1089
+ }
935
1090
  return {
936
1091
  contents: [
937
1092
  {
938
1093
  uri,
939
1094
  mimeType: resource.mimeType,
940
- text: typeof result === "string" ? result : JSON.stringify(result, null, 2)
1095
+ text
941
1096
  }
942
1097
  ]
943
1098
  };
@@ -1083,16 +1238,19 @@ var init_index = __esm({
1083
1238
  if (inputClass) {
1084
1239
  inputSchema = classToJsonSchemaWithConstraints(inputClass);
1085
1240
  }
1241
+ const toolMeta = Reflect.getMetadata?.("tool:meta", method) || {};
1086
1242
  this.tools.set(methodMeta.toolName, {
1087
1243
  name: methodMeta.toolName,
1088
1244
  description: methodMeta.toolDescription || "",
1089
1245
  inputSchema,
1090
1246
  method,
1091
1247
  instance,
1092
- propertyKey
1248
+ propertyKey,
1249
+ _meta: Object.keys(toolMeta).length > 0 ? toolMeta : void 0
1093
1250
  });
1094
1251
  if (this.logging) {
1095
- this.logger.debug(`Registered tool: ${methodMeta.toolName}${inputClass ? " (class-based schema)" : ""}`);
1252
+ const hasUi = toolMeta["ui/resourceUri"] ? " (with UI)" : "";
1253
+ this.logger.debug(`Registered tool: ${methodMeta.toolName}${inputClass ? " (class-based schema)" : ""}${hasUi}`);
1096
1254
  }
1097
1255
  }
1098
1256
  const promptMethods = getDecoratedMethods(cls, "prompt:name");
@@ -1145,6 +1303,53 @@ var init_index = __esm({
1145
1303
  }
1146
1304
  }
1147
1305
  /**
1306
+ * Load UI manifest and auto-register resources for pre-built @UIApp components.
1307
+ * The manifest is generated by `leanmcp dev` or `leanmcp start` commands.
1308
+ */
1309
+ async loadUIManifest() {
1310
+ try {
1311
+ const manifestPath = import_path.default.join(process.cwd(), "dist", "ui-manifest.json");
1312
+ if (!import_fs.default.existsSync(manifestPath)) {
1313
+ return;
1314
+ }
1315
+ const manifest = JSON.parse(import_fs.default.readFileSync(manifestPath, "utf-8"));
1316
+ for (const [uri, htmlPath] of Object.entries(manifest)) {
1317
+ if (this.resources.has(uri)) {
1318
+ if (this.logging) {
1319
+ this.logger.debug(`Skipping UI resource ${uri} - already registered`);
1320
+ }
1321
+ continue;
1322
+ }
1323
+ if (!import_fs.default.existsSync(htmlPath)) {
1324
+ if (this.logging) {
1325
+ this.logger.warn(`UI HTML file not found: ${htmlPath}`);
1326
+ }
1327
+ continue;
1328
+ }
1329
+ const html = import_fs.default.readFileSync(htmlPath, "utf-8");
1330
+ this.resources.set(uri, {
1331
+ uri,
1332
+ name: uri.replace("ui://", "").replace(/\//g, "-"),
1333
+ description: `Auto-generated UI resource from pre-built HTML`,
1334
+ mimeType: "text/html;profile=mcp-app",
1335
+ inputSchema: void 0,
1336
+ method: /* @__PURE__ */ __name(async () => ({
1337
+ text: html
1338
+ }), "method"),
1339
+ instance: null,
1340
+ propertyKey: "getUI"
1341
+ });
1342
+ if (this.logging) {
1343
+ this.logger.debug(`Registered UI resource from manifest: ${uri}`);
1344
+ }
1345
+ }
1346
+ } catch (error) {
1347
+ if (this.logging) {
1348
+ this.logger.warn(`Failed to load UI manifest: ${error.message}`);
1349
+ }
1350
+ }
1351
+ }
1352
+ /**
1148
1353
  * Get the underlying MCP SDK Server instance
1149
1354
  * Attaches waitForInit method for HTTP server initialization
1150
1355
  */