@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.mjs CHANGED
@@ -46,7 +46,7 @@ function Resource(options = {}) {
46
46
  return (target, propertyKey, descriptor) => {
47
47
  const resourceName = String(propertyKey);
48
48
  const className = target.constructor.name.toLowerCase().replace("service", "");
49
- const resourceUri = `${className}://${resourceName}`;
49
+ const resourceUri = options.uri ?? `ui://${className}/${resourceName}`;
50
50
  Reflect.defineMetadata("resource:uri", resourceUri, descriptor.value);
51
51
  Reflect.defineMetadata("resource:name", resourceName, descriptor.value);
52
52
  Reflect.defineMetadata("resource:description", options.description || "", descriptor.value);
@@ -291,6 +291,35 @@ var LogLevel = /* @__PURE__ */ (function(LogLevel2) {
291
291
  LogLevel2[LogLevel2["NONE"] = 4] = "NONE";
292
292
  return LogLevel2;
293
293
  })({});
294
+ var COLORS = {
295
+ reset: "\x1B[0m",
296
+ gray: "\x1B[38;5;244m",
297
+ blue: "\x1B[1;34m",
298
+ amber: "\x1B[38;5;214m",
299
+ red: "\x1B[1;31m"
300
+ };
301
+ var levelStyles = {
302
+ [0]: {
303
+ label: "DEBUG",
304
+ color: COLORS.gray
305
+ },
306
+ [1]: {
307
+ label: "INFO",
308
+ color: COLORS.blue
309
+ },
310
+ [2]: {
311
+ label: "WARN",
312
+ color: COLORS.amber
313
+ },
314
+ [3]: {
315
+ label: "ERROR",
316
+ color: COLORS.red
317
+ },
318
+ [4]: {
319
+ label: "NONE",
320
+ color: COLORS.gray
321
+ }
322
+ };
294
323
  var Logger = class {
295
324
  static {
296
325
  __name(this, "Logger");
@@ -298,38 +327,61 @@ var Logger = class {
298
327
  level;
299
328
  prefix;
300
329
  timestamps;
330
+ colorize;
331
+ context;
332
+ handlers;
301
333
  constructor(options = {}) {
302
334
  this.level = options.level ?? 1;
303
335
  this.prefix = options.prefix ?? "";
304
336
  this.timestamps = options.timestamps ?? true;
337
+ this.colorize = options.colorize ?? true;
338
+ this.context = options.context;
339
+ this.handlers = options.handlers ?? [];
305
340
  }
306
- format(level, message, ...args) {
341
+ format(level, message) {
342
+ const style = levelStyles[level];
307
343
  const timestamp = this.timestamps ? `[${(/* @__PURE__ */ new Date()).toISOString()}]` : "";
308
344
  const prefix = this.prefix ? `[${this.prefix}]` : "";
309
- return `${timestamp}${prefix}[${level}] ${message}`;
345
+ const context = this.context ? `[${this.context}]` : "";
346
+ const label = `[${style.label}]`;
347
+ const parts = `${timestamp}${prefix}${context}${label} ${message}`;
348
+ if (!this.colorize) return parts;
349
+ return `${style.color}${parts}${COLORS.reset}`;
310
350
  }
311
351
  shouldLog(level) {
312
- return level >= this.level;
352
+ return level >= this.level && this.level !== 4;
353
+ }
354
+ emit(level, message, consoleFn, ...args) {
355
+ if (!this.shouldLog(level)) return;
356
+ const payload = {
357
+ level,
358
+ levelLabel: levelStyles[level].label,
359
+ message,
360
+ args,
361
+ prefix: this.prefix,
362
+ context: this.context,
363
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
364
+ };
365
+ consoleFn(this.format(level, message), ...args);
366
+ this.handlers.forEach((handler) => {
367
+ try {
368
+ handler(payload);
369
+ } catch (err) {
370
+ console.debug("Logger handler error", err);
371
+ }
372
+ });
313
373
  }
314
374
  debug(message, ...args) {
315
- if (this.shouldLog(0)) {
316
- console.debug(this.format("DEBUG", message), ...args);
317
- }
375
+ this.emit(0, message, console.debug, ...args);
318
376
  }
319
377
  info(message, ...args) {
320
- if (this.shouldLog(1)) {
321
- console.info(this.format("INFO", message), ...args);
322
- }
378
+ this.emit(1, message, console.info, ...args);
323
379
  }
324
380
  warn(message, ...args) {
325
- if (this.shouldLog(2)) {
326
- console.warn(this.format("WARN", message), ...args);
327
- }
381
+ this.emit(2, message, console.warn, ...args);
328
382
  }
329
383
  error(message, ...args) {
330
- if (this.shouldLog(3)) {
331
- console.error(this.format("ERROR", message), ...args);
332
- }
384
+ this.emit(3, message, console.error, ...args);
333
385
  }
334
386
  setLevel(level) {
335
387
  this.level = level;
@@ -409,7 +461,8 @@ async function createHTTPServer(serverInput, options) {
409
461
  port: serverOptions.port,
410
462
  cors: serverOptions.cors,
411
463
  logging: serverOptions.logging,
412
- sessionTimeout: serverOptions.sessionTimeout
464
+ sessionTimeout: serverOptions.sessionTimeout,
465
+ stateless: serverOptions.stateless
413
466
  };
414
467
  }
415
468
  const [express, { StreamableHTTPServerTransport }, cors] = await Promise.all([
@@ -482,7 +535,7 @@ async function createHTTPServer(serverInput, options) {
482
535
  }, "startServerWithPortRetry");
483
536
  if (cors && httpOptions.cors) {
484
537
  const corsOptions = typeof httpOptions.cors === "object" ? {
485
- origin: httpOptions.cors.origin || false,
538
+ origin: httpOptions.cors.origin || "*",
486
539
  methods: [
487
540
  "GET",
488
541
  "POST",
@@ -500,21 +553,41 @@ async function createHTTPServer(serverInput, options) {
500
553
  ],
501
554
  credentials: httpOptions.cors.credentials ?? false,
502
555
  maxAge: 86400
503
- } : false;
504
- if (corsOptions) {
505
- app.use(cors.default(corsOptions));
506
- }
556
+ } : {
557
+ // When cors: true, use permissive defaults for development
558
+ origin: "*",
559
+ methods: [
560
+ "GET",
561
+ "POST",
562
+ "DELETE",
563
+ "OPTIONS"
564
+ ],
565
+ allowedHeaders: [
566
+ "Content-Type",
567
+ "mcp-session-id",
568
+ "mcp-protocol-version",
569
+ "Authorization"
570
+ ],
571
+ exposedHeaders: [
572
+ "mcp-session-id"
573
+ ],
574
+ credentials: false,
575
+ maxAge: 86400
576
+ };
577
+ app.use(cors.default(corsOptions));
507
578
  }
508
579
  app.use(express.json());
509
- console.log("Starting LeanMCP HTTP Server...");
580
+ const isStateless = httpOptions.stateless !== false;
581
+ console.log(`Starting LeanMCP HTTP Server (${isStateless ? "STATELESS" : "STATEFUL"})...`);
510
582
  app.get("/health", (req, res) => {
511
583
  res.json({
512
584
  status: "ok",
513
- activeSessions: Object.keys(transports).length,
585
+ mode: isStateless ? "stateless" : "stateful",
586
+ activeSessions: isStateless ? 0 : Object.keys(transports).length,
514
587
  uptime: process.uptime()
515
588
  });
516
589
  });
517
- const handleMCPRequest = /* @__PURE__ */ __name(async (req, res) => {
590
+ const handleMCPRequestStateful = /* @__PURE__ */ __name(async (req, res) => {
518
591
  const sessionId = req.headers["mcp-session-id"];
519
592
  let transport;
520
593
  const method = req.body?.method || "unknown";
@@ -577,9 +650,68 @@ async function createHTTPServer(serverInput, options) {
577
650
  });
578
651
  }
579
652
  }
580
- }, "handleMCPRequest");
581
- app.post("/mcp", handleMCPRequest);
582
- app.delete("/mcp", handleMCPRequest);
653
+ }, "handleMCPRequestStateful");
654
+ const handleMCPRequestStateless = /* @__PURE__ */ __name(async (req, res) => {
655
+ const method = req.body?.method || "unknown";
656
+ const params = req.body?.params;
657
+ let logMessage = `${req.method} /mcp - ${method}`;
658
+ if (params?.name) logMessage += ` [${params.name}]`;
659
+ else if (params?.uri) logMessage += ` [${params.uri}]`;
660
+ logger.info(logMessage);
661
+ try {
662
+ const freshServer = await serverFactory();
663
+ if (freshServer && typeof freshServer.waitForInit === "function") {
664
+ await freshServer.waitForInit();
665
+ }
666
+ const transport = new StreamableHTTPServerTransport({
667
+ sessionIdGenerator: void 0
668
+ });
669
+ await freshServer.connect(transport);
670
+ await transport.handleRequest(req, res, req.body);
671
+ res.on("close", () => {
672
+ transport.close();
673
+ freshServer.close();
674
+ });
675
+ } catch (error) {
676
+ logger.error("Error handling MCP request:", error);
677
+ if (!res.headersSent) {
678
+ res.status(500).json({
679
+ jsonrpc: "2.0",
680
+ error: {
681
+ code: -32603,
682
+ message: "Internal server error"
683
+ },
684
+ id: null
685
+ });
686
+ }
687
+ }
688
+ }, "handleMCPRequestStateless");
689
+ if (isStateless) {
690
+ app.post("/mcp", handleMCPRequestStateless);
691
+ app.get("/mcp", (_req, res) => {
692
+ res.status(405).json({
693
+ jsonrpc: "2.0",
694
+ error: {
695
+ code: -32e3,
696
+ message: "Method not allowed (stateless mode)"
697
+ },
698
+ id: null
699
+ });
700
+ });
701
+ app.delete("/mcp", (_req, res) => {
702
+ res.status(405).json({
703
+ jsonrpc: "2.0",
704
+ error: {
705
+ code: -32e3,
706
+ message: "Method not allowed (stateless mode)"
707
+ },
708
+ id: null
709
+ });
710
+ });
711
+ } else {
712
+ app.post("/mcp", handleMCPRequestStateful);
713
+ app.delete("/mcp", handleMCPRequestStateful);
714
+ }
583
715
  return new Promise(async (resolve, reject) => {
584
716
  let activeListener;
585
717
  try {
@@ -670,6 +802,7 @@ var MCPServer = class {
670
802
  if (options.autoDiscover !== false) {
671
803
  await this.autoDiscoverServices(options.mcpDir, options.serviceFactories);
672
804
  }
805
+ await this.loadUIManifest();
673
806
  }
674
807
  /**
675
808
  * Wait for initialization to complete
@@ -751,14 +884,18 @@ var MCPServer = class {
751
884
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
752
885
  const tools = [];
753
886
  for (const [name, tool] of this.tools.entries()) {
754
- tools.push({
887
+ const toolDef = {
755
888
  name,
756
889
  description: tool.description,
757
890
  inputSchema: tool.inputSchema || {
758
891
  type: "object",
759
892
  properties: {}
760
893
  }
761
- });
894
+ };
895
+ if (tool._meta && Object.keys(tool._meta).length > 0) {
896
+ toolDef._meta = tool._meta;
897
+ }
898
+ tools.push(toolDef);
762
899
  }
763
900
  return {
764
901
  tools
@@ -789,7 +926,7 @@ var MCPServer = class {
789
926
  } else {
790
927
  formattedResult = String(result);
791
928
  }
792
- return {
929
+ const response = {
793
930
  content: [
794
931
  {
795
932
  type: "text",
@@ -797,6 +934,10 @@ var MCPServer = class {
797
934
  }
798
935
  ]
799
936
  };
937
+ if (tool._meta && Object.keys(tool._meta).length > 0) {
938
+ response._meta = tool._meta;
939
+ }
940
+ return response;
800
941
  } catch (error) {
801
942
  return {
802
943
  content: [
@@ -835,12 +976,20 @@ var MCPServer = class {
835
976
  }
836
977
  try {
837
978
  const result = await resource.method.call(resource.instance);
979
+ let text;
980
+ if (typeof result === "string") {
981
+ text = result;
982
+ } else if (result && typeof result === "object" && "text" in result) {
983
+ text = result.text;
984
+ } else {
985
+ text = JSON.stringify(result, null, 2);
986
+ }
838
987
  return {
839
988
  contents: [
840
989
  {
841
990
  uri,
842
991
  mimeType: resource.mimeType,
843
- text: typeof result === "string" ? result : JSON.stringify(result, null, 2)
992
+ text
844
993
  }
845
994
  ]
846
995
  };
@@ -986,16 +1135,19 @@ var MCPServer = class {
986
1135
  if (inputClass) {
987
1136
  inputSchema = classToJsonSchemaWithConstraints(inputClass);
988
1137
  }
1138
+ const toolMeta = Reflect.getMetadata?.("tool:meta", method) || {};
989
1139
  this.tools.set(methodMeta.toolName, {
990
1140
  name: methodMeta.toolName,
991
1141
  description: methodMeta.toolDescription || "",
992
1142
  inputSchema,
993
1143
  method,
994
1144
  instance,
995
- propertyKey
1145
+ propertyKey,
1146
+ _meta: Object.keys(toolMeta).length > 0 ? toolMeta : void 0
996
1147
  });
997
1148
  if (this.logging) {
998
- this.logger.debug(`Registered tool: ${methodMeta.toolName}${inputClass ? " (class-based schema)" : ""}`);
1149
+ const hasUi = toolMeta["ui/resourceUri"] ? " (with UI)" : "";
1150
+ this.logger.debug(`Registered tool: ${methodMeta.toolName}${inputClass ? " (class-based schema)" : ""}${hasUi}`);
999
1151
  }
1000
1152
  }
1001
1153
  const promptMethods = getDecoratedMethods(cls, "prompt:name");
@@ -1048,6 +1200,53 @@ var MCPServer = class {
1048
1200
  }
1049
1201
  }
1050
1202
  /**
1203
+ * Load UI manifest and auto-register resources for pre-built @UIApp components.
1204
+ * The manifest is generated by `leanmcp dev` or `leanmcp start` commands.
1205
+ */
1206
+ async loadUIManifest() {
1207
+ try {
1208
+ const manifestPath = path.join(process.cwd(), "dist", "ui-manifest.json");
1209
+ if (!fs.existsSync(manifestPath)) {
1210
+ return;
1211
+ }
1212
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1213
+ for (const [uri, htmlPath] of Object.entries(manifest)) {
1214
+ if (this.resources.has(uri)) {
1215
+ if (this.logging) {
1216
+ this.logger.debug(`Skipping UI resource ${uri} - already registered`);
1217
+ }
1218
+ continue;
1219
+ }
1220
+ if (!fs.existsSync(htmlPath)) {
1221
+ if (this.logging) {
1222
+ this.logger.warn(`UI HTML file not found: ${htmlPath}`);
1223
+ }
1224
+ continue;
1225
+ }
1226
+ const html = fs.readFileSync(htmlPath, "utf-8");
1227
+ this.resources.set(uri, {
1228
+ uri,
1229
+ name: uri.replace("ui://", "").replace(/\//g, "-"),
1230
+ description: `Auto-generated UI resource from pre-built HTML`,
1231
+ mimeType: "text/html;profile=mcp-app",
1232
+ inputSchema: void 0,
1233
+ method: /* @__PURE__ */ __name(async () => ({
1234
+ text: html
1235
+ }), "method"),
1236
+ instance: null,
1237
+ propertyKey: "getUI"
1238
+ });
1239
+ if (this.logging) {
1240
+ this.logger.debug(`Registered UI resource from manifest: ${uri}`);
1241
+ }
1242
+ }
1243
+ } catch (error) {
1244
+ if (this.logging) {
1245
+ this.logger.warn(`Failed to load UI manifest: ${error.message}`);
1246
+ }
1247
+ }
1248
+ }
1249
+ /**
1051
1250
  * Get the underlying MCP SDK Server instance
1052
1251
  * Attaches waitForInit method for HTTP server initialization
1053
1252
  */
package/package.json CHANGED
@@ -1,71 +1,71 @@
1
- {
2
- "name": "@leanmcp/core",
3
- "version": "0.3.3",
4
- "description": "Core library implementing decorators, reflection, and MCP runtime server",
5
- "main": "dist/index.js",
6
- "module": "dist/index.mjs",
7
- "types": "dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "require": "./dist/index.js",
12
- "import": "./dist/index.mjs"
13
- }
14
- },
15
- "files": [
16
- "dist",
17
- "README.md",
18
- "LICENSE"
19
- ],
20
- "scripts": {
21
- "build": "tsup src/index.ts --format esm,cjs --dts",
22
- "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
23
- "test": "jest --passWithNoTests",
24
- "test:watch": "jest --watch"
25
- },
26
- "dependencies": {
27
- "@modelcontextprotocol/sdk": "^1.0.0",
28
- "ajv": "^8.12.0",
29
- "dotenv": "^16.3.1",
30
- "reflect-metadata": "^0.2.1"
31
- },
32
- "peerDependencies": {
33
- "cors": "^2.8.5",
34
- "express": "^5.0.0"
35
- },
36
- "peerDependenciesMeta": {
37
- "express": {
38
- "optional": true
39
- },
40
- "cors": {
41
- "optional": true
42
- }
43
- },
44
- "devDependencies": {
45
- "@types/cors": "^2.8.0",
46
- "@types/express": "^5.0.0",
47
- "@types/node": "^20.0.0"
48
- },
49
- "repository": {
50
- "type": "git",
51
- "url": "git+https://github.com/LeanMCP/leanmcp-sdk.git",
52
- "directory": "packages/core"
53
- },
54
- "homepage": "https://github.com/LeanMCP/leanmcp-sdk#readme",
55
- "bugs": {
56
- "url": "https://github.com/LeanMCP/leanmcp-sdk/issues"
57
- },
58
- "keywords": [
59
- "mcp",
60
- "model-context-protocol",
61
- "typescript",
62
- "decorators",
63
- "server",
64
- "runtime"
65
- ],
66
- "author": "LeanMCP <admin@leanmcp.com>",
67
- "license": "MIT",
68
- "publishConfig": {
69
- "access": "public"
70
- }
71
- }
1
+ {
2
+ "name": "@leanmcp/core",
3
+ "version": "0.3.5",
4
+ "description": "Core library implementing decorators, reflection, and MCP runtime server",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "require": "./dist/index.js",
12
+ "import": "./dist/index.mjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format esm,cjs --dts",
22
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
23
+ "test": "jest --passWithNoTests",
24
+ "test:watch": "jest --watch"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.0.0",
28
+ "ajv": "^8.12.0",
29
+ "dotenv": "^16.3.1",
30
+ "reflect-metadata": "^0.2.1"
31
+ },
32
+ "peerDependencies": {
33
+ "cors": "^2.8.5",
34
+ "express": "^5.0.0"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "express": {
38
+ "optional": true
39
+ },
40
+ "cors": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "devDependencies": {
45
+ "@types/cors": "^2.8.0",
46
+ "@types/express": "^5.0.0",
47
+ "@types/node": "^20.0.0"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/LeanMCP/leanmcp-sdk.git",
52
+ "directory": "packages/core"
53
+ },
54
+ "homepage": "https://github.com/LeanMCP/leanmcp-sdk#readme",
55
+ "bugs": {
56
+ "url": "https://github.com/LeanMCP/leanmcp-sdk/issues"
57
+ },
58
+ "keywords": [
59
+ "mcp",
60
+ "model-context-protocol",
61
+ "typescript",
62
+ "decorators",
63
+ "server",
64
+ "runtime"
65
+ ],
66
+ "author": "LeanMCP <admin@leanmcp.com>",
67
+ "license": "MIT",
68
+ "publishConfig": {
69
+ "access": "public"
70
+ }
71
+ }