@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/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(serverFactory, options = {}) {
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
- options.cors ? import("cors").catch(() => null) : Promise.resolve(null)
425
+ httpOptions.cors ? import("cors").catch(() => null) : Promise.resolve(null)
407
426
  ]);
408
427
  const app = express.default();
409
- const port = options.port || 3001;
428
+ const port = httpOptions.port || 3001;
410
429
  validatePort(port);
411
430
  const transports = {};
412
- const logger = options.logger || new Logger({
413
- level: options.logging ? LogLevel.INFO : LogLevel.NONE,
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 && options.cors) {
417
- const corsOptions = typeof options.cors === "object" ? {
418
- origin: options.cors.origin || false,
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: options.cors.credentials ?? false,
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
- const server = await serverFactory();
473
- await server.connect(transport);
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
- process.on("SIGINT", () => {
503
- logger.info("\nShutting down server...");
504
- Object.values(transports).forEach((t) => t.close?.());
505
- process.exit(0);
506
- });
507
- return new Promise((resolve) => {
508
- app.listen(port, () => {
509
- logger.info(`Server running on http://localhost:${port}`);
510
- logger.info(`MCP endpoint: http://localhost:${port}/mcp`);
511
- logger.info(`Health check: http://localhost:${port}/health`);
512
- resolve();
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: this.logging ? LogLevel.INFO : LogLevel.NONE,
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
- resources: {},
543
- prompts: {}
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.info(`Registered tool: ${methodMeta.toolName}${inputClass ? " (class-based schema)" : ""}`);
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.info(`Registered prompt: ${methodMeta.promptName}`);
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.info(`Registered resource: ${methodMeta.resourceUri}`);
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.1.2",
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
- "ajv": "^8.12.0"
30
+ "reflect-metadata": "^0.2.1"
31
31
  },
32
32
  "peerDependencies": {
33
- "express": "^5.0.0",
34
- "cors": "^2.8.5"
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/node": "^20.0.0",
45
+ "@types/cors": "^2.8.0",
46
46
  "@types/express": "^5.0.0",
47
- "@types/cors": "^2.8.0"
47
+ "@types/node": "^20.0.0"
48
48
  },
49
49
  "repository": {
50
50
  "type": "git",
@@ -1,6 +0,0 @@
1
- var __defProp = Object.defineProperty;
2
- var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
-
4
- export {
5
- __name
6
- };