/** * This file contains utility functions for handling MCPB configuration, * including variable replacement and MCP server configuration generation. */ /** * Recursively replaces variables in any value. Handles strings, arrays, and objects. * * @param value The value to process * @param variables Object containing variable replacements * @returns The processed value with all variables replaced */ export function replaceVariables(value, variables) { if (typeof value === "string") { let result = value; // Replace all variables in the string for (const [key, replacement] of Object.entries(variables)) { const pattern = new RegExp(`\\$\\{${key}\\}`, "g"); // Check if this pattern actually exists in the string if (result.match(pattern)) { if (Array.isArray(replacement)) { console.warn(`Cannot replace ${key} with array value in string context: "${value}"`, { key, replacement }); } else { result = result.replace(pattern, replacement); } } } return result; } else if (Array.isArray(value)) { // For arrays, we need to handle special case of array expansion const result = []; for (const item of value) { if (typeof item === "string" && item.match(/^\$\{user_config\.[^}]+\}$/)) { // This is a user config variable that might expand to multiple values const varName = item.match(/^\$\{([^}]+)\}$/)?.[1]; if (varName && variables[varName]) { const replacement = variables[varName]; if (Array.isArray(replacement)) { // Expand array inline result.push(...replacement); } else { result.push(replacement); } } else { // Variable not found, keep original result.push(item); } } else { // Recursively process non-variable items result.push(replaceVariables(item, variables)); } } return result; } else if (value && typeof value === "object") { const result = {}; for (const [key, val] of Object.entries(value)) { result[key] = replaceVariables(val, variables); } return result; } return value; } export async function getMcpConfigForManifest(options) { const { manifest, extensionPath, systemDirs, userConfig, pathSeparator, logger, } = options; const baseConfig = manifest.server?.mcp_config; if (!baseConfig) { return undefined; } let result = { ...baseConfig, }; if (baseConfig.platform_overrides) { if (process.platform in baseConfig.platform_overrides) { const platformConfig = baseConfig.platform_overrides[process.platform]; result.command = platformConfig.command || result.command; result.args = platformConfig.args || result.args; result.env = platformConfig.env || result.env; } } // Check if required configuration is missing if (hasRequiredConfigMissing({ manifest, userConfig })) { logger?.warn(`Extension ${manifest.name} has missing required configuration, skipping MCP config`); return undefined; } const variables = { __dirname: extensionPath, pathSeparator, "/": pathSeparator, ...systemDirs, }; // Build merged configuration from defaults and user settings const mergedConfig = {}; // First, add defaults from manifest if (manifest.user_config) { for (const [key, configOption] of Object.entries(manifest.user_config)) { if (configOption.default !== undefined) { mergedConfig[key] = configOption.default; } } } // Then, override with user settings if (userConfig) { Object.assign(mergedConfig, userConfig); } // Add merged configuration variables for substitution for (const [key, value] of Object.entries(mergedConfig)) { // Convert user config to the format expected by variable substitution const userConfigKey = `user_config.${key}`; if (Array.isArray(value)) { // Keep arrays as arrays for proper expansion variables[userConfigKey] = value.map(String); } else if (typeof value === "boolean") { // Convert booleans to "true"/"false" strings as per spec variables[userConfigKey] = value ? "true" : "false"; } else { // Convert other types to strings variables[userConfigKey] = String(value); } } // Replace all variables in the config result = replaceVariables(result, variables); return result; } function isInvalidSingleValue(value) { return value === undefined || value === null || value === ""; } /** * Check if an extension has missing required configuration * @param manifest The extension manifest * @param userConfig The user configuration * @returns true if required configuration is missing */ export function hasRequiredConfigMissing({ manifest, userConfig, }) { if (!manifest.user_config) { return false; } const config = userConfig || {}; for (const [key, configOption] of Object.entries(manifest.user_config)) { if (configOption.required) { const value = config[key]; if (isInvalidSingleValue(value) || (Array.isArray(value) && (value.length === 0 || value.some(isInvalidSingleValue)))) { return true; } } } return false; }