@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 +188 -306
- package/dist/index.d.mts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +89 -19
- package/dist/index.mjs +89 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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-
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **MCP
|
|
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
|
-
###
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
|
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({
|
|
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
|
|
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; //
|
|
272
|
-
autoDiscover?: boolean; //
|
|
273
|
-
mcpDir?: string; // Custom mcp directory path
|
|
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.
|
|
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
|
-
|
|
313
|
+
---
|
|
284
314
|
|
|
285
|
-
-
|
|
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
|
-
|
|
317
|
+
Services are automatically discovered from the `./mcp` directory:
|
|
291
318
|
|
|
292
|
-
|
|
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
|
-
|
|
295
|
-
```typescript
|
|
296
|
-
import { createHTTPServer } from "@leanmcp/core";
|
|
325
|
+
### Shared Dependencies
|
|
297
326
|
|
|
298
|
-
|
|
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
|
|
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:
|
|
353
|
+
async sendMessage(args: { channel: string; message: string }) {
|
|
355
354
|
// Implementation
|
|
356
355
|
}
|
|
357
356
|
}
|
|
358
357
|
```
|
|
359
358
|
|
|
360
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
371
|
+
Errors are automatically caught and returned in MCP format:
|
|
476
372
|
|
|
477
373
|
```typescript
|
|
478
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
MIT
|
|
420
|
+
- [Full Documentation](https://docs.leanmcp.com/sdk/core)
|
|
544
421
|
|
|
545
422
|
## Related Packages
|
|
546
423
|
|
|
547
|
-
- [@leanmcp/cli](
|
|
548
|
-
- [@leanmcp/auth](
|
|
549
|
-
- [@leanmcp/
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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
|
-
|
|
905
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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)
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
867
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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() {
|