# Barvinca universal hosted MCP installer [CmdletBinding()] param( [Parameter(Mandatory = $true)][ValidateSet('codex','claude','cursor','all')][string]$Client, [switch]$Check, [switch]$Update, [switch]$Uninstall, [switch]$NonInteractive, [ValidateSet('read','domains','admin')][string]$Profile = 'read' ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $ServerName = 'barvinca' $ServerUrl = 'https://mcp.barvinca.com/mcp' $ReadScopes = 'barvinca.read' $DomainScopes = 'barvinca.read,barvinca.issues.read,barvinca.rfis.read,barvinca.assets.read,barvinca.submittals.read,barvinca.forms.read' $AdminScopes = 'barvinca.read,barvinca.issues.read,barvinca.rfis.read,barvinca.assets.read,barvinca.submittals.read,barvinca.forms.read,barvinca.write' $SelectedModes = @($Check, $Update, $Uninstall).Where({ $_ }).Count if ($SelectedModes -gt 1) { Write-Error 'Choose only one of -Check, -Update, or -Uninstall.'; exit 2 } $Mode = if ($Check) { 'check' } elseif ($Update) { 'update' } elseif ($Uninstall) { 'uninstall' } else { 'install' } if ($Profile -ne 'read' -and $Client -ne 'codex' -and $Client -ne 'all') { Write-Error '--profile domains|admin is supported only with --client codex or all.'; exit 2 } $Scopes = if ($Profile -eq 'domains') { $DomainScopes } elseif ($Profile -eq 'admin') { $AdminScopes } else { $ReadScopes } function Fail([int]$Code, [string]$Message) { Write-Error $Message; exit $Code } function Find-CommandPath([string[]]$Names) { foreach ($Name in $Names) { $Found = Get-Command $Name -ErrorAction SilentlyContinue; if ($null -ne $Found) { return $Found.Source } } return $null } function Invoke-Codex { $Bin = Find-CommandPath @('codex'); if ($null -eq $Bin) { Fail 4 'Codex CLI was not found.' } $Existing = (& $Bin mcp get $ServerName --json 2>$null | Out-String); $Exists = $LASTEXITCODE -eq 0 if ($Exists -and -not $Existing.Contains($ServerUrl)) { Fail 3 'A different Codex MCP server already uses the name barvinca.' } if ($Mode -eq 'uninstall') { if ($Exists) { & $Bin mcp logout $ServerName 2>$null; & $Bin mcp remove $ServerName; if ($LASTEXITCODE -ne 0) { Fail 6 'Codex removal failed.' } }; Write-Host 'Codex: removed; revoke its OAuth connection in Barvinca Settings.'; return } if ($Mode -eq 'check' -and -not $Exists) { Fail 5 'Codex: barvinca is not configured.' } if ($Mode -eq 'update' -and $Exists) { & $Bin mcp remove $ServerName | Out-Null; if ($LASTEXITCODE -ne 0) { Fail 6 'Codex update failed.' }; $Exists = $false } if (-not $Exists) { & $Bin mcp add $ServerName --url $ServerUrl --oauth-resource $ServerUrl | Out-Null; if ($LASTEXITCODE -ne 0) { Fail 6 'Codex configuration failed.' } } if ($Mode -ne 'check' -and -not $NonInteractive) { & $Bin mcp login $ServerName --scopes $Scopes; if ($LASTEXITCODE -ne 0) { Fail 5 'Codex OAuth login failed.' } } elseif ($Mode -ne 'check') { Write-Host "Codex: configured; run codex mcp login barvinca --scopes $Scopes to authorize."; return } $Verified = (& $Bin mcp get $ServerName --json 2>$null | Out-String); if (-not $Verified.Contains($ServerUrl)) { Fail 5 'Codex configuration verification failed.' } & $Bin --ask-for-approval never -c 'mcp_servers.barvinca.url="https://mcp.barvinca.com/mcp"' -c 'mcp_servers.barvinca.oauth_resource="https://mcp.barvinca.com/mcp"' -c 'mcp_servers.barvinca.enabled_tools=["barvinca_connection_status"]' exec --ephemeral --sandbox read-only --skip-git-repo-check 'Call barvinca_connection_status exactly once. Do not call any other tool. Return only whether the Barvinca connection is healthy.' | Out-Null if ($LASTEXITCODE -ne 0) { Fail 5 'Codex could not execute barvinca_connection_status.' }; Write-Host 'Codex: verified.' } function Invoke-Claude { $Bin = Find-CommandPath @('claude'); if ($null -eq $Bin) { Fail 4 'Claude Code CLI was not found.' } $Existing = (& $Bin mcp get $ServerName 2>$null | Out-String); $Exists = $LASTEXITCODE -eq 0 if ($Exists -and -not $Existing.Contains($ServerUrl)) { Fail 3 'A different Claude MCP server already uses the name barvinca.' } if ($Mode -eq 'uninstall') { if ($Exists) { & $Bin mcp remove --scope user $ServerName | Out-Null; if ($LASTEXITCODE -ne 0) { Fail 6 'Claude Code removal failed.' } }; Write-Host 'Claude Code: removed; revoke its OAuth connection in Barvinca Settings.'; return } if ($Mode -eq 'check' -and -not $Exists) { Fail 5 'Claude Code: barvinca is not configured.' } if ($Mode -eq 'update' -and $Exists) { & $Bin mcp remove --scope user $ServerName | Out-Null; if ($LASTEXITCODE -ne 0) { Fail 6 'Claude Code update failed.' }; $Exists = $false } if (-not $Exists) { $Config = @{ type = 'http'; url = $ServerUrl; oauth = @{ scopes = $ReadScopes } } | ConvertTo-Json -Compress; & $Bin mcp add-json --scope user $ServerName $Config | Out-Null; if ($LASTEXITCODE -ne 0) { Fail 6 'Claude Code configuration failed.' } } $Verified = (& $Bin mcp get $ServerName 2>$null | Out-String); if (-not $Verified.Contains($ServerUrl)) { Fail 5 'Claude Code configuration verification failed.' } & $Bin mcp list | Out-Null; if ($LASTEXITCODE -ne 0) { Fail 5 'Claude Code MCP transport check failed.' } if ($NonInteractive -and $Mode -ne 'check') { Write-Host 'Claude Code: configured; open /mcp to authorize.'; return } & $Bin -p --max-turns 1 --tools '' --allowedTools 'mcp__barvinca__barvinca_connection_status' 'Call mcp__barvinca__barvinca_connection_status exactly once and return only whether it is healthy.' | Out-Null if ($LASTEXITCODE -ne 0) { Fail 5 'Claude Code could not execute barvinca_connection_status; open /mcp and retry --check.' }; Write-Host 'Claude Code: verified.' } function Update-CursorConfig { $ConfigPath = Join-Path $HOME '.cursor/mcp.json' $OwnerPath = Join-Path $HOME '.cursor/.barvinca-mcp-installer-owned' $CreatedByInstaller = -not (Test-Path $ConfigPath) if (Test-Path $ConfigPath) { $Item = Get-Item $ConfigPath; if (($Item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { Fail 6 "Refusing to modify symlinked Cursor configuration: $ConfigPath" } } if (Test-Path $ConfigPath) { try { $Data = Get-Content $ConfigPath -Raw | ConvertFrom-Json } catch { Fail 6 "Cannot parse Cursor configuration: $_" } } else { $Data = [pscustomobject]@{} } if ($null -eq $Data.PSObject.Properties['mcpServers']) { $Data | Add-Member -NotePropertyName mcpServers -NotePropertyValue ([pscustomobject]@{}) } $Servers = $Data.mcpServers; $Property = $Servers.PSObject.Properties[$ServerName] if ($null -ne $Property -and $Property.Value.url -ne $ServerUrl) { Fail 3 'A different Cursor MCP server already uses the name barvinca.' } if ($Mode -eq 'check') { if ($null -eq $Property) { Fail 5 'Cursor: barvinca is not configured.' }; return } if ($Mode -eq 'uninstall') { if ($null -eq $Property) { return }; $Servers.PSObject.Properties.Remove($ServerName); if ($Servers.PSObject.Properties.Count -eq 0) { $Data.PSObject.Properties.Remove('mcpServers') }; if ($Data.PSObject.Properties.Count -eq 0 -and (Test-Path $OwnerPath)) { Remove-Item $ConfigPath; Remove-Item $OwnerPath; return } } elseif ($null -eq $Property) { $Servers | Add-Member -NotePropertyName $ServerName -NotePropertyValue ([pscustomobject]@{ type = 'http'; url = $ServerUrl; auth = [pscustomobject]@{ scopes = $ReadScopes } }) } else { $Property.Value = [pscustomobject]@{ type = 'http'; url = $ServerUrl; auth = [pscustomobject]@{ scopes = $ReadScopes } } } $Directory = Split-Path $ConfigPath; if (-not (Test-Path $Directory)) { New-Item -ItemType Directory -Path $Directory | Out-Null } $Temporary = Join-Path $Directory ('.mcp.json.' + [guid]::NewGuid().ToString('N')) try { $Data | ConvertTo-Json -Depth 20 | Set-Content -Path $Temporary -Encoding utf8 & icacls $Temporary /inheritance:r /grant:r "$env:USERNAME`:(R,W)" | Out-Null if (Test-Path $ConfigPath) { [IO.File]::Replace($Temporary, $ConfigPath, $null) } else { [IO.File]::Move($Temporary, $ConfigPath) } if ($CreatedByInstaller) { Set-Content -Path $OwnerPath -Value '' -Encoding ascii; & icacls $OwnerPath /inheritance:r /grant:r "$env:USERNAME`:(R,W)" | Out-Null } } finally { if (Test-Path $Temporary) { Remove-Item $Temporary -Force } } } function Invoke-Cursor { $Bin = Find-CommandPath @('agent','cursor-agent'); if ($null -eq $Bin) { Fail 4 'Cursor Agent CLI was not found.' } Update-CursorConfig if ($Mode -eq 'uninstall') { Write-Host 'Cursor Agent: removed; revoke its OAuth connection in Barvinca Settings.'; return } if ($Mode -ne 'check' -and -not $NonInteractive) { & $Bin mcp login $ServerName; if ($LASTEXITCODE -ne 0) { Fail 5 'Cursor OAuth login failed.' } } elseif ($Mode -ne 'check') { Write-Host "Cursor Agent: configured; run $Bin mcp login barvinca to authorize."; return } $Servers = (& $Bin mcp list | Out-String); if ($LASTEXITCODE -ne 0 -or -not $Servers.Contains($ServerName) -or ($Servers -notmatch 'ready|connected')) { Fail 5 'Cursor MCP transport check failed.' } $Tools = (& $Bin mcp list-tools $ServerName | Out-String); if ($LASTEXITCODE -ne 0) { Fail 5 'Cursor tool discovery failed.' } $RequiredTools = @('barvinca_connection_status','barvinca_account_list','barvinca_project_list','barvinca_project_user_list','barvinca_account_user_list','barvinca_industry_role_list','barvinca_operation_list','barvinca_operation_get','barvinca_operation_evidence','barvinca_operation_recovery_status','barvinca_operation_export') foreach ($Tool in $RequiredTools) { if (-not $Tools.Contains($Tool)) { Fail 5 'Cursor read-only catalog is incomplete.' } } if ($Tools -match 'barvinca_(issue|rfi|asset|submittal|form|user_.*_preflight|operation_execute|operation_cancel)') { Fail 5 'Cursor catalog contains tools outside the read-only profile.' } Write-Host 'Cursor Agent: verified.' } Write-Host "Barvinca MCP: client=$Client mode=$Mode profile=$Profile" switch ($Client) { 'codex' { Invoke-Codex } 'claude' { Invoke-Claude } 'cursor' { Invoke-Cursor } 'all' { Invoke-Codex; Invoke-Claude; Invoke-Cursor } }