@leanmcp/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,11 +4,13 @@ Command-line tool for creating LeanMCP projects with production-ready templates.
4
4
 
5
5
  ## Features
6
6
 
7
+ - **Interactive setup** - Guided prompts for dependency installation and dev server
7
8
  - **Quick project scaffolding** - Create new MCP servers in seconds
8
9
  - **Complete setup** - Includes TypeScript, dependencies, and configuration
9
10
  - **Best practices** - Generated projects follow MCP standards
10
11
  - **Ready to run** - Start developing immediately with hot reload
11
12
  - **Example service** - Includes working examples to get started
13
+ - **Pure ESM** - Modern ES modules with full TypeScript support
12
14
 
13
15
  ## Installation
14
16
 
@@ -41,16 +43,25 @@ npx @leanmcp/cli create my-mcp-server
41
43
 
42
44
  ```bash
43
45
  $ leanmcp create my-sentiment-tool
44
- Creating project my-sentiment-tool...
46
+ Project my-sentiment-tool created!
45
47
 
46
- Project created successfully!
48
+ Success! Your MCP server is ready.
47
49
 
48
- Next steps:
50
+ Next, navigate to your project:
49
51
  cd my-sentiment-tool
50
- npm install
51
- npm run dev
52
52
 
53
- Your MCP server will be running on http://localhost:3001
53
+ ? Would you like to install dependencies now? (Y/n) Yes
54
+ ✔ Dependencies installed successfully!
55
+ ? Would you like to start the development server? (Y/n) Yes
56
+
57
+ Starting development server...
58
+
59
+ > my-sentiment-tool@1.0.0 dev
60
+ > tsx watch main.ts
61
+
62
+ [HTTP][INFO] Starting LeanMCP HTTP Server...
63
+ [HTTP][INFO] Server running on http://localhost:3001
64
+ [HTTP][INFO] MCP endpoint: http://localhost:3001/mcp
54
65
  ```
55
66
 
56
67
  ### Add a New Service
@@ -143,22 +154,49 @@ npm run clean # Remove build artifacts
143
154
 
144
155
  ## Development Workflow
145
156
 
146
- After creating a project:
157
+ ### Interactive Setup (Recommended)
158
+
159
+ The CLI provides an interactive setup experience:
147
160
 
148
161
  ```bash
149
- # 1. Install dependencies
162
+ # Create project
163
+ leanmcp create my-mcp-server
164
+
165
+ # The CLI will:
166
+ # 1. Create project structure
167
+ # 2. Ask if you want to install dependencies (Y/n)
168
+ # 3. If yes, ask if you want to start dev server (Y/n)
169
+ # 4. If yes, start server with hot reload
170
+
171
+ # If you choose "No" to installation:
150
172
  cd my-mcp-server
151
173
  npm install
174
+ npm run dev
175
+ ```
176
+
177
+ ### Manual Setup
152
178
 
153
- # 2. Start development server
179
+ If you prefer manual control:
180
+
181
+ ```bash
182
+ # 1. Create project (answer "No" to prompts)
183
+ leanmcp create my-mcp-server
184
+
185
+ # 2. Navigate to project
186
+ cd my-mcp-server
187
+
188
+ # 3. Install dependencies
189
+ npm install
190
+
191
+ # 4. Start development server
154
192
  npm run dev
155
193
 
156
- # 3. Server starts on http://localhost:3001
194
+ # 5. Server starts on http://localhost:3001
157
195
  # - Endpoint: http://localhost:3001/mcp
158
196
  # - Health check: http://localhost:3001/health
159
197
  # - Hot reload enabled
160
198
 
161
- # 4. Edit files in mcp/ directory
199
+ # 6. Edit files in mcp/ directory
162
200
  # Server automatically reloads on changes
163
201
  ```
164
202
 
@@ -342,6 +380,11 @@ Creates a complete MCP server project with:
342
380
  - Package.json with all dependencies
343
381
  - Development and build scripts
344
382
 
383
+ **Interactive Prompts:**
384
+ - Asks if you want to install dependencies
385
+ - If installed, asks if you want to start dev server
386
+ - Runs commands in the project directory automatically
387
+
345
388
  #### `add <service-name>`
346
389
  Adds a new service to an existing project:
347
390
  - Must be run inside a LeanMCP project directory
package/bin/leanmcp.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Always prefer ESM build
4
+ import('../dist/index.js');
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
- #!/usr/bin/env node
1
+
2
+ export { }
package/dist/index.js CHANGED
@@ -1,51 +1,33 @@
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 });
15
- }
16
- return to;
17
- };
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
- ));
26
3
 
27
4
  // 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"));
5
+ import { Command } from "commander";
6
+ import chalk from "chalk";
7
+ import fs from "fs-extra";
8
+ import path from "path";
9
+ import ora from "ora";
10
+ import { createRequire } from "module";
11
+ import { confirm } from "@inquirer/prompts";
12
+ import { spawn } from "child_process";
13
+ var require2 = createRequire(import.meta.url);
14
+ var pkg = require2("../package.json");
33
15
  function capitalize(str) {
34
16
  return str.charAt(0).toUpperCase() + str.slice(1);
35
17
  }
36
18
  __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");
19
+ var program = new Command();
20
+ program.name("leanmcp").description("LeanMCP CLI \u2014 create production-ready MCP servers with Streamable HTTP").version(pkg.version);
39
21
  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)) {
22
+ const spinner = ora(`Creating project ${projectName}...`).start();
23
+ const targetDir = path.join(process.cwd(), projectName);
24
+ if (fs.existsSync(targetDir)) {
43
25
  spinner.fail(`Folder ${projectName} already exists.`);
44
26
  process.exit(1);
45
27
  }
46
- await import_fs_extra.default.mkdirp(targetDir);
47
- await import_fs_extra.default.mkdirp(import_path.default.join(targetDir, "mcp"));
48
- const pkg = {
28
+ await fs.mkdirp(targetDir);
29
+ await fs.mkdirp(path.join(targetDir, "mcp", "example"));
30
+ const pkg2 = {
49
31
  name: projectName,
50
32
  version: "1.0.0",
51
33
  description: "MCP Server with Streamable HTTP Transport and LeanMCP SDK",
@@ -66,21 +48,16 @@ program.command("create <projectName>").description("Create a new LeanMCP projec
66
48
  author: "",
67
49
  license: "MIT",
68
50
  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"
51
+ "@leanmcp/core": "^0.2.0",
52
+ "dotenv": "^16.5.0"
74
53
  },
75
54
  devDependencies: {
76
- "@types/cors": "^2.8.19",
77
- "@types/express": "^5.0.3",
78
55
  "@types/node": "^20.0.0",
79
56
  "tsx": "^4.20.3",
80
57
  "typescript": "^5.6.3"
81
58
  }
82
59
  };
83
- await import_fs_extra.default.writeJSON(import_path.default.join(targetDir, "package.json"), pkg, {
60
+ await fs.writeJSON(path.join(targetDir, "package.json"), pkg2, {
84
61
  spaces: 2
85
62
  });
86
63
  const tsconfig = {
@@ -103,12 +80,11 @@ program.command("create <projectName>").description("Create a new LeanMCP projec
103
80
  "dist"
104
81
  ]
105
82
  };
106
- await import_fs_extra.default.writeJSON(import_path.default.join(targetDir, "tsconfig.json"), tsconfig, {
83
+ await fs.writeJSON(path.join(targetDir, "tsconfig.json"), tsconfig, {
107
84
  spaces: 2
108
85
  });
109
86
  const mainTs = `import dotenv from "dotenv";
110
87
  import { createHTTPServer, MCPServer } from "@leanmcp/core";
111
- import { ExampleService } from "./mcp/example.js";
112
88
 
113
89
  // Load environment variables
114
90
  dotenv.config();
@@ -117,27 +93,31 @@ const PORT = Number(process.env.PORT) || 3001;
117
93
 
118
94
  /**
119
95
  * Create and configure the MCP server
96
+ * Services are automatically discovered from ./mcp directory
120
97
  */
121
- function createMCPServer() {
98
+ const serverFactory = async () => {
122
99
  const server = new MCPServer({
123
100
  name: "${projectName}",
124
- version: "1.0.0"
101
+ version: "1.0.0",
102
+ logging: true
125
103
  });
126
104
 
127
- // Register your services here
128
- server.registerService(new ExampleService());
129
-
105
+ // Services are automatically discovered and registered from ./mcp
130
106
  return server.getServer();
131
- }
107
+ };
132
108
 
133
109
  // Start the HTTP server
134
- await createHTTPServer(createMCPServer, {
110
+ await createHTTPServer(serverFactory, {
135
111
  port: PORT,
136
112
  cors: true,
137
- logging: true
113
+ logging: true // Log HTTP requests
138
114
  });
115
+
116
+ console.log(\`\\n${projectName} MCP Server\`);
117
+ console.log(\`HTTP endpoint: http://localhost:\${PORT}/mcp\`);
118
+ console.log(\`Health check: http://localhost:\${PORT}/health\`);
139
119
  `;
140
- await import_fs_extra.default.writeFile(import_path.default.join(targetDir, "main.ts"), mainTs);
120
+ await fs.writeFile(path.join(targetDir, "main.ts"), mainTs);
141
121
  const exampleServiceTs = `import { Tool, Resource, Prompt, SchemaConstraint, Optional } from "@leanmcp/core";
142
122
 
143
123
  /**
@@ -243,7 +223,7 @@ export class ExampleService {
243
223
  }
244
224
  }
245
225
  `;
246
- await import_fs_extra.default.writeFile(import_path.default.join(targetDir, "mcp", "example.ts"), exampleServiceTs);
226
+ await fs.writeFile(path.join(targetDir, "mcp", "example", "index.ts"), exampleServiceTs);
247
227
  const gitignore = `node_modules
248
228
  dist
249
229
  .env
@@ -256,8 +236,8 @@ NODE_ENV=development
256
236
 
257
237
  # Add your environment variables here
258
238
  `;
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);
239
+ await fs.writeFile(path.join(targetDir, ".gitignore"), gitignore);
240
+ await fs.writeFile(path.join(targetDir, ".env"), env);
261
241
  const readme = `# ${projectName}
262
242
 
263
243
  MCP Server with Streamable HTTP Transport built with LeanMCP SDK
@@ -283,22 +263,36 @@ npm start
283
263
  \`\`\`
284
264
  ${projectName}/
285
265
  \u251C\u2500\u2500 main.ts # Server entry point
286
- \u251C\u2500\u2500 mcp/
287
- \u2502 \u2514\u2500\u2500 example.ts # Example service
266
+ \u251C\u2500\u2500 mcp/ # Services directory (auto-discovered)
267
+ \u2502 \u2514\u2500\u2500 example/
268
+ \u2502 \u2514\u2500\u2500 index.ts # Example service
288
269
  \u251C\u2500\u2500 .env # Environment variables
289
270
  \u2514\u2500\u2500 package.json
290
271
  \`\`\`
291
272
 
292
273
  ## Adding New Services
293
274
 
294
- Create a new service file in \`mcp/\`:
275
+ Create a new service directory in \`mcp/\`:
295
276
 
296
277
  \`\`\`typescript
297
- import { Tool } from "@leanmcp/core";
278
+ // mcp/myservice/index.ts
279
+ import { Tool, SchemaConstraint } from "@leanmcp/core";
280
+
281
+ // Define input schema
282
+ class MyToolInput {
283
+ @SchemaConstraint({
284
+ description: "Message to process",
285
+ minLength: 1
286
+ })
287
+ message!: string;
288
+ }
298
289
 
299
290
  export class MyService {
300
- @Tool({ description: "My awesome tool" })
301
- async myTool(input: { message: string }) {
291
+ @Tool({
292
+ description: "My awesome tool",
293
+ inputClass: MyToolInput
294
+ })
295
+ async myTool(input: MyToolInput) {
302
296
  return {
303
297
  content: [{
304
298
  type: "text",
@@ -309,12 +303,15 @@ export class MyService {
309
303
  }
310
304
  \`\`\`
311
305
 
312
- Then register it in \`main.ts\`:
306
+ Services are automatically discovered and registered - no need to modify \`main.ts\`!
313
307
 
314
- \`\`\`typescript
315
- import { MyService } from "./mcp/my-service.js";
316
- server.registerService(new MyService());
317
- \`\`\`
308
+ ## Features
309
+
310
+ - **Zero-config auto-discovery** - Services automatically registered from \`./mcp\` directory
311
+ - **Type-safe decorators** - \`@Tool\`, \`@Prompt\`, \`@Resource\` with full TypeScript support
312
+ - **Schema validation** - Automatic input validation with \`@SchemaConstraint\`
313
+ - **HTTP transport** - Production-ready HTTP server with session management
314
+ - **Hot reload** - Development mode with automatic restart on file changes
318
315
 
319
316
  ## Testing with MCP Inspector
320
317
 
@@ -326,28 +323,88 @@ npx @modelcontextprotocol/inspector http://localhost:3001/mcp
326
323
 
327
324
  MIT
328
325
  `;
329
- await import_fs_extra.default.writeFile(import_path.default.join(targetDir, "README.md"), readme);
326
+ await fs.writeFile(path.join(targetDir, "README.md"), readme);
330
327
  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`));
328
+ console.log(chalk.green("\nSuccess! Your MCP server is ready.\n"));
329
+ console.log(chalk.cyan(`Next, navigate to your project:
330
+ cd ${projectName}
331
+ `));
332
+ const shouldInstall = await confirm({
333
+ message: "Would you like to install dependencies now?",
334
+ default: true
335
+ });
336
+ if (shouldInstall) {
337
+ const installSpinner = ora("Installing dependencies...").start();
338
+ try {
339
+ await new Promise((resolve, reject) => {
340
+ const npmInstall = spawn("npm", [
341
+ "install"
342
+ ], {
343
+ cwd: targetDir,
344
+ stdio: "pipe",
345
+ shell: true
346
+ });
347
+ npmInstall.on("close", (code) => {
348
+ if (code === 0) {
349
+ resolve();
350
+ } else {
351
+ reject(new Error(`npm install failed with code ${code}`));
352
+ }
353
+ });
354
+ npmInstall.on("error", reject);
355
+ });
356
+ installSpinner.succeed("Dependencies installed successfully!");
357
+ const shouldStartDev = await confirm({
358
+ message: "Would you like to start the development server?",
359
+ default: true
360
+ });
361
+ if (shouldStartDev) {
362
+ console.log(chalk.cyan("\nStarting development server...\n"));
363
+ const devServer = spawn("npm", [
364
+ "run",
365
+ "dev"
366
+ ], {
367
+ cwd: targetDir,
368
+ stdio: "inherit",
369
+ shell: true
370
+ });
371
+ process.on("SIGINT", () => {
372
+ devServer.kill();
373
+ process.exit(0);
374
+ });
375
+ } else {
376
+ console.log(chalk.cyan("\nTo start the development server later:"));
377
+ console.log(chalk.gray(` cd ${projectName}`));
378
+ console.log(chalk.gray(` npm run dev`));
379
+ }
380
+ } catch (error) {
381
+ installSpinner.fail("Failed to install dependencies");
382
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
383
+ console.log(chalk.cyan("\nYou can install dependencies manually:"));
384
+ console.log(chalk.gray(` cd ${projectName}`));
385
+ console.log(chalk.gray(` npm install`));
386
+ }
387
+ } else {
388
+ console.log(chalk.cyan("\nTo get started:"));
389
+ console.log(chalk.gray(` cd ${projectName}`));
390
+ console.log(chalk.gray(` npm install`));
391
+ console.log(chalk.gray(` npm run dev`));
392
+ }
337
393
  });
338
394
  program.command("add <serviceName>").description("Add a new MCP service to your project").action(async (serviceName) => {
339
395
  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)."));
396
+ const mcpDir = path.join(cwd, "mcp");
397
+ if (!fs.existsSync(path.join(cwd, "main.ts"))) {
398
+ console.error(chalk.red("ERROR: Not a LeanMCP project (main.ts missing)."));
343
399
  process.exit(1);
344
400
  }
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.`));
401
+ const serviceDir = path.join(mcpDir, serviceName);
402
+ const serviceFile = path.join(serviceDir, "index.ts");
403
+ if (fs.existsSync(serviceDir)) {
404
+ console.error(chalk.red(`ERROR: Service ${serviceName} already exists.`));
349
405
  process.exit(1);
350
406
  }
407
+ await fs.mkdirp(serviceDir);
351
408
  const indexTs = `import { Tool, Resource, Prompt, Optional, SchemaConstraint } from "@leanmcp/core";
352
409
 
353
410
  // Input schema for greeting
@@ -407,34 +464,12 @@ export class ${capitalize(serviceName)}Service {
407
464
  }
408
465
  }
409
466
  `;
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!`));
467
+ await fs.writeFile(serviceFile, indexTs);
468
+ console.log(chalk.green(`\\nCreated new service: ${chalk.bold(serviceName)}`));
469
+ console.log(chalk.gray(` File: mcp/${serviceName}/index.ts`));
470
+ console.log(chalk.gray(` Tool: greet`));
471
+ console.log(chalk.gray(` Prompt: welcomePrompt`));
472
+ console.log(chalk.gray(` Resource: getStatus`));
473
+ console.log(chalk.green(`\\nService will be automatically discovered on next server start!`));
439
474
  });
440
475
  program.parse();
package/package.json CHANGED
@@ -1,38 +1,41 @@
1
1
  {
2
2
  "name": "@leanmcp/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Command-line interface for scaffolding LeanMCP projects",
5
5
  "bin": {
6
- "leanmcp": "dist/index.js"
6
+ "leanmcp": "bin/leanmcp.js"
7
7
  },
8
+ "type": "module",
8
9
  "main": "dist/index.js",
9
- "module": "dist/index.mjs",
10
10
  "types": "dist/index.d.ts",
11
11
  "exports": {
12
12
  ".": {
13
13
  "types": "./dist/index.d.ts",
14
- "require": "./dist/index.js",
15
- "import": "./dist/index.mjs"
14
+ "import": "./dist/index.js"
16
15
  }
17
16
  },
18
17
  "files": [
18
+ "bin",
19
19
  "dist",
20
20
  "README.md",
21
21
  "LICENSE"
22
22
  ],
23
23
  "scripts": {
24
- "build": "tsup src/index.ts --format esm,cjs --dts",
25
- "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
26
- "test": "echo \"No tests yet\" && exit 0"
24
+ "build": "tsup src/index.ts --format esm --dts",
25
+ "dev": "tsup src/index.ts --format esm --dts --watch",
26
+ "test": "echo \"No tests yet\" && exit 0",
27
+ "clean": "rimraf dist"
27
28
  },
28
29
  "dependencies": {
30
+ "@inquirer/prompts": "^7.0.1",
29
31
  "chalk": "^5.3.0",
30
32
  "commander": "^12.0.0",
31
33
  "fs-extra": "^11.2.0",
32
34
  "ora": "^8.1.0"
33
35
  },
34
36
  "devDependencies": {
35
- "@types/fs-extra": "^11.0.4"
37
+ "@types/fs-extra": "^11.0.4",
38
+ "rimraf": "^6.1.2"
36
39
  },
37
40
  "repository": {
38
41
  "type": "git",
package/dist/index.d.mts DELETED
@@ -1 +0,0 @@
1
- #!/usr/bin/env node
package/dist/index.mjs DELETED
@@ -1,418 +0,0 @@
1
- #!/usr/bin/env node
2
- var __defProp = Object.defineProperty;
3
- var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
-
5
- // src/index.ts
6
- import { Command } from "commander";
7
- import chalk from "chalk";
8
- import fs from "fs-extra";
9
- import path from "path";
10
- import ora from "ora";
11
- function capitalize(str) {
12
- return str.charAt(0).toUpperCase() + str.slice(1);
13
- }
14
- __name(capitalize, "capitalize");
15
- var program = new Command();
16
- program.name("leanmcp").description("LeanMCP CLI \u2014 create production-ready MCP servers with Streamable HTTP").version("0.1.0");
17
- program.command("create <projectName>").description("Create a new LeanMCP project with Streamable HTTP transport").action(async (projectName) => {
18
- const spinner = ora(`Creating project ${projectName}...`).start();
19
- const targetDir = path.join(process.cwd(), projectName);
20
- if (fs.existsSync(targetDir)) {
21
- spinner.fail(`Folder ${projectName} already exists.`);
22
- process.exit(1);
23
- }
24
- await fs.mkdirp(targetDir);
25
- await fs.mkdirp(path.join(targetDir, "mcp"));
26
- const pkg = {
27
- name: projectName,
28
- version: "1.0.0",
29
- description: "MCP Server with Streamable HTTP Transport and LeanMCP SDK",
30
- main: "dist/main.js",
31
- type: "module",
32
- scripts: {
33
- dev: "tsx watch main.ts",
34
- build: "tsc",
35
- start: "node dist/main.js",
36
- clean: "rm -rf dist"
37
- },
38
- keywords: [
39
- "mcp",
40
- "model-context-protocol",
41
- "streamable-http",
42
- "leanmcp"
43
- ],
44
- author: "",
45
- license: "MIT",
46
- dependencies: {
47
- "@leanmcp/core": "^0.1.0",
48
- "@modelcontextprotocol/sdk": "^1.0.0",
49
- "cors": "^2.8.5",
50
- "dotenv": "^16.5.0",
51
- "express": "^5.1.0"
52
- },
53
- devDependencies: {
54
- "@types/cors": "^2.8.19",
55
- "@types/express": "^5.0.3",
56
- "@types/node": "^20.0.0",
57
- "tsx": "^4.20.3",
58
- "typescript": "^5.6.3"
59
- }
60
- };
61
- await fs.writeJSON(path.join(targetDir, "package.json"), pkg, {
62
- spaces: 2
63
- });
64
- const tsconfig = {
65
- compilerOptions: {
66
- module: "ESNext",
67
- target: "ES2022",
68
- moduleResolution: "Node",
69
- esModuleInterop: true,
70
- strict: true,
71
- skipLibCheck: true,
72
- outDir: "dist",
73
- experimentalDecorators: true,
74
- emitDecoratorMetadata: true
75
- },
76
- include: [
77
- "**/*.ts"
78
- ],
79
- exclude: [
80
- "node_modules",
81
- "dist"
82
- ]
83
- };
84
- await fs.writeJSON(path.join(targetDir, "tsconfig.json"), tsconfig, {
85
- spaces: 2
86
- });
87
- const mainTs = `import dotenv from "dotenv";
88
- import { createHTTPServer, MCPServer } from "@leanmcp/core";
89
- import { ExampleService } from "./mcp/example.js";
90
-
91
- // Load environment variables
92
- dotenv.config();
93
-
94
- const PORT = Number(process.env.PORT) || 3001;
95
-
96
- /**
97
- * Create and configure the MCP server
98
- */
99
- function createMCPServer() {
100
- const server = new MCPServer({
101
- name: "${projectName}",
102
- version: "1.0.0"
103
- });
104
-
105
- // Register your services here
106
- server.registerService(new ExampleService());
107
-
108
- return server.getServer();
109
- }
110
-
111
- // Start the HTTP server
112
- await createHTTPServer(createMCPServer, {
113
- port: PORT,
114
- cors: true,
115
- logging: true
116
- });
117
- `;
118
- await fs.writeFile(path.join(targetDir, "main.ts"), mainTs);
119
- const exampleServiceTs = `import { Tool, Resource, Prompt, SchemaConstraint, Optional } from "@leanmcp/core";
120
-
121
- /**
122
- * Example service demonstrating LeanMCP SDK decorators
123
- *
124
- * This is a simple example to get you started. Add your own tools, resources, and prompts here!
125
- */
126
-
127
- // Input schema with validation decorators
128
- class CalculateInput {
129
- @SchemaConstraint({ description: "First number" })
130
- a!: number;
131
-
132
- @SchemaConstraint({ description: "Second number" })
133
- b!: number;
134
-
135
- @Optional()
136
- @SchemaConstraint({
137
- description: "Operation to perform",
138
- enum: ["add", "subtract", "multiply", "divide"],
139
- default: "add"
140
- })
141
- operation?: string;
142
- }
143
-
144
- export class ExampleService {
145
- @Tool({
146
- description: "Perform arithmetic operations with automatic schema validation",
147
- inputClass: CalculateInput
148
- })
149
- async calculate(input: CalculateInput) {
150
- let result: number;
151
-
152
- switch (input.operation || "add") {
153
- case "add":
154
- result = input.a + input.b;
155
- break;
156
- case "subtract":
157
- result = input.a - input.b;
158
- break;
159
- case "multiply":
160
- result = input.a * input.b;
161
- break;
162
- case "divide":
163
- if (input.b === 0) throw new Error("Cannot divide by zero");
164
- result = input.a / input.b;
165
- break;
166
- default:
167
- throw new Error("Invalid operation");
168
- }
169
-
170
- return {
171
- content: [{
172
- type: "text" as const,
173
- text: JSON.stringify({
174
- operation: input.operation || "add",
175
- operands: { a: input.a, b: input.b },
176
- result
177
- }, null, 2)
178
- }]
179
- };
180
- }
181
-
182
- @Tool({ description: "Echo a message back" })
183
- async echo(input: { message: string }) {
184
- return {
185
- content: [{
186
- type: "text" as const,
187
- text: JSON.stringify({
188
- echoed: input.message,
189
- timestamp: new Date().toISOString()
190
- }, null, 2)
191
- }]
192
- };
193
- }
194
-
195
- @Resource({ description: "Get server information" })
196
- async serverInfo() {
197
- return {
198
- contents: [{
199
- uri: "server://info",
200
- mimeType: "application/json",
201
- text: JSON.stringify({
202
- name: "${projectName}",
203
- version: "1.0.0",
204
- uptime: process.uptime()
205
- }, null, 2)
206
- }]
207
- };
208
- }
209
-
210
- @Prompt({ description: "Generate a greeting prompt" })
211
- async greeting(args: { name?: string }) {
212
- return {
213
- messages: [{
214
- role: "user" as const,
215
- content: {
216
- type: "text" as const,
217
- text: \`Hello \${args.name || 'there'}! Welcome to ${projectName}.\`
218
- }
219
- }]
220
- };
221
- }
222
- }
223
- `;
224
- await fs.writeFile(path.join(targetDir, "mcp", "example.ts"), exampleServiceTs);
225
- const gitignore = `node_modules
226
- dist
227
- .env
228
- .env.local
229
- *.log
230
- `;
231
- const env = `# Server Configuration
232
- PORT=3001
233
- NODE_ENV=development
234
-
235
- # Add your environment variables here
236
- `;
237
- await fs.writeFile(path.join(targetDir, ".gitignore"), gitignore);
238
- await fs.writeFile(path.join(targetDir, ".env"), env);
239
- const readme = `# ${projectName}
240
-
241
- MCP Server with Streamable HTTP Transport built with LeanMCP SDK
242
-
243
- ## Quick Start
244
-
245
- \`\`\`bash
246
- # Install dependencies
247
- npm install
248
-
249
- # Start development server (hot reload)
250
- npm run dev
251
-
252
- # Build for production
253
- npm run build
254
-
255
- # Run production server
256
- npm start
257
- \`\`\`
258
-
259
- ## Project Structure
260
-
261
- \`\`\`
262
- ${projectName}/
263
- \u251C\u2500\u2500 main.ts # Server entry point
264
- \u251C\u2500\u2500 mcp/
265
- \u2502 \u2514\u2500\u2500 example.ts # Example service
266
- \u251C\u2500\u2500 .env # Environment variables
267
- \u2514\u2500\u2500 package.json
268
- \`\`\`
269
-
270
- ## Adding New Services
271
-
272
- Create a new service file in \`mcp/\`:
273
-
274
- \`\`\`typescript
275
- import { Tool } from "@leanmcp/core";
276
-
277
- export class MyService {
278
- @Tool({ description: "My awesome tool" })
279
- async myTool(input: { message: string }) {
280
- return {
281
- content: [{
282
- type: "text",
283
- text: \`You said: \${input.message}\`
284
- }]
285
- };
286
- }
287
- }
288
- \`\`\`
289
-
290
- Then register it in \`main.ts\`:
291
-
292
- \`\`\`typescript
293
- import { MyService } from "./mcp/my-service.js";
294
- server.registerService(new MyService());
295
- \`\`\`
296
-
297
- ## Testing with MCP Inspector
298
-
299
- \`\`\`bash
300
- npx @modelcontextprotocol/inspector http://localhost:3001/mcp
301
- \`\`\`
302
-
303
- ## License
304
-
305
- MIT
306
- `;
307
- await fs.writeFile(path.join(targetDir, "README.md"), readme);
308
- spinner.succeed(`Project ${projectName} created!`);
309
- console.log(chalk.green("\\nSuccess! Your MCP server is ready.\\n"));
310
- console.log(chalk.cyan("Next steps:"));
311
- console.log(chalk.gray(` cd ${projectName}`));
312
- console.log(chalk.gray(` npm install`));
313
- console.log(chalk.gray(` npm run dev`));
314
- console.log(chalk.gray(`\\nServer will run on http://localhost:3001`));
315
- });
316
- program.command("add <serviceName>").description("Add a new MCP service to your project").action(async (serviceName) => {
317
- const cwd = process.cwd();
318
- const mcpDir = path.join(cwd, "mcp");
319
- if (!fs.existsSync(path.join(cwd, "main.ts"))) {
320
- console.error(chalk.red("ERROR: Not a LeanMCP project (main.ts missing)."));
321
- process.exit(1);
322
- }
323
- await fs.mkdirp(mcpDir);
324
- const serviceFile = path.join(mcpDir, `${serviceName}.ts`);
325
- if (fs.existsSync(serviceFile)) {
326
- console.error(chalk.red(`ERROR: Service ${serviceName} already exists.`));
327
- process.exit(1);
328
- }
329
- const indexTs = `import { Tool, Resource, Prompt, Optional, SchemaConstraint } from "@leanmcp/core";
330
-
331
- // Input schema for greeting
332
- class GreetInput {
333
- @SchemaConstraint({
334
- description: "Name to greet",
335
- minLength: 1
336
- })
337
- name!: string;
338
- }
339
-
340
- /**
341
- * ${capitalize(serviceName)} Service
342
- *
343
- * This service demonstrates the three types of MCP primitives:
344
- * - Tools: Callable functions (like API endpoints)
345
- * - Prompts: Reusable prompt templates
346
- * - Resources: Data sources/endpoints
347
- */
348
- export class ${capitalize(serviceName)}Service {
349
- // TOOL - Callable function
350
- // Tool name: "greet" (from function name)
351
- @Tool({
352
- description: "Greet a user by name",
353
- inputClass: GreetInput
354
- })
355
- greet(args: GreetInput) {
356
- return { message: \`Hello, \${args.name}! from ${serviceName}\` };
357
- }
358
-
359
- // PROMPT - Prompt template
360
- // Prompt name: "welcomePrompt" (from function name)
361
- @Prompt({ description: "Welcome message prompt template" })
362
- welcomePrompt(args: { userName?: string }) {
363
- return {
364
- messages: [
365
- {
366
- role: "user",
367
- content: {
368
- type: "text",
369
- text: \`Welcome \${args.userName || 'user'}! How can I help you with ${serviceName}?\`
370
- }
371
- }
372
- ]
373
- };
374
- }
375
-
376
- // RESOURCE - Data endpoint
377
- // Resource URI auto-generated from class and method name
378
- @Resource({ description: "${capitalize(serviceName)} service status" })
379
- getStatus() {
380
- return {
381
- service: "${serviceName}",
382
- status: "active",
383
- timestamp: new Date().toISOString()
384
- };
385
- }
386
- }
387
- `;
388
- await fs.writeFile(serviceFile, indexTs);
389
- const mainTsPath = path.join(cwd, "main.ts");
390
- let mainTsContent = await fs.readFile(mainTsPath, "utf-8");
391
- const serviceClassName = `${capitalize(serviceName)}Service`;
392
- const importStatement = `import { ${serviceClassName} } from "./mcp/${serviceName}.js";`;
393
- const registerStatement = ` server.registerService(new ${serviceClassName}());`;
394
- const lastImportMatch = mainTsContent.match(/import .* from .*;\n/g);
395
- if (lastImportMatch) {
396
- const lastImport = lastImportMatch[lastImportMatch.length - 1];
397
- const lastImportIndex = mainTsContent.lastIndexOf(lastImport);
398
- const afterLastImport = lastImportIndex + lastImport.length;
399
- mainTsContent = mainTsContent.slice(0, afterLastImport) + importStatement + "\n" + mainTsContent.slice(afterLastImport);
400
- }
401
- const registerPattern = /server\.registerService\(new \w+\(\)\);/g;
402
- const matches = [
403
- ...mainTsContent.matchAll(registerPattern)
404
- ];
405
- if (matches.length > 0) {
406
- const lastMatch = matches[matches.length - 1];
407
- const insertPosition = lastMatch.index + lastMatch[0].length;
408
- mainTsContent = mainTsContent.slice(0, insertPosition) + "\n" + registerStatement + mainTsContent.slice(insertPosition);
409
- }
410
- await fs.writeFile(mainTsPath, mainTsContent);
411
- console.log(chalk.green(`\\nCreated new service: ${chalk.bold(serviceName)}`));
412
- console.log(chalk.gray(` File: mcp/${serviceName}.ts`));
413
- console.log(chalk.gray(` Tool: greet`));
414
- console.log(chalk.gray(` Prompt: welcomePrompt`));
415
- console.log(chalk.gray(` Resource: getStatus`));
416
- console.log(chalk.green(`\\nService automatically registered in main.ts!`));
417
- });
418
- program.parse();