@leanmcp/cli 0.2.7 → 0.2.9

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.js CHANGED
@@ -1,51 +1,415 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- var __create = Object.create;
4
1
  var __defProp = Object.defineProperty;
5
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __getProtoOf = Object.getPrototypeOf;
8
- var __hasOwnProp = Object.prototype.hasOwnProperty;
9
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
3
+
4
+ // src/index.ts
5
+ import { Command } from "commander";
6
+ import chalk3 from "chalk";
7
+ import fs5 from "fs-extra";
8
+ import path5 from "path";
9
+ import ora3 from "ora";
10
+ import { createRequire } from "module";
11
+ import { confirm } from "@inquirer/prompts";
12
+ import { spawn as spawn3 } from "child_process";
13
+
14
+ // src/commands/dev.ts
15
+ import { spawn } from "child_process";
16
+ import chalk from "chalk";
17
+ import ora from "ora";
18
+ import path3 from "path";
19
+ import fs3 from "fs-extra";
20
+ import chokidar from "chokidar";
21
+
22
+ // src/vite/scanUIApp.ts
23
+ import fs from "fs-extra";
24
+ import path from "path";
25
+ import { glob } from "glob";
26
+ async function scanUIApp(projectDir) {
27
+ const mcpDir = path.join(projectDir, "mcp");
28
+ if (!await fs.pathExists(mcpDir)) {
29
+ return [];
30
+ }
31
+ const tsFiles = await glob("**/*.ts", {
32
+ cwd: mcpDir,
33
+ absolute: false,
34
+ ignore: [
35
+ "**/*.d.ts",
36
+ "**/node_modules/**"
37
+ ]
38
+ });
39
+ const results = [];
40
+ for (const relativeFile of tsFiles) {
41
+ const filePath = path.join(mcpDir, relativeFile);
42
+ const content = await fs.readFile(filePath, "utf-8");
43
+ if (!content.includes("@UIApp") || !content.includes("@leanmcp/ui")) {
44
+ continue;
45
+ }
46
+ const uiApps = parseUIAppDecorators(content, filePath);
47
+ results.push(...uiApps);
48
+ }
49
+ return results;
50
+ }
51
+ __name(scanUIApp, "scanUIApp");
52
+ function parseUIAppDecorators(content, filePath) {
53
+ const results = [];
54
+ const classMatch = content.match(/export\s+class\s+(\w+)/);
55
+ const serviceName = classMatch ? classMatch[1] : "Unknown";
56
+ const importMap = parseImports(content, filePath);
57
+ const uiAppRegex = /@UIApp\s*\(\s*\{([^}]+)\}\s*\)\s*(?:async\s+)?(\w+)/g;
58
+ let match;
59
+ while ((match = uiAppRegex.exec(content)) !== null) {
60
+ const decoratorBody = match[1];
61
+ const methodName = match[2];
62
+ const componentMatch = decoratorBody.match(/component\s*:\s*(\w+)/);
63
+ if (!componentMatch) continue;
64
+ const componentName = componentMatch[1];
65
+ const componentPath = importMap[componentName];
66
+ if (!componentPath) {
67
+ console.warn(`[scanUIApp] Could not resolve import for component: ${componentName}`);
68
+ continue;
69
+ }
70
+ const servicePrefix = serviceName.replace(/Service$/i, "").toLowerCase();
71
+ const resourceUri = `ui://${servicePrefix}/${methodName}`;
72
+ results.push({
73
+ servicePath: filePath,
74
+ componentPath,
75
+ componentName,
76
+ resourceUri,
77
+ methodName,
78
+ serviceName
79
+ });
80
+ }
81
+ return results;
82
+ }
83
+ __name(parseUIAppDecorators, "parseUIAppDecorators");
84
+ function parseImports(content, filePath) {
85
+ const importMap = {};
86
+ const dir = path.dirname(filePath);
87
+ const importRegex = /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g;
88
+ let match;
89
+ while ((match = importRegex.exec(content)) !== null) {
90
+ const importPath = match[2];
91
+ if (!importPath.startsWith(".")) continue;
92
+ const names = match[1].split(",").map((n) => n.trim().split(/\s+as\s+/).pop().trim());
93
+ let resolvedPath = path.resolve(dir, importPath);
94
+ if (!resolvedPath.endsWith(".tsx") && !resolvedPath.endsWith(".ts")) {
95
+ if (fs.existsSync(resolvedPath + ".tsx")) {
96
+ resolvedPath += ".tsx";
97
+ } else if (fs.existsSync(resolvedPath + ".ts")) {
98
+ resolvedPath += ".ts";
99
+ }
100
+ }
101
+ for (const name of names) {
102
+ importMap[name] = resolvedPath;
103
+ }
15
104
  }
16
- return to;
105
+ return importMap;
106
+ }
107
+ __name(parseImports, "parseImports");
108
+
109
+ // src/vite/buildUI.ts
110
+ import * as vite from "vite";
111
+ import react from "@vitejs/plugin-react";
112
+ import { viteSingleFile } from "vite-plugin-singlefile";
113
+ import fs2 from "fs-extra";
114
+ import path2 from "path";
115
+ async function buildUIComponent(uiApp, projectDir, isDev = false) {
116
+ const { componentPath, componentName, resourceUri } = uiApp;
117
+ const safeFileName = resourceUri.replace("ui://", "").replace(/\//g, "-") + ".html";
118
+ const outDir = path2.join(projectDir, "dist", "ui");
119
+ const htmlPath = path2.join(outDir, safeFileName);
120
+ await fs2.ensureDir(outDir);
121
+ const tempDir = path2.join(projectDir, ".leanmcp-temp");
122
+ await fs2.ensureDir(tempDir);
123
+ const entryHtml = path2.join(tempDir, "index.html");
124
+ const entryJs = path2.join(tempDir, "entry.tsx");
125
+ await fs2.writeFile(entryHtml, `<!DOCTYPE html>
126
+ <html lang="en">
127
+ <head>
128
+ <meta charset="UTF-8">
129
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
130
+ <title>MCP App</title>
131
+ </head>
132
+ <body>
133
+ <div id="root"></div>
134
+ <script type="module" src="./entry.tsx"></script>
135
+ </body>
136
+ </html>`);
137
+ const relativeComponentPath = path2.relative(tempDir, componentPath).replace(/\\/g, "/");
138
+ await fs2.writeFile(entryJs, `
139
+ import React, { StrictMode } from 'react';
140
+ import { createRoot } from 'react-dom/client';
141
+ import { AppProvider } from '@leanmcp/ui';
142
+ import { ${componentName} } from '${relativeComponentPath.replace(/\.tsx?$/, "")}';
143
+
144
+ const APP_INFO = {
145
+ name: '${componentName}',
146
+ version: '1.0.0'
17
147
  };
18
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
19
- // If the importer is in node compatibility mode or this is not an ESM
20
- // file that has been converted to a CommonJS file using a Babel-
21
- // compatible transform (i.e. "__esModule" has not been set), then set
22
- // "default" to the CommonJS "module.exports" for node compatibility.
23
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
24
- mod
25
- ));
148
+
149
+ function App() {
150
+ return (
151
+ <AppProvider appInfo={APP_INFO}>
152
+ <${componentName} />
153
+ </AppProvider>
154
+ );
155
+ }
156
+
157
+ createRoot(document.getElementById('root')!).render(
158
+ <StrictMode>
159
+ <App />
160
+ </StrictMode>
161
+ );
162
+ `);
163
+ try {
164
+ await vite.build({
165
+ root: tempDir,
166
+ plugins: [
167
+ react(),
168
+ viteSingleFile()
169
+ ],
170
+ build: {
171
+ outDir,
172
+ emptyOutDir: false,
173
+ sourcemap: isDev ? "inline" : false,
174
+ minify: !isDev,
175
+ rollupOptions: {
176
+ input: entryHtml,
177
+ output: {
178
+ entryFileNames: `[name].js`
179
+ }
180
+ }
181
+ },
182
+ logLevel: "warn"
183
+ });
184
+ const builtHtml = path2.join(outDir, "index.html");
185
+ if (await fs2.pathExists(builtHtml)) {
186
+ await fs2.move(builtHtml, htmlPath, {
187
+ overwrite: true
188
+ });
189
+ }
190
+ await fs2.remove(entryHtml);
191
+ await fs2.remove(entryJs);
192
+ return {
193
+ success: true,
194
+ htmlPath
195
+ };
196
+ } catch (error) {
197
+ return {
198
+ success: false,
199
+ htmlPath: "",
200
+ error: error.message
201
+ };
202
+ }
203
+ }
204
+ __name(buildUIComponent, "buildUIComponent");
205
+ async function writeUIManifest(manifest, projectDir) {
206
+ const manifestPath = path2.join(projectDir, "dist", "ui-manifest.json");
207
+ await fs2.ensureDir(path2.dirname(manifestPath));
208
+ await fs2.writeJson(manifestPath, manifest, {
209
+ spaces: 2
210
+ });
211
+ }
212
+ __name(writeUIManifest, "writeUIManifest");
213
+
214
+ // src/commands/dev.ts
215
+ async function devCommand() {
216
+ const cwd = process.cwd();
217
+ if (!await fs3.pathExists(path3.join(cwd, "main.ts"))) {
218
+ console.error(chalk.red("ERROR: Not a LeanMCP project (main.ts not found)."));
219
+ console.error(chalk.gray("Run this command from your project root."));
220
+ process.exit(1);
221
+ }
222
+ console.log(chalk.cyan("\n\u{1F680} LeanMCP Development Server\n"));
223
+ const scanSpinner = ora("Scanning for @UIApp components...").start();
224
+ const uiApps = await scanUIApp(cwd);
225
+ if (uiApps.length === 0) {
226
+ scanSpinner.succeed("No @UIApp components found");
227
+ } else {
228
+ scanSpinner.succeed(`Found ${uiApps.length} @UIApp component(s)`);
229
+ for (const app of uiApps) {
230
+ console.log(chalk.gray(` \u2022 ${app.componentName} \u2192 ${app.resourceUri}`));
231
+ }
232
+ }
233
+ const manifest = {};
234
+ if (uiApps.length > 0) {
235
+ const buildSpinner = ora("Building UI components...").start();
236
+ const errors = [];
237
+ for (const app of uiApps) {
238
+ const result = await buildUIComponent(app, cwd, true);
239
+ if (result.success) {
240
+ manifest[app.resourceUri] = result.htmlPath;
241
+ } else {
242
+ errors.push(`${app.componentName}: ${result.error}`);
243
+ }
244
+ }
245
+ await writeUIManifest(manifest, cwd);
246
+ if (errors.length > 0) {
247
+ buildSpinner.warn("Built with warnings");
248
+ for (const error of errors) {
249
+ console.error(chalk.yellow(` \u26A0 ${error}`));
250
+ }
251
+ } else {
252
+ buildSpinner.succeed("UI components built");
253
+ }
254
+ }
255
+ console.log(chalk.cyan("\nStarting development server...\n"));
256
+ const devServer = spawn("npx", [
257
+ "tsx",
258
+ "watch",
259
+ "main.ts"
260
+ ], {
261
+ cwd,
262
+ stdio: "inherit",
263
+ shell: true
264
+ });
265
+ let watcher = null;
266
+ if (uiApps.length > 0) {
267
+ const componentPaths = uiApps.map((app) => app.componentPath);
268
+ watcher = chokidar.watch(componentPaths, {
269
+ ignoreInitial: true
270
+ });
271
+ watcher.on("change", async (changedPath) => {
272
+ const app = uiApps.find((a) => a.componentPath === changedPath);
273
+ if (!app) return;
274
+ console.log(chalk.cyan(`
275
+ [UI] Rebuilding ${app.componentName}...`));
276
+ const result = await buildUIComponent(app, cwd, true);
277
+ if (result.success) {
278
+ manifest[app.resourceUri] = result.htmlPath;
279
+ await writeUIManifest(manifest, cwd);
280
+ console.log(chalk.green(`[UI] ${app.componentName} rebuilt successfully`));
281
+ } else {
282
+ console.log(chalk.yellow(`[UI] ${app.componentName} build failed: ${result.error}`));
283
+ }
284
+ });
285
+ }
286
+ const cleanup = /* @__PURE__ */ __name(() => {
287
+ console.log(chalk.gray("\nShutting down..."));
288
+ if (watcher) watcher.close();
289
+ devServer.kill();
290
+ process.exit(0);
291
+ }, "cleanup");
292
+ process.on("SIGINT", cleanup);
293
+ process.on("SIGTERM", cleanup);
294
+ devServer.on("exit", (code) => {
295
+ if (watcher) watcher.close();
296
+ process.exit(code ?? 0);
297
+ });
298
+ }
299
+ __name(devCommand, "devCommand");
300
+
301
+ // src/commands/start.ts
302
+ import { spawn as spawn2 } from "child_process";
303
+ import chalk2 from "chalk";
304
+ import ora2 from "ora";
305
+ import path4 from "path";
306
+ import fs4 from "fs-extra";
307
+ async function startCommand() {
308
+ const cwd = process.cwd();
309
+ if (!await fs4.pathExists(path4.join(cwd, "main.ts"))) {
310
+ console.error(chalk2.red("ERROR: Not a LeanMCP project (main.ts not found)."));
311
+ console.error(chalk2.gray("Run this command from your project root."));
312
+ process.exit(1);
313
+ }
314
+ console.log(chalk2.cyan("\n\u{1F680} LeanMCP Production Build\n"));
315
+ const scanSpinner = ora2("Scanning for @UIApp components...").start();
316
+ const uiApps = await scanUIApp(cwd);
317
+ if (uiApps.length === 0) {
318
+ scanSpinner.succeed("No @UIApp components found");
319
+ } else {
320
+ scanSpinner.succeed(`Found ${uiApps.length} @UIApp component(s)`);
321
+ }
322
+ const manifest = {};
323
+ if (uiApps.length > 0) {
324
+ const buildSpinner = ora2("Building UI components...").start();
325
+ const errors = [];
326
+ for (const app of uiApps) {
327
+ const result = await buildUIComponent(app, cwd, false);
328
+ if (result.success) {
329
+ manifest[app.resourceUri] = result.htmlPath;
330
+ } else {
331
+ errors.push(`${app.componentName}: ${result.error}`);
332
+ }
333
+ }
334
+ await writeUIManifest(manifest, cwd);
335
+ if (errors.length > 0) {
336
+ buildSpinner.fail("Build failed");
337
+ for (const error of errors) {
338
+ console.error(chalk2.red(` \u2717 ${error}`));
339
+ }
340
+ process.exit(1);
341
+ }
342
+ buildSpinner.succeed("UI components built");
343
+ }
344
+ const tscSpinner = ora2("Compiling TypeScript...").start();
345
+ try {
346
+ await new Promise((resolve, reject) => {
347
+ const tsc = spawn2("npx", [
348
+ "tsc"
349
+ ], {
350
+ cwd,
351
+ stdio: "pipe",
352
+ shell: true
353
+ });
354
+ let stderr = "";
355
+ tsc.stderr?.on("data", (data) => {
356
+ stderr += data;
357
+ });
358
+ tsc.on("close", (code) => {
359
+ if (code === 0) resolve();
360
+ else reject(new Error(stderr || `tsc exited with code ${code}`));
361
+ });
362
+ tsc.on("error", reject);
363
+ });
364
+ tscSpinner.succeed("TypeScript compiled");
365
+ } catch (error) {
366
+ tscSpinner.fail("TypeScript compilation failed");
367
+ console.error(chalk2.red(error instanceof Error ? error.message : String(error)));
368
+ process.exit(1);
369
+ }
370
+ console.log(chalk2.cyan("\nStarting production server...\n"));
371
+ const server = spawn2("node", [
372
+ "dist/main.js"
373
+ ], {
374
+ cwd,
375
+ stdio: "inherit",
376
+ shell: true
377
+ });
378
+ const cleanup = /* @__PURE__ */ __name(() => {
379
+ console.log(chalk2.gray("\nShutting down..."));
380
+ server.kill();
381
+ process.exit(0);
382
+ }, "cleanup");
383
+ process.on("SIGINT", cleanup);
384
+ process.on("SIGTERM", cleanup);
385
+ server.on("exit", (code) => {
386
+ process.exit(code ?? 0);
387
+ });
388
+ }
389
+ __name(startCommand, "startCommand");
26
390
 
27
391
  // src/index.ts
28
- var import_commander = require("commander");
29
- var import_chalk = __toESM(require("chalk"));
30
- var import_fs_extra = __toESM(require("fs-extra"));
31
- var import_path = __toESM(require("path"));
32
- var import_ora = __toESM(require("ora"));
392
+ var require2 = createRequire(import.meta.url);
393
+ var pkg = require2("../package.json");
33
394
  function capitalize(str) {
34
395
  return str.charAt(0).toUpperCase() + str.slice(1);
35
396
  }
36
397
  __name(capitalize, "capitalize");
37
- var program = new import_commander.Command();
38
- program.name("leanmcp").description("LeanMCP CLI \u2014 create production-ready MCP servers with Streamable HTTP").version("0.1.0");
39
- program.command("create <projectName>").description("Create a new LeanMCP project with Streamable HTTP transport").action(async (projectName) => {
40
- const spinner = (0, import_ora.default)(`Creating project ${projectName}...`).start();
41
- const targetDir = import_path.default.join(process.cwd(), projectName);
42
- if (import_fs_extra.default.existsSync(targetDir)) {
398
+ var program = new Command();
399
+ program.name("leanmcp").description("LeanMCP CLI \u2014 create production-ready MCP servers with Streamable HTTP").version(pkg.version).addHelpText("after", `
400
+ Examples:
401
+ $ leanmcp create my-app --allow-all # Scaffold without interactive prompts
402
+ `);
403
+ program.command("create <projectName>").description("Create a new LeanMCP project with Streamable HTTP transport").option("--allow-all", "Skip interactive confirmations and assume Yes").action(async (projectName, options) => {
404
+ const spinner = ora3(`Creating project ${projectName}...`).start();
405
+ const targetDir = path5.join(process.cwd(), projectName);
406
+ if (fs5.existsSync(targetDir)) {
43
407
  spinner.fail(`Folder ${projectName} already exists.`);
44
408
  process.exit(1);
45
409
  }
46
- await import_fs_extra.default.mkdirp(targetDir);
47
- await import_fs_extra.default.mkdirp(import_path.default.join(targetDir, "mcp"));
48
- const pkg = {
410
+ await fs5.mkdirp(targetDir);
411
+ await fs5.mkdirp(path5.join(targetDir, "mcp", "example"));
412
+ const pkg2 = {
49
413
  name: projectName,
50
414
  version: "1.0.0",
51
415
  description: "MCP Server with Streamable HTTP Transport and LeanMCP SDK",
@@ -66,21 +430,16 @@ program.command("create <projectName>").description("Create a new LeanMCP projec
66
430
  author: "",
67
431
  license: "MIT",
68
432
  dependencies: {
69
- "@leanmcp/core": "^0.1.0",
70
- "@modelcontextprotocol/sdk": "^1.0.0",
71
- "cors": "^2.8.5",
72
- "dotenv": "^16.5.0",
73
- "express": "^5.1.0"
433
+ "@leanmcp/core": "^0.3.5",
434
+ "dotenv": "^16.5.0"
74
435
  },
75
436
  devDependencies: {
76
- "@types/cors": "^2.8.19",
77
- "@types/express": "^5.0.3",
78
437
  "@types/node": "^20.0.0",
79
438
  "tsx": "^4.20.3",
80
439
  "typescript": "^5.6.3"
81
440
  }
82
441
  };
83
- await import_fs_extra.default.writeJSON(import_path.default.join(targetDir, "package.json"), pkg, {
442
+ await fs5.writeJSON(path5.join(targetDir, "package.json"), pkg2, {
84
443
  spaces: 2
85
444
  });
86
445
  const tsconfig = {
@@ -103,12 +462,11 @@ program.command("create <projectName>").description("Create a new LeanMCP projec
103
462
  "dist"
104
463
  ]
105
464
  };
106
- await import_fs_extra.default.writeJSON(import_path.default.join(targetDir, "tsconfig.json"), tsconfig, {
465
+ await fs5.writeJSON(path5.join(targetDir, "tsconfig.json"), tsconfig, {
107
466
  spaces: 2
108
467
  });
109
468
  const mainTs = `import dotenv from "dotenv";
110
469
  import { createHTTPServer, MCPServer } from "@leanmcp/core";
111
- import { ExampleService } from "./mcp/example.js";
112
470
 
113
471
  // Load environment variables
114
472
  dotenv.config();
@@ -117,27 +475,29 @@ const PORT = Number(process.env.PORT) || 3001;
117
475
 
118
476
  /**
119
477
  * Create and configure the MCP server
478
+ * Services are automatically discovered from ./mcp directory
120
479
  */
121
- function createMCPServer() {
480
+ const serverFactory = async () => {
122
481
  const server = new MCPServer({
123
482
  name: "${projectName}",
124
- version: "1.0.0"
483
+ version: "1.0.0",
484
+ logging: true
125
485
  });
126
486
 
127
- // Register your services here
128
- server.registerService(new ExampleService());
129
-
487
+ // Services are automatically discovered and registered from ./mcp
130
488
  return server.getServer();
131
- }
489
+ };
132
490
 
133
491
  // Start the HTTP server
134
- await createHTTPServer(createMCPServer, {
492
+ await createHTTPServer(serverFactory, {
135
493
  port: PORT,
136
494
  cors: true,
137
- logging: true
495
+ logging: true // Log HTTP requests
138
496
  });
497
+
498
+ console.log(\`\\n${projectName} MCP Server\`);
139
499
  `;
140
- await import_fs_extra.default.writeFile(import_path.default.join(targetDir, "main.ts"), mainTs);
500
+ await fs5.writeFile(path5.join(targetDir, "main.ts"), mainTs);
141
501
  const exampleServiceTs = `import { Tool, Resource, Prompt, SchemaConstraint, Optional } from "@leanmcp/core";
142
502
 
143
503
  /**
@@ -163,27 +523,38 @@ class CalculateInput {
163
523
  operation?: string;
164
524
  }
165
525
 
526
+ class EchoInput {
527
+ @SchemaConstraint({
528
+ description: "Message to echo back",
529
+ minLength: 1
530
+ })
531
+ message!: string;
532
+ }
533
+
166
534
  export class ExampleService {
167
535
  @Tool({
168
536
  description: "Perform arithmetic operations with automatic schema validation",
169
537
  inputClass: CalculateInput
170
538
  })
171
539
  async calculate(input: CalculateInput) {
540
+ // Ensure numerical operations by explicitly converting to numbers
541
+ const a = Number(input.a);
542
+ const b = Number(input.b);
172
543
  let result: number;
173
544
 
174
545
  switch (input.operation || "add") {
175
546
  case "add":
176
- result = input.a + input.b;
547
+ result = a + b;
177
548
  break;
178
549
  case "subtract":
179
- result = input.a - input.b;
550
+ result = a - b;
180
551
  break;
181
552
  case "multiply":
182
- result = input.a * input.b;
553
+ result = a * b;
183
554
  break;
184
555
  case "divide":
185
- if (input.b === 0) throw new Error("Cannot divide by zero");
186
- result = input.a / input.b;
556
+ if (b === 0) throw new Error("Cannot divide by zero");
557
+ result = a / b;
187
558
  break;
188
559
  default:
189
560
  throw new Error("Invalid operation");
@@ -201,8 +572,11 @@ export class ExampleService {
201
572
  };
202
573
  }
203
574
 
204
- @Tool({ description: "Echo a message back" })
205
- async echo(input: { message: string }) {
575
+ @Tool({
576
+ description: "Echo a message back",
577
+ inputClass: EchoInput
578
+ })
579
+ async echo(input: EchoInput) {
206
580
  return {
207
581
  content: [{
208
582
  type: "text" as const,
@@ -243,12 +617,148 @@ export class ExampleService {
243
617
  }
244
618
  }
245
619
  `;
246
- await import_fs_extra.default.writeFile(import_path.default.join(targetDir, "mcp", "example.ts"), exampleServiceTs);
247
- const gitignore = `node_modules
248
- dist
249
- .env
250
- .env.local
620
+ await fs5.writeFile(path5.join(targetDir, "mcp", "example", "index.ts"), exampleServiceTs);
621
+ const gitignore = `# Logs
622
+ logs
251
623
  *.log
624
+ npm-debug.log*
625
+ yarn-debug.log*
626
+ yarn-error.log*
627
+ lerna-debug.log*
628
+
629
+ # Diagnostic reports (https://nodejs.org/api/report.html)
630
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
631
+
632
+ # Runtime data
633
+ pids
634
+ *.pid
635
+ *.seed
636
+ *.pid.lock
637
+
638
+ # Directory for instrumented libs generated by jscoverage/JSCover
639
+ lib-cov
640
+
641
+ # Coverage directory used by tools like istanbul
642
+ coverage
643
+ *.lcov
644
+
645
+ # nyc test coverage
646
+ .nyc_output
647
+
648
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
649
+ .grunt
650
+
651
+ # Bower dependency directory (https://bower.io/)
652
+ bower_components
653
+
654
+ # node-waf configuration
655
+ .lock-wscript
656
+
657
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
658
+ build/Release
659
+
660
+ # Dependency directories
661
+ node_modules/
662
+ jspm_packages/
663
+
664
+ # Snowpack dependency directory (https://snowpack.dev/)
665
+ web_modules/
666
+
667
+ # TypeScript cache
668
+ *.tsbuildinfo
669
+
670
+ # Optional npm cache directory
671
+ .npm
672
+
673
+ # Optional eslint cache
674
+ .eslintcache
675
+
676
+ # Optional stylelint cache
677
+ .stylelintcache
678
+
679
+ # Optional REPL history
680
+ .node_repl_history
681
+
682
+ # Output of 'npm pack'
683
+ *.tgz
684
+
685
+ # Yarn Integrity file
686
+ .yarn-integrity
687
+
688
+ # dotenv environment variable files
689
+ .env
690
+ .env.*
691
+ !.env.example
692
+
693
+ # parcel-bundler cache (https://parceljs.org/)
694
+ .cache
695
+ .parcel-cache
696
+
697
+ # Next.js build output
698
+ .next
699
+ out
700
+
701
+ # Nuxt.js build / generate output
702
+ .nuxt
703
+ dist
704
+ .output
705
+
706
+ # Gatsby files
707
+ .cache/
708
+ # Comment in the public line in if your project uses Gatsby and not Next.js
709
+ # https://nextjs.org/blog/next-9-1#public-directory-support
710
+ # public
711
+
712
+ # vuepress build output
713
+ .vuepress/dist
714
+
715
+ # vuepress v2.x temp and cache directory
716
+ .temp
717
+ .cache
718
+
719
+ # Sveltekit cache directory
720
+ .svelte-kit/
721
+
722
+ # vitepress build output
723
+ **/.vitepress/dist
724
+
725
+ # vitepress cache directory
726
+ **/.vitepress/cache
727
+
728
+ # Docusaurus cache and generated files
729
+ .docusaurus
730
+
731
+ # Serverless directories
732
+ .serverless/
733
+
734
+ # FuseBox cache
735
+ .fusebox/
736
+
737
+ # DynamoDB Local files
738
+ .dynamodb/
739
+
740
+ # Firebase cache directory
741
+ .firebase/
742
+
743
+ # TernJS port file
744
+ .tern-port
745
+
746
+ # Stores VSCode versions used for testing VSCode extensions
747
+ .vscode-test
748
+
749
+ # yarn v3
750
+ .pnp.*
751
+ .yarn/*
752
+ !.yarn/patches
753
+ !.yarn/plugins
754
+ !.yarn/releases
755
+ !.yarn/sdks
756
+ !.yarn/versions
757
+
758
+ # Vite files
759
+ vite.config.js.timestamp-*
760
+ vite.config.ts.timestamp-*
761
+ .vite/
252
762
  `;
253
763
  const env = `# Server Configuration
254
764
  PORT=3001
@@ -256,8 +766,8 @@ NODE_ENV=development
256
766
 
257
767
  # Add your environment variables here
258
768
  `;
259
- await import_fs_extra.default.writeFile(import_path.default.join(targetDir, ".gitignore"), gitignore);
260
- await import_fs_extra.default.writeFile(import_path.default.join(targetDir, ".env"), env);
769
+ await fs5.writeFile(path5.join(targetDir, ".gitignore"), gitignore);
770
+ await fs5.writeFile(path5.join(targetDir, ".env"), env);
261
771
  const readme = `# ${projectName}
262
772
 
263
773
  MCP Server with Streamable HTTP Transport built with LeanMCP SDK
@@ -283,22 +793,36 @@ npm start
283
793
  \`\`\`
284
794
  ${projectName}/
285
795
  \u251C\u2500\u2500 main.ts # Server entry point
286
- \u251C\u2500\u2500 mcp/
287
- \u2502 \u2514\u2500\u2500 example.ts # Example service
796
+ \u251C\u2500\u2500 mcp/ # Services directory (auto-discovered)
797
+ \u2502 \u2514\u2500\u2500 example/
798
+ \u2502 \u2514\u2500\u2500 index.ts # Example service
288
799
  \u251C\u2500\u2500 .env # Environment variables
289
800
  \u2514\u2500\u2500 package.json
290
801
  \`\`\`
291
802
 
292
803
  ## Adding New Services
293
804
 
294
- Create a new service file in \`mcp/\`:
805
+ Create a new service directory in \`mcp/\`:
295
806
 
296
807
  \`\`\`typescript
297
- import { Tool } from "@leanmcp/core";
808
+ // mcp/myservice/index.ts
809
+ import { Tool, SchemaConstraint } from "@leanmcp/core";
810
+
811
+ // Define input schema
812
+ class MyToolInput {
813
+ @SchemaConstraint({
814
+ description: "Message to process",
815
+ minLength: 1
816
+ })
817
+ message!: string;
818
+ }
298
819
 
299
820
  export class MyService {
300
- @Tool({ description: "My awesome tool" })
301
- async myTool(input: { message: string }) {
821
+ @Tool({
822
+ description: "My awesome tool",
823
+ inputClass: MyToolInput
824
+ })
825
+ async myTool(input: MyToolInput) {
302
826
  return {
303
827
  content: [{
304
828
  type: "text",
@@ -309,12 +833,15 @@ export class MyService {
309
833
  }
310
834
  \`\`\`
311
835
 
312
- Then register it in \`main.ts\`:
836
+ Services are automatically discovered and registered - no need to modify \`main.ts\`!
313
837
 
314
- \`\`\`typescript
315
- import { MyService } from "./mcp/my-service.js";
316
- server.registerService(new MyService());
317
- \`\`\`
838
+ ## Features
839
+
840
+ - **Zero-config auto-discovery** - Services automatically registered from \`./mcp\` directory
841
+ - **Type-safe decorators** - \`@Tool\`, \`@Prompt\`, \`@Resource\` with full TypeScript support
842
+ - **Schema validation** - Automatic input validation with \`@SchemaConstraint\`
843
+ - **HTTP transport** - Production-ready HTTP server with session management
844
+ - **Hot reload** - Development mode with automatic restart on file changes
318
845
 
319
846
  ## Testing with MCP Inspector
320
847
 
@@ -326,28 +853,88 @@ npx @modelcontextprotocol/inspector http://localhost:3001/mcp
326
853
 
327
854
  MIT
328
855
  `;
329
- await import_fs_extra.default.writeFile(import_path.default.join(targetDir, "README.md"), readme);
856
+ await fs5.writeFile(path5.join(targetDir, "README.md"), readme);
330
857
  spinner.succeed(`Project ${projectName} created!`);
331
- console.log(import_chalk.default.green("\\nSuccess! Your MCP server is ready.\\n"));
332
- console.log(import_chalk.default.cyan("Next steps:"));
333
- console.log(import_chalk.default.gray(` cd ${projectName}`));
334
- console.log(import_chalk.default.gray(` npm install`));
335
- console.log(import_chalk.default.gray(` npm run dev`));
336
- console.log(import_chalk.default.gray(`\\nServer will run on http://localhost:3001`));
858
+ console.log(chalk3.green("\nSuccess! Your MCP server is ready.\n"));
859
+ console.log(chalk3.cyan(`Next, navigate to your project:
860
+ cd ${projectName}
861
+ `));
862
+ const shouldInstall = options.allowAll ? true : await confirm({
863
+ message: "Would you like to install dependencies now?",
864
+ default: true
865
+ });
866
+ if (shouldInstall) {
867
+ const installSpinner = ora3("Installing dependencies...").start();
868
+ try {
869
+ await new Promise((resolve, reject) => {
870
+ const npmInstall = spawn3("npm", [
871
+ "install"
872
+ ], {
873
+ cwd: targetDir,
874
+ stdio: "pipe",
875
+ shell: true
876
+ });
877
+ npmInstall.on("close", (code) => {
878
+ if (code === 0) {
879
+ resolve();
880
+ } else {
881
+ reject(new Error(`npm install failed with code ${code}`));
882
+ }
883
+ });
884
+ npmInstall.on("error", reject);
885
+ });
886
+ installSpinner.succeed("Dependencies installed successfully!");
887
+ const shouldStartDev = options.allowAll ? true : await confirm({
888
+ message: "Would you like to start the development server?",
889
+ default: true
890
+ });
891
+ if (shouldStartDev) {
892
+ console.log(chalk3.cyan("\nStarting development server...\n"));
893
+ const devServer = spawn3("npm", [
894
+ "run",
895
+ "dev"
896
+ ], {
897
+ cwd: targetDir,
898
+ stdio: "inherit",
899
+ shell: true
900
+ });
901
+ process.on("SIGINT", () => {
902
+ devServer.kill();
903
+ process.exit(0);
904
+ });
905
+ } else {
906
+ console.log(chalk3.cyan("\nTo start the development server later:"));
907
+ console.log(chalk3.gray(` cd ${projectName}`));
908
+ console.log(chalk3.gray(` npm run dev`));
909
+ }
910
+ } catch (error) {
911
+ installSpinner.fail("Failed to install dependencies");
912
+ console.error(chalk3.red(error instanceof Error ? error.message : String(error)));
913
+ console.log(chalk3.cyan("\nYou can install dependencies manually:"));
914
+ console.log(chalk3.gray(` cd ${projectName}`));
915
+ console.log(chalk3.gray(` npm install`));
916
+ }
917
+ } else {
918
+ console.log(chalk3.cyan("\nTo get started:"));
919
+ console.log(chalk3.gray(` cd ${projectName}`));
920
+ console.log(chalk3.gray(` npm install`));
921
+ console.log(chalk3.gray(` npm run dev`));
922
+ }
337
923
  });
338
924
  program.command("add <serviceName>").description("Add a new MCP service to your project").action(async (serviceName) => {
339
925
  const cwd = process.cwd();
340
- const mcpDir = import_path.default.join(cwd, "mcp");
341
- if (!import_fs_extra.default.existsSync(import_path.default.join(cwd, "main.ts"))) {
342
- console.error(import_chalk.default.red("ERROR: Not a LeanMCP project (main.ts missing)."));
926
+ const mcpDir = path5.join(cwd, "mcp");
927
+ if (!fs5.existsSync(path5.join(cwd, "main.ts"))) {
928
+ console.error(chalk3.red("ERROR: Not a LeanMCP project (main.ts missing)."));
343
929
  process.exit(1);
344
930
  }
345
- await import_fs_extra.default.mkdirp(mcpDir);
346
- const serviceFile = import_path.default.join(mcpDir, `${serviceName}.ts`);
347
- if (import_fs_extra.default.existsSync(serviceFile)) {
348
- console.error(import_chalk.default.red(`ERROR: Service ${serviceName} already exists.`));
931
+ const serviceDir = path5.join(mcpDir, serviceName);
932
+ const serviceFile = path5.join(serviceDir, "index.ts");
933
+ if (fs5.existsSync(serviceDir)) {
934
+ console.error(chalk3.red(`ERROR: Service ${serviceName} already exists.`));
349
935
  process.exit(1);
350
936
  }
937
+ await fs5.mkdirp(serviceDir);
351
938
  const indexTs = `import { Tool, Resource, Prompt, Optional, SchemaConstraint } from "@leanmcp/core";
352
939
 
353
940
  // Input schema for greeting
@@ -407,34 +994,14 @@ export class ${capitalize(serviceName)}Service {
407
994
  }
408
995
  }
409
996
  `;
410
- await import_fs_extra.default.writeFile(serviceFile, indexTs);
411
- const mainTsPath = import_path.default.join(cwd, "main.ts");
412
- let mainTsContent = await import_fs_extra.default.readFile(mainTsPath, "utf-8");
413
- const serviceClassName = `${capitalize(serviceName)}Service`;
414
- const importStatement = `import { ${serviceClassName} } from "./mcp/${serviceName}.js";`;
415
- const registerStatement = ` server.registerService(new ${serviceClassName}());`;
416
- const lastImportMatch = mainTsContent.match(/import .* from .*;\n/g);
417
- if (lastImportMatch) {
418
- const lastImport = lastImportMatch[lastImportMatch.length - 1];
419
- const lastImportIndex = mainTsContent.lastIndexOf(lastImport);
420
- const afterLastImport = lastImportIndex + lastImport.length;
421
- mainTsContent = mainTsContent.slice(0, afterLastImport) + importStatement + "\n" + mainTsContent.slice(afterLastImport);
422
- }
423
- const registerPattern = /server\.registerService\(new \w+\(\)\);/g;
424
- const matches = [
425
- ...mainTsContent.matchAll(registerPattern)
426
- ];
427
- if (matches.length > 0) {
428
- const lastMatch = matches[matches.length - 1];
429
- const insertPosition = lastMatch.index + lastMatch[0].length;
430
- mainTsContent = mainTsContent.slice(0, insertPosition) + "\n" + registerStatement + mainTsContent.slice(insertPosition);
431
- }
432
- await import_fs_extra.default.writeFile(mainTsPath, mainTsContent);
433
- console.log(import_chalk.default.green(`\\nCreated new service: ${import_chalk.default.bold(serviceName)}`));
434
- console.log(import_chalk.default.gray(` File: mcp/${serviceName}.ts`));
435
- console.log(import_chalk.default.gray(` Tool: greet`));
436
- console.log(import_chalk.default.gray(` Prompt: welcomePrompt`));
437
- console.log(import_chalk.default.gray(` Resource: getStatus`));
438
- console.log(import_chalk.default.green(`\\nService automatically registered in main.ts!`));
997
+ await fs5.writeFile(serviceFile, indexTs);
998
+ console.log(chalk3.green(`\\nCreated new service: ${chalk3.bold(serviceName)}`));
999
+ console.log(chalk3.gray(` File: mcp/${serviceName}/index.ts`));
1000
+ console.log(chalk3.gray(` Tool: greet`));
1001
+ console.log(chalk3.gray(` Prompt: welcomePrompt`));
1002
+ console.log(chalk3.gray(` Resource: getStatus`));
1003
+ console.log(chalk3.green(`\\nService will be automatically discovered on next server start!`));
439
1004
  });
1005
+ program.command("dev").description("Start development server with UI hot-reload (builds @UIApp components)").action(devCommand);
1006
+ program.command("start").description("Build UI components and start production server").action(startCommand);
440
1007
  program.parse();