@leanmcp/cli 0.2.6 → 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/bin/leanmcp.js +0 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +549 -118
- package/package.json +3 -2
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +0 -418
package/bin/leanmcp.js
CHANGED
|
File without changes
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
29
|
-
var
|
|
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
|
|
38
|
-
program.name("leanmcp").description("LeanMCP CLI \u2014 create production-ready MCP servers with Streamable HTTP").version(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
47
|
-
await
|
|
48
|
-
const
|
|
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.
|
|
70
|
-
"
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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(
|
|
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
|
|
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 =
|
|
547
|
+
result = a + b;
|
|
177
548
|
break;
|
|
178
549
|
case "subtract":
|
|
179
|
-
result =
|
|
550
|
+
result = a - b;
|
|
180
551
|
break;
|
|
181
552
|
case "multiply":
|
|
182
|
-
result =
|
|
553
|
+
result = a * b;
|
|
183
554
|
break;
|
|
184
555
|
case "divide":
|
|
185
|
-
if (
|
|
186
|
-
result =
|
|
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({
|
|
205
|
-
|
|
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
|
|
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
|
|
260
|
-
await
|
|
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
|
|
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
|
|
669
|
+
Create a new service directory in \`mcp/\`:
|
|
295
670
|
|
|
296
671
|
\`\`\`typescript
|
|
297
|
-
|
|
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({
|
|
301
|
-
|
|
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
|
-
|
|
700
|
+
Services are automatically discovered and registered - no need to modify \`main.ts\`!
|
|
313
701
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
720
|
+
await fs5.writeFile(path5.join(targetDir, "README.md"), readme);
|
|
330
721
|
spinner.succeed(`Project ${projectName} created!`);
|
|
331
|
-
console.log(
|
|
332
|
-
console.log(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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 =
|
|
341
|
-
if (!
|
|
342
|
-
console.error(
|
|
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
|
-
|
|
346
|
-
const serviceFile =
|
|
347
|
-
if (
|
|
348
|
-
console.error(
|
|
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
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Command-line interface for scaffolding LeanMCP projects",
|
|
5
5
|
"bin": {
|
|
6
6
|
"leanmcp": "bin/leanmcp.js"
|
|
@@ -35,11 +35,12 @@
|
|
|
35
35
|
"fs-extra": "^11.2.0",
|
|
36
36
|
"glob": "^11.0.0",
|
|
37
37
|
"ora": "^8.1.0",
|
|
38
|
-
"vite": "^
|
|
38
|
+
"vite": "^5.4.0",
|
|
39
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": {
|
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();
|