@leanmcp/core 0.1.1 → 0.2.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 CHANGED
@@ -75,19 +75,68 @@ export class SentimentService {
75
75
 
76
76
  ### 2. Create and Start Server
77
77
 
78
+ #### Option A: Zero-Config Auto-Discovery (Recommended)
79
+
80
+ Automatically discover and register all services from the `./mcp` directory:
81
+
82
+ ```typescript
83
+ import { createHTTPServer, MCPServer } from "@leanmcp/core";
84
+
85
+ // Create MCP server with auto-discovery
86
+ const serverFactory = async () => {
87
+ const server = new MCPServer({
88
+ name: "my-mcp-server",
89
+ version: "1.0.0",
90
+ logging: true // Enable HTTP server logs
91
+ });
92
+
93
+ // Services are automatically discovered and registered from ./mcp directory
94
+ return server.getServer();
95
+ };
96
+
97
+ // Start HTTP server
98
+ await createHTTPServer(serverFactory, {
99
+ port: 3000,
100
+ cors: true,
101
+ logging: true // Log HTTP requests
102
+ });
103
+
104
+ console.log('\nMCP Server running');
105
+ console.log('HTTP endpoint: http://localhost:3000/mcp');
106
+ console.log('Health check: http://localhost:3000/health');
107
+ ```
108
+
109
+ **Directory Structure:**
110
+ ```
111
+ your-project/
112
+ ├── main.ts
113
+ └── mcp/
114
+ ├── sentiment/
115
+ │ └── index.ts # export class SentimentService
116
+ ├── weather/
117
+ │ └── index.ts # export class WeatherService
118
+ └── database/
119
+ └── index.ts # export class DatabaseService
120
+ ```
121
+
122
+ #### Option B: Manual Registration
123
+
124
+ Manually import and register each service:
125
+
78
126
  ```typescript
79
127
  import { createHTTPServer, MCPServer } from "@leanmcp/core";
80
128
  import { SentimentService } from "./services/sentiment";
81
129
 
82
130
  // Create MCP server
83
- const serverFactory = () => {
131
+ const serverFactory = async () => {
84
132
  const server = new MCPServer({
85
133
  name: "my-mcp-server",
86
134
  version: "1.0.0",
87
- logging: true
135
+ logging: true,
136
+ autoDiscover: false // Disable auto-discovery for manual registration
88
137
  });
89
138
 
90
- // Register services
139
+ // Register services manually
91
140
  server.registerService(new SentimentService());
92
141
 
93
142
  return server.getServer();
@@ -97,7 +146,7 @@ const serverFactory = () => {
97
146
  await createHTTPServer(serverFactory, {
98
147
  port: 3000,
99
148
  cors: true,
100
- logging: true
149
+ logging: true // Log HTTP requests
101
150
  });
102
151
  ```
103
152
 
@@ -220,13 +269,137 @@ Main server class for registering services.
220
269
 
221
270
  ```typescript
222
271
  const server = new MCPServer({
223
- name: string; // Server name
224
- version: string; // Server version
225
- logging?: boolean; // Enable logging (default: false)
272
+ name: string; // Server name
273
+ version: string; // Server version
274
+ logging?: boolean; // Enable logging (default: false)
275
+ debug?: boolean; // Enable verbose debug logs (default: false)
276
+ autoDiscover?: boolean; // Enable auto-discovery (default: true)
277
+ mcpDir?: string; // Custom mcp directory path (optional)
226
278
  });
227
279
 
280
+ // Manual registration
228
281
  server.registerService(instance: any): void;
229
- server.getServer(): Server; // Get underlying MCP SDK server
282
+
283
+ // Get underlying MCP SDK server
284
+ server.getServer(): Server;
285
+ ```
286
+
287
+ **Options:**
288
+
289
+ - **`logging`**: Enable basic logging for server operations
290
+ - **`debug`**: Enable verbose debug logs showing detailed service registration (requires `logging: true`)
291
+ - **`autoDiscover`**: Automatically discover and register services from `./mcp` directory (default: `true`)
292
+ - **`mcpDir`**: Custom path to the mcp directory (default: auto-detected `./mcp`)
293
+
294
+ #### Zero-Config Auto-Discovery
295
+
296
+ Services are automatically discovered and registered from the `./mcp` directory when the server is created:
297
+
298
+ **Basic Usage:**
299
+ ```typescript
300
+ const serverFactory = async () => {
301
+ const server = new MCPServer({
302
+ name: "my-server",
303
+ version: "1.0.0",
304
+ logging: true // Enable logging
305
+ });
306
+
307
+ // Services are automatically discovered and registered
308
+ return server.getServer();
309
+ };
310
+ ```
311
+
312
+ **With Debug Logging:**
313
+ ```typescript
314
+ const serverFactory = async () => {
315
+ const server = new MCPServer({
316
+ name: "my-server",
317
+ version: "1.0.0",
318
+ logging: true,
319
+ debug: true // Show detailed service registration logs
320
+ });
321
+
322
+ return server.getServer();
323
+ };
324
+ ```
325
+
326
+ **With Shared Dependencies:**
327
+
328
+ For services that need shared dependencies, create a `config.ts` (example) file in your `mcp` directory:
329
+
330
+ ```typescript
331
+ // mcp/config.ts
332
+ import { AuthProvider } from "@leanmcp/auth";
333
+
334
+ if (!process.env.COGNITO_USER_POOL_ID || !process.env.COGNITO_CLIENT_ID) {
335
+ throw new Error('Missing required Cognito configuration');
336
+ }
337
+
338
+ export const authProvider = new AuthProvider('cognito', {
339
+ region: process.env.AWS_REGION || 'us-east-1',
340
+ userPoolId: process.env.COGNITO_USER_POOL_ID,
341
+ clientId: process.env.COGNITO_CLIENT_ID,
342
+ clientSecret: process.env.COGNITO_CLIENT_SECRET
343
+ });
344
+
345
+ await authProvider.init();
346
+ ```
347
+
348
+ Then import in your services:
349
+
350
+ ```typescript
351
+ // mcp/slack/index.ts
352
+ import { Tool } from "@leanmcp/core";
353
+ import { Authenticated } from "@leanmcp/auth";
354
+ import { authProvider } from "../config.js";
355
+
356
+ @Authenticated(authProvider)
357
+ export class SlackService {
358
+ constructor() {
359
+ // No parameters needed - use environment or imported config
360
+ }
361
+
362
+ @Tool({ description: 'Send a message' })
363
+ async sendMessage(args: any) {
364
+ // Implementation
365
+ }
366
+ }
367
+ ```
368
+
369
+ Your main file stays clean:
370
+
371
+ ```typescript
372
+ const serverFactory = async () => {
373
+ const server = new MCPServer({
374
+ name: "my-server",
375
+ version: "1.0.0",
376
+ logging: true
377
+ });
378
+
379
+ // Services are automatically discovered and registered
380
+ return server.getServer();
381
+ };
382
+ ```
383
+
384
+ **How It Works:**
385
+ - Automatically discovers and registers services from the `./mcp` directory during server initialization
386
+ - Recursively scans for `index.ts` or `index.js` files
387
+ - Dynamically imports each file and looks for exported classes
388
+ - Instantiates services with no-args constructors
389
+ - Registers all discovered services with their decorated methods
390
+
391
+ **Directory Structure:**
392
+ ```
393
+ your-project/
394
+ ├── main.ts
395
+ └── mcp/
396
+ ├── config.ts # Optional: shared dependencies
397
+ ├── slack/
398
+ │ └── index.ts # export class SlackService
399
+ ├── database/
400
+ │ └── index.ts # export class DatabaseService
401
+ └── auth/
402
+ └── index.ts # export class AuthService
230
403
  ```
231
404
 
232
405
  ### createHTTPServer
@@ -235,11 +408,11 @@ Create and start an HTTP server with streamable transport.
235
408
 
236
409
  ```typescript
237
410
  await createHTTPServer(
238
- serverFactory: () => Server,
411
+ serverFactory: () => Server | Promise<Server>,
239
412
  options: {
240
413
  port?: number; // Port number (default: 3000)
241
414
  cors?: boolean; // Enable CORS (default: false)
242
- logging?: boolean; // Enable logging (default: true)
415
+ logging?: boolean; // Enable HTTP request logging (default: false)
243
416
  }
244
417
  );
245
418
  ```
package/dist/index.d.mts CHANGED
@@ -225,8 +225,9 @@ interface MCPServerFactory {
225
225
  }
226
226
  /**
227
227
  * Create an HTTP server for MCP with Streamable HTTP transport
228
+ * Returns the HTTP server instance to keep the process alive
228
229
  */
229
- declare function createHTTPServer(serverFactory: MCPServerFactory, options?: HTTPServerOptions): Promise<void>;
230
+ declare function createHTTPServer(serverFactory: MCPServerFactory, options?: HTTPServerOptions): Promise<any>;
230
231
 
231
232
  /**
232
233
  * Input validation utilities for LeanMCP
@@ -315,6 +316,15 @@ interface MCPServerOptions {
315
316
  cors?: boolean;
316
317
  logging?: boolean;
317
318
  }
319
+ interface MCPServerConstructorOptions {
320
+ name: string;
321
+ version: string;
322
+ logging?: boolean;
323
+ debug?: boolean;
324
+ autoDiscover?: boolean;
325
+ mcpDir?: string;
326
+ serviceFactories?: Record<string, () => any>;
327
+ }
318
328
  interface RegisteredTool {
319
329
  name: string;
320
330
  description: string;
@@ -352,18 +362,63 @@ declare class MCPServer {
352
362
  private resources;
353
363
  private logging;
354
364
  private logger;
355
- constructor(options: {
356
- name: string;
357
- version: string;
358
- logging?: boolean;
359
- });
365
+ private options;
366
+ private initPromise;
367
+ private autoDiscovered;
368
+ constructor(options: MCPServerConstructorOptions);
369
+ /**
370
+ * Internal initialization - runs automatically in constructor
371
+ */
372
+ private autoInit;
373
+ /**
374
+ * Wait for initialization to complete
375
+ * This is called internally by createHTTPServer
376
+ */
377
+ waitForInit(): Promise<void>;
378
+ /**
379
+ * Automatically discover and register services from the mcp directory
380
+ * Called by init() unless autoDiscover is set to false
381
+ */
382
+ private autoDiscoverServices;
383
+ /**
384
+ * Get the file path of the caller (the file that instantiated MCPServer)
385
+ */
386
+ private getCallerFile;
360
387
  private setupHandlers;
388
+ /**
389
+ * Auto-register all services from the mcp directory
390
+ * Scans the directory recursively and registers all exported classes
391
+ *
392
+ * @param mcpDir - Path to the mcp directory containing service files
393
+ * @param serviceFactories - Optional map of service class names to factory functions for dependency injection
394
+ *
395
+ * @example
396
+ * // Auto-register services with no dependencies
397
+ * await server.autoRegisterServices('./mcp');
398
+ *
399
+ * @example
400
+ * // Auto-register with dependency injection
401
+ * await server.autoRegisterServices('./mcp', {
402
+ * SlackService: () => new SlackService(process.env.SLACK_TOKEN),
403
+ * AuthService: () => new AuthService(authProvider)
404
+ * });
405
+ */
406
+ autoRegisterServices(mcpDir: string, serviceFactories?: Record<string, () => any>): Promise<void>;
407
+ /**
408
+ * Recursively find all index.ts/index.js files in the mcp directory
409
+ */
410
+ private findServiceFiles;
411
+ /**
412
+ * Load a service file and register all exported classes
413
+ */
414
+ private loadAndRegisterService;
361
415
  /**
362
416
  * Register a service instance with decorated methods
363
417
  */
364
418
  registerService(instance: any): void;
365
419
  /**
366
420
  * Get the underlying MCP SDK Server instance
421
+ * Attaches waitForInit method for HTTP server initialization
367
422
  */
368
423
  getServer(): Server<{
369
424
  method: string;
@@ -432,4 +487,4 @@ declare class MCPServerRuntime {
432
487
  */
433
488
  declare function startMCPServer(options: MCPServerOptions): Promise<MCPServerRuntime>;
434
489
 
435
- export { Auth, type AuthOptions, Deprecated, type HTTPServerOptions, LogLevel, Logger, type LoggerOptions, MCPServer, type MCPServerFactory, type MCPServerOptions, MCPServerRuntime, Optional, Prompt, type PromptOptions, Render, Resource, type ResourceOptions, SchemaConstraint, Tool, type ToolOptions, UI, UserEnvs, classToJsonSchema, classToJsonSchemaWithConstraints, createHTTPServer, defaultLogger, getDecoratedMethods, getMethodMetadata, startMCPServer, validateNonEmpty, validatePath, validatePort, validateServiceName, validateUrl };
490
+ export { Auth, type AuthOptions, Deprecated, type HTTPServerOptions, LogLevel, Logger, type LoggerOptions, MCPServer, type MCPServerConstructorOptions, type MCPServerFactory, type MCPServerOptions, MCPServerRuntime, Optional, Prompt, type PromptOptions, Render, Resource, type ResourceOptions, SchemaConstraint, Tool, type ToolOptions, UI, UserEnvs, classToJsonSchema, classToJsonSchemaWithConstraints, createHTTPServer, defaultLogger, getDecoratedMethods, getMethodMetadata, startMCPServer, validateNonEmpty, validatePath, validatePort, validateServiceName, validateUrl };
package/dist/index.d.ts CHANGED
@@ -225,8 +225,9 @@ interface MCPServerFactory {
225
225
  }
226
226
  /**
227
227
  * Create an HTTP server for MCP with Streamable HTTP transport
228
+ * Returns the HTTP server instance to keep the process alive
228
229
  */
229
- declare function createHTTPServer(serverFactory: MCPServerFactory, options?: HTTPServerOptions): Promise<void>;
230
+ declare function createHTTPServer(serverFactory: MCPServerFactory, options?: HTTPServerOptions): Promise<any>;
230
231
 
231
232
  /**
232
233
  * Input validation utilities for LeanMCP
@@ -315,6 +316,15 @@ interface MCPServerOptions {
315
316
  cors?: boolean;
316
317
  logging?: boolean;
317
318
  }
319
+ interface MCPServerConstructorOptions {
320
+ name: string;
321
+ version: string;
322
+ logging?: boolean;
323
+ debug?: boolean;
324
+ autoDiscover?: boolean;
325
+ mcpDir?: string;
326
+ serviceFactories?: Record<string, () => any>;
327
+ }
318
328
  interface RegisteredTool {
319
329
  name: string;
320
330
  description: string;
@@ -352,18 +362,63 @@ declare class MCPServer {
352
362
  private resources;
353
363
  private logging;
354
364
  private logger;
355
- constructor(options: {
356
- name: string;
357
- version: string;
358
- logging?: boolean;
359
- });
365
+ private options;
366
+ private initPromise;
367
+ private autoDiscovered;
368
+ constructor(options: MCPServerConstructorOptions);
369
+ /**
370
+ * Internal initialization - runs automatically in constructor
371
+ */
372
+ private autoInit;
373
+ /**
374
+ * Wait for initialization to complete
375
+ * This is called internally by createHTTPServer
376
+ */
377
+ waitForInit(): Promise<void>;
378
+ /**
379
+ * Automatically discover and register services from the mcp directory
380
+ * Called by init() unless autoDiscover is set to false
381
+ */
382
+ private autoDiscoverServices;
383
+ /**
384
+ * Get the file path of the caller (the file that instantiated MCPServer)
385
+ */
386
+ private getCallerFile;
360
387
  private setupHandlers;
388
+ /**
389
+ * Auto-register all services from the mcp directory
390
+ * Scans the directory recursively and registers all exported classes
391
+ *
392
+ * @param mcpDir - Path to the mcp directory containing service files
393
+ * @param serviceFactories - Optional map of service class names to factory functions for dependency injection
394
+ *
395
+ * @example
396
+ * // Auto-register services with no dependencies
397
+ * await server.autoRegisterServices('./mcp');
398
+ *
399
+ * @example
400
+ * // Auto-register with dependency injection
401
+ * await server.autoRegisterServices('./mcp', {
402
+ * SlackService: () => new SlackService(process.env.SLACK_TOKEN),
403
+ * AuthService: () => new AuthService(authProvider)
404
+ * });
405
+ */
406
+ autoRegisterServices(mcpDir: string, serviceFactories?: Record<string, () => any>): Promise<void>;
407
+ /**
408
+ * Recursively find all index.ts/index.js files in the mcp directory
409
+ */
410
+ private findServiceFiles;
411
+ /**
412
+ * Load a service file and register all exported classes
413
+ */
414
+ private loadAndRegisterService;
361
415
  /**
362
416
  * Register a service instance with decorated methods
363
417
  */
364
418
  registerService(instance: any): void;
365
419
  /**
366
420
  * Get the underlying MCP SDK Server instance
421
+ * Attaches waitForInit method for HTTP server initialization
367
422
  */
368
423
  getServer(): Server<{
369
424
  method: string;
@@ -432,4 +487,4 @@ declare class MCPServerRuntime {
432
487
  */
433
488
  declare function startMCPServer(options: MCPServerOptions): Promise<MCPServerRuntime>;
434
489
 
435
- export { Auth, type AuthOptions, Deprecated, type HTTPServerOptions, LogLevel, Logger, type LoggerOptions, MCPServer, type MCPServerFactory, type MCPServerOptions, MCPServerRuntime, Optional, Prompt, type PromptOptions, Render, Resource, type ResourceOptions, SchemaConstraint, Tool, type ToolOptions, UI, UserEnvs, classToJsonSchema, classToJsonSchemaWithConstraints, createHTTPServer, defaultLogger, getDecoratedMethods, getMethodMetadata, startMCPServer, validateNonEmpty, validatePath, validatePort, validateServiceName, validateUrl };
490
+ export { Auth, type AuthOptions, Deprecated, type HTTPServerOptions, LogLevel, Logger, type LoggerOptions, MCPServer, type MCPServerConstructorOptions, type MCPServerFactory, type MCPServerOptions, MCPServerRuntime, Optional, Prompt, type PromptOptions, Render, Resource, type ResourceOptions, SchemaConstraint, Tool, type ToolOptions, UI, UserEnvs, classToJsonSchema, classToJsonSchemaWithConstraints, createHTTPServer, defaultLogger, getDecoratedMethods, getMethodMetadata, startMCPServer, validateNonEmpty, validatePath, validatePort, validateServiceName, validateUrl };
package/dist/index.js CHANGED
@@ -466,6 +466,7 @@ async function createHTTPServer(serverFactory, options = {}) {
466
466
  const port = options.port || 3001;
467
467
  validatePort(port);
468
468
  const transports = {};
469
+ let mcpServer = null;
469
470
  const logger = options.logger || new Logger({
470
471
  level: options.logging ? LogLevel.INFO : LogLevel.NONE,
471
472
  prefix: "HTTP"
@@ -507,6 +508,18 @@ async function createHTTPServer(serverFactory, options = {}) {
507
508
  const handleMCPRequest = /* @__PURE__ */ __name(async (req, res) => {
508
509
  const sessionId = req.headers["mcp-session-id"];
509
510
  let transport;
511
+ const method = req.body?.method || "unknown";
512
+ const params = req.body?.params;
513
+ let logMessage = `${req.method} /mcp - ${method}`;
514
+ if (params?.name) {
515
+ logMessage += ` [${params.name}]`;
516
+ } else if (params?.uri) {
517
+ logMessage += ` [${params.uri}]`;
518
+ }
519
+ if (sessionId) {
520
+ logMessage += ` (session: ${sessionId.substring(0, 8)}...)`;
521
+ }
522
+ logger.info(logMessage);
510
523
  try {
511
524
  if (sessionId && transports[sessionId]) {
512
525
  transport = transports[sessionId];
@@ -526,8 +539,10 @@ async function createHTTPServer(serverFactory, options = {}) {
526
539
  logger.debug(`Session cleaned up: ${transport.sessionId}`);
527
540
  }
528
541
  };
529
- const server = await serverFactory();
530
- await server.connect(transport);
542
+ if (!mcpServer) {
543
+ throw new Error("MCP server not initialized");
544
+ }
545
+ await mcpServer.connect(transport);
531
546
  } else {
532
547
  res.status(400).json({
533
548
  jsonrpc: "2.0",
@@ -556,18 +571,39 @@ async function createHTTPServer(serverFactory, options = {}) {
556
571
  }, "handleMCPRequest");
557
572
  app.post("/mcp", handleMCPRequest);
558
573
  app.delete("/mcp", handleMCPRequest);
559
- process.on("SIGINT", () => {
560
- logger.info("\nShutting down server...");
561
- Object.values(transports).forEach((t) => t.close?.());
562
- process.exit(0);
563
- });
564
- return new Promise((resolve) => {
565
- app.listen(port, () => {
566
- logger.info(`Server running on http://localhost:${port}`);
567
- logger.info(`MCP endpoint: http://localhost:${port}/mcp`);
568
- logger.info(`Health check: http://localhost:${port}/health`);
569
- resolve();
570
- });
574
+ return new Promise(async (resolve, reject) => {
575
+ try {
576
+ mcpServer = await serverFactory();
577
+ if (mcpServer && typeof mcpServer.waitForInit === "function") {
578
+ await mcpServer.waitForInit();
579
+ }
580
+ const listener = app.listen(port, () => {
581
+ logger.info(`Server running on http://localhost:${port}`);
582
+ logger.info(`MCP endpoint: http://localhost:${port}/mcp`);
583
+ logger.info(`Health check: http://localhost:${port}/health`);
584
+ resolve(listener);
585
+ });
586
+ listener.on("error", (error) => {
587
+ logger.error(`Server error: ${error.message}`);
588
+ reject(error);
589
+ });
590
+ const cleanup = /* @__PURE__ */ __name(() => {
591
+ logger.info("\nShutting down server...");
592
+ Object.values(transports).forEach((t) => t.close?.());
593
+ listener.close(() => {
594
+ logger.info("Server closed");
595
+ process.exit(0);
596
+ });
597
+ setTimeout(() => {
598
+ logger.warn("Forcing shutdown...");
599
+ process.exit(1);
600
+ }, 5e3);
601
+ }, "cleanup");
602
+ process.on("SIGINT", cleanup);
603
+ process.on("SIGTERM", cleanup);
604
+ } catch (error) {
605
+ reject(error);
606
+ }
571
607
  });
572
608
  }
573
609
  __name(createHTTPServer, "createHTTPServer");
@@ -584,10 +620,18 @@ var MCPServer = class {
584
620
  resources = /* @__PURE__ */ new Map();
585
621
  logging;
586
622
  logger;
623
+ options;
624
+ initPromise;
625
+ autoDiscovered = false;
587
626
  constructor(options) {
627
+ this.options = options;
588
628
  this.logging = options.logging || false;
629
+ let logLevel = LogLevel.NONE;
630
+ if (options.logging) {
631
+ logLevel = options.debug ? LogLevel.DEBUG : LogLevel.INFO;
632
+ }
589
633
  this.logger = new Logger({
590
- level: this.logging ? LogLevel.INFO : LogLevel.NONE,
634
+ level: logLevel,
591
635
  prefix: "MCPServer"
592
636
  });
593
637
  this.server = new import_server.Server({
@@ -596,11 +640,84 @@ var MCPServer = class {
596
640
  }, {
597
641
  capabilities: {
598
642
  tools: {},
599
- resources: {},
600
- prompts: {}
643
+ prompts: {},
644
+ resources: {}
601
645
  }
602
646
  });
603
647
  this.setupHandlers();
648
+ this.initPromise = this.autoInit();
649
+ }
650
+ /**
651
+ * Internal initialization - runs automatically in constructor
652
+ */
653
+ async autoInit() {
654
+ const options = this.options;
655
+ if (options.autoDiscover !== false) {
656
+ await this.autoDiscoverServices(options.mcpDir, options.serviceFactories);
657
+ }
658
+ }
659
+ /**
660
+ * Wait for initialization to complete
661
+ * This is called internally by createHTTPServer
662
+ */
663
+ async waitForInit() {
664
+ await this.initPromise;
665
+ }
666
+ /**
667
+ * Automatically discover and register services from the mcp directory
668
+ * Called by init() unless autoDiscover is set to false
669
+ */
670
+ async autoDiscoverServices(customMcpDir, serviceFactories) {
671
+ if (this.autoDiscovered) return;
672
+ this.autoDiscovered = true;
673
+ try {
674
+ let mcpDir;
675
+ if (customMcpDir) {
676
+ mcpDir = customMcpDir;
677
+ } else {
678
+ const callerFile = this.getCallerFile();
679
+ if (callerFile) {
680
+ const callerDir = import_path.default.dirname(callerFile);
681
+ mcpDir = import_path.default.join(callerDir, "mcp");
682
+ } else {
683
+ mcpDir = import_path.default.join(process.cwd(), "mcp");
684
+ }
685
+ }
686
+ if (import_fs.default.existsSync(mcpDir)) {
687
+ this.logger.debug(`Auto-discovering services from: ${mcpDir}`);
688
+ await this.autoRegisterServices(mcpDir, serviceFactories);
689
+ } else {
690
+ this.logger.debug(`MCP directory not found at ${mcpDir}, skipping auto-discovery`);
691
+ }
692
+ } catch (error) {
693
+ this.logger.warn(`Auto-discovery failed: ${error.message}`);
694
+ }
695
+ }
696
+ /**
697
+ * Get the file path of the caller (the file that instantiated MCPServer)
698
+ */
699
+ getCallerFile() {
700
+ const originalPrepareStackTrace = Error.prepareStackTrace;
701
+ try {
702
+ const err = new Error();
703
+ Error.prepareStackTrace = (_, stack2) => stack2;
704
+ const stack = err.stack;
705
+ for (let i = 0; i < stack.length; i++) {
706
+ let fileName = stack[i].getFileName();
707
+ 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"))) {
708
+ if (fileName.startsWith("file://")) {
709
+ fileName = fileName.replace("file:///", "").replace("file://", "");
710
+ if (process.platform === "win32" && fileName.startsWith("/")) {
711
+ fileName = fileName.substring(1);
712
+ }
713
+ }
714
+ return fileName;
715
+ }
716
+ }
717
+ return null;
718
+ } finally {
719
+ Error.prepareStackTrace = originalPrepareStackTrace;
720
+ }
604
721
  }
605
722
  setupHandlers() {
606
723
  this.server.setRequestHandler(import_types.ListToolsRequestSchema, async () => {
@@ -634,7 +751,7 @@ var MCPServer = class {
634
751
  }
635
752
  }
636
753
  try {
637
- const meta = request.params.arguments?._meta;
754
+ const meta = request.params._meta;
638
755
  const result = await tool.method.call(tool.instance, request.params.arguments, meta);
639
756
  let formattedResult = result;
640
757
  if (methodMeta.renderFormat === "markdown" && typeof result === "string") {
@@ -745,6 +862,90 @@ var MCPServer = class {
745
862
  });
746
863
  }
747
864
  /**
865
+ * Auto-register all services from the mcp directory
866
+ * Scans the directory recursively and registers all exported classes
867
+ *
868
+ * @param mcpDir - Path to the mcp directory containing service files
869
+ * @param serviceFactories - Optional map of service class names to factory functions for dependency injection
870
+ *
871
+ * @example
872
+ * // Auto-register services with no dependencies
873
+ * await server.autoRegisterServices('./mcp');
874
+ *
875
+ * @example
876
+ * // Auto-register with dependency injection
877
+ * await server.autoRegisterServices('./mcp', {
878
+ * SlackService: () => new SlackService(process.env.SLACK_TOKEN),
879
+ * AuthService: () => new AuthService(authProvider)
880
+ * });
881
+ */
882
+ async autoRegisterServices(mcpDir, serviceFactories) {
883
+ this.logger.debug(`Auto-registering services from: ${mcpDir}`);
884
+ if (!import_fs.default.existsSync(mcpDir)) {
885
+ this.logger.warn(`MCP directory not found: ${mcpDir}`);
886
+ return;
887
+ }
888
+ const serviceFiles = this.findServiceFiles(mcpDir);
889
+ this.logger.debug(`Found ${serviceFiles.length} service file(s)`);
890
+ for (const filePath of serviceFiles) {
891
+ try {
892
+ await this.loadAndRegisterService(filePath, serviceFactories);
893
+ } catch (error) {
894
+ this.logger.error(`Failed to load service from ${filePath}: ${error.message}`);
895
+ }
896
+ }
897
+ }
898
+ /**
899
+ * Recursively find all index.ts/index.js files in the mcp directory
900
+ */
901
+ findServiceFiles(dir) {
902
+ const files = [];
903
+ const entries = import_fs.default.readdirSync(dir, {
904
+ withFileTypes: true
905
+ });
906
+ for (const entry of entries) {
907
+ const fullPath = import_path.default.join(dir, entry.name);
908
+ if (entry.isDirectory()) {
909
+ files.push(...this.findServiceFiles(fullPath));
910
+ } else if (entry.isFile()) {
911
+ if (entry.name === "index.ts" || entry.name === "index.js") {
912
+ files.push(fullPath);
913
+ }
914
+ }
915
+ }
916
+ return files;
917
+ }
918
+ /**
919
+ * Load a service file and register all exported classes
920
+ */
921
+ async loadAndRegisterService(filePath, serviceFactories) {
922
+ this.logger.debug(`Loading service from: ${filePath}`);
923
+ const fileUrl = (0, import_url.pathToFileURL)(filePath).href;
924
+ const module2 = await import(fileUrl);
925
+ let registeredCount = 0;
926
+ for (const [exportName, exportValue] of Object.entries(module2)) {
927
+ if (typeof exportValue === "function" && exportValue.prototype) {
928
+ try {
929
+ let instance;
930
+ if (serviceFactories && serviceFactories[exportName]) {
931
+ instance = serviceFactories[exportName]();
932
+ this.logger.info(`Using factory for service: ${exportName}`);
933
+ } else {
934
+ instance = new exportValue();
935
+ }
936
+ this.registerService(instance);
937
+ registeredCount++;
938
+ this.logger.debug(`Registered service: ${exportName} from ${import_path.default.basename(filePath)}`);
939
+ } catch (error) {
940
+ this.logger.warn(`Skipped ${exportName}: ${error.message}`);
941
+ }
942
+ }
943
+ }
944
+ if (registeredCount === 0) {
945
+ this.logger.warn(`No services registered from ${filePath}`);
946
+ }
947
+ }
948
+ /**
748
949
  * Register a service instance with decorated methods
749
950
  */
750
951
  registerService(instance) {
@@ -766,7 +967,7 @@ var MCPServer = class {
766
967
  propertyKey
767
968
  });
768
969
  if (this.logging) {
769
- this.logger.info(`Registered tool: ${methodMeta.toolName}${inputClass ? " (class-based schema)" : ""}`);
970
+ this.logger.debug(`Registered tool: ${methodMeta.toolName}${inputClass ? " (class-based schema)" : ""}`);
770
971
  }
771
972
  }
772
973
  const promptMethods = getDecoratedMethods(cls, "prompt:name");
@@ -791,7 +992,7 @@ var MCPServer = class {
791
992
  propertyKey
792
993
  });
793
994
  if (this.logging) {
794
- this.logger.info(`Registered prompt: ${methodMeta.promptName}`);
995
+ this.logger.debug(`Registered prompt: ${methodMeta.promptName}`);
795
996
  }
796
997
  }
797
998
  const resourceMethods = getDecoratedMethods(cls, "resource:uri");
@@ -814,14 +1015,16 @@ var MCPServer = class {
814
1015
  propertyKey
815
1016
  });
816
1017
  if (this.logging) {
817
- this.logger.info(`Registered resource: ${methodMeta.resourceUri}`);
1018
+ this.logger.debug(`Registered resource: ${methodMeta.resourceUri}`);
818
1019
  }
819
1020
  }
820
1021
  }
821
1022
  /**
822
1023
  * Get the underlying MCP SDK Server instance
1024
+ * Attaches waitForInit method for HTTP server initialization
823
1025
  */
824
1026
  getServer() {
1027
+ this.server.waitForInit = () => this.waitForInit();
825
1028
  return this.server;
826
1029
  }
827
1030
  };
@@ -890,7 +1093,7 @@ var MCPServerRuntime = class {
890
1093
  }
891
1094
  }
892
1095
  try {
893
- const meta = request.params.arguments?._meta;
1096
+ const meta = request.params._meta;
894
1097
  const result = await tool.method.call(tool.instance, request.params.arguments, meta);
895
1098
  if (result && typeof result === "object" && result.type === "elicitation") {
896
1099
  return {
package/dist/index.mjs CHANGED
@@ -409,6 +409,7 @@ async function createHTTPServer(serverFactory, options = {}) {
409
409
  const port = options.port || 3001;
410
410
  validatePort(port);
411
411
  const transports = {};
412
+ let mcpServer = null;
412
413
  const logger = options.logger || new Logger({
413
414
  level: options.logging ? LogLevel.INFO : LogLevel.NONE,
414
415
  prefix: "HTTP"
@@ -450,6 +451,18 @@ async function createHTTPServer(serverFactory, options = {}) {
450
451
  const handleMCPRequest = /* @__PURE__ */ __name(async (req, res) => {
451
452
  const sessionId = req.headers["mcp-session-id"];
452
453
  let transport;
454
+ const method = req.body?.method || "unknown";
455
+ const params = req.body?.params;
456
+ let logMessage = `${req.method} /mcp - ${method}`;
457
+ if (params?.name) {
458
+ logMessage += ` [${params.name}]`;
459
+ } else if (params?.uri) {
460
+ logMessage += ` [${params.uri}]`;
461
+ }
462
+ if (sessionId) {
463
+ logMessage += ` (session: ${sessionId.substring(0, 8)}...)`;
464
+ }
465
+ logger.info(logMessage);
453
466
  try {
454
467
  if (sessionId && transports[sessionId]) {
455
468
  transport = transports[sessionId];
@@ -469,8 +482,10 @@ async function createHTTPServer(serverFactory, options = {}) {
469
482
  logger.debug(`Session cleaned up: ${transport.sessionId}`);
470
483
  }
471
484
  };
472
- const server = await serverFactory();
473
- await server.connect(transport);
485
+ if (!mcpServer) {
486
+ throw new Error("MCP server not initialized");
487
+ }
488
+ await mcpServer.connect(transport);
474
489
  } else {
475
490
  res.status(400).json({
476
491
  jsonrpc: "2.0",
@@ -499,18 +514,39 @@ async function createHTTPServer(serverFactory, options = {}) {
499
514
  }, "handleMCPRequest");
500
515
  app.post("/mcp", handleMCPRequest);
501
516
  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
- });
517
+ return new Promise(async (resolve, reject) => {
518
+ try {
519
+ mcpServer = await serverFactory();
520
+ if (mcpServer && typeof mcpServer.waitForInit === "function") {
521
+ await mcpServer.waitForInit();
522
+ }
523
+ const listener = app.listen(port, () => {
524
+ logger.info(`Server running on http://localhost:${port}`);
525
+ logger.info(`MCP endpoint: http://localhost:${port}/mcp`);
526
+ logger.info(`Health check: http://localhost:${port}/health`);
527
+ resolve(listener);
528
+ });
529
+ listener.on("error", (error) => {
530
+ logger.error(`Server error: ${error.message}`);
531
+ reject(error);
532
+ });
533
+ const cleanup = /* @__PURE__ */ __name(() => {
534
+ logger.info("\nShutting down server...");
535
+ Object.values(transports).forEach((t) => t.close?.());
536
+ listener.close(() => {
537
+ logger.info("Server closed");
538
+ process.exit(0);
539
+ });
540
+ setTimeout(() => {
541
+ logger.warn("Forcing shutdown...");
542
+ process.exit(1);
543
+ }, 5e3);
544
+ }, "cleanup");
545
+ process.on("SIGINT", cleanup);
546
+ process.on("SIGTERM", cleanup);
547
+ } catch (error) {
548
+ reject(error);
549
+ }
514
550
  });
515
551
  }
516
552
  __name(createHTTPServer, "createHTTPServer");
@@ -527,10 +563,18 @@ var MCPServer = class {
527
563
  resources = /* @__PURE__ */ new Map();
528
564
  logging;
529
565
  logger;
566
+ options;
567
+ initPromise;
568
+ autoDiscovered = false;
530
569
  constructor(options) {
570
+ this.options = options;
531
571
  this.logging = options.logging || false;
572
+ let logLevel = LogLevel.NONE;
573
+ if (options.logging) {
574
+ logLevel = options.debug ? LogLevel.DEBUG : LogLevel.INFO;
575
+ }
532
576
  this.logger = new Logger({
533
- level: this.logging ? LogLevel.INFO : LogLevel.NONE,
577
+ level: logLevel,
534
578
  prefix: "MCPServer"
535
579
  });
536
580
  this.server = new Server({
@@ -539,11 +583,84 @@ var MCPServer = class {
539
583
  }, {
540
584
  capabilities: {
541
585
  tools: {},
542
- resources: {},
543
- prompts: {}
586
+ prompts: {},
587
+ resources: {}
544
588
  }
545
589
  });
546
590
  this.setupHandlers();
591
+ this.initPromise = this.autoInit();
592
+ }
593
+ /**
594
+ * Internal initialization - runs automatically in constructor
595
+ */
596
+ async autoInit() {
597
+ const options = this.options;
598
+ if (options.autoDiscover !== false) {
599
+ await this.autoDiscoverServices(options.mcpDir, options.serviceFactories);
600
+ }
601
+ }
602
+ /**
603
+ * Wait for initialization to complete
604
+ * This is called internally by createHTTPServer
605
+ */
606
+ async waitForInit() {
607
+ await this.initPromise;
608
+ }
609
+ /**
610
+ * Automatically discover and register services from the mcp directory
611
+ * Called by init() unless autoDiscover is set to false
612
+ */
613
+ async autoDiscoverServices(customMcpDir, serviceFactories) {
614
+ if (this.autoDiscovered) return;
615
+ this.autoDiscovered = true;
616
+ try {
617
+ let mcpDir;
618
+ if (customMcpDir) {
619
+ mcpDir = customMcpDir;
620
+ } else {
621
+ const callerFile = this.getCallerFile();
622
+ if (callerFile) {
623
+ const callerDir = path.dirname(callerFile);
624
+ mcpDir = path.join(callerDir, "mcp");
625
+ } else {
626
+ mcpDir = path.join(process.cwd(), "mcp");
627
+ }
628
+ }
629
+ if (fs.existsSync(mcpDir)) {
630
+ this.logger.debug(`Auto-discovering services from: ${mcpDir}`);
631
+ await this.autoRegisterServices(mcpDir, serviceFactories);
632
+ } else {
633
+ this.logger.debug(`MCP directory not found at ${mcpDir}, skipping auto-discovery`);
634
+ }
635
+ } catch (error) {
636
+ this.logger.warn(`Auto-discovery failed: ${error.message}`);
637
+ }
638
+ }
639
+ /**
640
+ * Get the file path of the caller (the file that instantiated MCPServer)
641
+ */
642
+ getCallerFile() {
643
+ const originalPrepareStackTrace = Error.prepareStackTrace;
644
+ try {
645
+ const err = new Error();
646
+ Error.prepareStackTrace = (_, stack2) => stack2;
647
+ const stack = err.stack;
648
+ for (let i = 0; i < stack.length; i++) {
649
+ let fileName = stack[i].getFileName();
650
+ 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"))) {
651
+ if (fileName.startsWith("file://")) {
652
+ fileName = fileName.replace("file:///", "").replace("file://", "");
653
+ if (process.platform === "win32" && fileName.startsWith("/")) {
654
+ fileName = fileName.substring(1);
655
+ }
656
+ }
657
+ return fileName;
658
+ }
659
+ }
660
+ return null;
661
+ } finally {
662
+ Error.prepareStackTrace = originalPrepareStackTrace;
663
+ }
547
664
  }
548
665
  setupHandlers() {
549
666
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -577,7 +694,7 @@ var MCPServer = class {
577
694
  }
578
695
  }
579
696
  try {
580
- const meta = request.params.arguments?._meta;
697
+ const meta = request.params._meta;
581
698
  const result = await tool.method.call(tool.instance, request.params.arguments, meta);
582
699
  let formattedResult = result;
583
700
  if (methodMeta.renderFormat === "markdown" && typeof result === "string") {
@@ -688,6 +805,90 @@ var MCPServer = class {
688
805
  });
689
806
  }
690
807
  /**
808
+ * Auto-register all services from the mcp directory
809
+ * Scans the directory recursively and registers all exported classes
810
+ *
811
+ * @param mcpDir - Path to the mcp directory containing service files
812
+ * @param serviceFactories - Optional map of service class names to factory functions for dependency injection
813
+ *
814
+ * @example
815
+ * // Auto-register services with no dependencies
816
+ * await server.autoRegisterServices('./mcp');
817
+ *
818
+ * @example
819
+ * // Auto-register with dependency injection
820
+ * await server.autoRegisterServices('./mcp', {
821
+ * SlackService: () => new SlackService(process.env.SLACK_TOKEN),
822
+ * AuthService: () => new AuthService(authProvider)
823
+ * });
824
+ */
825
+ async autoRegisterServices(mcpDir, serviceFactories) {
826
+ this.logger.debug(`Auto-registering services from: ${mcpDir}`);
827
+ if (!fs.existsSync(mcpDir)) {
828
+ this.logger.warn(`MCP directory not found: ${mcpDir}`);
829
+ return;
830
+ }
831
+ const serviceFiles = this.findServiceFiles(mcpDir);
832
+ this.logger.debug(`Found ${serviceFiles.length} service file(s)`);
833
+ for (const filePath of serviceFiles) {
834
+ try {
835
+ await this.loadAndRegisterService(filePath, serviceFactories);
836
+ } catch (error) {
837
+ this.logger.error(`Failed to load service from ${filePath}: ${error.message}`);
838
+ }
839
+ }
840
+ }
841
+ /**
842
+ * Recursively find all index.ts/index.js files in the mcp directory
843
+ */
844
+ findServiceFiles(dir) {
845
+ const files = [];
846
+ const entries = fs.readdirSync(dir, {
847
+ withFileTypes: true
848
+ });
849
+ for (const entry of entries) {
850
+ const fullPath = path.join(dir, entry.name);
851
+ if (entry.isDirectory()) {
852
+ files.push(...this.findServiceFiles(fullPath));
853
+ } else if (entry.isFile()) {
854
+ if (entry.name === "index.ts" || entry.name === "index.js") {
855
+ files.push(fullPath);
856
+ }
857
+ }
858
+ }
859
+ return files;
860
+ }
861
+ /**
862
+ * Load a service file and register all exported classes
863
+ */
864
+ async loadAndRegisterService(filePath, serviceFactories) {
865
+ this.logger.debug(`Loading service from: ${filePath}`);
866
+ const fileUrl = pathToFileURL(filePath).href;
867
+ const module = await import(fileUrl);
868
+ let registeredCount = 0;
869
+ for (const [exportName, exportValue] of Object.entries(module)) {
870
+ if (typeof exportValue === "function" && exportValue.prototype) {
871
+ try {
872
+ let instance;
873
+ if (serviceFactories && serviceFactories[exportName]) {
874
+ instance = serviceFactories[exportName]();
875
+ this.logger.info(`Using factory for service: ${exportName}`);
876
+ } else {
877
+ instance = new exportValue();
878
+ }
879
+ this.registerService(instance);
880
+ registeredCount++;
881
+ this.logger.debug(`Registered service: ${exportName} from ${path.basename(filePath)}`);
882
+ } catch (error) {
883
+ this.logger.warn(`Skipped ${exportName}: ${error.message}`);
884
+ }
885
+ }
886
+ }
887
+ if (registeredCount === 0) {
888
+ this.logger.warn(`No services registered from ${filePath}`);
889
+ }
890
+ }
891
+ /**
691
892
  * Register a service instance with decorated methods
692
893
  */
693
894
  registerService(instance) {
@@ -709,7 +910,7 @@ var MCPServer = class {
709
910
  propertyKey
710
911
  });
711
912
  if (this.logging) {
712
- this.logger.info(`Registered tool: ${methodMeta.toolName}${inputClass ? " (class-based schema)" : ""}`);
913
+ this.logger.debug(`Registered tool: ${methodMeta.toolName}${inputClass ? " (class-based schema)" : ""}`);
713
914
  }
714
915
  }
715
916
  const promptMethods = getDecoratedMethods(cls, "prompt:name");
@@ -734,7 +935,7 @@ var MCPServer = class {
734
935
  propertyKey
735
936
  });
736
937
  if (this.logging) {
737
- this.logger.info(`Registered prompt: ${methodMeta.promptName}`);
938
+ this.logger.debug(`Registered prompt: ${methodMeta.promptName}`);
738
939
  }
739
940
  }
740
941
  const resourceMethods = getDecoratedMethods(cls, "resource:uri");
@@ -757,14 +958,16 @@ var MCPServer = class {
757
958
  propertyKey
758
959
  });
759
960
  if (this.logging) {
760
- this.logger.info(`Registered resource: ${methodMeta.resourceUri}`);
961
+ this.logger.debug(`Registered resource: ${methodMeta.resourceUri}`);
761
962
  }
762
963
  }
763
964
  }
764
965
  /**
765
966
  * Get the underlying MCP SDK Server instance
967
+ * Attaches waitForInit method for HTTP server initialization
766
968
  */
767
969
  getServer() {
970
+ this.server.waitForInit = () => this.waitForInit();
768
971
  return this.server;
769
972
  }
770
973
  };
@@ -833,7 +1036,7 @@ var MCPServerRuntime = class {
833
1036
  }
834
1037
  }
835
1038
  try {
836
- const meta = request.params.arguments?._meta;
1039
+ const meta = request.params._meta;
837
1040
  const result = await tool.method.call(tool.instance, request.params.arguments, meta);
838
1041
  if (result && typeof result === "object" && result.type === "elicitation") {
839
1042
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leanmcp/core",
3
- "version": "0.1.1",
3
+ "version": "0.2.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",