@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.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([
@@ -434,12 +487,18 @@ async function createHTTPServer(serverInput, options) {
434
487
  prefix: "HTTP"
435
488
  });
436
489
  const logPrimary = /* @__PURE__ */ __name((message) => {
437
- console.log(message);
438
- logger.info?.(message);
490
+ if (httpOptions.logging) {
491
+ logger.info?.(message);
492
+ } else {
493
+ console.log(message);
494
+ }
439
495
  }, "logPrimary");
440
496
  const warnPrimary = /* @__PURE__ */ __name((message) => {
441
- console.warn(message);
442
- logger.warn?.(message);
497
+ if (httpOptions.logging) {
498
+ logger.warn?.(message);
499
+ } else {
500
+ console.warn(message);
501
+ }
443
502
  }, "warnPrimary");
444
503
  const startServerWithPortRetry = /* @__PURE__ */ __name(async () => {
445
504
  const maxAttempts = 20;
@@ -476,7 +535,7 @@ async function createHTTPServer(serverInput, options) {
476
535
  }, "startServerWithPortRetry");
477
536
  if (cors && httpOptions.cors) {
478
537
  const corsOptions = typeof httpOptions.cors === "object" ? {
479
- origin: httpOptions.cors.origin || false,
538
+ origin: httpOptions.cors.origin || "*",
480
539
  methods: [
481
540
  "GET",
482
541
  "POST",
@@ -494,21 +553,41 @@ async function createHTTPServer(serverInput, options) {
494
553
  ],
495
554
  credentials: httpOptions.cors.credentials ?? false,
496
555
  maxAge: 86400
497
- } : false;
498
- if (corsOptions) {
499
- app.use(cors.default(corsOptions));
500
- }
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));
501
578
  }
502
579
  app.use(express.json());
503
- logPrimary("Starting LeanMCP HTTP Server...");
580
+ const isStateless = httpOptions.stateless !== false;
581
+ console.log(`Starting LeanMCP HTTP Server (${isStateless ? "STATELESS" : "STATEFUL"})...`);
504
582
  app.get("/health", (req, res) => {
505
583
  res.json({
506
584
  status: "ok",
507
- activeSessions: Object.keys(transports).length,
585
+ mode: isStateless ? "stateless" : "stateful",
586
+ activeSessions: isStateless ? 0 : Object.keys(transports).length,
508
587
  uptime: process.uptime()
509
588
  });
510
589
  });
511
- const handleMCPRequest = /* @__PURE__ */ __name(async (req, res) => {
590
+ const handleMCPRequestStateful = /* @__PURE__ */ __name(async (req, res) => {
512
591
  const sessionId = req.headers["mcp-session-id"];
513
592
  let transport;
514
593
  const method = req.body?.method || "unknown";
@@ -571,9 +650,68 @@ async function createHTTPServer(serverInput, options) {
571
650
  });
572
651
  }
573
652
  }
574
- }, "handleMCPRequest");
575
- app.post("/mcp", handleMCPRequest);
576
- 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
+ }
577
715
  return new Promise(async (resolve, reject) => {
578
716
  let activeListener;
579
717
  try {
@@ -585,9 +723,9 @@ async function createHTTPServer(serverInput, options) {
585
723
  activeListener = listener;
586
724
  process.env.PORT = String(port);
587
725
  listener.port = port;
588
- logPrimary(`Server running on http://localhost:${port}`);
589
- logPrimary(`MCP endpoint: http://localhost:${port}/mcp`);
590
- logPrimary(`Health check: http://localhost:${port}/health`);
726
+ console.log(`Server running on http://localhost:${port}`);
727
+ console.log(`MCP endpoint: http://localhost:${port}/mcp`);
728
+ console.log(`Health check: http://localhost:${port}/health`);
591
729
  resolve({
592
730
  listener,
593
731
  port
@@ -664,6 +802,7 @@ var MCPServer = class {
664
802
  if (options.autoDiscover !== false) {
665
803
  await this.autoDiscoverServices(options.mcpDir, options.serviceFactories);
666
804
  }
805
+ await this.loadUIManifest();
667
806
  }
668
807
  /**
669
808
  * Wait for initialization to complete
@@ -745,14 +884,18 @@ var MCPServer = class {
745
884
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
746
885
  const tools = [];
747
886
  for (const [name, tool] of this.tools.entries()) {
748
- tools.push({
887
+ const toolDef = {
749
888
  name,
750
889
  description: tool.description,
751
890
  inputSchema: tool.inputSchema || {
752
891
  type: "object",
753
892
  properties: {}
754
893
  }
755
- });
894
+ };
895
+ if (tool._meta && Object.keys(tool._meta).length > 0) {
896
+ toolDef._meta = tool._meta;
897
+ }
898
+ tools.push(toolDef);
756
899
  }
757
900
  return {
758
901
  tools
@@ -783,7 +926,7 @@ var MCPServer = class {
783
926
  } else {
784
927
  formattedResult = String(result);
785
928
  }
786
- return {
929
+ const response = {
787
930
  content: [
788
931
  {
789
932
  type: "text",
@@ -791,6 +934,10 @@ var MCPServer = class {
791
934
  }
792
935
  ]
793
936
  };
937
+ if (tool._meta && Object.keys(tool._meta).length > 0) {
938
+ response._meta = tool._meta;
939
+ }
940
+ return response;
794
941
  } catch (error) {
795
942
  return {
796
943
  content: [
@@ -829,12 +976,20 @@ var MCPServer = class {
829
976
  }
830
977
  try {
831
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
+ }
832
987
  return {
833
988
  contents: [
834
989
  {
835
990
  uri,
836
991
  mimeType: resource.mimeType,
837
- text: typeof result === "string" ? result : JSON.stringify(result, null, 2)
992
+ text
838
993
  }
839
994
  ]
840
995
  };
@@ -980,16 +1135,19 @@ var MCPServer = class {
980
1135
  if (inputClass) {
981
1136
  inputSchema = classToJsonSchemaWithConstraints(inputClass);
982
1137
  }
1138
+ const toolMeta = Reflect.getMetadata?.("tool:meta", method) || {};
983
1139
  this.tools.set(methodMeta.toolName, {
984
1140
  name: methodMeta.toolName,
985
1141
  description: methodMeta.toolDescription || "",
986
1142
  inputSchema,
987
1143
  method,
988
1144
  instance,
989
- propertyKey
1145
+ propertyKey,
1146
+ _meta: Object.keys(toolMeta).length > 0 ? toolMeta : void 0
990
1147
  });
991
1148
  if (this.logging) {
992
- 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}`);
993
1151
  }
994
1152
  }
995
1153
  const promptMethods = getDecoratedMethods(cls, "prompt:name");
@@ -1042,6 +1200,53 @@ var MCPServer = class {
1042
1200
  }
1043
1201
  }
1044
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
+ /**
1045
1250
  * Get the underlying MCP SDK Server instance
1046
1251
  * Attaches waitForInit method for HTTP server initialization
1047
1252
  */
package/package.json CHANGED
@@ -1,71 +1,71 @@
1
- {
2
- "name": "@leanmcp/core",
3
- "version": "0.3.2",
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.4",
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
+ }