@leanmcp/core 0.3.9 → 0.3.11

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
@@ -1,15 +1,42 @@
1
- # @leanmcp/core
2
-
3
- Core library for building Model Context Protocol (MCP) servers with TypeScript decorators and declarative schema definition.
1
+ <p align="center">
2
+ <img
3
+ src="https://raw.githubusercontent.com/LeanMCP/leanmcp-sdk/refs/heads/main/assets/logo.svg"
4
+ alt="LeanMCP Logo"
5
+ width="400"
6
+ />
7
+ </p>
8
+
9
+ <p align="center">
10
+ <strong>@leanmcp/core</strong><br/>
11
+ Core library for building MCP servers with TypeScript decorators and declarative schema definition.
12
+ </p>
13
+
14
+ <p align="center">
15
+ <a href="https://www.npmjs.com/package/@leanmcp/core">
16
+ <img src="https://img.shields.io/npm/v/@leanmcp/core" alt="npm version" />
17
+ </a>
18
+ <a href="https://www.npmjs.com/package/@leanmcp/core">
19
+ <img src="https://img.shields.io/npm/dm/@leanmcp/core" alt="npm downloads" />
20
+ </a>
21
+ <a href="https://docs.leanmcp.com/sdk/core">
22
+ <img src="https://img.shields.io/badge/Docs-leanmcp-0A66C2?" />
23
+ </a>
24
+ <a href="https://discord.com/invite/DsRcA3GwPy">
25
+ <img src="https://img.shields.io/badge/Discord-Join-5865F2?logo=discord&logoColor=white" />
26
+ </a>
27
+ <a href="https://x.com/LeanMcp">
28
+ <img src="https://img.shields.io/badge/@LeanMCP-f5f5f5?logo=x&logoColor=000000" />
29
+ </a>
30
+ </p>
4
31
 
5
32
  ## Features
6
33
 
7
- - **Type-safe decorators** - `@Tool`, `@Prompt`, `@Resource` with full TypeScript support
8
- - **Schema generation** - Define JSON Schema declaratively using `@SchemaConstraint` decorators on class properties
9
- - **Streamable HTTP transport** - Production-ready HTTP server with session management
10
- - **Input validation** - Built-in AJV validation for all inputs
11
- - **Clean API** - Function names automatically become tool/prompt/resource names
12
- - **MCP compliant** - Built on official `@modelcontextprotocol/sdk`
34
+ - **Type-Safe Decorators** `@Tool`, `@Prompt`, `@Resource` with full TypeScript support
35
+ - **Auto-Discovery** — Zero-config service discovery from `./mcp` directory
36
+ - **Schema Generation** Declarative JSON Schema with `@SchemaConstraint` decorators
37
+ - **HTTP Transport** Production-ready HTTP server with session management
38
+ - **Input Validation** — Built-in AJV validation for all inputs
39
+ - **MCP Compliant** Built on official `@modelcontextprotocol/sdk`
13
40
 
14
41
  ## Installation
15
42
 
@@ -17,8 +44,6 @@ Core library for building Model Context Protocol (MCP) servers with TypeScript d
17
44
  npm install @leanmcp/core
18
45
  ```
19
46
 
20
- ### Peer Dependencies
21
-
22
47
  For HTTP server support:
23
48
  ```bash
24
49
  npm install express cors
@@ -26,12 +51,42 @@ npm install express cors
26
51
 
27
52
  ## Quick Start
28
53
 
29
- ### 1. Define Your Service with Class-Based Schema
54
+ ### Zero-Config (Recommended)
55
+
56
+ The simplest way to create an MCP server with auto-discovery:
30
57
 
31
58
  ```typescript
59
+ import { createHTTPServer } from "@leanmcp/core";
60
+
61
+ await createHTTPServer({
62
+ name: "my-mcp-server",
63
+ version: "1.0.0",
64
+ port: 3001,
65
+ cors: true,
66
+ logging: true
67
+ });
68
+
69
+ // Services are automatically discovered from ./mcp directory
70
+ ```
71
+
72
+ **Directory Structure:**
73
+ ```
74
+ your-project/
75
+ ├── main.ts
76
+ └── mcp/
77
+ ├── sentiment/
78
+ │ └── index.ts # export class SentimentService
79
+ ├── weather/
80
+ │ └── index.ts # export class WeatherService
81
+ └── config.ts # Optional: shared dependencies
82
+ ```
83
+
84
+ ### Define a Service
85
+
86
+ ```typescript
87
+ // mcp/sentiment/index.ts
32
88
  import { Tool, SchemaConstraint, Optional } from "@leanmcp/core";
33
89
 
34
- // Define input schema as a class
35
90
  class AnalyzeSentimentInput {
36
91
  @SchemaConstraint({
37
92
  description: 'Text to analyze',
@@ -48,109 +103,27 @@ class AnalyzeSentimentInput {
48
103
  language?: string;
49
104
  }
50
105
 
51
- // Define output schema
52
- class AnalyzeSentimentOutput {
53
- @SchemaConstraint({ enum: ['positive', 'negative', 'neutral'] })
54
- sentiment!: string;
55
-
56
- @SchemaConstraint({ minimum: -1, maximum: 1 })
57
- score!: number;
58
- }
59
-
60
106
  export class SentimentService {
61
107
  @Tool({
62
108
  description: 'Analyze sentiment of text',
63
109
  inputClass: AnalyzeSentimentInput
64
110
  })
65
- async analyzeSentiment(input: AnalyzeSentimentInput): Promise<AnalyzeSentimentOutput> {
66
- // Your implementation
111
+ async analyzeSentiment(input: AnalyzeSentimentInput) {
67
112
  return {
68
113
  sentiment: 'positive',
69
- score: 0.8,
70
- confidence: 0.95
114
+ score: 0.8
71
115
  };
72
116
  }
73
117
  }
74
118
  ```
75
119
 
76
- ### 2. Create and Start Server
77
-
78
- #### Option A: Zero-Config Auto-Discovery (Recommended)
79
-
80
- The simplest way to create an HTTP server with auto-discovery:
81
-
82
- ```typescript
83
- import { createHTTPServer } from "@leanmcp/core";
84
-
85
- // Create and start HTTP server with auto-discovery
86
- await createHTTPServer({
87
- name: "my-mcp-server",
88
- version: "1.0.0",
89
- port: 3000,
90
- cors: true,
91
- logging: true
92
- });
93
-
94
- console.log('\nMCP Server running');
95
- console.log('HTTP endpoint: http://localhost:3000/mcp');
96
- console.log('Health check: http://localhost:3000/health');
97
- ```
98
-
99
- **What happens automatically:**
100
- - Services are discovered from `./mcp` directory
101
- - HTTP server is created and started
102
- - Session management is configured
103
- - CORS is enabled (if specified)
104
-
105
- **Directory Structure:**
106
- ```
107
- your-project/
108
- ├── main.ts
109
- └── mcp/
110
- ├── sentiment/
111
- │ └── index.ts # export class SentimentService
112
- ├── weather/
113
- │ └── index.ts # export class WeatherService
114
- └── database/
115
- └── index.ts # export class DatabaseService
116
- ```
117
-
118
- #### Option B: Factory Pattern (Advanced)
119
-
120
- For advanced use cases requiring manual service registration or custom configuration:
121
-
122
- ```typescript
123
- import { createHTTPServer, MCPServer } from "@leanmcp/core";
124
- import { SentimentService } from "./services/sentiment";
125
-
126
- // Create MCP server with factory function
127
- const serverFactory = async () => {
128
- const server = new MCPServer({
129
- name: "my-mcp-server",
130
- version: "1.0.0",
131
- logging: true,
132
- autoDiscover: false // Disable auto-discovery for manual registration
133
- });
134
-
135
- // Register services manually
136
- server.registerService(new SentimentService());
137
-
138
- return server.getServer();
139
- };
140
-
141
- // Start HTTP server with factory
142
- await createHTTPServer(serverFactory, {
143
- port: 3000,
144
- cors: true,
145
- logging: true
146
- });
147
- ```
120
+ ---
148
121
 
149
122
  ## Decorators
150
123
 
151
124
  ### @Tool
152
125
 
153
- Marks a method as an MCP tool (callable function). Use `inputClass` to specify the input schema class.
126
+ Marks a method as a callable MCP tool.
154
127
 
155
128
  ```typescript
156
129
  class CalculateInput {
@@ -170,9 +143,16 @@ async calculate(input: CalculateInput) {
170
143
  }
171
144
  ```
172
145
 
146
+ **Options:**
147
+
148
+ | Option | Type | Description |
149
+ |--------|------|-------------|
150
+ | `description` | `string` | Tool description for the AI |
151
+ | `inputClass` | `Class` | Class defining input schema |
152
+
173
153
  ### @Prompt
174
154
 
175
- Marks a method as an MCP prompt template. Input schema is automatically inferred from parameter type.
155
+ Marks a method as a reusable prompt template.
176
156
 
177
157
  ```typescript
178
158
  class CodeReviewInput {
@@ -202,7 +182,10 @@ codeReview(input: CodeReviewInput) {
202
182
  Marks a method as an MCP resource (data source).
203
183
 
204
184
  ```typescript
205
- @Resource({ description: 'Get system configuration', mimeType: 'application/json' })
185
+ @Resource({
186
+ description: 'Get system configuration',
187
+ mimeType: 'application/json'
188
+ })
206
189
  async getConfig() {
207
190
  return {
208
191
  version: "1.0.0",
@@ -213,7 +196,7 @@ async getConfig() {
213
196
 
214
197
  ### @SchemaConstraint
215
198
 
216
- Add validation constraints to class properties for automatic schema generation.
199
+ Add validation constraints to class properties.
217
200
 
218
201
  ```typescript
219
202
  class UserInput {
@@ -242,6 +225,14 @@ class UserInput {
242
225
  }
243
226
  ```
244
227
 
228
+ **Common constraints:**
229
+ - `description`, `default` — Documentation
230
+ - `minLength`, `maxLength` — String length
231
+ - `minimum`, `maximum` — Number range
232
+ - `enum` — Allowed values
233
+ - `format` — String format (`email`, `uri`, `date`, etc.)
234
+ - `pattern` — Regex pattern
235
+
245
236
  ### @Optional
246
237
 
247
238
  Marks a property as optional in the schema.
@@ -257,8 +248,50 @@ class SearchInput {
257
248
  }
258
249
  ```
259
250
 
251
+ ---
252
+
260
253
  ## API Reference
261
254
 
255
+ ### createHTTPServer
256
+
257
+ Create and start an HTTP server with auto-discovery.
258
+
259
+ **Simplified API (Recommended):**
260
+ ```typescript
261
+ await createHTTPServer({
262
+ name: string; // Server name (required)
263
+ version: string; // Server version (required)
264
+ port?: number; // Port (default: 3001)
265
+ cors?: boolean | object; // Enable CORS (default: false)
266
+ logging?: boolean; // Enable logging (default: false)
267
+ debug?: boolean; // Verbose debug logs (default: false)
268
+ autoDiscover?: boolean; // Auto-discover services (default: true)
269
+ mcpDir?: string; // Custom mcp directory path
270
+ sessionTimeout?: number; // Session timeout in ms
271
+ stateless?: boolean; // Stateless mode for Lambda/serverless (default: true)
272
+ dashboard?: boolean; // Serve dashboard UI at / (default: true)
273
+ });
274
+ ```
275
+
276
+ **Factory Pattern (Advanced):**
277
+ ```typescript
278
+ const serverFactory = async () => {
279
+ const server = new MCPServer({
280
+ name: "my-server",
281
+ version: "1.0.0",
282
+ autoDiscover: false // Disable for manual registration
283
+ });
284
+
285
+ server.registerService(new MyService());
286
+ return server.getServer();
287
+ };
288
+
289
+ await createHTTPServer(serverFactory, {
290
+ port: 3001,
291
+ cors: true
292
+ });
293
+ ```
294
+
262
295
  ### MCPServer
263
296
 
264
297
  Main server class for registering services.
@@ -268,69 +301,39 @@ const server = new MCPServer({
268
301
  name: string; // Server name
269
302
  version: string; // Server version
270
303
  logging?: boolean; // Enable logging (default: false)
271
- debug?: boolean; // Enable verbose debug logs (default: false)
272
- autoDiscover?: boolean; // Enable auto-discovery (default: true)
273
- mcpDir?: string; // Custom mcp directory path (optional)
304
+ debug?: boolean; // Verbose debug logs (default: false)
305
+ autoDiscover?: boolean; // Auto-discover from ./mcp (default: true)
306
+ mcpDir?: string; // Custom mcp directory path
274
307
  });
275
308
 
276
- // Manual registration
277
- server.registerService(instance: any): void;
278
-
279
- // Get underlying MCP SDK server
280
- server.getServer(): Server;
309
+ server.registerService(instance); // Manual registration
310
+ server.getServer(); // Get underlying MCP SDK server
281
311
  ```
282
312
 
283
- **Options:**
313
+ ---
284
314
 
285
- - **`logging`**: Enable basic logging for server operations
286
- - **`debug`**: Enable verbose debug logs showing detailed service registration (requires `logging: true`)
287
- - **`autoDiscover`**: Automatically discover and register services from `./mcp` directory (default: `true`)
288
- - **`mcpDir`**: Custom path to the mcp directory (default: auto-detected `./mcp`)
315
+ ## Auto-Discovery
289
316
 
290
- #### Zero-Config Auto-Discovery
317
+ Services are automatically discovered from the `./mcp` directory:
291
318
 
292
- Services are automatically discovered and registered from the `./mcp` directory when the server is created:
319
+ 1. Recursively scans for `index.ts` or `index.js` files
320
+ 2. Dynamically imports each file
321
+ 3. Looks for exported classes
322
+ 4. Instantiates with no-args constructors
323
+ 5. Registers all decorated methods
293
324
 
294
- **Basic Usage (Simplified API):**
295
- ```typescript
296
- import { createHTTPServer } from "@leanmcp/core";
325
+ ### Shared Dependencies
297
326
 
298
- await createHTTPServer({
299
- name: "my-server",
300
- version: "1.0.0",
301
- port: 3000,
302
- logging: true // Enable logging
303
- });
304
- ```
305
-
306
- **With Debug Logging:**
307
- ```typescript
308
- await createHTTPServer({
309
- name: "my-server",
310
- version: "1.0.0",
311
- port: 3000,
312
- logging: true,
313
- debug: true // Show detailed service registration logs
314
- });
315
- ```
316
-
317
- **With Shared Dependencies:**
318
-
319
- For services that need shared dependencies, create a `config.ts` (example) file in your `mcp` directory:
327
+ For services needing shared configuration (auth, database, etc.), create a `config.ts`:
320
328
 
321
329
  ```typescript
322
330
  // mcp/config.ts
323
331
  import { AuthProvider } from "@leanmcp/auth";
324
332
 
325
- if (!process.env.COGNITO_USER_POOL_ID || !process.env.COGNITO_CLIENT_ID) {
326
- throw new Error('Missing required Cognito configuration');
327
- }
328
-
329
333
  export const authProvider = new AuthProvider('cognito', {
330
- region: process.env.AWS_REGION || 'us-east-1',
334
+ region: process.env.AWS_REGION,
331
335
  userPoolId: process.env.COGNITO_USER_POOL_ID,
332
- clientId: process.env.COGNITO_CLIENT_ID,
333
- clientSecret: process.env.COGNITO_CLIENT_SECRET
336
+ clientId: process.env.COGNITO_CLIENT_ID
334
337
  });
335
338
 
336
339
  await authProvider.init();
@@ -346,147 +349,29 @@ import { authProvider } from "../config.js";
346
349
 
347
350
  @Authenticated(authProvider)
348
351
  export class SlackService {
349
- constructor() {
350
- // No parameters needed - use environment or imported config
351
- }
352
-
353
352
  @Tool({ description: 'Send a message' })
354
- async sendMessage(args: any) {
353
+ async sendMessage(args: { channel: string; message: string }) {
355
354
  // Implementation
356
355
  }
357
356
  }
358
357
  ```
359
358
 
360
- Your main file stays clean:
361
-
362
- ```typescript
363
- import { createHTTPServer } from "@leanmcp/core";
364
-
365
- await createHTTPServer({
366
- name: "my-server",
367
- version: "1.0.0",
368
- port: 3000,
369
- logging: true
370
- });
371
-
372
- // Services are automatically discovered and registered
373
- ```
374
-
375
- **How It Works:**
376
- - Automatically discovers and registers services from the `./mcp` directory during server initialization
377
- - Recursively scans for `index.ts` or `index.js` files
378
- - Dynamically imports each file and looks for exported classes
379
- - Instantiates services with no-args constructors
380
- - Registers all discovered services with their decorated methods
381
-
382
- **Directory Structure:**
383
- ```
384
- your-project/
385
- ├── main.ts
386
- └── mcp/
387
- ├── config.ts # Optional: shared dependencies
388
- ├── slack/
389
- │ └── index.ts # export class SlackService
390
- ├── database/
391
- │ └── index.ts # export class DatabaseService
392
- └── auth/
393
- └── index.ts # export class AuthService
394
- ```
395
-
396
- ### createHTTPServer
397
-
398
- Create and start an HTTP server with streamable transport.
399
-
400
- **Simplified API (Recommended):**
401
- ```typescript
402
- await createHTTPServer({
403
- name: string; // Server name (required)
404
- version: string; // Server version (required)
405
- port?: number; // Port number (default: 3001)
406
- cors?: boolean | object; // Enable CORS (default: false)
407
- logging?: boolean; // Enable logging (default: false)
408
- debug?: boolean; // Enable debug logs (default: false)
409
- autoDiscover?: boolean; // Auto-discover services (default: true)
410
- mcpDir?: string; // Custom mcp directory path (optional)
411
- sessionTimeout?: number; // Session timeout in ms (optional)
412
- });
413
- ```
414
-
415
- **Factory Pattern (Advanced):**
416
- ```typescript
417
- await createHTTPServer(
418
- serverFactory: () => Server | Promise<Server>,
419
- options: {
420
- port?: number; // Port number (default: 3001)
421
- cors?: boolean | object; // Enable CORS (default: false)
422
- logging?: boolean; // Enable HTTP request logging (default: false)
423
- sessionTimeout?: number; // Session timeout in ms (optional)
424
- }
425
- );
426
- ```
427
-
428
- **CORS Configuration:**
429
- ```typescript
430
- // Simple CORS (allow all origins - not recommended for production)
431
- await createHTTPServer({
432
- name: "my-server",
433
- version: "1.0.0",
434
- cors: true
435
- });
436
-
437
- // Advanced CORS configuration
438
- await createHTTPServer({
439
- name: "my-server",
440
- version: "1.0.0",
441
- cors: {
442
- origin: 'https://example.com', // Specific origin
443
- credentials: true // Allow credentials
444
- }
445
- });
446
- ```
447
-
448
- ### Schema Generation
449
-
450
- Generate JSON Schema from TypeScript classes:
451
-
452
- ```typescript
453
- import { classToJsonSchemaWithConstraints } from "@leanmcp/core";
454
-
455
- const schema = classToJsonSchemaWithConstraints(MyInputClass);
456
- ```
359
+ ---
457
360
 
458
361
  ## HTTP Endpoints
459
362
 
460
- When using `createHTTPServer`, the following endpoints are available:
461
-
462
- - `POST /mcp` - MCP protocol endpoint (accepts JSON-RPC 2.0 messages)
463
- - `GET /health` - Health check endpoint
464
- - `GET /` - Welcome message
465
-
466
- ## Environment Variables
467
-
468
- ```bash
469
- PORT=3000 # Server port (optional)
470
- NODE_ENV=production # Environment (optional)
471
- ```
363
+ | Endpoint | Method | Description |
364
+ |----------|--------|-------------|
365
+ | `/mcp` | POST | MCP protocol endpoint (JSON-RPC 2.0) |
366
+ | `/health` | GET | Health check |
367
+ | `/` | GET | Welcome message |
472
368
 
473
369
  ## Error Handling
474
370
 
475
- All tools automatically handle errors and return them in MCP format:
371
+ Errors are automatically caught and returned in MCP format:
476
372
 
477
373
  ```typescript
478
- class DivideInput {
479
- @SchemaConstraint({ description: 'Numerator' })
480
- a!: number;
481
-
482
- @SchemaConstraint({ description: 'Denominator' })
483
- b!: number;
484
- }
485
-
486
- @Tool({
487
- description: 'Divide numbers',
488
- inputClass: DivideInput
489
- })
374
+ @Tool({ description: 'Divide numbers', inputClass: DivideInput })
490
375
  async divide(input: DivideInput) {
491
376
  if (input.b === 0) {
492
377
  throw new Error("Division by zero");
@@ -495,7 +380,7 @@ async divide(input: DivideInput) {
495
380
  }
496
381
  ```
497
382
 
498
- Errors are returned as:
383
+ Returns:
499
384
  ```json
500
385
  {
501
386
  "content": [{"type": "text", "text": "Error: Division by zero"}],
@@ -503,9 +388,20 @@ Errors are returned as:
503
388
  }
504
389
  ```
505
390
 
391
+ ## Environment Variables
392
+
393
+ ```bash
394
+ PORT=3001 # Server port
395
+ NODE_ENV=production # Environment
396
+ ```
397
+
506
398
  ## TypeScript Support
507
399
 
508
- Full TypeScript support with type inference:
400
+ **Key Points:**
401
+ - Input schema is defined via `inputClass` in the decorator
402
+ - Output type is inferred from the return type
403
+ - For tools with no input, omit `inputClass`
404
+ - Use `@SchemaConstraint` for validation and documentation
509
405
 
510
406
  ```typescript
511
407
  class MyInput {
@@ -513,43 +409,29 @@ class MyInput {
513
409
  field!: string;
514
410
  }
515
411
 
516
- class MyOutput {
517
- result!: string;
518
- }
519
-
520
- // Input schema defined via inputClass, output type inferred from return type
521
- @Tool({
522
- description: 'My tool',
523
- inputClass: MyInput
524
- })
525
- async myTool(input: MyInput): Promise<MyOutput> {
526
- // TypeScript knows the exact types
527
- const result: MyOutput = {
528
- result: input.field.toUpperCase()
529
- // Full autocomplete and type checking
530
- };
531
- return result;
412
+ @Tool({ description: 'My tool', inputClass: MyInput })
413
+ async myTool(input: MyInput): Promise<{ result: string }> {
414
+ return { result: input.field.toUpperCase() };
532
415
  }
533
416
  ```
534
417
 
535
- **Key Points:**
536
- - Input schema is defined using `inputClass` in the `@Tool` decorator
537
- - Output schema is inferred from the return type
538
- - For tools with no input parameters, omit the `inputClass` option
539
- - Use `@SchemaConstraint` decorators to add validation and documentation to your input classes
418
+ ## Documentation
540
419
 
541
- ## License
542
-
543
- MIT
420
+ - [Full Documentation](https://docs.leanmcp.com/sdk/core)
544
421
 
545
422
  ## Related Packages
546
423
 
547
- - [@leanmcp/cli](../cli) - CLI tool for creating new projects
548
- - [@leanmcp/auth](../auth) - Authentication decorators and providers
549
- - [@leanmcp/utils](../utils) - Utility functions
424
+ - [@leanmcp/cli](https://www.npmjs.com/package/@leanmcp/cli) CLI tool for project creation
425
+ - [@leanmcp/auth](https://www.npmjs.com/package/@leanmcp/auth) Authentication decorators
426
+ - [@leanmcp/ui](https://www.npmjs.com/package/@leanmcp/ui) MCP App UI components
427
+ - [@leanmcp/elicitation](https://www.npmjs.com/package/@leanmcp/elicitation) — Structured user input
550
428
 
551
429
  ## Links
552
430
 
553
431
  - [GitHub Repository](https://github.com/LeanMCP/leanmcp-sdk)
432
+ - [NPM Package](https://www.npmjs.com/package/@leanmcp/core)
554
433
  - [MCP Specification](https://spec.modelcontextprotocol.io/)
555
- - [Documentation](https://github.com/LeanMCP/leanmcp-sdk#readme)
434
+
435
+ ## License
436
+
437
+ MIT
package/dist/index.d.mts CHANGED
@@ -465,6 +465,10 @@ declare class MCPServer {
465
465
  registerService(instance: any): void;
466
466
  /**
467
467
  * Watch UI manifest for changes and reload resources dynamically
468
+ *
469
+ * CRITICAL: Only for stateful mode. In stateless mode, each request
470
+ * creates a fresh server that reads the manifest directly, making
471
+ * watchers both unnecessary and a memory leak source.
468
472
  */
469
473
  private watchUIManifest;
470
474
  /**
@@ -520,6 +524,11 @@ declare class MCPServer {
520
524
  } | undefined;
521
525
  } | undefined;
522
526
  }>;
527
+ /**
528
+ * Clean up all registered services, watchers, and resources
529
+ * CRITICAL for stateless mode to prevent memory leaks
530
+ */
531
+ close(): void;
523
532
  /**
524
533
  * Cleanup resources (call on server shutdown)
525
534
  */
package/dist/index.d.ts CHANGED
@@ -465,6 +465,10 @@ declare class MCPServer {
465
465
  registerService(instance: any): void;
466
466
  /**
467
467
  * Watch UI manifest for changes and reload resources dynamically
468
+ *
469
+ * CRITICAL: Only for stateful mode. In stateless mode, each request
470
+ * creates a fresh server that reads the manifest directly, making
471
+ * watchers both unnecessary and a memory leak source.
468
472
  */
469
473
  private watchUIManifest;
470
474
  /**
@@ -520,6 +524,11 @@ declare class MCPServer {
520
524
  } | undefined;
521
525
  } | undefined;
522
526
  }>;
527
+ /**
528
+ * Clean up all registered services, watchers, and resources
529
+ * CRITICAL for stateless mode to prevent memory leaks
530
+ */
531
+ close(): void;
523
532
  /**
524
533
  * Cleanup resources (call on server shutdown)
525
534
  */
package/dist/index.js CHANGED
@@ -796,7 +796,11 @@ async function createHTTPServer(serverInput, options) {
796
796
  await transport.handleRequest(req, res, req.body);
797
797
  res.on("close", () => {
798
798
  transport.close();
799
- freshServer.close();
799
+ if ("close" in freshServer && typeof freshServer.close === "function") {
800
+ freshServer.close();
801
+ } else {
802
+ freshServer.close();
803
+ }
800
804
  });
801
805
  } catch (error) {
802
806
  logger.error("Error handling MCP request:", error);
@@ -889,20 +893,36 @@ async function createHTTPServer(serverInput, options) {
889
893
  logger.error(`Server error: ${error.message}`);
890
894
  reject(error);
891
895
  });
892
- const cleanup = /* @__PURE__ */ __name(() => {
896
+ let isShuttingDown = false;
897
+ const cleanup = /* @__PURE__ */ __name(async () => {
898
+ if (isShuttingDown) return;
899
+ isShuttingDown = true;
893
900
  logger.info("\nShutting down server...");
894
- Object.values(transports).forEach((t) => t.close?.());
895
- activeListener?.close(() => {
896
- logger.info("Server closed");
897
- process.exit(0);
898
- });
899
- setTimeout(() => {
900
- logger.warn("Forcing shutdown...");
901
- process.exit(1);
902
- }, 5e3);
901
+ for (const transport of Object.values(transports)) {
902
+ try {
903
+ transport.close?.();
904
+ } catch (e) {
905
+ }
906
+ }
907
+ if (activeListener) {
908
+ await new Promise((resolveClose) => {
909
+ activeListener.close((err) => {
910
+ if (err) {
911
+ logger.warn(`Error closing server: ${err.message}`);
912
+ } else {
913
+ logger.info("Server closed");
914
+ }
915
+ resolveClose();
916
+ });
917
+ });
918
+ }
903
919
  }, "cleanup");
904
- process.on("SIGINT", cleanup);
905
- process.on("SIGTERM", cleanup);
920
+ const handleShutdown = /* @__PURE__ */ __name(() => {
921
+ cleanup().finally(() => {
922
+ });
923
+ }, "handleShutdown");
924
+ process.once("SIGINT", handleShutdown);
925
+ process.once("SIGTERM", handleShutdown);
906
926
  } catch (error) {
907
927
  reject(error);
908
928
  }
@@ -1423,8 +1443,15 @@ var init_index = __esm({
1423
1443
  }
1424
1444
  /**
1425
1445
  * Watch UI manifest for changes and reload resources dynamically
1446
+ *
1447
+ * CRITICAL: Only for stateful mode. In stateless mode, each request
1448
+ * creates a fresh server that reads the manifest directly, making
1449
+ * watchers both unnecessary and a memory leak source.
1426
1450
  */
1427
1451
  watchUIManifest() {
1452
+ if (this.options.stateless) {
1453
+ return;
1454
+ }
1428
1455
  try {
1429
1456
  const manifestPath = import_path.default.join(process.cwd(), "dist", "ui-manifest.json");
1430
1457
  if (!import_fs.default.existsSync(manifestPath)) {
@@ -1491,7 +1518,11 @@ var init_index = __esm({
1491
1518
  }
1492
1519
  }
1493
1520
  }
1494
- for (const [uri, htmlPath] of Object.entries(manifest)) {
1521
+ for (const [uri, entry] of Object.entries(manifest)) {
1522
+ const isString = typeof entry === "string";
1523
+ const htmlPath = isString ? entry : entry.htmlPath;
1524
+ const isGPTApp = !isString && entry.isGPTApp;
1525
+ const gptMeta = !isString ? entry.gptMeta : void 0;
1495
1526
  if (!import_fs.default.existsSync(htmlPath)) {
1496
1527
  if (this.logging) {
1497
1528
  this.logger.warn(`UI HTML file not found: ${htmlPath}`);
@@ -1499,17 +1530,25 @@ var init_index = __esm({
1499
1530
  continue;
1500
1531
  }
1501
1532
  const wasRegistered = this.resources.has(uri);
1533
+ const mimeType = isGPTApp ? "text/html+skybridge" : "text/html;profile=mcp-app";
1534
+ const _meta = {};
1535
+ if (isGPTApp) {
1536
+ _meta["openai/outputTemplate"] = uri;
1537
+ if (gptMeta) Object.assign(_meta, gptMeta);
1538
+ if (_meta["openai/widgetPrefersBorder"] === void 0) _meta["openai/widgetPrefersBorder"] = true;
1539
+ }
1502
1540
  this.resources.set(uri, {
1503
1541
  uri,
1504
1542
  name: uri.replace("ui://", "").replace(/\//g, "-"),
1505
1543
  description: `Auto-generated UI resource from pre-built HTML`,
1506
- mimeType: "text/html;profile=mcp-app",
1544
+ mimeType,
1507
1545
  inputSchema: void 0,
1508
1546
  method: /* @__PURE__ */ __name(async () => {
1509
1547
  if (import_fs.default.existsSync(htmlPath)) {
1510
1548
  const html = import_fs.default.readFileSync(htmlPath, "utf-8");
1511
1549
  return {
1512
- text: html
1550
+ text: html,
1551
+ _meta: Object.keys(_meta).length > 0 ? _meta : void 0
1513
1552
  };
1514
1553
  }
1515
1554
  throw new Error(`UI HTML file not found: ${htmlPath}`);
@@ -1539,7 +1578,11 @@ var init_index = __esm({
1539
1578
  return;
1540
1579
  }
1541
1580
  const manifest = JSON.parse(import_fs.default.readFileSync(manifestPath, "utf-8"));
1542
- for (const [uri, htmlPath] of Object.entries(manifest)) {
1581
+ for (const [uri, entry] of Object.entries(manifest)) {
1582
+ const isString = typeof entry === "string";
1583
+ const htmlPath = isString ? entry : entry.htmlPath;
1584
+ const isGPTApp = !isString && entry.isGPTApp;
1585
+ const gptMeta = !isString ? entry.gptMeta : void 0;
1543
1586
  if (this.resources.has(uri)) {
1544
1587
  if (this.logging) {
1545
1588
  this.logger.debug(`Skipping UI resource ${uri} - already registered`);
@@ -1553,14 +1596,22 @@ var init_index = __esm({
1553
1596
  continue;
1554
1597
  }
1555
1598
  const html = import_fs.default.readFileSync(htmlPath, "utf-8");
1599
+ const mimeType = isGPTApp ? "text/html+skybridge" : "text/html;profile=mcp-app";
1600
+ const _meta = {};
1601
+ if (isGPTApp) {
1602
+ _meta["openai/outputTemplate"] = uri;
1603
+ if (gptMeta) Object.assign(_meta, gptMeta);
1604
+ if (_meta["openai/widgetPrefersBorder"] === void 0) _meta["openai/widgetPrefersBorder"] = true;
1605
+ }
1556
1606
  this.resources.set(uri, {
1557
1607
  uri,
1558
1608
  name: uri.replace("ui://", "").replace(/\//g, "-"),
1559
1609
  description: `Auto-generated UI resource from pre-built HTML`,
1560
- mimeType: "text/html;profile=mcp-app",
1610
+ mimeType,
1561
1611
  inputSchema: void 0,
1562
1612
  method: /* @__PURE__ */ __name(async () => ({
1563
- text: html
1613
+ text: html,
1614
+ _meta: Object.keys(_meta).length > 0 ? _meta : void 0
1564
1615
  }), "method"),
1565
1616
  instance: null,
1566
1617
  propertyKey: "getUI"
@@ -1584,6 +1635,25 @@ var init_index = __esm({
1584
1635
  return this.server;
1585
1636
  }
1586
1637
  /**
1638
+ * Clean up all registered services, watchers, and resources
1639
+ * CRITICAL for stateless mode to prevent memory leaks
1640
+ */
1641
+ close() {
1642
+ if (this.manifestWatcher) {
1643
+ try {
1644
+ this.manifestWatcher.close();
1645
+ } catch (e) {
1646
+ }
1647
+ this.manifestWatcher = null;
1648
+ }
1649
+ this.tools.clear();
1650
+ this.prompts.clear();
1651
+ this.resources.clear();
1652
+ if (this.server && typeof this.server.close === "function") {
1653
+ this.server.close();
1654
+ }
1655
+ }
1656
+ /**
1587
1657
  * Cleanup resources (call on server shutdown)
1588
1658
  */
1589
1659
  async cleanup() {
package/dist/index.mjs CHANGED
@@ -758,7 +758,11 @@ async function createHTTPServer(serverInput, options) {
758
758
  await transport.handleRequest(req, res, req.body);
759
759
  res.on("close", () => {
760
760
  transport.close();
761
- freshServer.close();
761
+ if ("close" in freshServer && typeof freshServer.close === "function") {
762
+ freshServer.close();
763
+ } else {
764
+ freshServer.close();
765
+ }
762
766
  });
763
767
  } catch (error) {
764
768
  logger.error("Error handling MCP request:", error);
@@ -851,20 +855,36 @@ async function createHTTPServer(serverInput, options) {
851
855
  logger.error(`Server error: ${error.message}`);
852
856
  reject(error);
853
857
  });
854
- const cleanup = /* @__PURE__ */ __name(() => {
858
+ let isShuttingDown = false;
859
+ const cleanup = /* @__PURE__ */ __name(async () => {
860
+ if (isShuttingDown) return;
861
+ isShuttingDown = true;
855
862
  logger.info("\nShutting down server...");
856
- Object.values(transports).forEach((t) => t.close?.());
857
- activeListener?.close(() => {
858
- logger.info("Server closed");
859
- process.exit(0);
860
- });
861
- setTimeout(() => {
862
- logger.warn("Forcing shutdown...");
863
- process.exit(1);
864
- }, 5e3);
863
+ for (const transport of Object.values(transports)) {
864
+ try {
865
+ transport.close?.();
866
+ } catch (e) {
867
+ }
868
+ }
869
+ if (activeListener) {
870
+ await new Promise((resolveClose) => {
871
+ activeListener.close((err) => {
872
+ if (err) {
873
+ logger.warn(`Error closing server: ${err.message}`);
874
+ } else {
875
+ logger.info("Server closed");
876
+ }
877
+ resolveClose();
878
+ });
879
+ });
880
+ }
865
881
  }, "cleanup");
866
- process.on("SIGINT", cleanup);
867
- process.on("SIGTERM", cleanup);
882
+ const handleShutdown = /* @__PURE__ */ __name(() => {
883
+ cleanup().finally(() => {
884
+ });
885
+ }, "handleShutdown");
886
+ process.once("SIGINT", handleShutdown);
887
+ process.once("SIGTERM", handleShutdown);
868
888
  } catch (error) {
869
889
  reject(error);
870
890
  }
@@ -1320,8 +1340,15 @@ var MCPServer = class {
1320
1340
  }
1321
1341
  /**
1322
1342
  * Watch UI manifest for changes and reload resources dynamically
1343
+ *
1344
+ * CRITICAL: Only for stateful mode. In stateless mode, each request
1345
+ * creates a fresh server that reads the manifest directly, making
1346
+ * watchers both unnecessary and a memory leak source.
1323
1347
  */
1324
1348
  watchUIManifest() {
1349
+ if (this.options.stateless) {
1350
+ return;
1351
+ }
1325
1352
  try {
1326
1353
  const manifestPath = path.join(process.cwd(), "dist", "ui-manifest.json");
1327
1354
  if (!fs.existsSync(manifestPath)) {
@@ -1388,7 +1415,11 @@ var MCPServer = class {
1388
1415
  }
1389
1416
  }
1390
1417
  }
1391
- for (const [uri, htmlPath] of Object.entries(manifest)) {
1418
+ for (const [uri, entry] of Object.entries(manifest)) {
1419
+ const isString = typeof entry === "string";
1420
+ const htmlPath = isString ? entry : entry.htmlPath;
1421
+ const isGPTApp = !isString && entry.isGPTApp;
1422
+ const gptMeta = !isString ? entry.gptMeta : void 0;
1392
1423
  if (!fs.existsSync(htmlPath)) {
1393
1424
  if (this.logging) {
1394
1425
  this.logger.warn(`UI HTML file not found: ${htmlPath}`);
@@ -1396,17 +1427,25 @@ var MCPServer = class {
1396
1427
  continue;
1397
1428
  }
1398
1429
  const wasRegistered = this.resources.has(uri);
1430
+ const mimeType = isGPTApp ? "text/html+skybridge" : "text/html;profile=mcp-app";
1431
+ const _meta = {};
1432
+ if (isGPTApp) {
1433
+ _meta["openai/outputTemplate"] = uri;
1434
+ if (gptMeta) Object.assign(_meta, gptMeta);
1435
+ if (_meta["openai/widgetPrefersBorder"] === void 0) _meta["openai/widgetPrefersBorder"] = true;
1436
+ }
1399
1437
  this.resources.set(uri, {
1400
1438
  uri,
1401
1439
  name: uri.replace("ui://", "").replace(/\//g, "-"),
1402
1440
  description: `Auto-generated UI resource from pre-built HTML`,
1403
- mimeType: "text/html;profile=mcp-app",
1441
+ mimeType,
1404
1442
  inputSchema: void 0,
1405
1443
  method: /* @__PURE__ */ __name(async () => {
1406
1444
  if (fs.existsSync(htmlPath)) {
1407
1445
  const html = fs.readFileSync(htmlPath, "utf-8");
1408
1446
  return {
1409
- text: html
1447
+ text: html,
1448
+ _meta: Object.keys(_meta).length > 0 ? _meta : void 0
1410
1449
  };
1411
1450
  }
1412
1451
  throw new Error(`UI HTML file not found: ${htmlPath}`);
@@ -1436,7 +1475,11 @@ var MCPServer = class {
1436
1475
  return;
1437
1476
  }
1438
1477
  const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1439
- for (const [uri, htmlPath] of Object.entries(manifest)) {
1478
+ for (const [uri, entry] of Object.entries(manifest)) {
1479
+ const isString = typeof entry === "string";
1480
+ const htmlPath = isString ? entry : entry.htmlPath;
1481
+ const isGPTApp = !isString && entry.isGPTApp;
1482
+ const gptMeta = !isString ? entry.gptMeta : void 0;
1440
1483
  if (this.resources.has(uri)) {
1441
1484
  if (this.logging) {
1442
1485
  this.logger.debug(`Skipping UI resource ${uri} - already registered`);
@@ -1450,14 +1493,22 @@ var MCPServer = class {
1450
1493
  continue;
1451
1494
  }
1452
1495
  const html = fs.readFileSync(htmlPath, "utf-8");
1496
+ const mimeType = isGPTApp ? "text/html+skybridge" : "text/html;profile=mcp-app";
1497
+ const _meta = {};
1498
+ if (isGPTApp) {
1499
+ _meta["openai/outputTemplate"] = uri;
1500
+ if (gptMeta) Object.assign(_meta, gptMeta);
1501
+ if (_meta["openai/widgetPrefersBorder"] === void 0) _meta["openai/widgetPrefersBorder"] = true;
1502
+ }
1453
1503
  this.resources.set(uri, {
1454
1504
  uri,
1455
1505
  name: uri.replace("ui://", "").replace(/\//g, "-"),
1456
1506
  description: `Auto-generated UI resource from pre-built HTML`,
1457
- mimeType: "text/html;profile=mcp-app",
1507
+ mimeType,
1458
1508
  inputSchema: void 0,
1459
1509
  method: /* @__PURE__ */ __name(async () => ({
1460
- text: html
1510
+ text: html,
1511
+ _meta: Object.keys(_meta).length > 0 ? _meta : void 0
1461
1512
  }), "method"),
1462
1513
  instance: null,
1463
1514
  propertyKey: "getUI"
@@ -1481,6 +1532,25 @@ var MCPServer = class {
1481
1532
  return this.server;
1482
1533
  }
1483
1534
  /**
1535
+ * Clean up all registered services, watchers, and resources
1536
+ * CRITICAL for stateless mode to prevent memory leaks
1537
+ */
1538
+ close() {
1539
+ if (this.manifestWatcher) {
1540
+ try {
1541
+ this.manifestWatcher.close();
1542
+ } catch (e) {
1543
+ }
1544
+ this.manifestWatcher = null;
1545
+ }
1546
+ this.tools.clear();
1547
+ this.prompts.clear();
1548
+ this.resources.clear();
1549
+ if (this.server && typeof this.server.close === "function") {
1550
+ this.server.close();
1551
+ }
1552
+ }
1553
+ /**
1484
1554
  * Cleanup resources (call on server shutdown)
1485
1555
  */
1486
1556
  async cleanup() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leanmcp/core",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "Core library implementing decorators, reflection, and MCP runtime server",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",