@leanmcp/core 0.3.3 → 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 +234 -35
- package/dist/index.mjs +233 -34
- 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([
|
|
@@ -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 ||
|
|
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
|
-
} :
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
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
|
-
|
|
624
|
+
mode: isStateless ? "stateless" : "stateful",
|
|
625
|
+
activeSessions: isStateless ? 0 : Object.keys(transports).length,
|
|
553
626
|
uptime: process.uptime()
|
|
554
627
|
});
|
|
555
628
|
});
|
|
556
|
-
const
|
|
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
|
-
}, "
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
*/
|