UNPKG

@aurracloud/mcp-cli

Version:

A command-line tool to install, manage, and setup MCP (Model Context Protocol) servers with Docker support

807 lines 31 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseGitHubIdentifier = parseGitHubIdentifier; exports.checkGitHubRepoExists = checkGitHubRepoExists; exports.cloneRepository = cloneRepository; exports.cloneRepositoryQuiet = cloneRepositoryQuiet; exports.readPackageJson = readPackageJson; exports.checkRepoExecutability = checkRepoExecutability; exports.checkPythonRepoExecutability = checkPythonRepoExecutability; exports.cleanupClonedRepo = cleanupClonedRepo; exports.findMcpServersInRepo = findMcpServersInRepo; exports.readPackageJsonFromSubdir = readPackageJsonFromSubdir; exports.readPythonPackageFromSubdir = readPythonPackageFromSubdir; exports.checkGitRepoExistence = checkGitRepoExistence; exports.isGitRepoNpxExecutable = isGitRepoNpxExecutable; exports.isGitRepoUvxExecutable = isGitRepoUvxExecutable; exports.getGitNpxCommand = getGitNpxCommand; exports.getGitUvxCommand = getGitUvxCommand; exports.checkGitRepoExistenceQuiet = checkGitRepoExistenceQuiet; const node_fetch_1 = __importDefault(require("node-fetch")); const child_process_1 = require("child_process"); const fs_1 = require("fs"); const path_1 = require("path"); const os_1 = require("os"); const logger_1 = require("./utils/logger"); const utils_1 = require("./lib/utils"); const python_1 = require("./python"); /** * Parses a GitHub URL or repository identifier to extract owner, repo name, and optional path * * @param repoIdentifier - GitHub URL or owner/repo format, optionally with tree/branch/path * @returns Object with owner, repo name, and optional path, or null if invalid */ function parseGitHubIdentifier(repoIdentifier) { try { // Remove trailing .git if present let cleanIdentifier = repoIdentifier.replace(/\.git$/, ''); // Handle npm scoped package format (@owner/repo) by removing the @ if (cleanIdentifier.startsWith('@')) { cleanIdentifier = cleanIdentifier.substring(1); } // Handle SSH format (git@github.com:owner/repo) if (cleanIdentifier.includes('git@github.com:')) { const match = cleanIdentifier.match(/git@github\.com:([^\/]+)\/([^\/\s]+)/); if (match) { return { owner: match[1], repo: match[2] }; } } // Handle full GitHub URLs with potential tree/branch/path if (cleanIdentifier.includes('github.com')) { // Match patterns like: // - https://github.com/owner/repo // - https://github.com/owner/repo/tree/main/src/path // - https://github.com/owner/repo/blob/main/src/path // - https://github.com/owner/repo/tree/branch-name/src/path const match = cleanIdentifier.match(/github\.com[\/:]([^\/]+)\/([^\/\s]+)(?:\/(tree|blob)\/([^\/]+)\/(.+))?/); if (match) { const [, owner, repo, , branch, path] = match; return { owner, repo, branch: branch || undefined, path: path || undefined }; } } // Handle owner/repo format if (cleanIdentifier.includes('/') && !cleanIdentifier.includes(' ')) { const parts = cleanIdentifier.split('/'); if (parts.length === 2 && parts[0] && parts[1]) { return { owner: parts[0], repo: parts[1] }; } } return null; } catch (error) { return null; } } /** * Checks if a GitHub repository exists * * @param owner - Repository owner * @param repo - Repository name * @returns Promise resolving to repository information or null if not found */ async function checkGitHubRepoExists(owner, repo) { try { const response = await (0, node_fetch_1.default)(`https://api.github.com/repos/${owner}/${repo}`); if (!response.ok) { if (response.status === 404) { return null; } const errorText = await response.text(); throw new Error(`Failed to check repository existence: ${response.status} ${response.statusText} - ${errorText}`); } const data = await response.json(); return { name: data.name, fullName: data.full_name, description: data.description, htmlUrl: data.html_url, cloneUrl: data.clone_url, defaultBranch: data.default_branch, language: data.language, topics: data.topics, license: data.license ? { name: data.license.name, spdxId: data.license.spdx_id } : undefined, owner: { login: data.owner.login, type: data.owner.type } }; } catch (error) { logger_1.logger.error(`Error checking GitHub repository existence for ${owner}/${repo}`, error); throw error; } } /** * Clones a GitHub repository to a temporary directory * * @param cloneUrl - The clone URL of the repository * @param repoName - Name of the repository for the directory * @returns Promise resolving to the path where the repository was cloned */ async function cloneRepository(cloneUrl, repoName) { const tempDir = (0, os_1.tmpdir)(); const clonePath = (0, path_1.join)(tempDir, `mcp-cli-${repoName}-${Date.now()}`); try { logger_1.logger.info(`Cloning repository to ${clonePath}...`); // Use git clone with depth 1 for faster cloning (0, child_process_1.execSync)(`git clone --depth 1 "${cloneUrl}" "${clonePath}"`, { stdio: 'pipe', timeout: 30000 // 30 second timeout }); logger_1.logger.success(`Repository cloned successfully to ${clonePath}`); return clonePath; } catch (error) { logger_1.logger.error(`Failed to clone repository from ${cloneUrl}`, error); // Clean up on failure try { if ((0, fs_1.existsSync)(clonePath)) { (0, fs_1.rmSync)(clonePath, { recursive: true, force: true }); } } catch (cleanupError) { logger_1.logger.warn(`Failed to clean up failed clone at ${clonePath}`); } throw error; } } /** * Clones a GitHub repository to a temporary directory (with optional JSON mode) * * @param cloneUrl - The clone URL of the repository * @param repoName - Name of the repository for the directory * @param jsonMode - Whether to suppress logging output (for JSON mode) - deprecated, use setLoggerMode instead * @returns Promise resolving to the path where the repository was cloned */ async function cloneRepositoryQuiet(cloneUrl, repoName, jsonMode = false) { const tempDir = (0, os_1.tmpdir)(); const clonePath = (0, path_1.join)(tempDir, `mcp-cli-${repoName}-${Date.now()}`); try { logger_1.logger.info(`Cloning repository to ${clonePath}...`); // Use git clone with depth 1 for faster cloning (0, child_process_1.execSync)(`git clone --depth 1 "${cloneUrl}" "${clonePath}"`, { stdio: 'pipe', timeout: 30000 // 30 second timeout }); logger_1.logger.success(`Repository cloned successfully to ${clonePath}`); return clonePath; } catch (error) { logger_1.logger.error(`Failed to clone repository from ${cloneUrl}`, error); // Clean up on failure try { if ((0, fs_1.existsSync)(clonePath)) { (0, fs_1.rmSync)(clonePath, { recursive: true, force: true }); } } catch (cleanupError) { logger_1.logger.warn(`Failed to clean up failed clone at ${clonePath}`); } throw error; } } /** * Reads and parses package.json from a cloned repository * * @param clonePath - Path to the cloned repository * @returns Package information or null if package.json doesn't exist or is invalid */ function readPackageJson(clonePath) { const packageJsonPath = (0, path_1.join)(clonePath, 'package.json'); if (!(0, fs_1.existsSync)(packageJsonPath)) { return null; } try { const packageJsonContent = (0, fs_1.readFileSync)(packageJsonPath, 'utf-8'); const packageInfo = JSON.parse(packageJsonContent); return packageInfo; } catch (error) { logger_1.logger.error(`Failed to parse package.json at ${packageJsonPath}`, error); return null; } } /** * Checks if a repository package is executable (has bin scripts or is a CLI tool) * * @param packageInfo - Package information from package.json * @returns Object with executability information */ function checkRepoExecutability(packageInfo) { // Check if package has bin scripts if (packageInfo.bin) { if (typeof packageInfo.bin === 'string') { // Single binary return { isExecutable: true, executableName: packageInfo.name, executablePath: packageInfo.bin }; } else { // Multiple binaries - use the first one const binEntries = Object.entries(packageInfo.bin); if (binEntries.length > 0) { const [binName, binPath] = binEntries[0]; return { isExecutable: true, executableName: binName, executablePath: binPath }; } } } // Check if it's likely a CLI tool based on name patterns const cliPatterns = ['-cli', 'cli-', '-tool', 'tool-']; const nameIndicatesCli = cliPatterns.some(pattern => packageInfo.name.toLowerCase().includes(pattern)); if (nameIndicatesCli) { return { isExecutable: true, executableName: packageInfo.name, executablePath: packageInfo.name }; } return { isExecutable: false }; } /** * Checks if a Python package is executable (has console scripts or entry points) * * @param pythonPackageInfo - Python package information from pyproject.toml * @returns Object with executability information */ function checkPythonRepoExecutability(pythonPackageInfo) { // Check for console scripts in entry-points if (pythonPackageInfo['entry-points']?.console_scripts) { const scripts = Object.entries(pythonPackageInfo['entry-points'].console_scripts); if (scripts.length > 0) { const [scriptName, scriptPath] = scripts[0]; return { isExecutable: true, executableName: scriptName, executablePath: scriptPath }; } } // Check for scripts in project.scripts if (pythonPackageInfo.scripts) { const scripts = Object.entries(pythonPackageInfo.scripts); if (scripts.length > 0) { const [scriptName, scriptPath] = scripts[0]; return { isExecutable: true, executableName: scriptName, executablePath: scriptPath }; } } // Check if package name suggests it's a tool (common patterns) const toolPatterns = ['-cli', 'cli-', 'tool', 'cmd', 'command']; const nameIndicatesTool = toolPatterns.some(pattern => pythonPackageInfo.name.toLowerCase().includes(pattern)); if (nameIndicatesTool) { return { isExecutable: true, executableName: pythonPackageInfo.name, executablePath: pythonPackageInfo.name }; } return { isExecutable: false }; } /** * Cleans up a cloned repository directory * * @param clonePath - Path to the cloned repository to clean up * @param jsonMode - Whether to suppress logging output (for JSON mode) - deprecated, use setLoggerMode instead */ function cleanupClonedRepo(clonePath, jsonMode = false) { try { if ((0, fs_1.existsSync)(clonePath)) { (0, fs_1.rmSync)(clonePath, { recursive: true, force: true }); logger_1.logger.info(`Cleaned up cloned repository at ${clonePath}`); } } catch (error) { logger_1.logger.warn(`Failed to clean up cloned repository at ${clonePath}: ${error.message || error}`); } } /** * Finds all MCP servers in a cloned repository * * @param clonePath - Path to the cloned repository * @returns Array of MCP server information */ async function findMcpServersInRepo(clonePath) { const mcpServers = []; try { // Find all package.json files with MCP configurations const mcpConfigPaths = await (0, utils_1.findMcpConfigs)(clonePath, true); for (const configPath of mcpConfigPaths) { try { const packageJsonContent = (0, fs_1.readFileSync)(configPath, 'utf-8'); const packageInfo = JSON.parse(packageJsonContent); if (packageInfo.mcpServer) { // Calculate relative path from repository root const relativePath = configPath.replace(clonePath, '').replace(/^\//, '').replace(/\/package\.json$/, '') || '.'; // Check executability const { isExecutable, executableName, executablePath } = checkRepoExecutability(packageInfo); const serverInfo = { name: packageInfo.name, path: relativePath, packageInfo, runtime: 'node', isExecutable, executableName, executablePath }; mcpServers.push(serverInfo); } } catch (error) { logger_1.logger.warn(`Failed to parse package.json at ${configPath}: ${error}`); } } } catch (error) { logger_1.logger.warn(`Failed to find MCP configurations in ${clonePath}: ${error}`); } return mcpServers; } /** * Reads package.json from a specific subdirectory in a cloned repository * * @param clonePath - Path to the cloned repository * @param subPath - Subdirectory path within the repository * @returns Package information or null if not found */ function readPackageJsonFromSubdir(clonePath, subPath) { const packageJsonPath = (0, path_1.join)(clonePath, subPath, 'package.json'); if (!(0, fs_1.existsSync)(packageJsonPath)) { return null; } try { const packageJsonContent = (0, fs_1.readFileSync)(packageJsonPath, 'utf-8'); const packageInfo = JSON.parse(packageJsonContent); return packageInfo; } catch (error) { logger_1.logger.error(`Failed to parse package.json at ${packageJsonPath}`, error); return null; } } /** * Reads Python package information from a specific subdirectory in a cloned repository * * @param clonePath - Path to the cloned repository * @param subPath - Subdirectory path within the repository * @returns Python package information or null if not found */ function readPythonPackageFromSubdir(clonePath, subPath) { const pyprojectPath = (0, path_1.join)(clonePath, subPath, 'pyproject.toml'); if (!(0, fs_1.existsSync)(pyprojectPath)) { return null; } try { const pyprojectData = (0, python_1.readPyProjectToml)(pyprojectPath); if (!pyprojectData?.project) { return null; } const project = pyprojectData.project; return { name: project.name || '', version: project.version || '', description: project.description, authors: project.authors, license: project.license, readme: project.readme, requires_python: project.requires_python, classifiers: project.classifiers, keywords: project.keywords, dependencies: project.dependencies, urls: project.urls, scripts: project.scripts, 'gui-scripts': project['gui-scripts'], 'entry-points': project['entry-points'], optional_dependencies: project.optional_dependencies }; } catch (error) { logger_1.logger.error(`Failed to parse pyproject.toml at ${pyprojectPath}`, error); return null; } } /** * Checks if a GitHub repository exists and whether it's executable via npx * * @param repoIdentifier - GitHub URL or owner/repo format * @returns Promise resolving to repository existence and executability information */ async function checkGitRepoExistence(repoIdentifier) { try { // Parse the repository identifier const parsed = parseGitHubIdentifier(repoIdentifier); if (!parsed) { return { exists: false, isExecutable: false }; } const { owner, repo, path, branch } = parsed; // Check if repository exists on GitHub const repoInfo = await checkGitHubRepoExists(owner, repo); if (!repoInfo) { return { exists: false, isExecutable: false }; } // Clone the repository let clonePath; try { clonePath = await cloneRepository(repoInfo.cloneUrl, repoInfo.name); } catch (error) { return { exists: true, isExecutable: false, repoInfo }; } // If a specific path was provided, check that subdirectory if (path) { const packageInfo = readPackageJsonFromSubdir(clonePath, path); const pythonPackageInfo = readPythonPackageFromSubdir(clonePath, path); // Check if we found either a Node.js or Python package if (!packageInfo && !pythonPackageInfo) { cleanupClonedRepo(clonePath); return { exists: true, isExecutable: false, repoInfo, clonePath, targetPath: path }; } let selectedServer; if (packageInfo) { // Node.js package found const { isExecutable, executableName, executablePath } = checkRepoExecutability(packageInfo); selectedServer = { name: packageInfo.name, path: path, packageInfo, runtime: 'node', isExecutable, executableName, executablePath }; } else if (pythonPackageInfo) { // Python package found const { isExecutable, executableName, executablePath } = checkPythonRepoExecutability(pythonPackageInfo); selectedServer = { name: pythonPackageInfo.name, path: path, pythonPackageInfo, runtime: 'python', isExecutable, executableName, executablePath }; } else { // This shouldn't happen due to the check above, but for type safety cleanupClonedRepo(clonePath); return { exists: true, isExecutable: false, repoInfo, clonePath, targetPath: path }; } return { exists: true, isExecutable: selectedServer.isExecutable, repoInfo, packageInfo: selectedServer.packageInfo, // For backward compatibility executableName: selectedServer.executableName, executablePath: selectedServer.executablePath, clonePath, targetPath: path, selectedServer }; } // No specific path provided - check root and find all MCP servers const rootPackageInfo = readPackageJson(clonePath); const mcpServers = await findMcpServersInRepo(clonePath); // For backward compatibility, use root package.json if it exists let isExecutable = false; let executableName; let executablePath; if (rootPackageInfo) { const executability = checkRepoExecutability(rootPackageInfo); isExecutable = executability.isExecutable; executableName = executability.executableName; executablePath = executability.executablePath; } return { exists: true, isExecutable, repoInfo, packageInfo: rootPackageInfo || undefined, executableName, executablePath, clonePath, mcpServers }; } catch (error) { logger_1.logger.error(`Error checking git repository existence for ${repoIdentifier}`, error); throw error; } } /** * Verifies if a GitHub repository can be installed and executed via npx * * @param repoIdentifier - GitHub URL or owner/repo format * @returns Promise resolving to a boolean indicating if the repository is npx-executable */ async function isGitRepoNpxExecutable(repoIdentifier) { try { const result = await checkGitRepoExistence(repoIdentifier); if (!result.exists) { logger_1.logger.warn(`Repository ${repoIdentifier} does not exist on GitHub`); return false; } if (!result.packageInfo) { logger_1.logger.warn(`Repository ${repoIdentifier} exists but does not have a package.json`); if (result.clonePath) { cleanupClonedRepo(result.clonePath); } return false; } if (!result.isExecutable) { logger_1.logger.warn(`Repository ${repoIdentifier} has package.json but is not executable`); if (result.clonePath) { cleanupClonedRepo(result.clonePath); } return false; } return true; } catch (error) { logger_1.logger.error(`Error checking if ${repoIdentifier} is npx-executable`, error); return false; } } /** * Verifies if a GitHub repository can be installed and executed via uvx (Python) * * @param repoIdentifier - GitHub URL or owner/repo format * @returns Promise resolving to a boolean indicating if the repository is uvx-executable */ async function isGitRepoUvxExecutable(repoIdentifier) { try { const result = await checkGitRepoExistence(repoIdentifier); if (!result.exists) { logger_1.logger.warn(`Repository ${repoIdentifier} does not exist on GitHub`); return false; } // Check if it's a Python package (either selected server or has Python package files) if (result.selectedServer?.runtime === 'python' && result.selectedServer.isExecutable) { return true; } // For repositories without specific path, check if it has Python package files if (!result.selectedServer && result.clonePath) { const hasPythonFiles = (0, python_1.hasPythonPackageFiles)(result.clonePath); if (result.clonePath) { cleanupClonedRepo(result.clonePath); } return hasPythonFiles; } if (result.clonePath) { cleanupClonedRepo(result.clonePath); } return false; } catch (error) { logger_1.logger.error(`Error checking if ${repoIdentifier} is uvx-executable`, error); return false; } } /** * Gets the recommended npx command for a GitHub repository * * @param repoIdentifier - GitHub URL or owner/repo format * @returns Promise resolving to the npx command structure or null if not executable */ async function getGitNpxCommand(repoIdentifier) { try { const result = await checkGitRepoExistence(repoIdentifier); if (!result.exists || !result.isExecutable || !result.packageInfo) { return null; } // Use the executable name if available, otherwise use package name const executableName = result.executableName || result.packageInfo.name; // Clean up the cloned repository since we only needed to check if (result.clonePath) { cleanupClonedRepo(result.clonePath); } return { command: 'npx', args: ['-y', repoIdentifier] }; } catch (error) { logger_1.logger.error(`Error getting npx command for ${repoIdentifier}`, error); return null; } } /** * Gets the recommended uvx command for a GitHub repository (Python) * * @param repoIdentifier - GitHub URL or owner/repo format * @returns Promise resolving to the uvx command structure or null if not executable */ async function getGitUvxCommand(repoIdentifier) { try { const result = await checkGitRepoExistence(repoIdentifier); if (!result.exists || !result.selectedServer || result.selectedServer.runtime !== 'python' || !result.selectedServer.isExecutable) { return null; } // Use the executable name if available, otherwise use package name const executableName = result.selectedServer.executableName || result.selectedServer.pythonPackageInfo?.name; // Clean up the cloned repository since we only needed to check if (result.clonePath) { cleanupClonedRepo(result.clonePath); } if (!executableName) { return null; } return { command: 'uvx', args: [executableName] }; } catch (error) { logger_1.logger.error(`Error getting uvx command for ${repoIdentifier}`, error); return null; } } /** * Checks if a GitHub repository exists and can be cloned (quiet version for JSON mode) * * @param repoIdentifier - GitHub URL, owner/repo, or @owner/repo format * @param jsonMode - Whether to suppress logging output * @returns Promise resolving to repository existence information */ async function checkGitRepoExistenceQuiet(repoIdentifier, jsonMode = false) { try { const parsedIdentifier = parseGitHubIdentifier(repoIdentifier); if (!parsedIdentifier) { return { exists: false, isExecutable: false }; } const { owner, repo, path: targetPath } = parsedIdentifier; // Check if repository exists on GitHub const repoInfo = await checkGitHubRepoExists(owner, repo); if (!repoInfo) { return { exists: false, isExecutable: false }; } // Clone the repository to check its contents let clonePath; try { clonePath = await cloneRepositoryQuiet(repoInfo.cloneUrl, repoInfo.name, jsonMode); } catch (error) { return { exists: true, repoInfo, isExecutable: false }; } // If a specific path was provided, check that path if (targetPath && targetPath !== '.') { const serverPath = (0, path_1.join)(clonePath, targetPath); if (!(0, fs_1.existsSync)(serverPath)) { cleanupClonedRepo(clonePath); return { exists: true, repoInfo, isExecutable: false, targetPath }; } // Check if it's a Node.js package const packageInfo = readPackageJsonFromSubdir(clonePath, targetPath); if (packageInfo) { const execInfo = checkRepoExecutability(packageInfo); return { exists: true, repoInfo, clonePath, targetPath, isExecutable: execInfo.isExecutable, executableName: execInfo.executableName, executablePath: execInfo.executablePath, selectedServer: { name: packageInfo.name, path: targetPath, packageInfo, isExecutable: execInfo.isExecutable, executableName: execInfo.executableName, executablePath: execInfo.executablePath, runtime: 'node' } }; } // Check if it's a Python package const pythonPackageInfo = readPythonPackageFromSubdir(clonePath, targetPath); if (pythonPackageInfo) { const execInfo = checkPythonRepoExecutability(pythonPackageInfo); return { exists: true, repoInfo, clonePath, targetPath, isExecutable: execInfo.isExecutable, executableName: execInfo.executableName, selectedServer: { name: pythonPackageInfo.name, path: targetPath, pythonPackageInfo, isExecutable: execInfo.isExecutable, executableName: execInfo.executableName, runtime: 'python' } }; } // Path exists but no package files found cleanupClonedRepo(clonePath); return { exists: true, repoInfo, isExecutable: false, targetPath }; } // No specific path provided, check the repository structure const packageInfo = readPackageJson(clonePath); // Find all MCP servers in the repository const mcpServers = await findMcpServersInRepo(clonePath); // Check if it's executable (has bin field) const execInfo = packageInfo ? checkRepoExecutability(packageInfo) : { isExecutable: false }; return { exists: true, repoInfo, packageInfo: packageInfo || undefined, clonePath, isExecutable: execInfo.isExecutable, executableName: execInfo.executableName, executablePath: execInfo.executablePath, mcpServers: mcpServers.length > 0 ? mcpServers : undefined }; } catch (error) { return { exists: false, isExecutable: false }; } } //# sourceMappingURL=git.js.map