@leanmcp/core 0.3.3 → 0.3.5

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([
@@ -521,7 +574,7 @@ async function createHTTPServer(serverInput, options) {
521
574
  }, "startServerWithPortRetry");
522
575
  if (cors && httpOptions.cors) {
523
576
  const corsOptions = typeof httpOptions.cors === "object" ? {
524
- origin: httpOptions.cors.origin || false,
577
+ origin: httpOptions.cors.origin || "*",
525
578
  methods: [
526
579
  "GET",
527
580
  "POST",
@@ -539,21 +592,41 @@ async function createHTTPServer(serverInput, options) {
539
592
  ],
540
593
  credentials: httpOptions.cors.credentials ?? false,
541
594
  maxAge: 86400
542
- } : false;
543
- if (corsOptions) {
544
- app.use(cors.default(corsOptions));
545
- }
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));
546
617
  }
547
618
  app.use(express.json());
548
- console.log("Starting LeanMCP HTTP Server...");
619
+ const isStateless = httpOptions.stateless !== false;
620
+ console.log(`Starting LeanMCP HTTP Server (${isStateless ? "STATELESS" : "STATEFUL"})...`);
549
621
  app.get("/health", (req, res) => {
550
622
  res.json({
551
623
  status: "ok",
552
- activeSessions: Object.keys(transports).length,
624
+ mode: isStateless ? "stateless" : "stateful",
625
+ activeSessions: isStateless ? 0 : Object.keys(transports).length,
553
626
  uptime: process.uptime()
554
627
  });
555
628
  });
556
- const handleMCPRequest = /* @__PURE__ */ __name(async (req, res) => {
629
+ const handleMCPRequestStateful = /* @__PURE__ */ __name(async (req, res) => {
557
630
  const sessionId = req.headers["mcp-session-id"];
558
631
  let transport;
559
632
  const method = req.body?.method || "unknown";
@@ -616,9 +689,68 @@ async function createHTTPServer(serverInput, options) {
616
689
  });
617
690
  }
618
691
  }
619
- }, "handleMCPRequest");
620
- app.post("/mcp", handleMCPRequest);
621
- 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
+ }
622
754
  return new Promise(async (resolve, reject) => {
623
755
  let activeListener;
624
756
  try {
@@ -773,6 +905,7 @@ var init_index = __esm({
773
905
  if (options.autoDiscover !== false) {
774
906
  await this.autoDiscoverServices(options.mcpDir, options.serviceFactories);
775
907
  }
908
+ await this.loadUIManifest();
776
909
  }
777
910
  /**
778
911
  * Wait for initialization to complete
@@ -854,14 +987,18 @@ var init_index = __esm({
854
987
  this.server.setRequestHandler(import_types.ListToolsRequestSchema, async () => {
855
988
  const tools = [];
856
989
  for (const [name, tool] of this.tools.entries()) {
857
- tools.push({
990
+ const toolDef = {
858
991
  name,
859
992
  description: tool.description,
860
993
  inputSchema: tool.inputSchema || {
861
994
  type: "object",
862
995
  properties: {}
863
996
  }
864
- });
997
+ };
998
+ if (tool._meta && Object.keys(tool._meta).length > 0) {
999
+ toolDef._meta = tool._meta;
1000
+ }
1001
+ tools.push(toolDef);
865
1002
  }
866
1003
  return {
867
1004
  tools
@@ -892,7 +1029,7 @@ var init_index = __esm({
892
1029
  } else {
893
1030
  formattedResult = String(result);
894
1031
  }
895
- return {
1032
+ const response = {
896
1033
  content: [
897
1034
  {
898
1035
  type: "text",
@@ -900,6 +1037,10 @@ var init_index = __esm({
900
1037
  }
901
1038
  ]
902
1039
  };
1040
+ if (tool._meta && Object.keys(tool._meta).length > 0) {
1041
+ response._meta = tool._meta;
1042
+ }
1043
+ return response;
903
1044
  } catch (error) {
904
1045
  return {
905
1046
  content: [
@@ -938,12 +1079,20 @@ var init_index = __esm({
938
1079
  }
939
1080
  try {
940
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
+ }
941
1090
  return {
942
1091
  contents: [
943
1092
  {
944
1093
  uri,
945
1094
  mimeType: resource.mimeType,
946
- text: typeof result === "string" ? result : JSON.stringify(result, null, 2)
1095
+ text
947
1096
  }
948
1097
  ]
949
1098
  };
@@ -1089,16 +1238,19 @@ var init_index = __esm({
1089
1238
  if (inputClass) {
1090
1239
  inputSchema = classToJsonSchemaWithConstraints(inputClass);
1091
1240
  }
1241
+ const toolMeta = Reflect.getMetadata?.("tool:meta", method) || {};
1092
1242
  this.tools.set(methodMeta.toolName, {
1093
1243
  name: methodMeta.toolName,
1094
1244
  description: methodMeta.toolDescription || "",
1095
1245
  inputSchema,
1096
1246
  method,
1097
1247
  instance,
1098
- propertyKey
1248
+ propertyKey,
1249
+ _meta: Object.keys(toolMeta).length > 0 ? toolMeta : void 0
1099
1250
  });
1100
1251
  if (this.logging) {
1101
- 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}`);
1102
1254
  }
1103
1255
  }
1104
1256
  const promptMethods = getDecoratedMethods(cls, "prompt:name");
@@ -1151,6 +1303,53 @@ var init_index = __esm({
1151
1303
  }
1152
1304
  }
1153
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
+ /**
1154
1353
  * Get the underlying MCP SDK Server instance
1155
1354
  * Attaches waitForInit method for HTTP server initialization
1156
1355
  */