diff --git a/.gitignore b/.gitignore index a8f5e48..a54761e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ temp/ tmp/ *.tmp *.temp +extract_ref/ # Logs logs/ diff --git a/package-lock.json b/package-lock.json index b7b7971..21c5d67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.13", "license": "MIT", "dependencies": { + "@iarna/toml": "^2.2.5", + "@types/iarna__toml": "^2.0.5", "chalk": "^4.1.2", "cli-table3": "^0.6.5", "dotenv": "^16.4.5", @@ -50,6 +52,11 @@ "node": ">=12" } }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -99,6 +106,14 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/iarna__toml": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/iarna__toml/-/iarna__toml-2.0.5.tgz", + "integrity": "sha512-I55y+SxI0ayM4MBU6yfGJGmi4wRll6wtSeKiFYAZj+Z5Q1DVbMgBSVDYY+xQZbjIlLs/pN4fidnvR8faDrmxPg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/inquirer": { "version": "8.2.10", "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.10.tgz", @@ -121,8 +136,7 @@ "node_modules/@types/node": { "version": "14.18.63", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", - "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", - "dev": true + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" }, "node_modules/@types/through": { "version": "0.0.33", diff --git a/package.json b/package.json index abdeaa8..822d699 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "mcp-get": "dist/index.js" }, "dependencies": { + "@iarna/toml": "^2.2.5", + "@types/iarna__toml": "^2.0.5", "chalk": "^4.1.2", "cli-table3": "^0.6.5", "dotenv": "^16.4.5", diff --git a/packages/package-list.json b/packages/package-list.json index 1996078..5262e2f 100644 --- a/packages/package-list.json +++ b/packages/package-list.json @@ -5,7 +5,8 @@ "vendor": "Anthropic, PBC (https://anthropic.com)", "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/brave-search", "homepage": "https://modelcontextprotocol.io", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@modelcontextprotocol/server-everything", @@ -13,7 +14,8 @@ "vendor": "Anthropic, PBC (https://anthropic.com)", "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/everything", "homepage": "https://modelcontextprotocol.io", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@modelcontextprotocol/server-filesystem", @@ -21,7 +23,8 @@ "vendor": "Anthropic, PBC (https://anthropic.com)", "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem", "homepage": "https://modelcontextprotocol.io", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@modelcontextprotocol/server-gdrive", @@ -29,7 +32,8 @@ "vendor": "Anthropic, PBC (https://anthropic.com)", "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/gdrive", "homepage": "https://modelcontextprotocol.io", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@modelcontextprotocol/server-github", @@ -37,7 +41,8 @@ "vendor": "Anthropic, PBC (https://anthropic.com)", "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/github", "homepage": "https://modelcontextprotocol.io", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@modelcontextprotocol/server-gitlab", @@ -45,7 +50,8 @@ "vendor": "GitLab, PBC (https://gitlab.com)", "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/gitlab", "homepage": "https://modelcontextprotocol.io", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@modelcontextprotocol/server-google-maps", @@ -53,7 +59,8 @@ "vendor": "Anthropic, PBC (https://anthropic.com)", "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/google-maps", "homepage": "https://modelcontextprotocol.io", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@modelcontextprotocol/server-memory", @@ -61,7 +68,8 @@ "vendor": "Anthropic, PBC (https://anthropic.com)", "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/memory", "homepage": "https://modelcontextprotocol.io", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@modelcontextprotocol/server-postgres", @@ -69,7 +77,8 @@ "vendor": "Anthropic, PBC (https://anthropic.com)", "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/postgres", "homepage": "https://modelcontextprotocol.io", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@modelcontextprotocol/server-puppeteer", @@ -77,7 +86,8 @@ "vendor": "Anthropic, PBC (https://anthropic.com)", "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/puppeteer", "homepage": "https://modelcontextprotocol.io", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@modelcontextprotocol/server-slack", @@ -85,7 +95,8 @@ "vendor": "Anthropic, PBC (https://anthropic.com)", "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/slack", "homepage": "https://modelcontextprotocol.io", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@cloudflare/mcp-server-cloudflare", @@ -93,7 +104,8 @@ "vendor": "Cloudflare, Inc. (https://cloudflare.com)", "sourceUrl": "https://github.com/cloudflare/mcp-server-cloudflare", "homepage": "https://github.com/cloudflare/mcp-server-cloudflare", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@raygun.io/mcp-server-raygun", @@ -101,7 +113,8 @@ "vendor": "Raygun (https://raygun.com)", "sourceUrl": "https://github.com/MindscapeHQ/mcp-server-raygun", "homepage": "https://raygun.com", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@kimtaeyoon83/mcp-server-youtube-transcript", @@ -109,7 +122,8 @@ "vendor": "Freddie (https://github.com/kimtaeyoon83)", "sourceUrl": "https://github.com/kimtaeyoon83/mcp-server-youtube-transcript", "homepage": "https://github.com/kimtaeyoon83/mcp-server-youtube-transcript", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@kagi/mcp-server-kagi", @@ -117,7 +131,8 @@ "vendor": "ac3xx (https://github.com/ac3xx)", "sourceUrl": "https://github.com/ac3xx/mcp-servers-kagi", "homepage": "https://github.com/ac3xx/mcp-servers-kagi", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@exa/mcp-server", @@ -125,7 +140,8 @@ "vendor": "Exa Labs (https://exa.ai)", "sourceUrl": "https://github.com/exa-labs/exa-mcp-server", "homepage": "https://exa.ai", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@search1api/mcp-server", @@ -133,7 +149,8 @@ "vendor": "fatwang2 (https://github.com/fatwang2)", "sourceUrl": "https://github.com/fatwang2/search1api-mcp", "homepage": "https://github.com/fatwang2/search1api-mcp", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@calclavia/mcp-obsidian", @@ -141,7 +158,8 @@ "vendor": "Calclavia (https://github.com/calclavia)", "sourceUrl": "https://github.com/calclavia/mcp-obsidian", "homepage": "https://github.com/calclavia/mcp-obsidian", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@anaisbetts/mcp-youtube", @@ -149,7 +167,8 @@ "vendor": "Anaïs Betts (https://github.com/anaisbetts)", "sourceUrl": "https://github.com/anaisbetts/mcp-youtube", "homepage": "https://github.com/anaisbetts/mcp-youtube", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@modelcontextprotocol/server-everart", @@ -157,7 +176,8 @@ "vendor": "Anthropic, PBC (https://anthropic.com)", "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/everart", "homepage": "https://modelcontextprotocol.io", - "license": "MIT" + "license": "MIT", + "runtime": "node" }, { "name": "@modelcontextprotocol/server-sequential-thinking", @@ -165,6 +185,52 @@ "vendor": "Anthropic, PBC (https://anthropic.com)", "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/sequentialthinking", "homepage": "https://modelcontextprotocol.io", - "license": "MIT" + "license": "MIT", + "runtime": "node" + }, + { + "name": "mcp-server-fetch", + "description": "A Model Context Protocol server providing tools to fetch and convert web content for usage by LLMs", + "vendor": "Anthropic, PBC.", + "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/fetch", + "homepage": "https://github.com/modelcontextprotocol/servers", + "license": "MIT", + "runtime": "python" + }, + { + "name": "mcp-server-git", + "description": "A Model Context Protocol server providing tools to read, search, and manipulate Git repositories programmatically via LLMs", + "vendor": "Anthropic, PBC.", + "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/git", + "homepage": "https://github.com/modelcontextprotocol/servers", + "license": "MIT", + "runtime": "python" + }, + { + "name": "mcp-server-sentry", + "description": "MCP server for retrieving issues from sentry.io", + "vendor": "Unknown", + "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/sentry", + "homepage": "https://github.com/modelcontextprotocol/servers", + "license": "Unknown", + "runtime": "python" + }, + { + "name": "mcp-server-sqlite", + "description": "A simple SQLite MCP server", + "vendor": "Unknown", + "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/sqlite", + "homepage": "https://github.com/modelcontextprotocol/servers", + "license": "Unknown", + "runtime": "python" + }, + { + "name": "mcp-server-time", + "description": "A Model Context Protocol server providing tools for time queries and timezone conversions for LLMs", + "vendor": "Mariusz 'maledorak' Korzekwa", + "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/time", + "homepage": "https://github.com/modelcontextprotocol/servers", + "license": "MIT", + "runtime": "python" } ] \ No newline at end of file diff --git a/src/extractors/modelcontextprotocol-extractor.ts b/src/extractors/modelcontextprotocol-extractor.ts index ed33995..fa251ab 100644 --- a/src/extractors/modelcontextprotocol-extractor.ts +++ b/src/extractors/modelcontextprotocol-extractor.ts @@ -4,6 +4,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; +import * as TOML from '@iarna/toml'; const execAsync = promisify(exec); const __filename = fileURLToPath(import.meta.url); @@ -16,6 +17,104 @@ interface PackageInfo { sourceUrl: string; homepage: string; license: string; + runtime?: 'node' | 'python'; +} + +interface RepoConfig { + url: string; + branch: string; + packagePath: string; + runtime: 'node' | 'python' | 'mixed'; +} + +interface PyProjectToml { + project?: { + name?: string; + description?: string; + authors?: Array<{ name: string; email?: string }>; + maintainers?: Array<{ name: string; email?: string }>; + license?: { text: string } | string; + homepage?: string; + repository?: string; + }; +} + +const REPOS: RepoConfig[] = [ + { + url: 'https://github.com/modelcontextprotocol/servers.git', + branch: 'main', + packagePath: 'src', + runtime: 'mixed' + } +]; + +async function cloneRepo(config: RepoConfig, tempDir: string): Promise { + const repoDir = path.join(tempDir, path.basename(config.url, '.git')); + console.log(`Cloning ${config.url} into ${repoDir}...`); + + await execAsync( + `git clone --depth 1 --branch ${config.branch} ${config.url} ${repoDir}`, + { cwd: tempDir } + ); + + return; +} + +async function extractNodePackage(packageJsonPath: string, repoUrl: string, subPath: string): Promise { + try { + const packageData = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + const repoPath = `${repoUrl}/blob/main/${subPath}`; + + return { + name: packageData.name || '', + description: packageData.description || '', + vendor: packageData.author || '', + sourceUrl: repoPath, + homepage: packageData.homepage || '', + license: packageData.license || '', + runtime: 'node' + }; + } catch (error) { + console.error(`Error extracting Node package from ${packageJsonPath}:`, error); + return null; + } +} + +async function extractPythonPackage(pyprojectPath: string, repoUrl: string, subPath: string): Promise { + try { + const pyprojectContent = fs.readFileSync(pyprojectPath, 'utf8'); + const pyproject = TOML.parse(pyprojectContent) as PyProjectToml; + + if (!pyproject.project) { + console.error(`No [project] section found in ${pyprojectPath}`); + return null; + } + + const { project } = pyproject; + + // Get the first author/maintainer for vendor info + const vendor = project.authors?.[0]?.name || + project.maintainers?.[0]?.name || + 'Unknown'; + + // Handle license field which can be either a string or an object + const license = typeof project.license === 'string' + ? project.license + : project.license?.text || 'Unknown'; + + return { + name: project.name || '', + description: project.description || '', + vendor, + sourceUrl: `${repoUrl}/blob/main/${subPath}`, + homepage: project.homepage || project.repository || repoUrl, + license, + runtime: 'python' + }; + } catch (error) { + console.error(`Error extracting Python package from ${pyprojectPath}:`, error); + return null; + } } export async function extractPackageInfo(): Promise { @@ -23,6 +122,8 @@ export async function extractPackageInfo(): Promise { const outputPath = path.join(__dirname, '../../packages/package-list.json'); const commitMsgPath = path.join(__dirname, '../../temp/commit-msg.txt'); + console.log("Starting package extraction..."); + try { // Load existing packages let existingPackages: PackageInfo[] = []; @@ -39,34 +140,49 @@ export async function extractPackageInfo(): Promise { let commitMsg = "chore(packages): update MCP package list\n\nChanges:\n"; let changes: string[] = []; - // Clone the repository - console.log('Cloning repository...'); - await execAsync( - 'git clone https://github.com/modelcontextprotocol/servers.git temp', - { cwd: path.join(__dirname, '../..') } - ); - - // Read all directories in src - const srcPath = path.join(tempDir, 'src'); + // Process each repository const newPackages: PackageInfo[] = []; - - const dirs = fs.readdirSync(srcPath); - for (const dir of dirs) { - const packageJsonPath = path.join(srcPath, dir, 'package.json'); + for (const repo of REPOS) { + await cloneRepo(repo, tempDir); + + const repoDir = path.join(tempDir, path.basename(repo.url, '.git')); + const packagePath = path.join(repoDir, repo.packagePath); + + if (!fs.existsSync(packagePath)) continue; - if (fs.existsSync(packageJsonPath)) { - const packageData = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - const repoPath = `https://github.com/modelcontextprotocol/servers/blob/main/src/${dir}`; + const dirs = fs.readdirSync(packagePath); + + for (const dir of dirs) { + const fullPath = path.join(packagePath, dir); + if (!fs.statSync(fullPath).isDirectory()) continue; - newPackages.push({ - name: packageData.name || '', - description: packageData.description || '', - vendor: packageData.author || '', - sourceUrl: repoPath, - homepage: packageData.homepage || '', - license: packageData.license || '' - }); + // Try to extract as Node.js package + const packageJsonPath = path.join(fullPath, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + const nodePackage = await extractNodePackage( + packageJsonPath, + repo.url.replace('.git', ''), + path.join(repo.packagePath, dir) + ); + if (nodePackage) { + newPackages.push(nodePackage); + continue; + } + } + + // Try to extract as Python package + const pyprojectPath = path.join(fullPath, 'pyproject.toml'); + if (fs.existsSync(pyprojectPath)) { + const pythonPackage = await extractPythonPackage( + pyprojectPath, + repo.url.replace('.git', ''), + path.join(repo.packagePath, dir) + ); + if (pythonPackage) { + newPackages.push(pythonPackage); + } + } } } @@ -91,6 +207,7 @@ export async function extractPackageInfo(): Promise { if (oldPkg.license !== newPkg.license) changeDetails.push('license'); if (oldPkg.homepage !== newPkg.homepage) changeDetails.push('homepage'); if (oldPkg.sourceUrl !== newPkg.sourceUrl) changeDetails.push('sourceUrl'); + if (oldPkg.runtime !== newPkg.runtime) changeDetails.push('runtime'); changes.push(`- Updated ${newPkg.name} (changed: ${changeDetails.join(', ')})`); } @@ -98,8 +215,8 @@ export async function extractPackageInfo(): Promise { // Add new package mergedPackages.push(newPkg); hasChanges = true; - console.log(`Added new package: ${newPkg.name}`); - changes.push(`- Added new package: ${newPkg.name}`); + console.log(`Added new package: ${newPkg.name} (${newPkg.runtime})`); + changes.push(`- Added new package: ${newPkg.name} (${newPkg.runtime})`); } } diff --git a/src/installed.ts b/src/installed.ts index 30dc33e..8159382 100644 --- a/src/installed.ts +++ b/src/installed.ts @@ -3,7 +3,7 @@ import chalk from 'chalk'; import { readConfig } from './utils/config.js'; import { displayPackageDetailsWithActions } from './utils/display.js'; import { uninstallPackage } from './utils/package-management.js'; -import { Package } from './types'; +import { Package } from './types/index.js'; import { readFileSync } from 'fs'; import { join } from 'path'; import { fileURLToPath } from 'url'; diff --git a/src/list.ts b/src/list.ts index edfc9e4..bf45806 100644 --- a/src/list.ts +++ b/src/list.ts @@ -7,7 +7,7 @@ import { fileURLToPath } from 'url'; import { dirname } from 'path'; import { Package } from './types/index.js'; import { displayPackageDetailsWithActions } from './utils/display.js'; -import { installPackage, uninstallPackage } from './utils/package-management.js'; +import { installPackage, uninstallPackage, isPackageInstalled } from './utils/package-management.js'; import { createInterface } from 'readline'; import Table from 'cli-table3'; // Import cli-table3 import stringWidth from 'string-width'; // Import string-width @@ -24,7 +24,10 @@ export async function list() { let packages: Package[]; try { const data = fs.readFileSync(packageListPath, 'utf8'); - packages = JSON.parse(data); + packages = JSON.parse(data).map((pkg: Package) => ({ + ...pkg, + isInstalled: isPackageInstalled(pkg.name) + })); if (!Array.isArray(packages)) { throw new Error('Package list is not an array'); } @@ -38,7 +41,7 @@ export async function list() { // Prepare choices for inquirer using table-like format const choices = packages.map((pkg, index) => ({ - name: `${pkg.name.padEnd(24)} │ ${ + name: `${pkg.isInstalled ? '✓ ' : ' '}${pkg.name.padEnd(22)} │ ${ pkg.description.length > 47 ? `${pkg.description.slice(0, 44)}...` : pkg.description.padEnd(49) } │ ${pkg.vendor.padEnd(19)} │ ${pkg.license.padEnd(14)}`, value: pkg, diff --git a/src/types/index.ts b/src/types/index.ts index 8978aa4..4c308c9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,6 +5,8 @@ export interface Package { sourceUrl: string; homepage: string; license: string; + runtime: 'node' | 'python'; + isInstalled?: boolean; } export interface PackageHelper { @@ -12,9 +14,11 @@ export interface PackageHelper { [key: string]: { description: string; required: boolean; + argName?: string; } }; configureEnv?: (config: any) => Promise; + runtime?: 'node' | 'python'; } export interface PackageHelpers { diff --git a/src/uninstall.ts b/src/uninstall.ts index 503c617..7d3ab46 100644 --- a/src/uninstall.ts +++ b/src/uninstall.ts @@ -4,7 +4,7 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; -import { Package } from './types'; +import { Package } from './types/index.js'; import { uninstallPackage } from './utils/package-management.js'; import { displayPackageDetailsWithActions } from './utils/display.js'; import { list } from './list.js'; diff --git a/src/utils/config.ts b/src/utils/config.ts index cc5b4ea..78d6d65 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,11 +1,17 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); export interface MCPServerConfig { command: string; args: string[]; env?: Record; + runtime?: 'node' | 'python'; } export interface ClaudeConfig { @@ -13,6 +19,17 @@ export interface ClaudeConfig { [key: string]: any; } +function getPackageRuntime(packageName: string): 'node' | 'python' { + const packageListPath = path.join(dirname(__dirname), '../packages/package-list.json'); + if (!fs.existsSync(packageListPath)) { + return 'node'; // Default to node if package list doesn't exist + } + + const packageList = JSON.parse(fs.readFileSync(packageListPath, 'utf8')); + const pkg = packageList.find((p: any) => p.name === packageName); + return pkg?.runtime || 'node'; +} + export function getConfigPath(): string { if (process.platform === 'win32') { return path.join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json'); @@ -41,19 +58,30 @@ export function writeConfig(config: ClaudeConfig): void { fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); } -export function installMCPServer(packageName: string, envVars?: Record): void { +export function installMCPServer(packageName: string, envVars?: Record, runtime?: 'node' | 'python'): void { const config = readConfig(); const serverName = packageName.replace(/\//g, '-'); + const effectiveRuntime = runtime || getPackageRuntime(packageName); + if (!config.mcpServers) { config.mcpServers = {}; } - config.mcpServers[serverName] = { - command: 'npx', - args: ['-y', packageName], - env: envVars + const serverConfig: MCPServerConfig = { + runtime: effectiveRuntime, + env: envVars, + command: effectiveRuntime === 'python' ? 'uvx' : 'npx', + args: effectiveRuntime === 'python' ? [packageName] : ['-y', packageName] }; + config.mcpServers[serverName] = serverConfig; writeConfig(config); +} + +export function envVarsToArgs(envVars: Record): string[] { + return Object.entries(envVars).map(([key, value]) => { + const argName = key.toLowerCase().replace(/_/g, '-'); + return [`--${argName}`, value]; + }).flat(); } \ No newline at end of file diff --git a/src/utils/display.ts b/src/utils/display.ts index 3587707..5637232 100644 --- a/src/utils/display.ts +++ b/src/utils/display.ts @@ -1,74 +1,26 @@ import chalk from 'chalk'; -import stringWidth from 'string-width'; -import { Package } from '../types'; +import { Package } from '../types/index.js'; import inquirer from 'inquirer'; -import { readConfig } from './config.js'; -export function padString(str: string, width: number): string { - const length = stringWidth(str); - return str + ' '.repeat(Math.max(0, width - length)); -} +export async function displayPackageDetailsWithActions(pkg: Package): Promise<'install' | 'uninstall' | 'open' | 'back' | 'exit'> { + console.log('\n' + chalk.bold.cyan('Package Details:')); + console.log(chalk.bold('Name: ') + pkg.name); + console.log(chalk.bold('Description: ') + pkg.description); + console.log(chalk.bold('Vendor: ') + pkg.vendor); + console.log(chalk.bold('License: ') + pkg.license); + console.log(chalk.bold('Runtime: ') + (pkg.runtime || 'node')); + console.log(chalk.bold('Source: ') + (pkg.sourceUrl || 'Not available')); + console.log(chalk.bold('Homepage: ') + (pkg.homepage || 'Not available')); -export function displayPackageDetails(pkg: Package) { - const boxWidth = 100; - const padding = 2; - const labelWidth = 13; - const horizontalLine = '─'.repeat(boxWidth - 2); - - const contentWidth = boxWidth - 2; - - console.log(chalk.gray('┌' + horizontalLine + '┐')); - - // Title line with package name - const titleContent = ' '.repeat(padding) + pkg.name; - const titlePadded = padString(titleContent, contentWidth); - console.log(chalk.gray('│') + chalk.bold.green(titlePadded) + chalk.gray('│')); - - console.log(chalk.gray('├' + horizontalLine + '┤')); - - const lines = [ - ['Description', pkg.description], - ['Vendor', pkg.vendor], - ['License', pkg.license], - ['Homepage', pkg.homepage], - ['Source', pkg.sourceUrl], - ]; - - lines.forEach(([label, value]) => { - if (value) { // Only display if value exists - const labelStr = chalk.yellow(`${label}:`.padEnd(labelWidth)); - const lineStart = ' '.repeat(padding) + labelStr; - const content = lineStart + value; - const paddedContent = padString(content, contentWidth); - console.log(chalk.gray('│') + paddedContent + chalk.gray('│')); - } - }); - - console.log(chalk.gray('└' + horizontalLine + '┘')); - console.log(''); // Add spacing between packages -} - -export function formatPackageInfo(pkg: Package): string { - return `${pkg.name} - ${pkg.description} (${pkg.vendor})`; -} - -export async function displayPackageDetailsWithActions(pkg: Package) { - displayPackageDetails(pkg); - - // Check if package is installed - const config = readConfig(); - const serverName = pkg.name.replace(/\//g, '-'); - const isInstalled = config.mcpServers && serverName in config.mcpServers; - const choices = [ - ...(isInstalled ? [] : [{ name: 'Install package', value: 'install' }]), - ...(isInstalled ? [{ name: 'Uninstall package', value: 'uninstall' }] : []), - { name: 'Open source URL', value: 'open' }, - { name: 'Back to package list', value: 'back' }, - { name: 'Exit', value: 'exit' } + { name: pkg.isInstalled ? '🔄 Reinstall this package' : '📦 Install this package', value: 'install' }, + ...(pkg.isInstalled ? [{ name: '🗑️ Uninstall this package', value: 'uninstall' }] : []), + ...(pkg.sourceUrl ? [{ name: '🔗 Open source URL', value: 'open' }] : []), + { name: '⬅️ Back to list', value: 'back' }, + { name: '❌ Exit', value: 'exit' } ]; - const { action } = await inquirer.prompt<{ action: string }>([ + const { action } = await inquirer.prompt<{ action: 'install' | 'uninstall' | 'open' | 'back' | 'exit' }>([ { type: 'list', name: 'action', diff --git a/src/utils/package-management.ts b/src/utils/package-management.ts index 7282f03..e1e2704 100644 --- a/src/utils/package-management.ts +++ b/src/utils/package-management.ts @@ -1,9 +1,14 @@ import inquirer from 'inquirer'; -import { Package } from '../types'; +import { Package } from '../types/index.js'; import { getConfigPath, installMCPServer, readConfig, writeConfig } from './config.js'; import { exec } from 'child_process'; import { promisify } from 'util'; import { packageHelpers } from '../helpers/index.js'; +import { checkUVInstalled, promptForUVInstall } from './runtime-utils.js'; +import path from 'path'; +import fs from 'fs'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; const execAsync = promisify(exec); @@ -151,9 +156,20 @@ async function promptForRestart(): Promise { export async function installPackage(pkg: Package): Promise { try { + // Check for UV if it's a Python package + if (pkg.runtime === 'python') { + const hasUV = await checkUVInstalled(); + if (!hasUV) { + const installed = await promptForUVInstall(inquirer); + if (!installed) { + console.log('Proceeding with installation, but uvx commands may fail...'); + } + } + } + const envVars = await promptForEnvVars(pkg.name); - installMCPServer(pkg.name, envVars); + installMCPServer(pkg.name, envVars, pkg.runtime); console.log('Updated Claude desktop configuration'); await promptForRestart(); } catch (error) { @@ -181,4 +197,26 @@ export async function uninstallPackage(packageName: string): Promise { console.error('Failed to uninstall package:', error); throw error; } +} + +export function isPackageInstalled(packageName: string): boolean { + const config = readConfig(); + return packageName in (config.mcpServers || {}); +} + +export function getPackageDetails(packageName: string): Package { + // Read package list from JSON file + const packageListPath = path.join(dirname(fileURLToPath(import.meta.url)), '../../packages/package-list.json'); + const packages: Package[] = JSON.parse(fs.readFileSync(packageListPath, 'utf8')); + + // Find the package + const pkg = packages.find(p => p.name === packageName); + if (!pkg) { + throw new Error(`Package ${packageName} not found`); + } + + return { + ...pkg, + isInstalled: isPackageInstalled(packageName) + }; } diff --git a/src/utils/runtime-utils.ts b/src/utils/runtime-utils.ts new file mode 100644 index 0000000..fcc2ed2 --- /dev/null +++ b/src/utils/runtime-utils.ts @@ -0,0 +1,39 @@ +import { exec } from 'child_process'; +import { promisify } from 'util'; +import inquirer from 'inquirer'; +import chalk from 'chalk'; + +const execAsync = promisify(exec); + +export async function checkUVInstalled(): Promise { + try { + await execAsync('uvx --version'); + return true; + } catch (error) { + return false; + } +} + +export async function promptForUVInstall(inquirerInstance: typeof inquirer): Promise { + const { shouldInstall } = await inquirerInstance.prompt<{ shouldInstall: boolean }>([{ + type: 'confirm', + name: 'shouldInstall', + message: 'UV package manager is required for Python MCP servers. Would you like to install it?', + default: true + }]); + + if (!shouldInstall) { + console.warn(chalk.yellow('UV installation was declined. You can install it manually from https://astral.sh/uv')); + return false; + } + + console.log('Installing uv package manager...'); + try { + await execAsync('curl -LsSf https://astral.sh/uv/install.sh | sh'); + console.log(chalk.green('✓ UV installed successfully')); + return true; + } catch (error) { + console.warn(chalk.yellow('Failed to install UV. You can install it manually from https://astral.sh/uv')); + return false; + } +} \ No newline at end of file