@leanmcp/core 0.1.2 → 0.3.0
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/README.md +214 -13
- package/dist/index.d.mts +72 -7
- package/dist/index.d.ts +72 -7
- package/dist/index.js +931 -665
- package/dist/index.mjs +251 -29
- package/package.json +7 -7
- package/dist/chunk-O6YSETKJ.mjs +0 -6
- package/dist/dist-LQ4M5W3M.mjs +0 -587
package/dist/index.mjs
CHANGED
|
@@ -392,7 +392,26 @@ function isInitializeRequest(body) {
|
|
|
392
392
|
return body && body.method === "initialize";
|
|
393
393
|
}
|
|
394
394
|
__name(isInitializeRequest, "isInitializeRequest");
|
|
395
|
-
async function createHTTPServer(
|
|
395
|
+
async function createHTTPServer(serverInput, options) {
|
|
396
|
+
let serverFactory;
|
|
397
|
+
let httpOptions;
|
|
398
|
+
if (typeof serverInput === "function") {
|
|
399
|
+
serverFactory = serverInput;
|
|
400
|
+
httpOptions = options || {};
|
|
401
|
+
} else {
|
|
402
|
+
const serverOptions = serverInput;
|
|
403
|
+
const { MCPServer: MCPServer2 } = await import("./index.mjs");
|
|
404
|
+
serverFactory = /* @__PURE__ */ __name(async () => {
|
|
405
|
+
const mcpServer2 = new MCPServer2(serverOptions);
|
|
406
|
+
return mcpServer2.getServer();
|
|
407
|
+
}, "serverFactory");
|
|
408
|
+
httpOptions = {
|
|
409
|
+
port: serverOptions.port,
|
|
410
|
+
cors: serverOptions.cors,
|
|
411
|
+
logging: serverOptions.logging,
|
|
412
|
+
sessionTimeout: serverOptions.sessionTimeout
|
|
413
|
+
};
|
|
414
|
+
}
|
|
396
415
|
const [express, { StreamableHTTPServerTransport }, cors] = await Promise.all([
|
|
397
416
|
// @ts-ignore
|
|
398
417
|
import("express").catch(() => {
|
|
@@ -403,19 +422,20 @@ async function createHTTPServer(serverFactory, options = {}) {
|
|
|
403
422
|
throw new Error("MCP SDK not found. Install with: npm install @modelcontextprotocol/sdk");
|
|
404
423
|
}),
|
|
405
424
|
// @ts-ignore
|
|
406
|
-
|
|
425
|
+
httpOptions.cors ? import("cors").catch(() => null) : Promise.resolve(null)
|
|
407
426
|
]);
|
|
408
427
|
const app = express.default();
|
|
409
|
-
const port =
|
|
428
|
+
const port = httpOptions.port || 3001;
|
|
410
429
|
validatePort(port);
|
|
411
430
|
const transports = {};
|
|
412
|
-
|
|
413
|
-
|
|
431
|
+
let mcpServer = null;
|
|
432
|
+
const logger = httpOptions.logger || new Logger({
|
|
433
|
+
level: httpOptions.logging ? LogLevel.INFO : LogLevel.NONE,
|
|
414
434
|
prefix: "HTTP"
|
|
415
435
|
});
|
|
416
|
-
if (cors &&
|
|
417
|
-
const corsOptions = typeof
|
|
418
|
-
origin:
|
|
436
|
+
if (cors && httpOptions.cors) {
|
|
437
|
+
const corsOptions = typeof httpOptions.cors === "object" ? {
|
|
438
|
+
origin: httpOptions.cors.origin || false,
|
|
419
439
|
methods: [
|
|
420
440
|
"GET",
|
|
421
441
|
"POST",
|
|
@@ -431,7 +451,7 @@ async function createHTTPServer(serverFactory, options = {}) {
|
|
|
431
451
|
exposedHeaders: [
|
|
432
452
|
"mcp-session-id"
|
|
433
453
|
],
|
|
434
|
-
credentials:
|
|
454
|
+
credentials: httpOptions.cors.credentials ?? false,
|
|
435
455
|
maxAge: 86400
|
|
436
456
|
} : false;
|
|
437
457
|
if (corsOptions) {
|
|
@@ -450,6 +470,18 @@ async function createHTTPServer(serverFactory, options = {}) {
|
|
|
450
470
|
const handleMCPRequest = /* @__PURE__ */ __name(async (req, res) => {
|
|
451
471
|
const sessionId = req.headers["mcp-session-id"];
|
|
452
472
|
let transport;
|
|
473
|
+
const method = req.body?.method || "unknown";
|
|
474
|
+
const params = req.body?.params;
|
|
475
|
+
let logMessage = `${req.method} /mcp - ${method}`;
|
|
476
|
+
if (params?.name) {
|
|
477
|
+
logMessage += ` [${params.name}]`;
|
|
478
|
+
} else if (params?.uri) {
|
|
479
|
+
logMessage += ` [${params.uri}]`;
|
|
480
|
+
}
|
|
481
|
+
if (sessionId) {
|
|
482
|
+
logMessage += ` (session: ${sessionId.substring(0, 8)}...)`;
|
|
483
|
+
}
|
|
484
|
+
logger.info(logMessage);
|
|
453
485
|
try {
|
|
454
486
|
if (sessionId && transports[sessionId]) {
|
|
455
487
|
transport = transports[sessionId];
|
|
@@ -469,8 +501,10 @@ async function createHTTPServer(serverFactory, options = {}) {
|
|
|
469
501
|
logger.debug(`Session cleaned up: ${transport.sessionId}`);
|
|
470
502
|
}
|
|
471
503
|
};
|
|
472
|
-
|
|
473
|
-
|
|
504
|
+
if (!mcpServer) {
|
|
505
|
+
throw new Error("MCP server not initialized");
|
|
506
|
+
}
|
|
507
|
+
await mcpServer.connect(transport);
|
|
474
508
|
} else {
|
|
475
509
|
res.status(400).json({
|
|
476
510
|
jsonrpc: "2.0",
|
|
@@ -499,18 +533,39 @@ async function createHTTPServer(serverFactory, options = {}) {
|
|
|
499
533
|
}, "handleMCPRequest");
|
|
500
534
|
app.post("/mcp", handleMCPRequest);
|
|
501
535
|
app.delete("/mcp", handleMCPRequest);
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
536
|
+
return new Promise(async (resolve, reject) => {
|
|
537
|
+
try {
|
|
538
|
+
mcpServer = await serverFactory();
|
|
539
|
+
if (mcpServer && typeof mcpServer.waitForInit === "function") {
|
|
540
|
+
await mcpServer.waitForInit();
|
|
541
|
+
}
|
|
542
|
+
const listener = app.listen(port, () => {
|
|
543
|
+
logger.info(`Server running on http://localhost:${port}`);
|
|
544
|
+
logger.info(`MCP endpoint: http://localhost:${port}/mcp`);
|
|
545
|
+
logger.info(`Health check: http://localhost:${port}/health`);
|
|
546
|
+
resolve(listener);
|
|
547
|
+
});
|
|
548
|
+
listener.on("error", (error) => {
|
|
549
|
+
logger.error(`Server error: ${error.message}`);
|
|
550
|
+
reject(error);
|
|
551
|
+
});
|
|
552
|
+
const cleanup = /* @__PURE__ */ __name(() => {
|
|
553
|
+
logger.info("\nShutting down server...");
|
|
554
|
+
Object.values(transports).forEach((t) => t.close?.());
|
|
555
|
+
listener.close(() => {
|
|
556
|
+
logger.info("Server closed");
|
|
557
|
+
process.exit(0);
|
|
558
|
+
});
|
|
559
|
+
setTimeout(() => {
|
|
560
|
+
logger.warn("Forcing shutdown...");
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}, 5e3);
|
|
563
|
+
}, "cleanup");
|
|
564
|
+
process.on("SIGINT", cleanup);
|
|
565
|
+
process.on("SIGTERM", cleanup);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
reject(error);
|
|
568
|
+
}
|
|
514
569
|
});
|
|
515
570
|
}
|
|
516
571
|
__name(createHTTPServer, "createHTTPServer");
|
|
@@ -527,10 +582,18 @@ var MCPServer = class {
|
|
|
527
582
|
resources = /* @__PURE__ */ new Map();
|
|
528
583
|
logging;
|
|
529
584
|
logger;
|
|
585
|
+
options;
|
|
586
|
+
initPromise;
|
|
587
|
+
autoDiscovered = false;
|
|
530
588
|
constructor(options) {
|
|
589
|
+
this.options = options;
|
|
531
590
|
this.logging = options.logging || false;
|
|
591
|
+
let logLevel = LogLevel.NONE;
|
|
592
|
+
if (options.logging) {
|
|
593
|
+
logLevel = options.debug ? LogLevel.DEBUG : LogLevel.INFO;
|
|
594
|
+
}
|
|
532
595
|
this.logger = new Logger({
|
|
533
|
-
level:
|
|
596
|
+
level: logLevel,
|
|
534
597
|
prefix: "MCPServer"
|
|
535
598
|
});
|
|
536
599
|
this.server = new Server({
|
|
@@ -539,11 +602,84 @@ var MCPServer = class {
|
|
|
539
602
|
}, {
|
|
540
603
|
capabilities: {
|
|
541
604
|
tools: {},
|
|
542
|
-
|
|
543
|
-
|
|
605
|
+
prompts: {},
|
|
606
|
+
resources: {}
|
|
544
607
|
}
|
|
545
608
|
});
|
|
546
609
|
this.setupHandlers();
|
|
610
|
+
this.initPromise = this.autoInit();
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Internal initialization - runs automatically in constructor
|
|
614
|
+
*/
|
|
615
|
+
async autoInit() {
|
|
616
|
+
const options = this.options;
|
|
617
|
+
if (options.autoDiscover !== false) {
|
|
618
|
+
await this.autoDiscoverServices(options.mcpDir, options.serviceFactories);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Wait for initialization to complete
|
|
623
|
+
* This is called internally by createHTTPServer
|
|
624
|
+
*/
|
|
625
|
+
async waitForInit() {
|
|
626
|
+
await this.initPromise;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Automatically discover and register services from the mcp directory
|
|
630
|
+
* Called by init() unless autoDiscover is set to false
|
|
631
|
+
*/
|
|
632
|
+
async autoDiscoverServices(customMcpDir, serviceFactories) {
|
|
633
|
+
if (this.autoDiscovered) return;
|
|
634
|
+
this.autoDiscovered = true;
|
|
635
|
+
try {
|
|
636
|
+
let mcpDir;
|
|
637
|
+
if (customMcpDir) {
|
|
638
|
+
mcpDir = customMcpDir;
|
|
639
|
+
} else {
|
|
640
|
+
const callerFile = this.getCallerFile();
|
|
641
|
+
if (callerFile) {
|
|
642
|
+
const callerDir = path.dirname(callerFile);
|
|
643
|
+
mcpDir = path.join(callerDir, "mcp");
|
|
644
|
+
} else {
|
|
645
|
+
mcpDir = path.join(process.cwd(), "mcp");
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (fs.existsSync(mcpDir)) {
|
|
649
|
+
this.logger.debug(`Auto-discovering services from: ${mcpDir}`);
|
|
650
|
+
await this.autoRegisterServices(mcpDir, serviceFactories);
|
|
651
|
+
} else {
|
|
652
|
+
this.logger.debug(`MCP directory not found at ${mcpDir}, skipping auto-discovery`);
|
|
653
|
+
}
|
|
654
|
+
} catch (error) {
|
|
655
|
+
this.logger.warn(`Auto-discovery failed: ${error.message}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Get the file path of the caller (the file that instantiated MCPServer)
|
|
660
|
+
*/
|
|
661
|
+
getCallerFile() {
|
|
662
|
+
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
663
|
+
try {
|
|
664
|
+
const err = new Error();
|
|
665
|
+
Error.prepareStackTrace = (_, stack2) => stack2;
|
|
666
|
+
const stack = err.stack;
|
|
667
|
+
for (let i = 0; i < stack.length; i++) {
|
|
668
|
+
let fileName = stack[i].getFileName();
|
|
669
|
+
if (fileName && !fileName.includes("@leanmcp") && !fileName.includes("leanmcp-sdk\\packages\\core") && !fileName.includes("leanmcp-sdk/packages/core") && (fileName.endsWith(".ts") || fileName.endsWith(".js") || fileName.endsWith(".mjs"))) {
|
|
670
|
+
if (fileName.startsWith("file://")) {
|
|
671
|
+
fileName = fileName.replace("file:///", "").replace("file://", "");
|
|
672
|
+
if (process.platform === "win32" && fileName.startsWith("/")) {
|
|
673
|
+
fileName = fileName.substring(1);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return fileName;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return null;
|
|
680
|
+
} finally {
|
|
681
|
+
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
682
|
+
}
|
|
547
683
|
}
|
|
548
684
|
setupHandlers() {
|
|
549
685
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -688,6 +824,90 @@ var MCPServer = class {
|
|
|
688
824
|
});
|
|
689
825
|
}
|
|
690
826
|
/**
|
|
827
|
+
* Auto-register all services from the mcp directory
|
|
828
|
+
* Scans the directory recursively and registers all exported classes
|
|
829
|
+
*
|
|
830
|
+
* @param mcpDir - Path to the mcp directory containing service files
|
|
831
|
+
* @param serviceFactories - Optional map of service class names to factory functions for dependency injection
|
|
832
|
+
*
|
|
833
|
+
* @example
|
|
834
|
+
* // Auto-register services with no dependencies
|
|
835
|
+
* await server.autoRegisterServices('./mcp');
|
|
836
|
+
*
|
|
837
|
+
* @example
|
|
838
|
+
* // Auto-register with dependency injection
|
|
839
|
+
* await server.autoRegisterServices('./mcp', {
|
|
840
|
+
* SlackService: () => new SlackService(process.env.SLACK_TOKEN),
|
|
841
|
+
* AuthService: () => new AuthService(authProvider)
|
|
842
|
+
* });
|
|
843
|
+
*/
|
|
844
|
+
async autoRegisterServices(mcpDir, serviceFactories) {
|
|
845
|
+
this.logger.debug(`Auto-registering services from: ${mcpDir}`);
|
|
846
|
+
if (!fs.existsSync(mcpDir)) {
|
|
847
|
+
this.logger.warn(`MCP directory not found: ${mcpDir}`);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const serviceFiles = this.findServiceFiles(mcpDir);
|
|
851
|
+
this.logger.debug(`Found ${serviceFiles.length} service file(s)`);
|
|
852
|
+
for (const filePath of serviceFiles) {
|
|
853
|
+
try {
|
|
854
|
+
await this.loadAndRegisterService(filePath, serviceFactories);
|
|
855
|
+
} catch (error) {
|
|
856
|
+
this.logger.error(`Failed to load service from ${filePath}: ${error.message}`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Recursively find all index.ts/index.js files in the mcp directory
|
|
862
|
+
*/
|
|
863
|
+
findServiceFiles(dir) {
|
|
864
|
+
const files = [];
|
|
865
|
+
const entries = fs.readdirSync(dir, {
|
|
866
|
+
withFileTypes: true
|
|
867
|
+
});
|
|
868
|
+
for (const entry of entries) {
|
|
869
|
+
const fullPath = path.join(dir, entry.name);
|
|
870
|
+
if (entry.isDirectory()) {
|
|
871
|
+
files.push(...this.findServiceFiles(fullPath));
|
|
872
|
+
} else if (entry.isFile()) {
|
|
873
|
+
if (entry.name === "index.ts" || entry.name === "index.js") {
|
|
874
|
+
files.push(fullPath);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return files;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Load a service file and register all exported classes
|
|
882
|
+
*/
|
|
883
|
+
async loadAndRegisterService(filePath, serviceFactories) {
|
|
884
|
+
this.logger.debug(`Loading service from: ${filePath}`);
|
|
885
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
886
|
+
const module = await import(fileUrl);
|
|
887
|
+
let registeredCount = 0;
|
|
888
|
+
for (const [exportName, exportValue] of Object.entries(module)) {
|
|
889
|
+
if (typeof exportValue === "function" && exportValue.prototype) {
|
|
890
|
+
try {
|
|
891
|
+
let instance;
|
|
892
|
+
if (serviceFactories && serviceFactories[exportName]) {
|
|
893
|
+
instance = serviceFactories[exportName]();
|
|
894
|
+
this.logger.info(`Using factory for service: ${exportName}`);
|
|
895
|
+
} else {
|
|
896
|
+
instance = new exportValue();
|
|
897
|
+
}
|
|
898
|
+
this.registerService(instance);
|
|
899
|
+
registeredCount++;
|
|
900
|
+
this.logger.debug(`Registered service: ${exportName} from ${path.basename(filePath)}`);
|
|
901
|
+
} catch (error) {
|
|
902
|
+
this.logger.warn(`Skipped ${exportName}: ${error.message}`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (registeredCount === 0) {
|
|
907
|
+
this.logger.warn(`No services registered from ${filePath}`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
691
911
|
* Register a service instance with decorated methods
|
|
692
912
|
*/
|
|
693
913
|
registerService(instance) {
|
|
@@ -709,7 +929,7 @@ var MCPServer = class {
|
|
|
709
929
|
propertyKey
|
|
710
930
|
});
|
|
711
931
|
if (this.logging) {
|
|
712
|
-
this.logger.
|
|
932
|
+
this.logger.debug(`Registered tool: ${methodMeta.toolName}${inputClass ? " (class-based schema)" : ""}`);
|
|
713
933
|
}
|
|
714
934
|
}
|
|
715
935
|
const promptMethods = getDecoratedMethods(cls, "prompt:name");
|
|
@@ -734,7 +954,7 @@ var MCPServer = class {
|
|
|
734
954
|
propertyKey
|
|
735
955
|
});
|
|
736
956
|
if (this.logging) {
|
|
737
|
-
this.logger.
|
|
957
|
+
this.logger.debug(`Registered prompt: ${methodMeta.promptName}`);
|
|
738
958
|
}
|
|
739
959
|
}
|
|
740
960
|
const resourceMethods = getDecoratedMethods(cls, "resource:uri");
|
|
@@ -757,14 +977,16 @@ var MCPServer = class {
|
|
|
757
977
|
propertyKey
|
|
758
978
|
});
|
|
759
979
|
if (this.logging) {
|
|
760
|
-
this.logger.
|
|
980
|
+
this.logger.debug(`Registered resource: ${methodMeta.resourceUri}`);
|
|
761
981
|
}
|
|
762
982
|
}
|
|
763
983
|
}
|
|
764
984
|
/**
|
|
765
985
|
* Get the underlying MCP SDK Server instance
|
|
986
|
+
* Attaches waitForInit method for HTTP server initialization
|
|
766
987
|
*/
|
|
767
988
|
getServer() {
|
|
989
|
+
this.server.waitForInit = () => this.waitForInit();
|
|
768
990
|
return this.server;
|
|
769
991
|
}
|
|
770
992
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leanmcp/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Core library implementing decorators, reflection, and MCP runtime server",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
"test:watch": "jest --watch"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"reflect-metadata": "^0.2.1",
|
|
28
27
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
28
|
+
"ajv": "^8.12.0",
|
|
29
29
|
"dotenv": "^16.3.1",
|
|
30
|
-
"
|
|
30
|
+
"reflect-metadata": "^0.2.1"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"
|
|
34
|
-
"
|
|
33
|
+
"cors": "^2.8.5",
|
|
34
|
+
"express": "^5.0.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"express": {
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@types/
|
|
45
|
+
"@types/cors": "^2.8.0",
|
|
46
46
|
"@types/express": "^5.0.0",
|
|
47
|
-
"@types/
|
|
47
|
+
"@types/node": "^20.0.0"
|
|
48
48
|
},
|
|
49
49
|
"repository": {
|
|
50
50
|
"type": "git",
|