import { confirm, input, select } from "@inquirer/prompts"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { basename, join, resolve } from "path"; import { CURRENT_MANIFEST_VERSION } from "../schemas.js"; export function readPackageJson(dirPath) { const packageJsonPath = join(dirPath, "package.json"); if (existsSync(packageJsonPath)) { try { return JSON.parse(readFileSync(packageJsonPath, "utf-8")); } catch (e) { // Ignore package.json parsing errors } } return {}; } export function getDefaultAuthorName(packageData) { if (typeof packageData.author === "string") { return packageData.author; } return packageData.author?.name || ""; } export function getDefaultAuthorEmail(packageData) { if (typeof packageData.author === "object") { return packageData.author?.email || ""; } return ""; } export function getDefaultAuthorUrl(packageData) { if (typeof packageData.author === "object") { return packageData.author?.url || ""; } return ""; } export function getDefaultRepositoryUrl(packageData) { if (typeof packageData.repository === "string") { return packageData.repository; } return packageData.repository?.url || ""; } export function getDefaultBasicInfo(packageData, resolvedPath) { const name = packageData.name || basename(resolvedPath); const authorName = getDefaultAuthorName(packageData) || "Unknown Author"; const displayName = name; const version = packageData.version || "1.0.0"; const description = packageData.description || "A MCPB bundle"; return { name, authorName, displayName, version, description }; } export function getDefaultAuthorInfo(packageData) { return { authorEmail: getDefaultAuthorEmail(packageData), authorUrl: getDefaultAuthorUrl(packageData), }; } export function getDefaultServerConfig(packageData) { const serverType = "node"; const entryPoint = getDefaultEntryPoint(serverType, packageData); const mcp_config = createMcpConfig(serverType, entryPoint); return { serverType, entryPoint, mcp_config }; } export function getDefaultOptionalFields(packageData) { return { keywords: "", license: packageData.license || "MIT", repository: undefined, }; } export function createMcpConfig(serverType, entryPoint) { switch (serverType) { case "node": return { command: "node", args: ["${__dirname}/" + entryPoint], env: {}, }; case "python": return { command: "python", args: ["${__dirname}/" + entryPoint], env: { PYTHONPATH: "${__dirname}/server/lib", }, }; case "binary": return { command: "${__dirname}/" + entryPoint, args: [], env: {}, }; } } export function getDefaultEntryPoint(serverType, packageData) { switch (serverType) { case "node": return packageData?.main || "server/index.js"; case "python": return "server/main.py"; case "binary": return "server/my-server"; } } export async function promptBasicInfo(packageData, resolvedPath) { const defaultName = packageData.name || basename(resolvedPath); const name = await input({ message: "Extension name:", default: defaultName, validate: (value) => value.trim().length > 0 || "Name is required", }); const authorName = await input({ message: "Author name:", default: getDefaultAuthorName(packageData), validate: (value) => value.trim().length > 0 || "Author name is required", }); const displayName = await input({ message: "Display name (optional):", default: name, }); const version = await input({ message: "Version:", default: packageData.version || "1.0.0", validate: (value) => { if (!value.trim()) return "Version is required"; if (!/^\d+\.\d+\.\d+/.test(value)) { return "Version must follow semantic versioning (e.g., 1.0.0)"; } return true; }, }); const description = await input({ message: "Description:", default: packageData.description || "", validate: (value) => value.trim().length > 0 || "Description is required", }); return { name, authorName, displayName, version, description }; } export async function promptAuthorInfo(packageData) { const authorEmail = await input({ message: "Author email (optional):", default: getDefaultAuthorEmail(packageData), }); const authorUrl = await input({ message: "Author URL (optional):", default: getDefaultAuthorUrl(packageData), }); return { authorEmail, authorUrl }; } export async function promptServerConfig(packageData) { const serverType = (await select({ message: "Server type:", choices: [ { name: "Node.js", value: "node" }, { name: "Python", value: "python" }, { name: "Binary", value: "binary" }, ], default: "node", })); const entryPoint = await input({ message: "Entry point:", default: getDefaultEntryPoint(serverType, packageData), }); const mcp_config = createMcpConfig(serverType, entryPoint); return { serverType, entryPoint, mcp_config }; } export async function promptTools() { const addTools = await confirm({ message: "Does your MCP Server provide tools you want to advertise (optional)?", default: true, }); const tools = []; let toolsGenerated = false; if (addTools) { let addMore = true; while (addMore) { const toolName = await input({ message: "Tool name:", validate: (value) => value.trim().length > 0 || "Tool name is required", }); const toolDescription = await input({ message: "Tool description (optional):", }); tools.push({ name: toolName, ...(toolDescription ? { description: toolDescription } : {}), }); addMore = await confirm({ message: "Add another tool?", default: false, }); } // Ask about generated tools toolsGenerated = await confirm({ message: "Does your server generate additional tools at runtime?", default: false, }); } return { tools, toolsGenerated }; } export async function promptPrompts() { const addPrompts = await confirm({ message: "Does your MCP Server provide prompts you want to advertise (optional)?", default: false, }); const prompts = []; let promptsGenerated = false; if (addPrompts) { let addMore = true; while (addMore) { const promptName = await input({ message: "Prompt name:", validate: (value) => value.trim().length > 0 || "Prompt name is required", }); const promptDescription = await input({ message: "Prompt description (optional):", }); // Ask about arguments const hasArguments = await confirm({ message: "Does this prompt have arguments?", default: false, }); const argumentNames = []; if (hasArguments) { let addMoreArgs = true; while (addMoreArgs) { const argName = await input({ message: "Argument name:", validate: (value) => { if (!value.trim()) return "Argument name is required"; if (argumentNames.includes(value)) { return "Argument names must be unique"; } return true; }, }); argumentNames.push(argName); addMoreArgs = await confirm({ message: "Add another argument?", default: false, }); } } // Prompt for the text template const promptText = await input({ message: hasArguments ? `Prompt text (use \${arguments.name} for arguments: ${argumentNames.join(", ")}):` : "Prompt text:", validate: (value) => value.trim().length > 0 || "Prompt text is required", }); prompts.push({ name: promptName, ...(promptDescription ? { description: promptDescription } : {}), ...(argumentNames.length > 0 ? { arguments: argumentNames } : {}), text: promptText, }); addMore = await confirm({ message: "Add another prompt?", default: false, }); } // Ask about generated prompts promptsGenerated = await confirm({ message: "Does your server generate additional prompts at runtime?", default: false, }); } return { prompts, promptsGenerated }; } export async function promptOptionalFields(packageData) { const keywords = await input({ message: "Keywords (comma-separated, optional):", default: "", }); const license = await input({ message: "License:", default: packageData.license || "MIT", }); const addRepository = await confirm({ message: "Add repository information?", default: !!packageData.repository, }); let repository; if (addRepository) { const repoUrl = await input({ message: "Repository URL:", default: getDefaultRepositoryUrl(packageData), }); if (repoUrl) { repository = { type: "git", url: repoUrl, }; } } return { keywords, license, repository }; } export async function promptLongDescription(description) { const hasLongDescription = await confirm({ message: "Add a detailed long description?", default: false, }); if (hasLongDescription) { const longDescription = await input({ message: "Long description (supports basic markdown):", default: description, }); return longDescription; } return undefined; } export async function promptUrls() { const homepage = await input({ message: "Homepage URL (optional):", validate: (value) => { if (!value.trim()) return true; try { new URL(value); return true; } catch { return "Must be a valid URL (e.g., https://example.com)"; } }, }); const documentation = await input({ message: "Documentation URL (optional):", validate: (value) => { if (!value.trim()) return true; try { new URL(value); return true; } catch { return "Must be a valid URL"; } }, }); const support = await input({ message: "Support URL (optional):", validate: (value) => { if (!value.trim()) return true; try { new URL(value); return true; } catch { return "Must be a valid URL"; } }, }); return { homepage, documentation, support }; } export async function promptVisualAssets() { const icon = await input({ message: "Icon file path (optional, relative to manifest):", validate: (value) => { if (!value.trim()) return true; if (value.includes("..")) return "Relative paths cannot include '..'"; return true; }, }); const addScreenshots = await confirm({ message: "Add screenshots?", default: false, }); const screenshots = []; if (addScreenshots) { let addMore = true; while (addMore) { const screenshot = await input({ message: "Screenshot file path (relative to manifest):", validate: (value) => { if (!value.trim()) return "Screenshot path is required"; if (value.includes("..")) return "Relative paths cannot include '..'"; return true; }, }); screenshots.push(screenshot); addMore = await confirm({ message: "Add another screenshot?", default: false, }); } } return { icon, screenshots }; } export async function promptCompatibility(serverType) { const addCompatibility = await confirm({ message: "Add compatibility constraints?", default: false, }); if (!addCompatibility) { return undefined; } const addPlatforms = await confirm({ message: "Specify supported platforms?", default: false, }); let platforms; if (addPlatforms) { const selectedPlatforms = []; const supportsDarwin = await confirm({ message: "Support macOS (darwin)?", default: true, }); if (supportsDarwin) selectedPlatforms.push("darwin"); const supportsWin32 = await confirm({ message: "Support Windows (win32)?", default: true, }); if (supportsWin32) selectedPlatforms.push("win32"); const supportsLinux = await confirm({ message: "Support Linux?", default: true, }); if (supportsLinux) selectedPlatforms.push("linux"); platforms = selectedPlatforms.length > 0 ? selectedPlatforms : undefined; } let runtimes; if (serverType !== "binary") { const addRuntimes = await confirm({ message: "Specify runtime version constraints?", default: false, }); if (addRuntimes) { if (serverType === "python") { const pythonVersion = await input({ message: "Python version constraint (e.g., >=3.8,<4.0):", validate: (value) => value.trim().length > 0 || "Python version constraint is required", }); runtimes = { python: pythonVersion }; } else if (serverType === "node") { const nodeVersion = await input({ message: "Node.js version constraint (e.g., >=16.0.0):", validate: (value) => value.trim().length > 0 || "Node.js version constraint is required", }); runtimes = { node: nodeVersion }; } } } return { ...(platforms ? { platforms } : {}), ...(runtimes ? { runtimes } : {}), }; } export async function promptUserConfig() { const addUserConfig = await confirm({ message: "Add user-configurable options?", default: false, }); if (!addUserConfig) { return {}; } const userConfig = {}; let addMore = true; while (addMore) { const optionKey = await input({ message: "Configuration option key (unique identifier):", validate: (value) => { if (!value.trim()) return "Key is required"; if (userConfig[value]) return "Key must be unique"; return true; }, }); const optionType = (await select({ message: "Option type:", choices: [ { name: "String", value: "string" }, { name: "Number", value: "number" }, { name: "Boolean", value: "boolean" }, { name: "Directory", value: "directory" }, { name: "File", value: "file" }, ], })); const optionTitle = await input({ message: "Option title (human-readable name):", validate: (value) => value.trim().length > 0 || "Title is required", }); const optionDescription = await input({ message: "Option description:", validate: (value) => value.trim().length > 0 || "Description is required", }); const optionRequired = await confirm({ message: "Is this option required?", default: false, }); const optionSensitive = await confirm({ message: "Is this option sensitive (like a password)?", default: false, }); // Build the option object const option = { type: optionType, title: optionTitle, description: optionDescription, required: optionRequired, sensitive: optionSensitive, }; // Add default value if not required if (!optionRequired) { let defaultValue; if (optionType === "boolean") { defaultValue = await confirm({ message: "Default value:", default: false, }); } else if (optionType === "number") { const defaultStr = await input({ message: "Default value (number):", validate: (value) => { if (!value.trim()) return true; return !isNaN(Number(value)) || "Must be a valid number"; }, }); defaultValue = defaultStr ? Number(defaultStr) : undefined; } else { defaultValue = await input({ message: "Default value (optional):", }); } if (defaultValue !== undefined && defaultValue !== "") { option.default = defaultValue; } } // Add constraints for number types if (optionType === "number") { const addConstraints = await confirm({ message: "Add min/max constraints?", default: false, }); if (addConstraints) { const min = await input({ message: "Minimum value (optional):", validate: (value) => { if (!value.trim()) return true; return !isNaN(Number(value)) || "Must be a valid number"; }, }); const max = await input({ message: "Maximum value (optional):", validate: (value) => { if (!value.trim()) return true; return !isNaN(Number(value)) || "Must be a valid number"; }, }); if (min) option.min = Number(min); if (max) option.max = Number(max); } } userConfig[optionKey] = option; addMore = await confirm({ message: "Add another configuration option?", default: false, }); } return userConfig; } export function buildManifest(basicInfo, longDescription, authorInfo, urls, visualAssets, serverConfig, tools, toolsGenerated, prompts, promptsGenerated, compatibility, userConfig, optionalFields) { const { name, displayName, version, description, authorName } = basicInfo; const { authorEmail, authorUrl } = authorInfo; const { serverType, entryPoint, mcp_config } = serverConfig; const { keywords, license, repository } = optionalFields; return { manifest_version: CURRENT_MANIFEST_VERSION, name, ...(displayName && displayName !== name ? { display_name: displayName } : {}), version, description, ...(longDescription ? { long_description: longDescription } : {}), author: { name: authorName, ...(authorEmail ? { email: authorEmail } : {}), ...(authorUrl ? { url: authorUrl } : {}), }, ...(urls.homepage ? { homepage: urls.homepage } : {}), ...(urls.documentation ? { documentation: urls.documentation } : {}), ...(urls.support ? { support: urls.support } : {}), ...(visualAssets.icon ? { icon: visualAssets.icon } : {}), ...(visualAssets.screenshots.length > 0 ? { screenshots: visualAssets.screenshots } : {}), server: { type: serverType, entry_point: entryPoint, mcp_config, }, ...(tools.length > 0 ? { tools } : {}), ...(toolsGenerated ? { tools_generated: true } : {}), ...(prompts.length > 0 ? { prompts } : {}), ...(promptsGenerated ? { prompts_generated: true } : {}), ...(compatibility ? { compatibility } : {}), ...(Object.keys(userConfig).length > 0 ? { user_config: userConfig } : {}), ...(keywords ? { keywords: keywords .split(",") .map((k) => k.trim()) .filter((k) => k), } : {}), ...(license ? { license } : {}), ...(repository ? { repository } : {}), }; } export function printNextSteps() { console.log("\nNext steps:"); console.log(`1. Ensure all your production dependencies are in this directory`); console.log(`2. Run 'mcpb pack' to create your .mcpb file`); } export async function initExtension(targetPath = process.cwd(), nonInteractive = false) { const resolvedPath = resolve(targetPath); const manifestPath = join(resolvedPath, "manifest.json"); if (existsSync(manifestPath)) { if (nonInteractive) { console.log("manifest.json already exists. Use --force to overwrite in non-interactive mode."); return false; } const overwrite = await confirm({ message: "manifest.json already exists. Overwrite?", default: false, }); if (!overwrite) { console.log("Cancelled"); return false; } } if (!nonInteractive) { console.log("This utility will help you create a manifest.json file for your MCPB bundle."); console.log("Press ^C at any time to quit.\n"); } else { console.log("Creating manifest.json with default values..."); } try { const packageData = readPackageJson(resolvedPath); // Prompt for all information or use defaults const basicInfo = nonInteractive ? getDefaultBasicInfo(packageData, resolvedPath) : await promptBasicInfo(packageData, resolvedPath); const longDescription = nonInteractive ? undefined : await promptLongDescription(basicInfo.description); const authorInfo = nonInteractive ? getDefaultAuthorInfo(packageData) : await promptAuthorInfo(packageData); const urls = nonInteractive ? { homepage: "", documentation: "", support: "" } : await promptUrls(); const visualAssets = nonInteractive ? { icon: "", screenshots: [] } : await promptVisualAssets(); const serverConfig = nonInteractive ? getDefaultServerConfig(packageData) : await promptServerConfig(packageData); const toolsData = nonInteractive ? { tools: [], toolsGenerated: false } : await promptTools(); const promptsData = nonInteractive ? { prompts: [], promptsGenerated: false } : await promptPrompts(); const compatibility = nonInteractive ? undefined : await promptCompatibility(serverConfig.serverType); const userConfig = nonInteractive ? {} : await promptUserConfig(); const optionalFields = nonInteractive ? getDefaultOptionalFields(packageData) : await promptOptionalFields(packageData); // Build manifest const manifest = buildManifest(basicInfo, longDescription, authorInfo, urls, visualAssets, serverConfig, toolsData.tools, toolsData.toolsGenerated, promptsData.prompts, promptsData.promptsGenerated, compatibility, userConfig, optionalFields); // Write manifest writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n"); console.log(`\nCreated manifest.json at ${manifestPath}`); printNextSteps(); return true; } catch (error) { if (error instanceof Error && error.message.includes("User force closed")) { console.log("\nCancelled"); return false; } throw error; } }