Files
ai-agent-deep-dive/extracted-source/node_modules/@anthropic-ai/mcpb/dist/cli/init.js

706 lines
25 KiB
JavaScript

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;
}
}