@leanmcp/core 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -10,8 +10,8 @@ import {
10
10
 
11
11
  // src/index.ts
12
12
  import "reflect-metadata";
13
- import fs from "fs";
14
- import path from "path";
13
+ import fs3 from "fs";
14
+ import path2 from "path";
15
15
  import { pathToFileURL } from "url";
16
16
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
17
17
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -168,6 +168,191 @@ __name(getDecoratedMethods, "getDecoratedMethods");
168
168
 
169
169
  // src/schema-generator.ts
170
170
  import "reflect-metadata";
171
+
172
+ // src/type-parser.ts
173
+ import fs from "fs";
174
+ import { createRequire } from "module";
175
+ var typeCache = /* @__PURE__ */ new Map();
176
+ var classSourceMap = /* @__PURE__ */ new Map();
177
+ var tsMorphProject = null;
178
+ var tsMorphNode = null;
179
+ var tsMorphLoaded = false;
180
+ function loadTsMorph() {
181
+ if (tsMorphLoaded) {
182
+ return tsMorphProject !== null;
183
+ }
184
+ tsMorphLoaded = true;
185
+ try {
186
+ const customRequire = createRequire(import.meta.url);
187
+ const tsMorph = customRequire("ts-morph");
188
+ tsMorphProject = tsMorph.Project;
189
+ tsMorphNode = tsMorph.Node;
190
+ return true;
191
+ } catch (error) {
192
+ console.warn(`[type-parser] ts-morph not available: ${error.message}`);
193
+ return false;
194
+ }
195
+ }
196
+ __name(loadTsMorph, "loadTsMorph");
197
+ function registerClassSource(className, sourceFile) {
198
+ classSourceMap.set(className, sourceFile);
199
+ }
200
+ __name(registerClassSource, "registerClassSource");
201
+ function parseClassTypesSync(classConstructor, sourceFilePath) {
202
+ if (!loadTsMorph()) {
203
+ return /* @__PURE__ */ new Map();
204
+ }
205
+ const className = classConstructor.name;
206
+ let filePath = sourceFilePath || classSourceMap.get(className) || findSourceFile();
207
+ if (!filePath) {
208
+ return /* @__PURE__ */ new Map();
209
+ }
210
+ if (filePath.endsWith(".js")) {
211
+ const tsPath = filePath.replace(/\.js$/, ".ts");
212
+ if (fs.existsSync(tsPath)) {
213
+ filePath = tsPath;
214
+ }
215
+ }
216
+ const cacheKey = `${filePath}:${className}`;
217
+ if (typeCache.has(cacheKey)) {
218
+ return typeCache.get(cacheKey);
219
+ }
220
+ if (!fs.existsSync(filePath)) {
221
+ return /* @__PURE__ */ new Map();
222
+ }
223
+ try {
224
+ const project = new tsMorphProject({
225
+ skipAddingFilesFromTsConfig: true,
226
+ skipFileDependencyResolution: true,
227
+ useInMemoryFileSystem: false
228
+ });
229
+ const sourceFile = project.addSourceFileAtPath(filePath);
230
+ const classDecl = sourceFile.getClass(className);
231
+ if (!classDecl) {
232
+ return /* @__PURE__ */ new Map();
233
+ }
234
+ const propertyTypes = /* @__PURE__ */ new Map();
235
+ for (const prop of classDecl.getInstanceProperties()) {
236
+ if (tsMorphNode.isPropertyDeclaration(prop)) {
237
+ const propName = prop.getName();
238
+ const typeNode = prop.getTypeNode();
239
+ if (typeNode) {
240
+ const typeText = typeNode.getText();
241
+ const jsonSchemaType = typeTextToJsonSchema(typeText);
242
+ propertyTypes.set(propName, jsonSchemaType);
243
+ }
244
+ }
245
+ }
246
+ typeCache.set(cacheKey, propertyTypes);
247
+ return propertyTypes;
248
+ } catch (error) {
249
+ console.warn(`[type-parser] Failed to parse ${filePath}: ${error.message}`);
250
+ return /* @__PURE__ */ new Map();
251
+ }
252
+ }
253
+ __name(parseClassTypesSync, "parseClassTypesSync");
254
+ function typeTextToJsonSchema(typeText) {
255
+ const type = typeText.trim().replace(/\?$/, "");
256
+ if (type.endsWith("[]")) {
257
+ const elementType = type.slice(0, -2);
258
+ return {
259
+ type: "array",
260
+ items: typeTextToJsonSchema(elementType)
261
+ };
262
+ }
263
+ const arrayMatch = type.match(/^Array<(.+)>$/);
264
+ if (arrayMatch) {
265
+ return {
266
+ type: "array",
267
+ items: typeTextToJsonSchema(arrayMatch[1])
268
+ };
269
+ }
270
+ const lowerType = type.toLowerCase();
271
+ switch (lowerType) {
272
+ case "string":
273
+ return {
274
+ type: "string"
275
+ };
276
+ case "number":
277
+ return {
278
+ type: "number"
279
+ };
280
+ case "integer":
281
+ return {
282
+ type: "integer"
283
+ };
284
+ case "boolean":
285
+ return {
286
+ type: "boolean"
287
+ };
288
+ case "object":
289
+ return {
290
+ type: "object"
291
+ };
292
+ case "any":
293
+ case "unknown":
294
+ return {};
295
+ // No constraints
296
+ case "null":
297
+ return {
298
+ type: "null"
299
+ };
300
+ }
301
+ if (type.includes("|")) {
302
+ const parts = type.split("|").map((p) => p.trim());
303
+ const nonNullParts = parts.filter((p) => p.toLowerCase() !== "null");
304
+ if (nonNullParts.length === 1) {
305
+ return typeTextToJsonSchema(nonNullParts[0]);
306
+ }
307
+ return {
308
+ anyOf: nonNullParts.map((p) => typeTextToJsonSchema(p))
309
+ };
310
+ }
311
+ return {
312
+ type: "object"
313
+ };
314
+ }
315
+ __name(typeTextToJsonSchema, "typeTextToJsonSchema");
316
+ function findSourceFile() {
317
+ const originalPrepareStackTrace = Error.prepareStackTrace;
318
+ try {
319
+ Error.prepareStackTrace = (_, stack2) => stack2;
320
+ const err = new Error();
321
+ const stack = err.stack;
322
+ for (const site of stack) {
323
+ const fileName = site.getFileName();
324
+ if (fileName && !fileName.includes("node_modules") && !fileName.includes("type-parser") && !fileName.includes("schema-generator") && (fileName.endsWith(".ts") || fileName.endsWith(".js"))) {
325
+ if (fileName.endsWith(".js")) {
326
+ const tsPath = fileName.replace(/\.js$/, ".ts");
327
+ if (fs.existsSync(tsPath)) {
328
+ return tsPath;
329
+ }
330
+ }
331
+ return fileName;
332
+ }
333
+ }
334
+ } finally {
335
+ Error.prepareStackTrace = originalPrepareStackTrace;
336
+ }
337
+ return null;
338
+ }
339
+ __name(findSourceFile, "findSourceFile");
340
+ function clearTypeCache() {
341
+ typeCache.clear();
342
+ }
343
+ __name(clearTypeCache, "clearTypeCache");
344
+
345
+ // src/schema-generator.ts
346
+ import path from "path";
347
+ import fs2 from "fs";
348
+ var schemaMetadata = null;
349
+ try {
350
+ const metadataPath = path.join(process.cwd(), "dist", "schema-metadata.json");
351
+ if (fs2.existsSync(metadataPath)) {
352
+ schemaMetadata = JSON.parse(fs2.readFileSync(metadataPath, "utf-8"));
353
+ }
354
+ } catch {
355
+ }
171
356
  function classToJsonSchema(classConstructor) {
172
357
  const instance = new classConstructor();
173
358
  const properties = {};
@@ -224,20 +409,52 @@ __name(Optional, "Optional");
224
409
  function SchemaConstraint(constraints) {
225
410
  return (target, propertyKey) => {
226
411
  Reflect.defineMetadata("schema:constraints", constraints, target, propertyKey);
412
+ const originalPrepareStackTrace = Error.prepareStackTrace;
413
+ try {
414
+ Error.prepareStackTrace = (_, stack2) => stack2;
415
+ const err = new Error();
416
+ const stack = err.stack;
417
+ for (const site of stack) {
418
+ const fileName = site.getFileName();
419
+ if (fileName && !fileName.includes("node_modules") && !fileName.includes("schema-generator") && (fileName.endsWith(".ts") || fileName.endsWith(".js"))) {
420
+ const className = target.constructor.name;
421
+ if (className && className !== "Object") {
422
+ registerClassSource(className, fileName);
423
+ }
424
+ break;
425
+ }
426
+ }
427
+ } finally {
428
+ Error.prepareStackTrace = originalPrepareStackTrace;
429
+ }
227
430
  };
228
431
  }
229
432
  __name(SchemaConstraint, "SchemaConstraint");
230
- function classToJsonSchemaWithConstraints(classConstructor) {
433
+ function classToJsonSchemaWithConstraints(classConstructor, sourceFilePath) {
231
434
  const instance = new classConstructor();
232
435
  const properties = {};
233
436
  const required = [];
437
+ const className = classConstructor.name;
438
+ const precomputedTypes = schemaMetadata?.[className];
439
+ const parsedTypes = precomputedTypes ? null : parseClassTypesSync(classConstructor, sourceFilePath);
234
440
  const propertyNames = Object.keys(instance);
235
441
  for (const propertyName of propertyNames) {
236
442
  const propertyType = Reflect.getMetadata("design:type", instance, propertyName);
237
443
  const constraints = Reflect.getMetadata("schema:constraints", instance, propertyName);
238
444
  const isOptional = Reflect.getMetadata("optional", instance, propertyName);
239
- let jsonSchemaType = "string";
240
- if (propertyType) {
445
+ let parsedType;
446
+ if (precomputedTypes) {
447
+ parsedType = precomputedTypes[propertyName];
448
+ } else if (parsedTypes) {
449
+ parsedType = parsedTypes.get(propertyName);
450
+ }
451
+ let propertySchema;
452
+ if (parsedType && parsedType.type) {
453
+ propertySchema = {
454
+ ...parsedType
455
+ };
456
+ } else if (propertyType) {
457
+ let jsonSchemaType = "string";
241
458
  switch (propertyType.name) {
242
459
  case "String":
243
460
  jsonSchemaType = "string";
@@ -257,8 +474,16 @@ function classToJsonSchemaWithConstraints(classConstructor) {
257
474
  default:
258
475
  jsonSchemaType = "object";
259
476
  }
477
+ propertySchema = {
478
+ type: jsonSchemaType
479
+ };
260
480
  } else if (constraints) {
261
- if (constraints.minLength !== void 0 || constraints.maxLength !== void 0 || constraints.pattern) {
481
+ let jsonSchemaType = "string";
482
+ if (constraints.type) {
483
+ jsonSchemaType = constraints.type;
484
+ } else if (constraints.items) {
485
+ jsonSchemaType = "array";
486
+ } else if (constraints.minLength !== void 0 || constraints.maxLength !== void 0 || constraints.pattern) {
262
487
  jsonSchemaType = "string";
263
488
  } else if (constraints.minimum !== void 0 || constraints.maximum !== void 0) {
264
489
  jsonSchemaType = "number";
@@ -272,12 +497,25 @@ function classToJsonSchemaWithConstraints(classConstructor) {
272
497
  jsonSchemaType = "string";
273
498
  }
274
499
  }
500
+ propertySchema = {
501
+ type: jsonSchemaType
502
+ };
503
+ } else {
504
+ propertySchema = {
505
+ type: "string"
506
+ };
275
507
  }
276
- const propertySchema = {
277
- type: jsonSchemaType,
278
- ...constraints || {}
279
- };
280
- if (jsonSchemaType === "array" && !propertySchema.items) {
508
+ if (constraints) {
509
+ const parsedItems = propertySchema.items;
510
+ propertySchema = {
511
+ ...propertySchema,
512
+ ...constraints
513
+ };
514
+ if (parsedItems && !constraints.items) {
515
+ propertySchema.items = parsedItems;
516
+ }
517
+ }
518
+ if (propertySchema.type === "array" && !propertySchema.items) {
281
519
  propertySchema.items = {
282
520
  type: "string"
283
521
  };
@@ -305,9 +543,9 @@ function validatePort(port) {
305
543
  }
306
544
  }
307
545
  __name(validatePort, "validatePort");
308
- function validatePath(path2) {
309
- if (path2.includes("..") || path2.includes("~")) {
310
- throw new Error(`Invalid path: ${path2}. Path traversal patterns are not allowed`);
546
+ function validatePath(path3) {
547
+ if (path3.includes("..") || path3.includes("~")) {
548
+ throw new Error(`Invalid path: ${path3}. Path traversal patterns are not allowed`);
311
549
  }
312
550
  }
313
551
  __name(validatePath, "validatePath");
@@ -396,9 +634,9 @@ async function createHTTPServer(serverInput, options) {
396
634
  if (!serverOptions.mcpDir) {
397
635
  const callerFile = getCallerFile();
398
636
  if (callerFile) {
399
- const path2 = await import("path");
400
- const callerDir = path2.dirname(callerFile);
401
- resolvedMcpDir = path2.join(callerDir, "mcp");
637
+ const path3 = await import("path");
638
+ const callerDir = path3.dirname(callerFile);
639
+ resolvedMcpDir = path3.join(callerDir, "mcp");
402
640
  }
403
641
  } else {
404
642
  resolvedMcpDir = serverOptions.mcpDir;
@@ -1328,13 +1566,13 @@ var MCPServer = class {
1328
1566
  } else {
1329
1567
  const callerFile = this.getCallerFile();
1330
1568
  if (callerFile) {
1331
- const callerDir = path.dirname(callerFile);
1332
- mcpDir = path.join(callerDir, "mcp");
1569
+ const callerDir = path2.dirname(callerFile);
1570
+ mcpDir = path2.join(callerDir, "mcp");
1333
1571
  } else {
1334
- mcpDir = path.join(process.cwd(), "mcp");
1572
+ mcpDir = path2.join(process.cwd(), "mcp");
1335
1573
  }
1336
1574
  }
1337
- if (fs.existsSync(mcpDir)) {
1575
+ if (fs3.existsSync(mcpDir)) {
1338
1576
  this.logger.debug(`Auto-discovering services from: ${mcpDir}`);
1339
1577
  await this.autoRegisterServices(mcpDir, serviceFactories);
1340
1578
  } else {
@@ -1584,7 +1822,7 @@ var MCPServer = class {
1584
1822
  */
1585
1823
  async autoRegisterServices(mcpDir, serviceFactories) {
1586
1824
  this.logger.debug(`Auto-registering services from: ${mcpDir}`);
1587
- if (!fs.existsSync(mcpDir)) {
1825
+ if (!fs3.existsSync(mcpDir)) {
1588
1826
  this.logger.warn(`MCP directory not found: ${mcpDir}`);
1589
1827
  return;
1590
1828
  }
@@ -1603,11 +1841,11 @@ var MCPServer = class {
1603
1841
  */
1604
1842
  findServiceFiles(dir) {
1605
1843
  const files = [];
1606
- const entries = fs.readdirSync(dir, {
1844
+ const entries = fs3.readdirSync(dir, {
1607
1845
  withFileTypes: true
1608
1846
  });
1609
1847
  for (const entry of entries) {
1610
- const fullPath = path.join(dir, entry.name);
1848
+ const fullPath = path2.join(dir, entry.name);
1611
1849
  if (entry.isDirectory()) {
1612
1850
  files.push(...this.findServiceFiles(fullPath));
1613
1851
  } else if (entry.isFile()) {
@@ -1638,7 +1876,7 @@ var MCPServer = class {
1638
1876
  }
1639
1877
  this.registerService(instance);
1640
1878
  registeredCount++;
1641
- this.logger.debug(`Registered service: ${exportName} from ${path.basename(filePath)}`);
1879
+ this.logger.debug(`Registered service: ${exportName} from ${path2.basename(filePath)}`);
1642
1880
  } catch (error) {
1643
1881
  this.logger.warn(`Skipped ${exportName}: ${error.message}`);
1644
1882
  }
@@ -1737,8 +1975,8 @@ var MCPServer = class {
1737
1975
  return;
1738
1976
  }
1739
1977
  try {
1740
- const manifestPath = path.join(process.cwd(), "dist", "ui-manifest.json");
1741
- if (!fs.existsSync(manifestPath)) {
1978
+ const manifestPath = path2.join(process.cwd(), "dist", "ui-manifest.json");
1979
+ if (!fs3.existsSync(manifestPath)) {
1742
1980
  return;
1743
1981
  }
1744
1982
  if (this.logging) {
@@ -1780,8 +2018,8 @@ var MCPServer = class {
1780
2018
  */
1781
2019
  async reloadUIManifest() {
1782
2020
  try {
1783
- const manifestPath = path.join(process.cwd(), "dist", "ui-manifest.json");
1784
- if (!fs.existsSync(manifestPath)) {
2021
+ const manifestPath = path2.join(process.cwd(), "dist", "ui-manifest.json");
2022
+ if (!fs3.existsSync(manifestPath)) {
1785
2023
  const uiResourceUris = Array.from(this.resources.keys()).filter((uri) => uri.startsWith("ui://"));
1786
2024
  for (const uri of uiResourceUris) {
1787
2025
  this.resources.delete(uri);
@@ -1791,7 +2029,7 @@ var MCPServer = class {
1791
2029
  }
1792
2030
  return;
1793
2031
  }
1794
- const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
2032
+ const manifest = JSON.parse(fs3.readFileSync(manifestPath, "utf-8"));
1795
2033
  const currentUIUris = new Set(Object.keys(manifest));
1796
2034
  const registeredUIUris = Array.from(this.resources.keys()).filter((uri) => uri.startsWith("ui://"));
1797
2035
  for (const uri of registeredUIUris) {
@@ -1807,7 +2045,7 @@ var MCPServer = class {
1807
2045
  const htmlPath = isString ? entry : entry.htmlPath;
1808
2046
  const isGPTApp = !isString && entry.isGPTApp;
1809
2047
  const gptMeta = !isString ? entry.gptMeta : void 0;
1810
- if (!fs.existsSync(htmlPath)) {
2048
+ if (!fs3.existsSync(htmlPath)) {
1811
2049
  if (this.logging) {
1812
2050
  this.logger.warn(`UI HTML file not found: ${htmlPath}`);
1813
2051
  }
@@ -1828,8 +2066,8 @@ var MCPServer = class {
1828
2066
  mimeType,
1829
2067
  inputSchema: void 0,
1830
2068
  method: /* @__PURE__ */ __name(async () => {
1831
- if (fs.existsSync(htmlPath)) {
1832
- const html = fs.readFileSync(htmlPath, "utf-8");
2069
+ if (fs3.existsSync(htmlPath)) {
2070
+ const html = fs3.readFileSync(htmlPath, "utf-8");
1833
2071
  return {
1834
2072
  text: html,
1835
2073
  _meta: Object.keys(_meta).length > 0 ? _meta : void 0
@@ -1857,11 +2095,11 @@ var MCPServer = class {
1857
2095
  */
1858
2096
  async loadUIManifest() {
1859
2097
  try {
1860
- const manifestPath = path.join(process.cwd(), "dist", "ui-manifest.json");
1861
- if (!fs.existsSync(manifestPath)) {
2098
+ const manifestPath = path2.join(process.cwd(), "dist", "ui-manifest.json");
2099
+ if (!fs3.existsSync(manifestPath)) {
1862
2100
  return;
1863
2101
  }
1864
- const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
2102
+ const manifest = JSON.parse(fs3.readFileSync(manifestPath, "utf-8"));
1865
2103
  for (const [uri, entry] of Object.entries(manifest)) {
1866
2104
  const isString = typeof entry === "string";
1867
2105
  const htmlPath = isString ? entry : entry.htmlPath;
@@ -1873,13 +2111,13 @@ var MCPServer = class {
1873
2111
  }
1874
2112
  continue;
1875
2113
  }
1876
- if (!fs.existsSync(htmlPath)) {
2114
+ if (!fs3.existsSync(htmlPath)) {
1877
2115
  if (this.logging) {
1878
2116
  this.logger.warn(`UI HTML file not found: ${htmlPath}`);
1879
2117
  }
1880
2118
  continue;
1881
2119
  }
1882
- const html = fs.readFileSync(htmlPath, "utf-8");
2120
+ const html = fs3.readFileSync(htmlPath, "utf-8");
1883
2121
  const mimeType = isGPTApp ? "text/html+skybridge" : "text/html;profile=mcp-app";
1884
2122
  const _meta = {};
1885
2123
  if (isGPTApp) {
@@ -2130,19 +2368,19 @@ var MCPServerRuntime = class {
2130
2368
  });
2131
2369
  }
2132
2370
  async loadServices() {
2133
- const absPath = path.resolve(this.options.servicesDir);
2134
- if (!fs.existsSync(absPath)) {
2371
+ const absPath = path2.resolve(this.options.servicesDir);
2372
+ if (!fs3.existsSync(absPath)) {
2135
2373
  this.logger.error(`Services directory not found: ${absPath}`);
2136
2374
  return;
2137
2375
  }
2138
- const files = fs.readdirSync(absPath);
2376
+ const files = fs3.readdirSync(absPath);
2139
2377
  let toolCount = 0;
2140
2378
  let promptCount = 0;
2141
2379
  let resourceCount = 0;
2142
2380
  for (const dir of files) {
2143
- const modulePath = path.join(absPath, dir, "index.ts");
2144
- const modulePathJs = path.join(absPath, dir, "index.js");
2145
- const finalPath = fs.existsSync(modulePath) ? modulePath : fs.existsSync(modulePathJs) ? modulePathJs : null;
2381
+ const modulePath = path2.join(absPath, dir, "index.ts");
2382
+ const modulePathJs = path2.join(absPath, dir, "index.js");
2383
+ const finalPath = fs3.existsSync(modulePath) ? modulePath : fs3.existsSync(modulePathJs) ? modulePathJs : null;
2146
2384
  if (finalPath) {
2147
2385
  try {
2148
2386
  const fileUrl = pathToFileURL(finalPath).href;
@@ -2277,6 +2515,7 @@ export {
2277
2515
  UserEnvs,
2278
2516
  classToJsonSchema,
2279
2517
  classToJsonSchemaWithConstraints,
2518
+ clearTypeCache,
2280
2519
  createAuthError,
2281
2520
  createHTTPServer,
2282
2521
  createProtectedResourceMetadata,
@@ -2285,6 +2524,8 @@ export {
2285
2524
  getDecoratedMethods,
2286
2525
  getMethodMetadata,
2287
2526
  isAuthError,
2527
+ parseClassTypesSync,
2528
+ registerClassSource,
2288
2529
  startMCPServer,
2289
2530
  validateNonEmpty,
2290
2531
  validatePath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leanmcp/core",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Core library implementing decorators, reflection, and MCP runtime server",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -32,6 +32,9 @@
32
32
  "dotenv": "^16.3.1",
33
33
  "reflect-metadata": "^0.2.1"
34
34
  },
35
+ "optionalDependencies": {
36
+ "ts-morph": "^24.0.0"
37
+ },
35
38
  "peerDependencies": {
36
39
  "cors": "^2.8.5",
37
40
  "express": "^5.0.0",
@@ -75,4 +78,4 @@
75
78
  "publishConfig": {
76
79
  "access": "public"
77
80
  }
78
- }
81
+ }