@leanmcp/core 0.3.10 → 0.3.12

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,43 @@
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
+ - **Structured Content** Automatic `structuredContent` for ChatGPT Apps SDK compatibility
40
+ - **MCP Compliant** — Built on official `@modelcontextprotocol/sdk`
13
41
 
14
42
  ## Installation
15
43
 
@@ -17,8 +45,6 @@ Core library for building Model Context Protocol (MCP) servers with TypeScript d
17
45
  npm install @leanmcp/core
18
46
  ```
19
47
 
20
- ### Peer Dependencies
21
-
22
48
  For HTTP server support:
23
49
  ```bash
24
50
  npm install express cors
@@ -26,12 +52,42 @@ npm install express cors
26
52
 
27
53
  ## Quick Start
28
54
 
29
- ### 1. Define Your Service with Class-Based Schema
55
+ ### Zero-Config (Recommended)
56
+
57
+ The simplest way to create an MCP server with auto-discovery:
58
+
59
+ ```typescript
60
+ import { createHTTPServer } from "@leanmcp/core";
61
+
62
+ await createHTTPServer({
63
+ name: "my-mcp-server",
64
+ version: "1.0.0",
65
+ port: 3001,
66
+ cors: true,
67
+ logging: true
68
+ });
69
+
70
+ // Services are automatically discovered from ./mcp directory
71
+ ```
72
+
73
+ **Directory Structure:**
74
+ ```
75
+ your-project/
76
+ ├── main.ts
77
+ └── mcp/
78
+ ├── sentiment/
79
+ │ └── index.ts # export class SentimentService
80
+ ├── weather/
81
+ │ └── index.ts # export class WeatherService
82
+ └── config.ts # Optional: shared dependencies
83
+ ```
84
+
85
+ ### Define a Service
30
86
 
31
87
  ```typescript
88
+ // mcp/sentiment/index.ts
32
89
  import { Tool, SchemaConstraint, Optional } from "@leanmcp/core";
33
90
 
34
- // Define input schema as a class
35
91
  class AnalyzeSentimentInput {
36
92
  @SchemaConstraint({
37
93
  description: 'Text to analyze',
@@ -48,109 +104,27 @@ class AnalyzeSentimentInput {
48
104
  language?: string;
49
105
  }
50
106
 
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
107
  export class SentimentService {
61
108
  @Tool({
62
109
  description: 'Analyze sentiment of text',
63
110
  inputClass: AnalyzeSentimentInput
64
111
  })
65
- async analyzeSentiment(input: AnalyzeSentimentInput): Promise<AnalyzeSentimentOutput> {
66
- // Your implementation
112
+ async analyzeSentiment(input: AnalyzeSentimentInput) {
67
113
  return {
68
114
  sentiment: 'positive',
69
- score: 0.8,
70
- confidence: 0.95
115
+ score: 0.8
71
116
  };
72
117
  }
73
118
  }
74
119
  ```
75
120
 
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
- ```
121
+ ---
148
122
 
149
123
  ## Decorators
150
124
 
151
125
  ### @Tool
152
126
 
153
- Marks a method as an MCP tool (callable function). Use `inputClass` to specify the input schema class.
127
+ Marks a method as a callable MCP tool.
154
128
 
155
129
  ```typescript
156
130
  class CalculateInput {
@@ -170,9 +144,16 @@ async calculate(input: CalculateInput) {
170
144
  }
171
145
  ```
172
146
 
147
+ **Options:**
148
+
149
+ | Option | Type | Description |
150
+ |--------|------|-------------|
151
+ | `description` | `string` | Tool description for the AI |
152
+ | `inputClass` | `Class` | Class defining input schema |
153
+
173
154
  ### @Prompt
174
155
 
175
- Marks a method as an MCP prompt template. Input schema is automatically inferred from parameter type.
156
+ Marks a method as a reusable prompt template.
176
157
 
177
158
  ```typescript
178
159
  class CodeReviewInput {
@@ -202,7 +183,10 @@ codeReview(input: CodeReviewInput) {
202
183
  Marks a method as an MCP resource (data source).
203
184
 
204
185
  ```typescript
205
- @Resource({ description: 'Get system configuration', mimeType: 'application/json' })
186
+ @Resource({
187
+ description: 'Get system configuration',
188
+ mimeType: 'application/json'
189
+ })
206
190
  async getConfig() {
207
191
  return {
208
192
  version: "1.0.0",
@@ -213,7 +197,7 @@ async getConfig() {
213
197
 
214
198
  ### @SchemaConstraint
215
199
 
216
- Add validation constraints to class properties for automatic schema generation.
200
+ Add validation constraints to class properties.
217
201
 
218
202
  ```typescript
219
203
  class UserInput {
@@ -242,6 +226,14 @@ class UserInput {
242
226
  }
243
227
  ```
244
228
 
229
+ **Common constraints:**
230
+ - `description`, `default` — Documentation
231
+ - `minLength`, `maxLength` — String length
232
+ - `minimum`, `maximum` — Number range
233
+ - `enum` — Allowed values
234
+ - `format` — String format (`email`, `uri`, `date`, etc.)
235
+ - `pattern` — Regex pattern
236
+
245
237
  ### @Optional
246
238
 
247
239
  Marks a property as optional in the schema.
@@ -257,8 +249,50 @@ class SearchInput {
257
249
  }
258
250
  ```
259
251
 
252
+ ---
253
+
260
254
  ## API Reference
261
255
 
256
+ ### createHTTPServer
257
+
258
+ Create and start an HTTP server with auto-discovery.
259
+
260
+ **Simplified API (Recommended):**
261
+ ```typescript
262
+ await createHTTPServer({
263
+ name: string; // Server name (required)
264
+ version: string; // Server version (required)
265
+ port?: number; // Port (default: 3001)
266
+ cors?: boolean | object; // Enable CORS (default: false)
267
+ logging?: boolean; // Enable logging (default: false)
268
+ debug?: boolean; // Verbose debug logs (default: false)
269
+ autoDiscover?: boolean; // Auto-discover services (default: true)
270
+ mcpDir?: string; // Custom mcp directory path
271
+ sessionTimeout?: number; // Session timeout in ms
272
+ stateless?: boolean; // Stateless mode for Lambda/serverless (default: true)
273
+ dashboard?: boolean; // Serve dashboard UI at / (default: true)
274
+ });
275
+ ```
276
+
277
+ **Factory Pattern (Advanced):**
278
+ ```typescript
279
+ const serverFactory = async () => {
280
+ const server = new MCPServer({
281
+ name: "my-server",
282
+ version: "1.0.0",
283
+ autoDiscover: false // Disable for manual registration
284
+ });
285
+
286
+ server.registerService(new MyService());
287
+ return server.getServer();
288
+ };
289
+
290
+ await createHTTPServer(serverFactory, {
291
+ port: 3001,
292
+ cors: true
293
+ });
294
+ ```
295
+
262
296
  ### MCPServer
263
297
 
264
298
  Main server class for registering services.
@@ -268,69 +302,39 @@ const server = new MCPServer({
268
302
  name: string; // Server name
269
303
  version: string; // Server version
270
304
  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)
305
+ debug?: boolean; // Verbose debug logs (default: false)
306
+ autoDiscover?: boolean; // Auto-discover from ./mcp (default: true)
307
+ mcpDir?: string; // Custom mcp directory path
274
308
  });
275
309
 
276
- // Manual registration
277
- server.registerService(instance: any): void;
278
-
279
- // Get underlying MCP SDK server
280
- server.getServer(): Server;
310
+ server.registerService(instance); // Manual registration
311
+ server.getServer(); // Get underlying MCP SDK server
281
312
  ```
282
313
 
283
- **Options:**
284
-
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`)
289
-
290
- #### Zero-Config Auto-Discovery
314
+ ---
291
315
 
292
- Services are automatically discovered and registered from the `./mcp` directory when the server is created:
316
+ ## Auto-Discovery
293
317
 
294
- **Basic Usage (Simplified API):**
295
- ```typescript
296
- import { createHTTPServer } from "@leanmcp/core";
297
-
298
- await createHTTPServer({
299
- name: "my-server",
300
- version: "1.0.0",
301
- port: 3000,
302
- logging: true // Enable logging
303
- });
304
- ```
318
+ Services are automatically discovered from the `./mcp` directory:
305
319
 
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
- ```
320
+ 1. Recursively scans for `index.ts` or `index.js` files
321
+ 2. Dynamically imports each file
322
+ 3. Looks for exported classes
323
+ 4. Instantiates with no-args constructors
324
+ 5. Registers all decorated methods
316
325
 
317
- **With Shared Dependencies:**
326
+ ### Shared Dependencies
318
327
 
319
- For services that need shared dependencies, create a `config.ts` (example) file in your `mcp` directory:
328
+ For services needing shared configuration (auth, database, etc.), create a `config.ts`:
320
329
 
321
330
  ```typescript
322
331
  // mcp/config.ts
323
332
  import { AuthProvider } from "@leanmcp/auth";
324
333
 
325
- if (!process.env.COGNITO_USER_POOL_ID || !process.env.COGNITO_CLIENT_ID) {
326
- throw new Error('Missing required Cognito configuration');
327
- }
328
-
329
334
  export const authProvider = new AuthProvider('cognito', {
330
- region: process.env.AWS_REGION || 'us-east-1',
335
+ region: process.env.AWS_REGION,
331
336
  userPoolId: process.env.COGNITO_USER_POOL_ID,
332
- clientId: process.env.COGNITO_CLIENT_ID,
333
- clientSecret: process.env.COGNITO_CLIENT_SECRET
337
+ clientId: process.env.COGNITO_CLIENT_ID
334
338
  });
335
339
 
336
340
  await authProvider.init();
@@ -346,147 +350,65 @@ import { authProvider } from "../config.js";
346
350
 
347
351
  @Authenticated(authProvider)
348
352
  export class SlackService {
349
- constructor() {
350
- // No parameters needed - use environment or imported config
351
- }
352
-
353
353
  @Tool({ description: 'Send a message' })
354
- async sendMessage(args: any) {
354
+ async sendMessage(args: { channel: string; message: string }) {
355
355
  // Implementation
356
356
  }
357
357
  }
358
358
  ```
359
359
 
360
- Your main file stays clean:
360
+ ---
361
361
 
362
- ```typescript
363
- import { createHTTPServer } from "@leanmcp/core";
362
+ ## Structured Content
364
363
 
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
- ```
364
+ Tool return values are automatically exposed as `structuredContent` in the MCP response, enabling ChatGPT Apps SDK compatibility.
395
365
 
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
- ```
366
+ **Automatic Handling:**
414
367
 
415
- **Factory Pattern (Advanced):**
416
368
  ```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
- );
369
+ @Tool({ description: 'List channels' })
370
+ async listChannels() {
371
+ // Return a plain object - it becomes structuredContent automatically
372
+ return { channels: [...] };
373
+ }
426
374
  ```
427
375
 
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
- });
376
+ The response includes both `content` (text) and `structuredContent` (object):
436
377
 
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
- });
378
+ ```json
379
+ {
380
+ "content": [{ "type": "text", "text": "{\"channels\": [...]}" }],
381
+ "structuredContent": { "channels": [...] }
382
+ }
446
383
  ```
447
384
 
448
- ### Schema Generation
385
+ **Manual MCP Response:**
449
386
 
450
- Generate JSON Schema from TypeScript classes:
387
+ If your tool returns a manual MCP response (with `content` array), the SDK extracts data from `content[0].text`:
451
388
 
452
389
  ```typescript
453
- import { classToJsonSchemaWithConstraints } from "@leanmcp/core";
454
-
455
- const schema = classToJsonSchemaWithConstraints(MyInputClass);
390
+ return {
391
+ content: [{ type: 'text', text: JSON.stringify({ channels }) }]
392
+ };
393
+ // structuredContent will be { channels: [...] }
456
394
  ```
457
395
 
458
- ## HTTP Endpoints
459
-
460
- When using `createHTTPServer`, the following endpoints are available:
396
+ ---
461
397
 
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
398
+ ## HTTP Endpoints
467
399
 
468
- ```bash
469
- PORT=3000 # Server port (optional)
470
- NODE_ENV=production # Environment (optional)
471
- ```
400
+ | Endpoint | Method | Description |
401
+ |----------|--------|-------------|
402
+ | `/mcp` | POST | MCP protocol endpoint (JSON-RPC 2.0) |
403
+ | `/health` | GET | Health check |
404
+ | `/` | GET | Welcome message |
472
405
 
473
406
  ## Error Handling
474
407
 
475
- All tools automatically handle errors and return them in MCP format:
408
+ Errors are automatically caught and returned in MCP format:
476
409
 
477
410
  ```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
- })
411
+ @Tool({ description: 'Divide numbers', inputClass: DivideInput })
490
412
  async divide(input: DivideInput) {
491
413
  if (input.b === 0) {
492
414
  throw new Error("Division by zero");
@@ -495,7 +417,7 @@ async divide(input: DivideInput) {
495
417
  }
496
418
  ```
497
419
 
498
- Errors are returned as:
420
+ Returns:
499
421
  ```json
500
422
  {
501
423
  "content": [{"type": "text", "text": "Error: Division by zero"}],
@@ -503,9 +425,20 @@ Errors are returned as:
503
425
  }
504
426
  ```
505
427
 
428
+ ## Environment Variables
429
+
430
+ ```bash
431
+ PORT=3001 # Server port
432
+ NODE_ENV=production # Environment
433
+ ```
434
+
506
435
  ## TypeScript Support
507
436
 
508
- Full TypeScript support with type inference:
437
+ **Key Points:**
438
+ - Input schema is defined via `inputClass` in the decorator
439
+ - Output type is inferred from the return type
440
+ - For tools with no input, omit `inputClass`
441
+ - Use `@SchemaConstraint` for validation and documentation
509
442
 
510
443
  ```typescript
511
444
  class MyInput {
@@ -513,43 +446,29 @@ class MyInput {
513
446
  field!: string;
514
447
  }
515
448
 
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;
449
+ @Tool({ description: 'My tool', inputClass: MyInput })
450
+ async myTool(input: MyInput): Promise<{ result: string }> {
451
+ return { result: input.field.toUpperCase() };
532
452
  }
533
453
  ```
534
454
 
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
540
-
541
- ## License
455
+ ## Documentation
542
456
 
543
- MIT
457
+ - [Full Documentation](https://docs.leanmcp.com/sdk/core)
544
458
 
545
459
  ## Related Packages
546
460
 
547
- - [@leanmcp/cli](../cli) - CLI tool for creating new projects
548
- - [@leanmcp/auth](../auth) - Authentication decorators and providers
549
- - [@leanmcp/utils](../utils) - Utility functions
461
+ - [@leanmcp/cli](https://www.npmjs.com/package/@leanmcp/cli) CLI tool for project creation
462
+ - [@leanmcp/auth](https://www.npmjs.com/package/@leanmcp/auth) Authentication decorators
463
+ - [@leanmcp/ui](https://www.npmjs.com/package/@leanmcp/ui) MCP App UI components
464
+ - [@leanmcp/elicitation](https://www.npmjs.com/package/@leanmcp/elicitation) — Structured user input
550
465
 
551
466
  ## Links
552
467
 
553
468
  - [GitHub Repository](https://github.com/LeanMCP/leanmcp-sdk)
469
+ - [NPM Package](https://www.npmjs.com/package/@leanmcp/core)
554
470
  - [MCP Specification](https://spec.modelcontextprotocol.io/)
555
- - [Documentation](https://github.com/LeanMCP/leanmcp-sdk#readme)
471
+
472
+ ## License
473
+
474
+ 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);
@@ -1157,10 +1161,27 @@ var init_index = __esm({
1157
1161
  const meta = request.params._meta;
1158
1162
  const result = await tool.method.call(tool.instance, request.params.arguments, meta);
1159
1163
  let formattedResult = result;
1164
+ let structuredContent = void 0;
1160
1165
  if (methodMeta.renderFormat === "markdown" && typeof result === "string") {
1161
1166
  formattedResult = result;
1162
- } else if (methodMeta.renderFormat === "json" || typeof result === "object") {
1163
- formattedResult = JSON.stringify(result, null, 2);
1167
+ } else if (typeof result === "object" && result !== null) {
1168
+ if ("structuredContent" in result && Object.keys(result).length === 1) {
1169
+ structuredContent = result.structuredContent;
1170
+ formattedResult = JSON.stringify(structuredContent, null, 2);
1171
+ } else if ("content" in result && Array.isArray(result.content)) {
1172
+ const textItem = result.content.find((c) => c.type === "text");
1173
+ if (textItem?.text) {
1174
+ try {
1175
+ structuredContent = JSON.parse(textItem.text);
1176
+ } catch {
1177
+ structuredContent = textItem.text;
1178
+ }
1179
+ }
1180
+ formattedResult = JSON.stringify(result, null, 2);
1181
+ } else {
1182
+ structuredContent = result;
1183
+ formattedResult = JSON.stringify(result, null, 2);
1184
+ }
1164
1185
  } else {
1165
1186
  formattedResult = String(result);
1166
1187
  }
@@ -1172,6 +1193,12 @@ var init_index = __esm({
1172
1193
  }
1173
1194
  ]
1174
1195
  };
1196
+ if (structuredContent) {
1197
+ response.structuredContent = structuredContent;
1198
+ if (this.logger) {
1199
+ this.logger.debug(`[MCPServer] Setting structuredContent: ${JSON.stringify(structuredContent).slice(0, 100)}...`);
1200
+ }
1201
+ }
1175
1202
  if (tool._meta && Object.keys(tool._meta).length > 0) {
1176
1203
  response._meta = tool._meta;
1177
1204
  }
@@ -1439,8 +1466,15 @@ var init_index = __esm({
1439
1466
  }
1440
1467
  /**
1441
1468
  * Watch UI manifest for changes and reload resources dynamically
1469
+ *
1470
+ * CRITICAL: Only for stateful mode. In stateless mode, each request
1471
+ * creates a fresh server that reads the manifest directly, making
1472
+ * watchers both unnecessary and a memory leak source.
1442
1473
  */
1443
1474
  watchUIManifest() {
1475
+ if (this.options.stateless) {
1476
+ return;
1477
+ }
1444
1478
  try {
1445
1479
  const manifestPath = import_path.default.join(process.cwd(), "dist", "ui-manifest.json");
1446
1480
  if (!import_fs.default.existsSync(manifestPath)) {
@@ -1624,6 +1658,25 @@ var init_index = __esm({
1624
1658
  return this.server;
1625
1659
  }
1626
1660
  /**
1661
+ * Clean up all registered services, watchers, and resources
1662
+ * CRITICAL for stateless mode to prevent memory leaks
1663
+ */
1664
+ close() {
1665
+ if (this.manifestWatcher) {
1666
+ try {
1667
+ this.manifestWatcher.close();
1668
+ } catch (e) {
1669
+ }
1670
+ this.manifestWatcher = null;
1671
+ }
1672
+ this.tools.clear();
1673
+ this.prompts.clear();
1674
+ this.resources.clear();
1675
+ if (this.server && typeof this.server.close === "function") {
1676
+ this.server.close();
1677
+ }
1678
+ }
1679
+ /**
1627
1680
  * Cleanup resources (call on server shutdown)
1628
1681
  */
1629
1682
  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);
@@ -1054,10 +1058,27 @@ var MCPServer = class {
1054
1058
  const meta = request.params._meta;
1055
1059
  const result = await tool.method.call(tool.instance, request.params.arguments, meta);
1056
1060
  let formattedResult = result;
1061
+ let structuredContent = void 0;
1057
1062
  if (methodMeta.renderFormat === "markdown" && typeof result === "string") {
1058
1063
  formattedResult = result;
1059
- } else if (methodMeta.renderFormat === "json" || typeof result === "object") {
1060
- formattedResult = JSON.stringify(result, null, 2);
1064
+ } else if (typeof result === "object" && result !== null) {
1065
+ if ("structuredContent" in result && Object.keys(result).length === 1) {
1066
+ structuredContent = result.structuredContent;
1067
+ formattedResult = JSON.stringify(structuredContent, null, 2);
1068
+ } else if ("content" in result && Array.isArray(result.content)) {
1069
+ const textItem = result.content.find((c) => c.type === "text");
1070
+ if (textItem?.text) {
1071
+ try {
1072
+ structuredContent = JSON.parse(textItem.text);
1073
+ } catch {
1074
+ structuredContent = textItem.text;
1075
+ }
1076
+ }
1077
+ formattedResult = JSON.stringify(result, null, 2);
1078
+ } else {
1079
+ structuredContent = result;
1080
+ formattedResult = JSON.stringify(result, null, 2);
1081
+ }
1061
1082
  } else {
1062
1083
  formattedResult = String(result);
1063
1084
  }
@@ -1069,6 +1090,12 @@ var MCPServer = class {
1069
1090
  }
1070
1091
  ]
1071
1092
  };
1093
+ if (structuredContent) {
1094
+ response.structuredContent = structuredContent;
1095
+ if (this.logger) {
1096
+ this.logger.debug(`[MCPServer] Setting structuredContent: ${JSON.stringify(structuredContent).slice(0, 100)}...`);
1097
+ }
1098
+ }
1072
1099
  if (tool._meta && Object.keys(tool._meta).length > 0) {
1073
1100
  response._meta = tool._meta;
1074
1101
  }
@@ -1336,8 +1363,15 @@ var MCPServer = class {
1336
1363
  }
1337
1364
  /**
1338
1365
  * Watch UI manifest for changes and reload resources dynamically
1366
+ *
1367
+ * CRITICAL: Only for stateful mode. In stateless mode, each request
1368
+ * creates a fresh server that reads the manifest directly, making
1369
+ * watchers both unnecessary and a memory leak source.
1339
1370
  */
1340
1371
  watchUIManifest() {
1372
+ if (this.options.stateless) {
1373
+ return;
1374
+ }
1341
1375
  try {
1342
1376
  const manifestPath = path.join(process.cwd(), "dist", "ui-manifest.json");
1343
1377
  if (!fs.existsSync(manifestPath)) {
@@ -1521,6 +1555,25 @@ var MCPServer = class {
1521
1555
  return this.server;
1522
1556
  }
1523
1557
  /**
1558
+ * Clean up all registered services, watchers, and resources
1559
+ * CRITICAL for stateless mode to prevent memory leaks
1560
+ */
1561
+ close() {
1562
+ if (this.manifestWatcher) {
1563
+ try {
1564
+ this.manifestWatcher.close();
1565
+ } catch (e) {
1566
+ }
1567
+ this.manifestWatcher = null;
1568
+ }
1569
+ this.tools.clear();
1570
+ this.prompts.clear();
1571
+ this.resources.clear();
1572
+ if (this.server && typeof this.server.close === "function") {
1573
+ this.server.close();
1574
+ }
1575
+ }
1576
+ /**
1524
1577
  * Cleanup resources (call on server shutdown)
1525
1578
  */
1526
1579
  async cleanup() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leanmcp/core",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Core library implementing decorators, reflection, and MCP runtime server",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",