@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/LICENSE +21 -21
- package/README.md +555 -555
- package/dist/index.d.mts +50 -44
- package/dist/index.d.ts +50 -44
- package/dist/index.js +247 -42
- package/dist/index.mjs +246 -41
- package/package.json +71 -71
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
477
|
-
|
|
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
|
-
|
|
481
|
-
|
|
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 ||
|
|
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
|
-
} :
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
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
|
-
|
|
624
|
+
mode: isStateless ? "stateless" : "stateful",
|
|
625
|
+
activeSessions: isStateless ? 0 : Object.keys(transports).length,
|
|
547
626
|
uptime: process.uptime()
|
|
548
627
|
});
|
|
549
628
|
});
|
|
550
|
-
const
|
|
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
|
-
}, "
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
*/
|