# Instalador de koryfi-cpanel-mcp (Windows / PowerShell). UN comando: # irm https://mcplocal.koryfi.com/install.ps1 | iex # Descarga el paquete, VERIFICA su SHA-256, lo instala con npm y lo registra en tus # clientes de IA (Claude Code/Desktop, Antigravity, Codex). # El paquete lleva dentro sus dependencias: no se descarga nada mas de internet. # Variables opcionales: CPANEL_MCP_BASE_URL, CPANEL_MCP_PACKAGE, CPANEL_MCP_SHA256, # CPANEL_MCP_NO_REGISTER=1 (solo instalar), CPANEL_MCP_NO_CONNECT=1, # CPANEL_MCP_ASSUME_YES=1, CPANEL_MCP_CLIENTS=claude-code (solo en esas IA, separadas por comas) # (Este archivo se mantiene en ASCII: Windows PowerShell 5.1 lee mal los acentos sin BOM.) function Install-CpanelPersonalMcp { $ErrorActionPreference = 'Stop' # Se anade TLS 1.2 sin quitar lo que la sesion ya tuviera (p. ej. TLS 1.3). [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $baseUrl = if ($env:CPANEL_MCP_BASE_URL) { $env:CPANEL_MCP_BASE_URL } else { 'https://mcplocal.koryfi.com' } $pkg = if ($env:CPANEL_MCP_PACKAGE) { $env:CPANEL_MCP_PACKAGE } else { 'koryfi-cpanel-mcp-latest.tgz' } $expected = $env:CPANEL_MCP_SHA256 # Solo https (http se admite unicamente contra este mismo equipo, para pruebas). if ($baseUrl -notmatch '^(https://|http://(127\.0\.0\.1|localhost):)') { throw 'la direccion de descarga debe empezar por https://' } if ($pkg -match '[\\/]|\.\.') { throw 'nombre de paquete no valido' } function Test-NodeOk { if (-not (Get-Command node -ErrorAction SilentlyContinue)) { return $false } # npm.cmd y no npm: con la politica "Restricted", PowerShell prefiere npm.ps1 y falla. if (-not (Get-Command npm.cmd -ErrorAction SilentlyContinue)) { return $false } try { return ([version]((node --version).TrimStart('v')) -ge [version]'20.0.0') } catch { return $false } } function Confirm-Yes([string]$question) { if ($env:CPANEL_MCP_ASSUME_YES -eq '1') { return $true } # [char]0xED es la "i" con tilde ("si" acentuado), escrita asi para que el archivo siga siendo ASCII. try { return ((Read-Host "$question [s/N]") -match ('^(s|si|s' + [char]0xED + '|y|yes)$')) } catch { return $false } } if (-not (Test-NodeOk)) { Write-Host 'Node.js 20 o superior no esta instalado (o es demasiado antiguo).' if ((Get-Command winget -ErrorAction SilentlyContinue) -and (Confirm-Yes 'Instalarlo ahora con winget (Node.js LTS)? Windows puede pedirte permiso')) { winget install --id OpenJS.NodeJS.LTS -e --silent --accept-package-agreements --accept-source-agreements $env:Path = "$env:Path;" + [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User') } if (-not (Test-NodeOk)) { throw 'Instala Node.js: entra a https://nodejs.org, descarga el instalador para Windows (boton LTS), abrelo y sigue los pasos. Despues abre una PowerShell nueva y vuelve a ejecutar este comando.' } } $tmp = Join-Path ([IO.Path]::GetTempPath()) ("cpanel-mcp-" + [Guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $tmp | Out-Null try { $file = Join-Path $tmp $pkg Write-Host "Descargando $baseUrl/$pkg ..." Invoke-WebRequest -UseBasicParsing -Uri "$baseUrl/$pkg" -OutFile $file if (-not $expected) { $sumsFile = Join-Path $tmp 'SHA256SUMS.txt' Invoke-WebRequest -UseBasicParsing -Uri "$baseUrl/SHA256SUMS.txt" -OutFile $sumsFile foreach ($line in (Get-Content -Path $sumsFile)) { $parts = $line.Trim() -split '\s+' if ($parts.Count -ge 2 -and $parts[1] -eq $pkg) { $expected = $parts[0] } } } if (-not $expected) { throw "no hay hash publicado para $pkg" } $actual = (Get-FileHash -Algorithm SHA256 -Path $file).Hash.ToLower() if ($actual -ne $expected.ToLower()) { throw "el SHA-256 no coincide (esperado $expected, obtenido $actual). No se instala nada." } Write-Host "SHA-256 verificado: $actual" Write-Host 'Instalando (puede tardar un minuto) ...' # --ignore-scripts: ningun paquete ejecuta codigo durante la instalacion. & npm.cmd install -g --ignore-scripts $file if ($LASTEXITCODE -ne 0) { throw 'npm no pudo instalar el paquete. Copia este mensaje y envialo a soporte de Koryfi.' } $root = ((& npm.cmd root -g) | Select-Object -First 1).Trim() $entry = Join-Path $root 'koryfi-cpanel-mcp\dist\src\server.js' if (-not (Test-Path $entry)) { throw "la instalacion termino pero no encuentro $entry" } # npm crea tres lanzadores (sin extension, .cmd y .ps1). Con la politica de ejecucion por # defecto de Windows (Restricted), PowerShell elige el .ps1 y lo bloquea; sin el, usa el .cmd. $shim = Join-Path (Split-Path $root -Parent) 'koryfi-cpanel-mcp.ps1' if (Test-Path $shim) { Remove-Item -Force $shim -ErrorAction SilentlyContinue } Write-Host ("Instalado: " + (& node $entry --version)) Write-Host '' $interactive = [Environment]::UserInteractive -and -not [Console]::IsInputRedirected -and -not [Console]::IsOutputRedirected if ($env:CPANEL_MCP_NO_REGISTER -eq '1') { Write-Host 'Registro automatico omitido. Cuando quieras: koryfi-cpanel-mcp --install' } else { # Con mas de una IA instalada y una terminal delante, se pregunta en cuales registrar (con una sola, no). # Con ErrorActionPreference=Stop, cualquier aviso de node por stderr abortaria el instalador: aqui solo se lee. $eap = $ErrorActionPreference $ErrorActionPreference = 'Continue' $detected = @(& node $entry --detect 2>$null | Where-Object { $_ -match "`t" }) $ErrorActionPreference = $eap $target = '' # Si lo instala una IA, registra solo en ella (CPANEL_MCP_CLIENTS) y no en las demas sin preguntar. if ($env:CPANEL_MCP_CLIENTS) { # Se normaliza antes de validar: "Claude-Code" vale igual que "claude-code". $clients = $env:CPANEL_MCP_CLIENTS.ToLowerInvariant() if ($clients -cnotmatch '^[a-z,-]+$') { throw 'CPANEL_MCP_CLIENTS solo admite nombres como claude-code o claude-code,codex' } $target = $clients } elseif ($detected.Count -ge 2 -and $interactive -and $env:CPANEL_MCP_ASSUME_YES -ne '1') { Write-Host 'Se detectaron varias IA en este equipo:' foreach ($d in $detected) { Write-Host (' - ' + ($d -split "`t")[1]) } $ans = '' try { $ans = Read-Host 'Registrar Koryfi cPanel MCP en todas? [S/n]' } catch { $ans = '' } if ($ans -match '^(n|no)$') { $sel = @() foreach ($d in $detected) { $parts = $d -split "`t" if (Confirm-Yes ('Registrar en ' + $parts[1] + '?')) { $sel += $parts[0] } } if ($sel.Count -eq 0) { $target = $null; Write-Host 'No se registro en ninguna IA. Cuando quieras: koryfi-cpanel-mcp --install ' } else { $target = ($sel -join ',') } } } if ($null -ne $target) { if ($target -eq '') { & node $entry --install } else { & node $entry --install $target } if ($LASTEXITCODE -ne 0) { # Si se pidio una IA concreta (la que esta instalando), no registrarla es un fallo: no se dice "COMPLETADA". if ($env:CPANEL_MCP_CLIENTS) { throw ('el MCP quedo instalado, pero no se pudo registrar en ' + $env:CPANEL_MCP_CLIENTS + '. Mira el detalle arriba o usa: koryfi-cpanel-mcp --print-config ' + $env:CPANEL_MCP_CLIENTS) } Write-Host 'No se pudo registrar en algun cliente; mira el detalle arriba o usa: koryfi-cpanel-mcp --print-config ' } } } # Conectar la cuenta aqui mismo: se abre el area de clientes de Koryfi y el token llega directo, sin pasar por el chat. if ($env:CPANEL_MCP_NO_CONNECT -ne '1' -and $interactive) { $ans = '' try { $ans = Read-Host "`nConectamos ahora tu cuenta? Se abrira tu area de clientes de Koryfi para que elijas el hosting. Pulsa Enter para seguir (o escribe n para hacerlo despues)" } catch { $ans = 'n' } if ($ans -match '^(n|no)$') { Write-Host 'Cuando quieras: koryfi-cpanel-mcp connect' } else { & node $entry connect if ($LASTEXITCODE -ne 0) { Write-Host 'Puedes repetirlo con: koryfi-cpanel-mcp connect' } } } else { Write-Host '' Write-Host 'Ultimo paso: cierra tu IA por completo, vuelve a abrirla y pidele: conecta mi cuenta de Koryfi' Write-Host '(o, en esta ventana: koryfi-cpanel-mcp connect)' } Write-Host '' Write-Host ('INSTALACION COMPLETADA. Si "koryfi-cpanel-mcp" no se reconoce, usa: node "' + $entry + '"') } finally { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue } } $koryfiFailed = $false try { Install-CpanelPersonalMcp } catch { Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red; Write-Host 'INSTALACION FALLIDA.'; $koryfiFailed = $true } if ($koryfiFailed) { $global:LASTEXITCODE = 1 # Si PowerShell se lanzo solo para ejecutar esto (una IA, un script), se sale con codigo 1 para # que el fallo se note. En una ventana interactiva no se cierra la ventana del usuario. if (([Environment]::GetCommandLineArgs() -join ' ') -match '(?i)\s-(NonInteractive|Command|c|File|EncodedCommand)\b') { exit 1 } }