@leanmcp/cli 0.2.7 → 0.2.8

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.d.ts CHANGED
@@ -1 +1,2 @@
1
- #!/usr/bin/env node
1
+
2
+ export { }
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);
15
48
  }
16
- return to;
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
+ }
104
+ }
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,7 +617,7 @@ 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);
620
+ await fs5.writeFile(path5.join(targetDir, "mcp", "example", "index.ts"), exampleServiceTs);
247
621
  const gitignore = `node_modules
248
622
  dist
249
623
  .env
@@ -256,8 +630,8 @@ NODE_ENV=development
256
630
 
257
631
  # Add your environment variables here
258
632
  `;
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);
633
+ await fs5.writeFile(path5.join(targetDir, ".gitignore"), gitignore);
634
+ await fs5.writeFile(path5.join(targetDir, ".env"), env);
261
635
  const readme = `# ${projectName}
262
636
 
263
637
  MCP Server with Streamable HTTP Transport built with LeanMCP SDK
@@ -283,22 +657,36 @@ npm start
283
657
  \`\`\`
284
658
  ${projectName}/
285
659
  \u251C\u2500\u2500 main.ts # Server entry point
286
- \u251C\u2500\u2500 mcp/
287
- \u2502 \u2514\u2500\u2500 example.ts # Example service
660
+ \u251C\u2500\u2500 mcp/ # Services directory (auto-discovered)
661
+ \u2502 \u2514\u2500\u2500 example/
662
+ \u2502 \u2514\u2500\u2500 index.ts # Example service
288
663
  \u251C\u2500\u2500 .env # Environment variables
289
664
  \u2514\u2500\u2500 package.json
290
665
  \`\`\`
291
666
 
292
667
  ## Adding New Services
293
668
 
294
- Create a new service file in \`mcp/\`:
669
+ Create a new service directory in \`mcp/\`:
295
670
 
296
671
  \`\`\`typescript
297
- import { Tool } from "@leanmcp/core";
672
+ // mcp/myservice/index.ts
673
+ import { Tool, SchemaConstraint } from "@leanmcp/core";
674
+
675
+ // Define input schema
676
+ class MyToolInput {
677
+ @SchemaConstraint({
678
+ description: "Message to process",
679
+ minLength: 1
680
+ })
681
+ message!: string;
682
+ }
298
683
 
299
684
  export class MyService {
300
- @Tool({ description: "My awesome tool" })
301
- async myTool(input: { message: string }) {
685
+ @Tool({
686
+ description: "My awesome tool",
687
+ inputClass: MyToolInput
688
+ })
689
+ async myTool(input: MyToolInput) {
302
690
  return {
303
691
  content: [{
304
692
  type: "text",
@@ -309,12 +697,15 @@ export class MyService {
309
697
  }
310
698
  \`\`\`
311
699
 
312
- Then register it in \`main.ts\`:
700
+ Services are automatically discovered and registered - no need to modify \`main.ts\`!
313
701
 
314
- \`\`\`typescript
315
- import { MyService } from "./mcp/my-service.js";
316
- server.registerService(new MyService());
317
- \`\`\`
702
+ ## Features
703
+
704
+ - **Zero-config auto-discovery** - Services automatically registered from \`./mcp\` directory
705
+ - **Type-safe decorators** - \`@Tool\`, \`@Prompt\`, \`@Resource\` with full TypeScript support
706
+ - **Schema validation** - Automatic input validation with \`@SchemaConstraint\`
707
+ - **HTTP transport** - Production-ready HTTP server with session management
708
+ - **Hot reload** - Development mode with automatic restart on file changes
318
709
 
319
710
  ## Testing with MCP Inspector
320
711
 
@@ -326,28 +717,88 @@ npx @modelcontextprotocol/inspector http://localhost:3001/mcp
326
717
 
327
718
  MIT
328
719
  `;
329
- await import_fs_extra.default.writeFile(import_path.default.join(targetDir, "README.md"), readme);
720
+ await fs5.writeFile(path5.join(targetDir, "README.md"), readme);
330
721
  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`));
722
+ console.log(chalk3.green("\nSuccess! Your MCP server is ready.\n"));
723
+ console.log(chalk3.cyan(`Next, navigate to your project:
724
+ cd ${projectName}
725
+ `));
726
+ const shouldInstall = options.allowAll ? true : await confirm({
727
+ message: "Would you like to install dependencies now?",
728
+ default: true
729
+ });
730
+ if (shouldInstall) {
731
+ const installSpinner = ora3("Installing dependencies...").start();
732
+ try {
733
+ await new Promise((resolve, reject) => {
734
+ const npmInstall = spawn3("npm", [
735
+ "install"
736
+ ], {
737
+ cwd: targetDir,
738
+ stdio: "pipe",
739
+ shell: true
740
+ });
741
+ npmInstall.on("close", (code) => {
742
+ if (code === 0) {
743
+ resolve();
744
+ } else {
745
+ reject(new Error(`npm install failed with code ${code}`));
746
+ }
747
+ });
748
+ npmInstall.on("error", reject);
749
+ });
750
+ installSpinner.succeed("Dependencies installed successfully!");
751
+ const shouldStartDev = options.allowAll ? true : await confirm({
752
+ message: "Would you like to start the development server?",
753
+ default: true
754
+ });
755
+ if (shouldStartDev) {
756
+ console.log(chalk3.cyan("\nStarting development server...\n"));
757
+ const devServer = spawn3("npm", [
758
+ "run",
759
+ "dev"
760
+ ], {
761
+ cwd: targetDir,
762
+ stdio: "inherit",
763
+ shell: true
764
+ });
765
+ process.on("SIGINT", () => {
766
+ devServer.kill();
767
+ process.exit(0);
768
+ });
769
+ } else {
770
+ console.log(chalk3.cyan("\nTo start the development server later:"));
771
+ console.log(chalk3.gray(` cd ${projectName}`));
772
+ console.log(chalk3.gray(` npm run dev`));
773
+ }
774
+ } catch (error) {
775
+ installSpinner.fail("Failed to install dependencies");
776
+ console.error(chalk3.red(error instanceof Error ? error.message : String(error)));
777
+ console.log(chalk3.cyan("\nYou can install dependencies manually:"));
778
+ console.log(chalk3.gray(` cd ${projectName}`));
779
+ console.log(chalk3.gray(` npm install`));
780
+ }
781
+ } else {
782
+ console.log(chalk3.cyan("\nTo get started:"));
783
+ console.log(chalk3.gray(` cd ${projectName}`));
784
+ console.log(chalk3.gray(` npm install`));
785
+ console.log(chalk3.gray(` npm run dev`));
786
+ }
337
787
  });
338
788
  program.command("add <serviceName>").description("Add a new MCP service to your project").action(async (serviceName) => {
339
789
  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)."));
790
+ const mcpDir = path5.join(cwd, "mcp");
791
+ if (!fs5.existsSync(path5.join(cwd, "main.ts"))) {
792
+ console.error(chalk3.red("ERROR: Not a LeanMCP project (main.ts missing)."));
343
793
  process.exit(1);
344
794
  }
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.`));
795
+ const serviceDir = path5.join(mcpDir, serviceName);
796
+ const serviceFile = path5.join(serviceDir, "index.ts");
797
+ if (fs5.existsSync(serviceDir)) {
798
+ console.error(chalk3.red(`ERROR: Service ${serviceName} already exists.`));
349
799
  process.exit(1);
350
800
  }
801
+ await fs5.mkdirp(serviceDir);
351
802
  const indexTs = `import { Tool, Resource, Prompt, Optional, SchemaConstraint } from "@leanmcp/core";
352
803
 
353
804
  // Input schema for greeting
@@ -407,34 +858,14 @@ export class ${capitalize(serviceName)}Service {
407
858
  }
408
859
  }
409
860
  `;
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!`));
861
+ await fs5.writeFile(serviceFile, indexTs);
862
+ console.log(chalk3.green(`\\nCreated new service: ${chalk3.bold(serviceName)}`));
863
+ console.log(chalk3.gray(` File: mcp/${serviceName}/index.ts`));
864
+ console.log(chalk3.gray(` Tool: greet`));
865
+ console.log(chalk3.gray(` Prompt: welcomePrompt`));
866
+ console.log(chalk3.gray(` Resource: getStatus`));
867
+ console.log(chalk3.green(`\\nService will be automatically discovered on next server start!`));
439
868
  });
869
+ program.command("dev").description("Start development server with UI hot-reload (builds @UIApp components)").action(devCommand);
870
+ program.command("start").description("Build UI components and start production server").action(startCommand);
440
871
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leanmcp/cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Command-line interface for scaffolding LeanMCP projects",
5
5
  "bin": {
6
6
  "leanmcp": "bin/leanmcp.js"
@@ -28,18 +28,19 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@inquirer/prompts": "^7.0.1",
31
+ "@vitejs/plugin-react": "^4.3.0",
31
32
  "chalk": "^5.3.0",
33
+ "chokidar": "^4.0.0",
32
34
  "commander": "^12.0.0",
33
35
  "fs-extra": "^11.2.0",
36
+ "glob": "^11.0.0",
34
37
  "ora": "^8.1.0",
35
38
  "vite": "^5.4.0",
36
- "vite-plugin-singlefile": "^2.3.0",
37
- "@vitejs/plugin-react": "^4.3.0",
38
- "chokidar": "^4.0.0",
39
- "glob": "^11.0.0"
39
+ "vite-plugin-singlefile": "^2.3.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/fs-extra": "^11.0.4",
43
+ "@types/node": "^25.0.2",
43
44
  "rimraf": "^6.1.2"
44
45
  },
45
46
  "repository": {
@@ -64,4 +65,4 @@
64
65
  "publishConfig": {
65
66
  "access": "public"
66
67
  }
67
- }
68
+ }
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();