@leanmcp/cli 0.4.5 → 0.5.1

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.
Files changed (2) hide show
  1. package/dist/index.js +921 -118
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -3,11 +3,11 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
3
3
 
4
4
  // src/index.ts
5
5
  import { Command } from "commander";
6
- import fs8 from "fs-extra";
7
- import path8 from "path";
8
- import ora7 from "ora";
6
+ import fs10 from "fs-extra";
7
+ import path10 from "path";
8
+ import ora8 from "ora";
9
9
  import { createRequire } from "module";
10
- import { confirm as confirm3 } from "@inquirer/prompts";
10
+ import { confirm as confirm4 } from "@inquirer/prompts";
11
11
  import { spawn as spawn4 } from "child_process";
12
12
 
13
13
  // src/commands/dev.ts
@@ -1261,8 +1261,8 @@ __name(whoamiCommand, "whoamiCommand");
1261
1261
 
1262
1262
  // src/commands/deploy.ts
1263
1263
  import ora5 from "ora";
1264
- import path7 from "path";
1265
- import fs7 from "fs-extra";
1264
+ import path8 from "path";
1265
+ import fs8 from "fs-extra";
1266
1266
  import os3 from "os";
1267
1267
  import archiver from "archiver";
1268
1268
  import { input as input2, confirm as confirm2, select } from "@inquirer/prompts";
@@ -1444,6 +1444,128 @@ function generateProjectName() {
1444
1444
  }
1445
1445
  __name(generateProjectName, "generateProjectName");
1446
1446
 
1447
+ // src/utils/env-parser.ts
1448
+ import fs7 from "fs-extra";
1449
+ import path7 from "path";
1450
+ var RESERVED_ENV_KEYS = [
1451
+ "AWS_REGION",
1452
+ "AWS_ACCESS_KEY_ID",
1453
+ "AWS_SECRET_ACCESS_KEY",
1454
+ "AWS_SESSION_TOKEN",
1455
+ "AWS_LAMBDA_FUNCTION_NAME",
1456
+ "AWS_LAMBDA_FUNCTION_MEMORY_SIZE",
1457
+ "AWS_LAMBDA_FUNCTION_VERSION",
1458
+ "AWS_LAMBDA_LOG_GROUP_NAME",
1459
+ "AWS_LAMBDA_LOG_STREAM_NAME",
1460
+ "_HANDLER",
1461
+ "_X_AMZN_TRACE_ID"
1462
+ ];
1463
+ var SYSTEM_ENV_KEYS = [
1464
+ "PORT",
1465
+ "AWS_LWA_PORT",
1466
+ "AWS_LWA_INVOKE_MODE",
1467
+ "AWS_LWA_READINESS_CHECK_MIN_UNHEALTHY_STATUS"
1468
+ ];
1469
+ function parseEnvVar(input3) {
1470
+ if (!input3 || typeof input3 !== "string") {
1471
+ return null;
1472
+ }
1473
+ const trimmed = input3.trim();
1474
+ const equalIndex = trimmed.indexOf("=");
1475
+ if (equalIndex === -1) {
1476
+ return null;
1477
+ }
1478
+ const key = trimmed.substring(0, equalIndex).trim();
1479
+ let value = trimmed.substring(equalIndex + 1);
1480
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1481
+ value = value.slice(1, -1);
1482
+ }
1483
+ if (!isValidEnvKey(key)) {
1484
+ return null;
1485
+ }
1486
+ return {
1487
+ key,
1488
+ value
1489
+ };
1490
+ }
1491
+ __name(parseEnvVar, "parseEnvVar");
1492
+ function parseEnvFile(content) {
1493
+ const result = {};
1494
+ if (!content || typeof content !== "string") {
1495
+ return result;
1496
+ }
1497
+ const lines = content.split(/\r?\n/);
1498
+ for (const line of lines) {
1499
+ const trimmed = line.trim();
1500
+ if (!trimmed || trimmed.startsWith("#")) {
1501
+ continue;
1502
+ }
1503
+ const parsed = parseEnvVar(trimmed);
1504
+ if (parsed) {
1505
+ result[parsed.key] = parsed.value;
1506
+ }
1507
+ }
1508
+ return result;
1509
+ }
1510
+ __name(parseEnvFile, "parseEnvFile");
1511
+ async function loadEnvFile(filePath) {
1512
+ const absolutePath = path7.resolve(filePath);
1513
+ if (!await fs7.pathExists(absolutePath)) {
1514
+ throw new Error(`Env file not found: ${absolutePath}`);
1515
+ }
1516
+ const content = await fs7.readFile(absolutePath, "utf-8");
1517
+ return parseEnvFile(content);
1518
+ }
1519
+ __name(loadEnvFile, "loadEnvFile");
1520
+ async function writeEnvFile(filePath, vars) {
1521
+ const absolutePath = path7.resolve(filePath);
1522
+ const lines = [
1523
+ "# Environment variables",
1524
+ `# Generated by leanmcp CLI at ${(/* @__PURE__ */ new Date()).toISOString()}`,
1525
+ ""
1526
+ ];
1527
+ const sortedKeys = Object.keys(vars).sort();
1528
+ for (const key of sortedKeys) {
1529
+ const value = vars[key];
1530
+ if (value.includes(" ") || value.includes("#") || value.includes('"')) {
1531
+ lines.push(`${key}="${value.replace(/"/g, '\\"')}"`);
1532
+ } else {
1533
+ lines.push(`${key}=${value}`);
1534
+ }
1535
+ }
1536
+ lines.push("");
1537
+ await fs7.writeFile(absolutePath, lines.join("\n"));
1538
+ }
1539
+ __name(writeEnvFile, "writeEnvFile");
1540
+ function isValidEnvKey(key) {
1541
+ if (!key || typeof key !== "string") {
1542
+ return false;
1543
+ }
1544
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
1545
+ }
1546
+ __name(isValidEnvKey, "isValidEnvKey");
1547
+ function isReservedKey(key) {
1548
+ return RESERVED_ENV_KEYS.includes(key);
1549
+ }
1550
+ __name(isReservedKey, "isReservedKey");
1551
+ function isSystemKey(key) {
1552
+ return SYSTEM_ENV_KEYS.includes(key);
1553
+ }
1554
+ __name(isSystemKey, "isSystemKey");
1555
+ function formatEnvVarsForDisplay(vars, reveal = false) {
1556
+ const keys = Object.keys(vars).sort();
1557
+ if (keys.length === 0) {
1558
+ return " (no environment variables)";
1559
+ }
1560
+ const lines = keys.map((key) => {
1561
+ const value = reveal ? vars[key] : "***";
1562
+ const keyType = isSystemKey(key) ? " (system)" : "";
1563
+ return ` ${key}=${value}${keyType}`;
1564
+ });
1565
+ return lines.join("\n");
1566
+ }
1567
+ __name(formatEnvVarsForDisplay, "formatEnvVarsForDisplay");
1568
+
1447
1569
  // src/commands/deploy.ts
1448
1570
  var DEBUG_MODE3 = false;
1449
1571
  function setDeployDebugMode(enabled) {
@@ -1490,10 +1612,10 @@ var API_ENDPOINTS = {
1490
1612
  var LEANMCP_CONFIG_DIR = ".leanmcp";
1491
1613
  var LEANMCP_CONFIG_FILE = "config.json";
1492
1614
  async function readLeanMCPConfig(projectPath) {
1493
- const configPath = path7.join(projectPath, LEANMCP_CONFIG_DIR, LEANMCP_CONFIG_FILE);
1615
+ const configPath = path8.join(projectPath, LEANMCP_CONFIG_DIR, LEANMCP_CONFIG_FILE);
1494
1616
  try {
1495
- if (await fs7.pathExists(configPath)) {
1496
- const config = await fs7.readJSON(configPath);
1617
+ if (await fs8.pathExists(configPath)) {
1618
+ const config = await fs8.readJSON(configPath);
1497
1619
  debug3("Found existing .leanmcp config:", config);
1498
1620
  return config;
1499
1621
  }
@@ -1504,10 +1626,10 @@ async function readLeanMCPConfig(projectPath) {
1504
1626
  }
1505
1627
  __name(readLeanMCPConfig, "readLeanMCPConfig");
1506
1628
  async function writeLeanMCPConfig(projectPath, config) {
1507
- const configDir = path7.join(projectPath, LEANMCP_CONFIG_DIR);
1508
- const configPath = path7.join(configDir, LEANMCP_CONFIG_FILE);
1509
- await fs7.ensureDir(configDir);
1510
- await fs7.writeJSON(configPath, config, {
1629
+ const configDir = path8.join(projectPath, LEANMCP_CONFIG_DIR);
1630
+ const configPath = path8.join(configDir, LEANMCP_CONFIG_FILE);
1631
+ await fs8.ensureDir(configDir);
1632
+ await fs8.writeJSON(configPath, config, {
1511
1633
  spaces: 2
1512
1634
  });
1513
1635
  debug3("Saved .leanmcp config:", config);
@@ -1515,7 +1637,7 @@ async function writeLeanMCPConfig(projectPath, config) {
1515
1637
  __name(writeLeanMCPConfig, "writeLeanMCPConfig");
1516
1638
  async function createZipArchive(folderPath, outputPath) {
1517
1639
  return new Promise((resolve, reject) => {
1518
- const output = fs7.createWriteStream(outputPath);
1640
+ const output = fs8.createWriteStream(outputPath);
1519
1641
  const archive = archiver("zip", {
1520
1642
  zlib: {
1521
1643
  level: 9
@@ -1613,16 +1735,21 @@ async function deployCommand(folderPath, options = {}) {
1613
1735
  }
1614
1736
  const apiUrl = await getApiUrl();
1615
1737
  debug3("API URL:", apiUrl);
1616
- const absolutePath = path7.resolve(process.cwd(), folderPath);
1617
- if (!await fs7.pathExists(absolutePath)) {
1738
+ const absolutePath = path8.resolve(process.cwd(), folderPath);
1739
+ if (!await fs8.pathExists(absolutePath)) {
1618
1740
  logger.error(`Folder not found: ${absolutePath}`);
1619
1741
  process.exit(1);
1620
1742
  }
1621
- const hasMainTs = await fs7.pathExists(path7.join(absolutePath, "main.ts"));
1622
- const hasPackageJson = await fs7.pathExists(path7.join(absolutePath, "package.json"));
1623
- if (!hasMainTs && !hasPackageJson) {
1743
+ const hasMainTs = await fs8.pathExists(path8.join(absolutePath, "main.ts"));
1744
+ const hasPackageJson = await fs8.pathExists(path8.join(absolutePath, "package.json"));
1745
+ const hasMainPy = await fs8.pathExists(path8.join(absolutePath, "main.py"));
1746
+ const hasRequirementsTxt = await fs8.pathExists(path8.join(absolutePath, "requirements.txt"));
1747
+ const hasPyprojectToml = await fs8.pathExists(path8.join(absolutePath, "pyproject.toml"));
1748
+ const isNodeProject = hasMainTs || hasPackageJson;
1749
+ const isPythonProject = hasMainPy || hasRequirementsTxt || hasPyprojectToml;
1750
+ if (!isNodeProject && !isPythonProject) {
1624
1751
  logger.error("Not a valid project folder.");
1625
- logger.gray("Expected main.ts or package.json in the folder.\n");
1752
+ logger.gray("Expected one of: main.ts, package.json, main.py, requirements.txt, or pyproject.toml\n");
1626
1753
  process.exit(1);
1627
1754
  }
1628
1755
  const existingConfig = await readLeanMCPConfig(absolutePath);
@@ -1689,10 +1816,10 @@ Generated project name: ${chalk.bold(projectName)}
1689
1816
  } catch (e) {
1690
1817
  debug3("Could not fetch existing projects");
1691
1818
  }
1692
- let folderName = path7.basename(absolutePath);
1819
+ let folderName = path8.basename(absolutePath);
1693
1820
  if (hasPackageJson) {
1694
1821
  try {
1695
- const pkg2 = await fs7.readJSON(path7.join(absolutePath, "package.json"));
1822
+ const pkg2 = await fs8.readJSON(path8.join(absolutePath, "package.json"));
1696
1823
  folderName = pkg2.name || folderName;
1697
1824
  } catch (e) {
1698
1825
  }
@@ -1843,7 +1970,7 @@ ${error instanceof Error ? error.message : String(error)}`);
1843
1970
  }
1844
1971
  const uploadSpinner = ora5("Packaging and uploading...").start();
1845
1972
  try {
1846
- const tempZip = path7.join(os3.tmpdir(), `leanmcp-${Date.now()}.zip`);
1973
+ const tempZip = path8.join(os3.tmpdir(), `leanmcp-${Date.now()}.zip`);
1847
1974
  const zipSize = await createZipArchive(absolutePath, tempZip);
1848
1975
  uploadSpinner.text = `Packaging... (${Math.round(zipSize / 1024)}KB)`;
1849
1976
  debug3("Step 2a: Getting upload URL for project:", projectId);
@@ -1870,7 +1997,7 @@ ${error instanceof Error ? error.message : String(error)}`);
1870
1997
  throw new Error("Backend did not return upload URL");
1871
1998
  }
1872
1999
  debug3("Step 2b: Uploading to S3...");
1873
- const zipBuffer = await fs7.readFile(tempZip);
2000
+ const zipBuffer = await fs8.readFile(tempZip);
1874
2001
  const s3Response = await fetch(uploadUrl, {
1875
2002
  method: "PUT",
1876
2003
  body: zipBuffer,
@@ -1893,7 +2020,7 @@ ${error instanceof Error ? error.message : String(error)}`);
1893
2020
  s3Location
1894
2021
  })
1895
2022
  });
1896
- await fs7.remove(tempZip);
2023
+ await fs8.remove(tempZip);
1897
2024
  uploadSpinner.succeed("Project uploaded");
1898
2025
  } catch (error) {
1899
2026
  uploadSpinner.fail("Failed to upload");
@@ -2123,8 +2250,8 @@ async function projectsDeleteCommand(projectId, options = {}) {
2123
2250
  process.exit(1);
2124
2251
  }
2125
2252
  if (!options.force) {
2126
- const { confirm: confirm4 } = await import("@inquirer/prompts");
2127
- const shouldDelete = await confirm4({
2253
+ const { confirm: confirm5 } = await import("@inquirer/prompts");
2254
+ const shouldDelete = await confirm5({
2128
2255
  message: `Are you sure you want to delete project ${projectId}?`,
2129
2256
  default: false
2130
2257
  });
@@ -2159,6 +2286,383 @@ ${error instanceof Error ? error.message : String(error)}`);
2159
2286
  }
2160
2287
  __name(projectsDeleteCommand, "projectsDeleteCommand");
2161
2288
 
2289
+ // src/commands/env.ts
2290
+ import ora7 from "ora";
2291
+ import path9 from "path";
2292
+ import fs9 from "fs-extra";
2293
+ import { confirm as confirm3 } from "@inquirer/prompts";
2294
+ var DEBUG_MODE4 = false;
2295
+ function setEnvDebugMode(enabled) {
2296
+ DEBUG_MODE4 = enabled;
2297
+ }
2298
+ __name(setEnvDebugMode, "setEnvDebugMode");
2299
+ function debug4(message, ...args) {
2300
+ if (DEBUG_MODE4) {
2301
+ console.log(chalk.gray(`[DEBUG] ${message}`), ...args);
2302
+ }
2303
+ }
2304
+ __name(debug4, "debug");
2305
+ async function debugFetch2(url, options = {}) {
2306
+ debug4(`HTTP ${options.method || "GET"} ${url}`);
2307
+ if (options.body && typeof options.body === "string") {
2308
+ try {
2309
+ const body = JSON.parse(options.body);
2310
+ debug4("Request body:", JSON.stringify(body, null, 2));
2311
+ } catch {
2312
+ debug4("Request body:", options.body);
2313
+ }
2314
+ }
2315
+ const startTime = Date.now();
2316
+ const response = await fetch(url, options);
2317
+ const duration = Date.now() - startTime;
2318
+ debug4(`Response: ${response.status} ${response.statusText} (${duration}ms)`);
2319
+ return response;
2320
+ }
2321
+ __name(debugFetch2, "debugFetch");
2322
+ var LEANMCP_CONFIG_DIR2 = ".leanmcp";
2323
+ var LEANMCP_CONFIG_FILE2 = "config.json";
2324
+ async function readLeanMCPConfig2(projectPath) {
2325
+ const configPath = path9.join(projectPath, LEANMCP_CONFIG_DIR2, LEANMCP_CONFIG_FILE2);
2326
+ try {
2327
+ if (await fs9.pathExists(configPath)) {
2328
+ const config = await fs9.readJSON(configPath);
2329
+ debug4("Found existing .leanmcp config:", config);
2330
+ return config;
2331
+ }
2332
+ } catch (e) {
2333
+ debug4("Could not read .leanmcp config:", e);
2334
+ }
2335
+ return null;
2336
+ }
2337
+ __name(readLeanMCPConfig2, "readLeanMCPConfig");
2338
+ async function getDeploymentContext(folderPath) {
2339
+ const apiKey = await getApiKey();
2340
+ if (!apiKey) {
2341
+ logger.error("Not logged in.");
2342
+ logger.gray("Run `leanmcp login` first to authenticate.\n");
2343
+ return null;
2344
+ }
2345
+ const apiUrl = await getApiUrl();
2346
+ const absolutePath = path9.resolve(process.cwd(), folderPath);
2347
+ const config = await readLeanMCPConfig2(absolutePath);
2348
+ if (!config) {
2349
+ logger.error("No deployment found.");
2350
+ logger.gray(`No .leanmcp/config.json found in ${absolutePath}`);
2351
+ logger.gray("Deploy first with: leanmcp deploy .\n");
2352
+ return null;
2353
+ }
2354
+ if (!config.deploymentId) {
2355
+ logger.error("Deployment ID not found in config.");
2356
+ logger.gray("Please redeploy with: leanmcp deploy .\n");
2357
+ return null;
2358
+ }
2359
+ return {
2360
+ apiKey,
2361
+ apiUrl,
2362
+ config
2363
+ };
2364
+ }
2365
+ __name(getDeploymentContext, "getDeploymentContext");
2366
+ async function envListCommand(folderPath, options = {}) {
2367
+ logger.info("\nLeanMCP Environment Variables\n");
2368
+ const context = await getDeploymentContext(folderPath);
2369
+ if (!context) {
2370
+ process.exit(1);
2371
+ }
2372
+ const { apiKey, apiUrl, config } = context;
2373
+ const spinner = ora7("Fetching environment variables...").start();
2374
+ try {
2375
+ const reveal = options.reveal ? "?reveal=true" : "";
2376
+ const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env${reveal}`, {
2377
+ headers: {
2378
+ "Authorization": `Bearer ${apiKey}`
2379
+ }
2380
+ });
2381
+ if (!response.ok) {
2382
+ const error = await response.text();
2383
+ throw new Error(`Failed to fetch env vars: ${error}`);
2384
+ }
2385
+ const envVars = await response.json();
2386
+ spinner.succeed("Environment variables retrieved");
2387
+ logger.info(`
2388
+ Project: ${config.projectName}`);
2389
+ logger.gray(`Deployment: ${config.deploymentId.substring(0, 8)}...`);
2390
+ logger.gray(`URL: ${config.url}
2391
+ `);
2392
+ const formatted = formatEnvVarsForDisplay(envVars, options.reveal);
2393
+ logger.log(formatted);
2394
+ logger.log("");
2395
+ if (!options.reveal) {
2396
+ logger.gray("Use --reveal to show actual values\n");
2397
+ }
2398
+ } catch (error) {
2399
+ spinner.fail("Failed to fetch environment variables");
2400
+ logger.error(`
2401
+ ${error instanceof Error ? error.message : String(error)}`);
2402
+ process.exit(1);
2403
+ }
2404
+ }
2405
+ __name(envListCommand, "envListCommand");
2406
+ async function envSetCommand(keyValue, folderPath, options = {}) {
2407
+ const context = await getDeploymentContext(folderPath);
2408
+ if (!context) {
2409
+ process.exit(1);
2410
+ }
2411
+ const { apiKey, apiUrl, config } = context;
2412
+ let variables = {};
2413
+ if (options.file) {
2414
+ const spinner2 = ora7(`Loading from ${options.file}...`).start();
2415
+ try {
2416
+ variables = await loadEnvFile(options.file);
2417
+ spinner2.succeed(`Loaded ${Object.keys(variables).length} variable(s) from ${options.file}`);
2418
+ } catch (error) {
2419
+ spinner2.fail(`Failed to load ${options.file}`);
2420
+ logger.error(`
2421
+ ${error instanceof Error ? error.message : String(error)}`);
2422
+ process.exit(1);
2423
+ }
2424
+ } else {
2425
+ const parsed = parseEnvVar(keyValue);
2426
+ if (!parsed) {
2427
+ logger.error("Invalid format. Expected: KEY=VALUE");
2428
+ logger.gray("Example: leanmcp env set API_KEY=secret123\n");
2429
+ process.exit(1);
2430
+ }
2431
+ if (isReservedKey(parsed.key)) {
2432
+ logger.error(`Cannot set reserved key: ${parsed.key}`);
2433
+ logger.gray("This key is managed by AWS Lambda.\n");
2434
+ process.exit(1);
2435
+ }
2436
+ if (isSystemKey(parsed.key) && !options.force) {
2437
+ logger.warn(`Warning: ${parsed.key} is a system key.`);
2438
+ const shouldContinue = await confirm3({
2439
+ message: "Are you sure you want to modify it?",
2440
+ default: false
2441
+ });
2442
+ if (!shouldContinue) {
2443
+ logger.gray("\nCancelled.\n");
2444
+ return;
2445
+ }
2446
+ }
2447
+ variables = {
2448
+ [parsed.key]: parsed.value
2449
+ };
2450
+ }
2451
+ if (Object.keys(variables).length === 0) {
2452
+ logger.warn("No variables to set.\n");
2453
+ return;
2454
+ }
2455
+ const spinner = ora7("Updating environment variables...").start();
2456
+ try {
2457
+ const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env`, {
2458
+ method: "PUT",
2459
+ headers: {
2460
+ "Authorization": `Bearer ${apiKey}`,
2461
+ "Content-Type": "application/json"
2462
+ },
2463
+ body: JSON.stringify({
2464
+ variables
2465
+ })
2466
+ });
2467
+ if (!response.ok) {
2468
+ const error = await response.text();
2469
+ throw new Error(`Failed to update env vars: ${error}`);
2470
+ }
2471
+ const result = await response.json();
2472
+ spinner.succeed("Environment variables updated");
2473
+ logger.info(`
2474
+ ${result.message || "Variables updated successfully"}`);
2475
+ logger.gray("Note: Lambda will cold-start on next invocation.\n");
2476
+ } catch (error) {
2477
+ spinner.fail("Failed to update environment variables");
2478
+ logger.error(`
2479
+ ${error instanceof Error ? error.message : String(error)}`);
2480
+ process.exit(1);
2481
+ }
2482
+ }
2483
+ __name(envSetCommand, "envSetCommand");
2484
+ async function envGetCommand(key, folderPath, options = {}) {
2485
+ const context = await getDeploymentContext(folderPath);
2486
+ if (!context) {
2487
+ process.exit(1);
2488
+ }
2489
+ const { apiKey, apiUrl, config } = context;
2490
+ try {
2491
+ const reveal = options.reveal ? "?reveal=true" : "";
2492
+ const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env${reveal}`, {
2493
+ headers: {
2494
+ "Authorization": `Bearer ${apiKey}`
2495
+ }
2496
+ });
2497
+ if (!response.ok) {
2498
+ const error = await response.text();
2499
+ throw new Error(`Failed to fetch env vars: ${error}`);
2500
+ }
2501
+ const envVars = await response.json();
2502
+ if (key in envVars) {
2503
+ const value = envVars[key];
2504
+ logger.log(`${key}=${value}`);
2505
+ } else {
2506
+ logger.warn(`Variable '${key}' not found.`);
2507
+ process.exit(1);
2508
+ }
2509
+ } catch (error) {
2510
+ logger.error(`
2511
+ ${error instanceof Error ? error.message : String(error)}`);
2512
+ process.exit(1);
2513
+ }
2514
+ }
2515
+ __name(envGetCommand, "envGetCommand");
2516
+ async function envRemoveCommand(key, folderPath, options = {}) {
2517
+ const context = await getDeploymentContext(folderPath);
2518
+ if (!context) {
2519
+ process.exit(1);
2520
+ }
2521
+ const { apiKey, apiUrl, config } = context;
2522
+ if (isReservedKey(key)) {
2523
+ logger.error(`Cannot remove reserved key: ${key}`);
2524
+ process.exit(1);
2525
+ }
2526
+ if (isSystemKey(key)) {
2527
+ logger.error(`Cannot remove system key: ${key}`);
2528
+ process.exit(1);
2529
+ }
2530
+ if (!options.force) {
2531
+ const shouldDelete = await confirm3({
2532
+ message: `Remove environment variable '${key}'?`,
2533
+ default: false
2534
+ });
2535
+ if (!shouldDelete) {
2536
+ logger.gray("\nCancelled.\n");
2537
+ return;
2538
+ }
2539
+ }
2540
+ const spinner = ora7(`Removing ${key}...`).start();
2541
+ try {
2542
+ const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env/${key}`, {
2543
+ method: "DELETE",
2544
+ headers: {
2545
+ "Authorization": `Bearer ${apiKey}`
2546
+ }
2547
+ });
2548
+ if (!response.ok) {
2549
+ const error = await response.text();
2550
+ throw new Error(`Failed to remove env var: ${error}`);
2551
+ }
2552
+ spinner.succeed(`Removed ${key}`);
2553
+ logger.gray("Note: Lambda will cold-start on next invocation.\n");
2554
+ } catch (error) {
2555
+ spinner.fail(`Failed to remove ${key}`);
2556
+ logger.error(`
2557
+ ${error instanceof Error ? error.message : String(error)}`);
2558
+ process.exit(1);
2559
+ }
2560
+ }
2561
+ __name(envRemoveCommand, "envRemoveCommand");
2562
+ async function envPullCommand(folderPath, options = {}) {
2563
+ const context = await getDeploymentContext(folderPath);
2564
+ if (!context) {
2565
+ process.exit(1);
2566
+ }
2567
+ const { apiKey, apiUrl, config } = context;
2568
+ const outputFile = options.file || ".env.remote";
2569
+ const spinner = ora7("Fetching environment variables...").start();
2570
+ try {
2571
+ const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env?reveal=true`, {
2572
+ headers: {
2573
+ "Authorization": `Bearer ${apiKey}`
2574
+ }
2575
+ });
2576
+ if (!response.ok) {
2577
+ const error = await response.text();
2578
+ throw new Error(`Failed to fetch env vars: ${error}`);
2579
+ }
2580
+ const envVars = await response.json();
2581
+ const userVars = {};
2582
+ for (const [key, value] of Object.entries(envVars)) {
2583
+ if (!isSystemKey(key)) {
2584
+ userVars[key] = value;
2585
+ }
2586
+ }
2587
+ spinner.text = `Writing to ${outputFile}...`;
2588
+ await writeEnvFile(outputFile, userVars);
2589
+ spinner.succeed(`Saved ${Object.keys(userVars).length} variable(s) to ${outputFile}`);
2590
+ logger.gray(`
2591
+ System variables (PORT, AWS_LWA_*) are not included.
2592
+ `);
2593
+ } catch (error) {
2594
+ spinner.fail("Failed to pull environment variables");
2595
+ logger.error(`
2596
+ ${error instanceof Error ? error.message : String(error)}`);
2597
+ process.exit(1);
2598
+ }
2599
+ }
2600
+ __name(envPullCommand, "envPullCommand");
2601
+ async function envPushCommand(folderPath, options = {}) {
2602
+ const context = await getDeploymentContext(folderPath);
2603
+ if (!context) {
2604
+ process.exit(1);
2605
+ }
2606
+ const { apiKey, apiUrl, config } = context;
2607
+ const inputFile = options.file || ".env";
2608
+ const loadSpinner = ora7(`Loading from ${inputFile}...`).start();
2609
+ let variables;
2610
+ try {
2611
+ variables = await loadEnvFile(inputFile);
2612
+ loadSpinner.succeed(`Loaded ${Object.keys(variables).length} variable(s) from ${inputFile}`);
2613
+ } catch (error) {
2614
+ loadSpinner.fail(`Failed to load ${inputFile}`);
2615
+ logger.error(`
2616
+ ${error instanceof Error ? error.message : String(error)}`);
2617
+ process.exit(1);
2618
+ }
2619
+ if (Object.keys(variables).length === 0) {
2620
+ logger.warn("No variables found in file.\n");
2621
+ return;
2622
+ }
2623
+ logger.warn("\nThis will REPLACE ALL current environment variables.");
2624
+ logger.gray("System variables (PORT, AWS_LWA_*) will be preserved.\n");
2625
+ if (!options.force) {
2626
+ const shouldPush = await confirm3({
2627
+ message: `Replace all env vars with ${Object.keys(variables).length} variables from ${inputFile}?`,
2628
+ default: false
2629
+ });
2630
+ if (!shouldPush) {
2631
+ logger.gray("\nCancelled.\n");
2632
+ return;
2633
+ }
2634
+ }
2635
+ const pushSpinner = ora7("Pushing environment variables...").start();
2636
+ try {
2637
+ const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env`, {
2638
+ method: "PUT",
2639
+ headers: {
2640
+ "Authorization": `Bearer ${apiKey}`,
2641
+ "Content-Type": "application/json"
2642
+ },
2643
+ body: JSON.stringify({
2644
+ variables,
2645
+ replaceAll: true
2646
+ })
2647
+ });
2648
+ if (!response.ok) {
2649
+ const error = await response.text();
2650
+ throw new Error(`Failed to push env vars: ${error}`);
2651
+ }
2652
+ const result = await response.json();
2653
+ pushSpinner.succeed("Environment variables pushed");
2654
+ logger.info(`
2655
+ ${result.message || "Variables updated successfully"}`);
2656
+ logger.gray("Note: Lambda will cold-start on next invocation.\n");
2657
+ } catch (error) {
2658
+ pushSpinner.fail("Failed to push environment variables");
2659
+ logger.error(`
2660
+ ${error instanceof Error ? error.message : String(error)}`);
2661
+ process.exit(1);
2662
+ }
2663
+ }
2664
+ __name(envPushCommand, "envPushCommand");
2665
+
2162
2666
  // src/templates/readme_v1.ts
2163
2667
  var getReadmeTemplate = /* @__PURE__ */ __name((projectName) => `<p align="center">
2164
2668
  <img
@@ -2661,6 +3165,219 @@ export class ${capitalizedName}Service {
2661
3165
  }
2662
3166
  `, "getServiceIndexTemplate");
2663
3167
 
3168
+ // src/templates/python/main_py_v1.ts
3169
+ var getPythonMainTemplate = /* @__PURE__ */ __name((projectName) => `#!/usr/bin/env python3
3170
+ """
3171
+ ${projectName} - MCP Server with Streamable HTTP Transport
3172
+ """
3173
+ import os
3174
+ import uvicorn
3175
+ from dotenv import load_dotenv
3176
+ from mcp.server.fastmcp import FastMCP
3177
+
3178
+ # Load environment variables
3179
+ load_dotenv()
3180
+
3181
+ # Create the MCP server
3182
+ mcp = FastMCP("${projectName}")
3183
+
3184
+
3185
+ # === Define your tools, resources, and prompts below ===
3186
+
3187
+ @mcp.tool()
3188
+ def calculate(a: float, b: float, operation: str = "add") -> dict:
3189
+ """
3190
+ Perform arithmetic operations.
3191
+
3192
+ Args:
3193
+ a: First number
3194
+ b: Second number
3195
+ operation: Operation to perform (add, subtract, multiply, divide)
3196
+ """
3197
+ if operation == "add":
3198
+ result = a + b
3199
+ elif operation == "subtract":
3200
+ result = a - b
3201
+ elif operation == "multiply":
3202
+ result = a * b
3203
+ elif operation == "divide":
3204
+ if b == 0:
3205
+ raise ValueError("Cannot divide by zero")
3206
+ result = a / b
3207
+ else:
3208
+ raise ValueError(f"Invalid operation: {operation}")
3209
+
3210
+ return {"operation": operation, "a": a, "b": b, "result": result}
3211
+
3212
+
3213
+ @mcp.tool()
3214
+ def echo(message: str) -> dict:
3215
+ """Echo a message back with a timestamp."""
3216
+ from datetime import datetime
3217
+ return {"echoed": message, "timestamp": datetime.now().isoformat()}
3218
+
3219
+
3220
+ @mcp.resource("server://info")
3221
+ def server_info() -> str:
3222
+ """Get server information."""
3223
+ import json
3224
+ import time
3225
+ return json.dumps({
3226
+ "name": "${projectName}",
3227
+ "version": "1.0.0",
3228
+ "uptime": time.process_time()
3229
+ }, indent=2)
3230
+
3231
+
3232
+ @mcp.prompt()
3233
+ def greeting(name: str = "there") -> str:
3234
+ """Generate a greeting prompt."""
3235
+ return f"Hello {name}! Welcome to ${projectName}."
3236
+
3237
+
3238
+ if __name__ == "__main__":
3239
+ port = int(os.getenv("PORT", "3001"))
3240
+ print(f"\\n${projectName} MCP Server starting on port {port}...")
3241
+
3242
+ # Run with streamable HTTP transport
3243
+ uvicorn.run(
3244
+ mcp.streamable_http_app(),
3245
+ host="127.0.0.1",
3246
+ port=port,
3247
+ log_level="info"
3248
+ )
3249
+ `, "getPythonMainTemplate");
3250
+
3251
+ // src/templates/python/requirements_v1.ts
3252
+ var getPythonRequirementsTemplate = /* @__PURE__ */ __name(() => `# MCP Server Dependencies
3253
+ mcp>=1.0.0
3254
+ fastmcp>=0.1.0
3255
+ uvicorn>=0.30.0
3256
+ python-dotenv>=1.0.0
3257
+ pydantic>=2.0.0
3258
+
3259
+ # Optional: Add your dependencies below
3260
+ # requests>=2.31.0
3261
+ # httpx>=0.27.0
3262
+ `, "getPythonRequirementsTemplate");
3263
+
3264
+ // src/templates/python/gitignore_py_v1.ts
3265
+ var pythonGitignoreTemplate = `# Byte-compiled / optimized / DLL files
3266
+ __pycache__/
3267
+ *.py[cod]
3268
+ *$py.class
3269
+
3270
+ # Virtual environments
3271
+ venv/
3272
+ .venv/
3273
+ ENV/
3274
+ env/
3275
+
3276
+ # Distribution / packaging
3277
+ build/
3278
+ dist/
3279
+ *.egg-info/
3280
+ *.egg
3281
+
3282
+ # IDE
3283
+ .idea/
3284
+ .vscode/
3285
+ *.swp
3286
+ *.swo
3287
+
3288
+ # Environment variables
3289
+ .env
3290
+ .env.local
3291
+ .env.*.local
3292
+
3293
+ # Testing
3294
+ .pytest_cache/
3295
+ .coverage
3296
+ htmlcov/
3297
+
3298
+ # Logs
3299
+ *.log
3300
+
3301
+ # OS
3302
+ .DS_Store
3303
+ Thumbs.db
3304
+ `;
3305
+
3306
+ // src/templates/python/readme_py_v1.ts
3307
+ var getPythonReadmeTemplate = /* @__PURE__ */ __name((projectName) => `# ${projectName}
3308
+
3309
+ A Python MCP (Model Context Protocol) server with Streamable HTTP transport.
3310
+
3311
+ ## Quick Start
3312
+
3313
+ ### Prerequisites
3314
+
3315
+ - Python 3.10+
3316
+ - pip or uv package manager
3317
+
3318
+ ### Installation
3319
+
3320
+ \`\`\`bash
3321
+ # Create virtual environment
3322
+ python -m venv venv
3323
+ source venv/bin/activate # On Windows: venv\\Scripts\\activate
3324
+
3325
+ # Install dependencies
3326
+ pip install -r requirements.txt
3327
+ \`\`\`
3328
+
3329
+ ### Development
3330
+
3331
+ \`\`\`bash
3332
+ # Start development server
3333
+ python main.py
3334
+ \`\`\`
3335
+
3336
+ Server runs at http://localhost:3001
3337
+
3338
+ ### Test with MCP Inspector
3339
+
3340
+ \`\`\`bash
3341
+ npx @modelcontextprotocol/inspector http://localhost:3001/mcp
3342
+ \`\`\`
3343
+
3344
+ ## Project Structure
3345
+
3346
+ \`\`\`
3347
+ ${projectName}/
3348
+ main.py # Server entry point with tools/resources/prompts
3349
+ requirements.txt # Python dependencies
3350
+ .env # Environment variables
3351
+ \`\`\`
3352
+
3353
+ ## Adding New Tools
3354
+
3355
+ Add tools directly in \`main.py\` using the \`@mcp.tool()\` decorator:
3356
+
3357
+ \`\`\`python
3358
+ @mcp.tool()
3359
+ def my_tool(param: str) -> dict:
3360
+ """Tool description shown to AI.
3361
+
3362
+ Args:
3363
+ param: Parameter description
3364
+ """
3365
+ return {"result": param}
3366
+ \`\`\`
3367
+
3368
+ ## Deploy to LeanMCP Cloud
3369
+
3370
+ \`\`\`bash
3371
+ leanmcp deploy .
3372
+ \`\`\`
3373
+
3374
+ ## Resources
3375
+
3376
+ - [MCP Documentation](https://modelcontextprotocol.io)
3377
+ - [FastMCP Documentation](https://github.com/jlowin/fastmcp)
3378
+ - [LeanMCP Documentation](https://docs.leanmcp.com)
3379
+ `, "getPythonReadmeTemplate");
3380
+
2664
3381
  // src/index.ts
2665
3382
  var require2 = createRequire(import.meta.url);
2666
3383
  var pkg = require2("../package.json");
@@ -2675,6 +3392,7 @@ function enableDebugIfNeeded() {
2675
3392
  setDebugMode(true);
2676
3393
  setDebugMode2(true);
2677
3394
  setDeployDebugMode(true);
3395
+ setEnvDebugMode(true);
2678
3396
  debug("Debug mode enabled globally");
2679
3397
  }
2680
3398
  }
@@ -2682,7 +3400,8 @@ __name(enableDebugIfNeeded, "enableDebugIfNeeded");
2682
3400
  enableDebugIfNeeded();
2683
3401
  program.name("leanmcp").description("LeanMCP CLI \u2014 create production-ready MCP servers with Streamable HTTP").version(pkg.version, "-v, --version", "Output the current version").helpOption("-h, --help", "Display help for command").option("-d, --debug", "Enable debug logging for all commands").addHelpText("after", `
2684
3402
  Examples:
2685
- $ leanmcp create my-app # Create new project (interactive)
3403
+ $ leanmcp create my-app # Create new TypeScript project (interactive)
3404
+ $ leanmcp create my-app --python # Create new Python project
2686
3405
  $ leanmcp create my-app -i # Create and install deps (non-interactive)
2687
3406
  $ leanmcp create my-app --no-install # Create without installing deps
2688
3407
  $ leanmcp dev # Start development server
@@ -2692,103 +3411,122 @@ Examples:
2692
3411
  $ leanmcp deploy ./my-app # Deploy to LeanMCP cloud
2693
3412
  $ leanmcp projects list # List your cloud projects
2694
3413
  $ leanmcp projects delete <id> # Delete a cloud project
3414
+ $ leanmcp env list # List environment variables
3415
+ $ leanmcp env set KEY=VALUE # Set an environment variable
2695
3416
 
2696
3417
  Global Options:
2697
3418
  -v, --version Output the current version
2698
3419
  -h, --help Display help for command
2699
3420
  -d, --debug Enable debug logging for all commands
2700
3421
  `);
2701
- program.command("create <projectName>").description("Create a new LeanMCP project with Streamable HTTP transport").option("--allow-all", "Skip interactive confirmations and assume Yes").option("--no-dashboard", "Disable dashboard UI at / and /mcp GET endpoints").option("-i, --install", "Install dependencies automatically (non-interactive, no dev server)").option("--no-install", "Skip dependency installation (non-interactive)").action(async (projectName, options) => {
3422
+ program.command("create <projectName>").description("Create a new LeanMCP project with Streamable HTTP transport").option("--allow-all", "Skip interactive confirmations and assume Yes").option("--no-dashboard", "Disable dashboard UI at / and /mcp GET endpoints").option("-i, --install", "Install dependencies automatically (non-interactive, no dev server)").option("--no-install", "Skip dependency installation (non-interactive)").option("--python", "Create a Python MCP project instead of TypeScript").action(async (projectName, options) => {
2702
3423
  trackCommand("create", {
2703
3424
  projectName,
2704
3425
  ...options
2705
3426
  });
2706
- const spinner = ora7(`Creating project ${projectName}...`).start();
2707
- const targetDir = path8.join(process.cwd(), projectName);
2708
- if (fs8.existsSync(targetDir)) {
3427
+ const spinner = ora8(`Creating project ${projectName}...`).start();
3428
+ const targetDir = path10.join(process.cwd(), projectName);
3429
+ if (fs10.existsSync(targetDir)) {
2709
3430
  spinner.fail(`Folder ${projectName} already exists.`);
2710
3431
  process.exit(1);
2711
3432
  }
2712
- await fs8.mkdirp(targetDir);
2713
- await fs8.mkdirp(path8.join(targetDir, "mcp", "example"));
2714
- const pkg2 = {
2715
- name: projectName,
2716
- version: "1.0.0",
2717
- description: "MCP Server with Streamable HTTP Transport and LeanMCP SDK",
2718
- main: "dist/main.js",
2719
- type: "module",
2720
- scripts: {
2721
- dev: "leanmcp dev",
2722
- build: "leanmcp build",
2723
- start: "leanmcp start",
2724
- inspect: "npx @modelcontextprotocol/inspector node dist/main.js",
2725
- "start:node": "node dist/main.js",
2726
- clean: "rm -rf dist"
2727
- },
2728
- keywords: [
2729
- "mcp",
2730
- "model-context-protocol",
2731
- "streamable-http",
2732
- "leanmcp"
2733
- ],
2734
- author: "",
2735
- license: "MIT",
2736
- dependencies: {
2737
- "@leanmcp/core": "^0.3.14",
2738
- "@leanmcp/ui": "^0.2.1",
2739
- "@leanmcp/auth": "^0.4.0",
2740
- "dotenv": "^16.5.0"
2741
- },
2742
- devDependencies: {
2743
- "@leanmcp/cli": "^0.4.0",
2744
- "@types/node": "^20.0.0",
2745
- "tsx": "^4.20.3",
2746
- "typescript": "^5.6.3"
2747
- }
2748
- };
2749
- await fs8.writeJSON(path8.join(targetDir, "package.json"), pkg2, {
2750
- spaces: 2
2751
- });
2752
- const tsconfig = {
2753
- compilerOptions: {
2754
- module: "ESNext",
2755
- target: "ES2022",
2756
- moduleResolution: "Node",
2757
- esModuleInterop: true,
2758
- strict: true,
2759
- skipLibCheck: true,
2760
- outDir: "dist",
2761
- experimentalDecorators: true,
2762
- emitDecoratorMetadata: true
2763
- },
2764
- include: [
2765
- "**/*.ts"
2766
- ],
2767
- exclude: [
2768
- "node_modules",
2769
- "dist"
2770
- ]
2771
- };
2772
- await fs8.writeJSON(path8.join(targetDir, "tsconfig.json"), tsconfig, {
2773
- spaces: 2
2774
- });
2775
- const dashboardLine = options.dashboard === false ? `
3433
+ await fs10.mkdirp(targetDir);
3434
+ const isPython = options.python === true;
3435
+ if (isPython) {
3436
+ const requirements = getPythonRequirementsTemplate();
3437
+ await fs10.writeFile(path10.join(targetDir, "requirements.txt"), requirements);
3438
+ const mainPy = getPythonMainTemplate(projectName);
3439
+ await fs10.writeFile(path10.join(targetDir, "main.py"), mainPy);
3440
+ await fs10.writeFile(path10.join(targetDir, ".gitignore"), pythonGitignoreTemplate);
3441
+ const env = `# Server Configuration
3442
+ PORT=3001
3443
+
3444
+ # Add your environment variables here
3445
+ `;
3446
+ await fs10.writeFile(path10.join(targetDir, ".env"), env);
3447
+ const readme = getPythonReadmeTemplate(projectName);
3448
+ await fs10.writeFile(path10.join(targetDir, "README.md"), readme);
3449
+ } else {
3450
+ await fs10.mkdirp(path10.join(targetDir, "mcp", "example"));
3451
+ const pkg2 = {
3452
+ name: projectName,
3453
+ version: "1.0.0",
3454
+ description: "MCP Server with Streamable HTTP Transport and LeanMCP SDK",
3455
+ main: "dist/main.js",
3456
+ type: "module",
3457
+ scripts: {
3458
+ dev: "leanmcp dev",
3459
+ build: "leanmcp build",
3460
+ start: "leanmcp start",
3461
+ inspect: "npx @modelcontextprotocol/inspector node dist/main.js",
3462
+ "start:node": "node dist/main.js",
3463
+ clean: "rm -rf dist"
3464
+ },
3465
+ keywords: [
3466
+ "mcp",
3467
+ "model-context-protocol",
3468
+ "streamable-http",
3469
+ "leanmcp"
3470
+ ],
3471
+ author: "",
3472
+ license: "MIT",
3473
+ dependencies: {
3474
+ "@leanmcp/core": "^0.3.14",
3475
+ "@leanmcp/ui": "^0.2.1",
3476
+ "@leanmcp/auth": "^0.4.0",
3477
+ "dotenv": "^16.5.0"
3478
+ },
3479
+ devDependencies: {
3480
+ "@leanmcp/cli": "^0.4.0",
3481
+ "@types/node": "^20.0.0",
3482
+ "tsx": "^4.20.3",
3483
+ "typescript": "^5.6.3"
3484
+ }
3485
+ };
3486
+ await fs10.writeJSON(path10.join(targetDir, "package.json"), pkg2, {
3487
+ spaces: 2
3488
+ });
3489
+ const tsconfig = {
3490
+ compilerOptions: {
3491
+ module: "ESNext",
3492
+ target: "ES2022",
3493
+ moduleResolution: "Node",
3494
+ esModuleInterop: true,
3495
+ strict: true,
3496
+ skipLibCheck: true,
3497
+ outDir: "dist",
3498
+ experimentalDecorators: true,
3499
+ emitDecoratorMetadata: true
3500
+ },
3501
+ include: [
3502
+ "**/*.ts"
3503
+ ],
3504
+ exclude: [
3505
+ "node_modules",
3506
+ "dist"
3507
+ ]
3508
+ };
3509
+ await fs10.writeJSON(path10.join(targetDir, "tsconfig.json"), tsconfig, {
3510
+ spaces: 2
3511
+ });
3512
+ const dashboardLine = options.dashboard === false ? `
2776
3513
  dashboard: false, // Dashboard disabled via --no-dashboard` : "";
2777
- const mainTs = getMainTsTemplate(projectName, dashboardLine);
2778
- await fs8.writeFile(path8.join(targetDir, "main.ts"), mainTs);
2779
- const exampleServiceTs = getExampleServiceTemplate(projectName);
2780
- await fs8.writeFile(path8.join(targetDir, "mcp", "example", "index.ts"), exampleServiceTs);
2781
- const gitignore = gitignoreTemplate;
2782
- const env = `# Server Configuration
3514
+ const mainTs = getMainTsTemplate(projectName, dashboardLine);
3515
+ await fs10.writeFile(path10.join(targetDir, "main.ts"), mainTs);
3516
+ const exampleServiceTs = getExampleServiceTemplate(projectName);
3517
+ await fs10.writeFile(path10.join(targetDir, "mcp", "example", "index.ts"), exampleServiceTs);
3518
+ const gitignore = gitignoreTemplate;
3519
+ const env = `# Server Configuration
2783
3520
  PORT=3001
2784
3521
  NODE_ENV=development
2785
3522
 
2786
3523
  # Add your environment variables here
2787
3524
  `;
2788
- await fs8.writeFile(path8.join(targetDir, ".gitignore"), gitignore);
2789
- await fs8.writeFile(path8.join(targetDir, ".env"), env);
2790
- const readme = getReadmeTemplate(projectName);
2791
- await fs8.writeFile(path8.join(targetDir, "README.md"), readme);
3525
+ await fs10.writeFile(path10.join(targetDir, ".gitignore"), gitignore);
3526
+ await fs10.writeFile(path10.join(targetDir, ".env"), env);
3527
+ const readme = getReadmeTemplate(projectName);
3528
+ await fs10.writeFile(path10.join(targetDir, "README.md"), readme);
3529
+ }
2792
3530
  spinner.succeed(`Project ${projectName} created!`);
2793
3531
  logger.log("\nSuccess! Your MCP server is ready.\n", chalk.green);
2794
3532
  logger.log("To deploy to LeanMCP cloud:", chalk.cyan);
@@ -2801,20 +3539,40 @@ NODE_ENV=development
2801
3539
  if (options.install === false) {
2802
3540
  logger.log("To get started:", chalk.cyan);
2803
3541
  logger.log(` cd ${projectName}`, chalk.gray);
2804
- logger.log(` npm install`, chalk.gray);
2805
- logger.log(` npm run dev`, chalk.gray);
3542
+ if (isPython) {
3543
+ logger.log(` python -m venv venv`, chalk.gray);
3544
+ logger.log(` source venv/bin/activate # On Windows: venv\\Scripts\\activate`, chalk.gray);
3545
+ logger.log(` pip install -r requirements.txt`, chalk.gray);
3546
+ logger.log(` python main.py`, chalk.gray);
3547
+ } else {
3548
+ logger.log(` npm install`, chalk.gray);
3549
+ logger.log(` npm run dev`, chalk.gray);
3550
+ }
2806
3551
  logger.log("");
2807
3552
  logger.log("To deploy to LeanMCP cloud:", chalk.cyan);
2808
3553
  logger.log(` cd ${projectName}`, chalk.gray);
2809
3554
  logger.log(` leanmcp deploy .`, chalk.gray);
2810
3555
  return;
2811
3556
  }
2812
- const shouldInstall = isNonInteractive ? true : await confirm3({
3557
+ if (isPython) {
3558
+ logger.log("\nTo get started:", chalk.cyan);
3559
+ logger.log(` cd ${projectName}`, chalk.gray);
3560
+ logger.log(` python -m venv venv`, chalk.gray);
3561
+ logger.log(` source venv/bin/activate # On Windows: venv\\Scripts\\activate`, chalk.gray);
3562
+ logger.log(` pip install -r requirements.txt`, chalk.gray);
3563
+ logger.log(` python main.py`, chalk.gray);
3564
+ logger.log("");
3565
+ logger.log("To deploy to LeanMCP cloud:", chalk.cyan);
3566
+ logger.log(` cd ${projectName}`, chalk.gray);
3567
+ logger.log(` leanmcp deploy .`, chalk.gray);
3568
+ return;
3569
+ }
3570
+ const shouldInstall = isNonInteractive ? true : await confirm4({
2813
3571
  message: "Would you like to install dependencies now?",
2814
3572
  default: true
2815
3573
  });
2816
3574
  if (shouldInstall) {
2817
- const installSpinner = ora7("Installing dependencies...").start();
3575
+ const installSpinner = ora8("Installing dependencies...").start();
2818
3576
  try {
2819
3577
  await new Promise((resolve, reject) => {
2820
3578
  const npmInstall = spawn4("npm", [
@@ -2844,7 +3602,7 @@ NODE_ENV=development
2844
3602
  logger.log(` leanmcp deploy .`, chalk.gray);
2845
3603
  return;
2846
3604
  }
2847
- const shouldStartDev = options.allowAll ? true : await confirm3({
3605
+ const shouldStartDev = options.allowAll ? true : await confirm4({
2848
3606
  message: "Would you like to start the development server?",
2849
3607
  default: true
2850
3608
  });
@@ -2891,20 +3649,20 @@ NODE_ENV=development
2891
3649
  });
2892
3650
  program.command("add <serviceName>").description("Add a new MCP service to your project").action(async (serviceName) => {
2893
3651
  const cwd = process.cwd();
2894
- const mcpDir = path8.join(cwd, "mcp");
2895
- if (!fs8.existsSync(path8.join(cwd, "main.ts"))) {
3652
+ const mcpDir = path10.join(cwd, "mcp");
3653
+ if (!fs10.existsSync(path10.join(cwd, "main.ts"))) {
2896
3654
  logger.log("ERROR: Not a LeanMCP project (main.ts missing).", chalk.red);
2897
3655
  process.exit(1);
2898
3656
  }
2899
- const serviceDir = path8.join(mcpDir, serviceName);
2900
- const serviceFile = path8.join(serviceDir, "index.ts");
2901
- if (fs8.existsSync(serviceDir)) {
3657
+ const serviceDir = path10.join(mcpDir, serviceName);
3658
+ const serviceFile = path10.join(serviceDir, "index.ts");
3659
+ if (fs10.existsSync(serviceDir)) {
2902
3660
  logger.log(`ERROR: Service ${serviceName} already exists.`, chalk.red);
2903
3661
  process.exit(1);
2904
3662
  }
2905
- await fs8.mkdirp(serviceDir);
3663
+ await fs10.mkdirp(serviceDir);
2906
3664
  const indexTs = getServiceIndexTemplate(serviceName, capitalize(serviceName));
2907
- await fs8.writeFile(serviceFile, indexTs);
3665
+ await fs10.writeFile(serviceFile, indexTs);
2908
3666
  logger.log(`\\nCreated new service: ${chalk.bold(serviceName)}`, chalk.green);
2909
3667
  logger.log(` File: mcp/${serviceName}/index.ts`, chalk.gray);
2910
3668
  logger.log(` Tool: greet`, chalk.gray);
@@ -2966,4 +3724,49 @@ projectsCmd.command("delete <projectId>").alias("rm").description("Delete a proj
2966
3724
  });
2967
3725
  projectsDeleteCommand(projectId, options);
2968
3726
  });
3727
+ var envCmd = program.command("env").description("Manage environment variables for deployed projects");
3728
+ envCmd.command("list [folder]").alias("ls").description("List all environment variables").option("--reveal", "Show actual values instead of masked").option("--project-id <id>", "Specify project ID").action((folder, options) => {
3729
+ trackCommand("env_list", {
3730
+ folder,
3731
+ ...options
3732
+ });
3733
+ envListCommand(folder || ".", options);
3734
+ });
3735
+ envCmd.command("set <keyValue> [folder]").description("Set an environment variable (KEY=VALUE)").option("-f, --file <file>", "Load from env file").option("--force", "Skip confirmation for reserved keys").action((keyValue, folder, options) => {
3736
+ trackCommand("env_set", {
3737
+ folder,
3738
+ ...options
3739
+ });
3740
+ envSetCommand(keyValue, folder || ".", options);
3741
+ });
3742
+ envCmd.command("get <key> [folder]").description("Get an environment variable value").option("--reveal", "Show actual value").action((key, folder, options) => {
3743
+ trackCommand("env_get", {
3744
+ key,
3745
+ folder,
3746
+ ...options
3747
+ });
3748
+ envGetCommand(key, folder || ".", options);
3749
+ });
3750
+ envCmd.command("remove <key> [folder]").alias("rm").description("Remove an environment variable").option("--force", "Skip confirmation").action((key, folder, options) => {
3751
+ trackCommand("env_remove", {
3752
+ key,
3753
+ folder,
3754
+ ...options
3755
+ });
3756
+ envRemoveCommand(key, folder || ".", options);
3757
+ });
3758
+ envCmd.command("pull [folder]").description("Download environment variables to local .env file").option("-f, --file <file>", "Output file", ".env.remote").action((folder, options) => {
3759
+ trackCommand("env_pull", {
3760
+ folder,
3761
+ ...options
3762
+ });
3763
+ envPullCommand(folder || ".", options);
3764
+ });
3765
+ envCmd.command("push [folder]").description("Upload environment variables from local .env file (replaces all)").option("-f, --file <file>", "Input file", ".env").option("--force", "Skip confirmation").action((folder, options) => {
3766
+ trackCommand("env_push", {
3767
+ folder,
3768
+ ...options
3769
+ });
3770
+ envPushCommand(folder || ".", options);
3771
+ });
2969
3772
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leanmcp/cli",
3
- "version": "0.4.5",
3
+ "version": "0.5.1",
4
4
  "description": "Command-line interface for scaffolding LeanMCP projects",
5
5
  "bin": {
6
6
  "leanmcp": "bin/leanmcp.js"
@@ -70,4 +70,4 @@
70
70
  "publishConfig": {
71
71
  "access": "public"
72
72
  }
73
- }
73
+ }