[CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidatePattern('^[a-z0-9][a-z0-9-]*$')] [string]$Project, [Parameter(Mandatory = $true)] [ValidatePattern('^[a-z0-9][a-z0-9-]*$')] [string]$Environment, [Parameter(Mandatory = $true)] [ValidatePattern('^[0-9a-fA-F-]{36}$')] [string]$HubClientId, [Parameter(Mandatory = $true)] [ValidatePattern('^[a-zA-Z0-9_.@-]+$')] [string]$OAuthUsername, [Parameter(Mandatory = $true)] [string]$SecretFile, [string]$PublicBaseUrl = 'https://mcp.example.com', [string]$TokenEndpoint = 'https://auth.example-host.com/application/o/token/', [string]$OAuthClientId = 'itworx-mcp-hub', [string]$PythonExecutable = '', [string]$CodexExecutable = '', [switch]$Force ) $ErrorActionPreference = 'Stop' $repositoryRoot = Split-Path -Parent $PSScriptRoot $bridge = Join-Path $PSScriptRoot 'mcp_stdio_bridge.py' $resolvedSecret = (Resolve-Path -LiteralPath $SecretFile).Path if (-not (Test-Path -LiteralPath $bridge -PathType Leaf)) { throw 'MCP bridge script is missing.' } $secretInfo = Get-Item -LiteralPath $resolvedSecret if (-not $secretInfo.PSIsContainer -and $secretInfo.Length -gt 0 -and $secretInfo.Length -le 8192) { # Continue; content is deliberately never read or printed by this installer. } else { throw 'Secret file must be a non-empty regular file no larger than 8192 bytes.' } if (-not $PublicBaseUrl.StartsWith('https://') -or -not $TokenEndpoint.StartsWith('https://')) { throw 'Public MCP and OAuth endpoints must use HTTPS.' } if (-not $PythonExecutable) { $PythonExecutable = (Get-Command python -ErrorAction Stop).Source } if (-not $CodexExecutable) { $CodexExecutable = (Get-Command codex -ErrorAction Stop).Source } $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name $currentAcl = Get-Acl -LiteralPath $resolvedSecret if ($currentAcl.Owner -ne $identity) { throw 'Secret file must already be owned by the current Windows identity.' } $verifiedAcl = $currentAcl $verifiedRules = @($verifiedAcl.Access) if ( -not $verifiedAcl.AreAccessRulesProtected -or $verifiedRules.Count -ne 1 -or $verifiedRules[0].IdentityReference.Value -ne $identity -or $verifiedRules[0].AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or ($verifiedRules[0].FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne [System.Security.AccessControl.FileSystemRights]::FullControl ) { throw 'Secret file ACL must grant only the current Windows identity FullControl; use new_codex_credential.ps1.' } $name = "itworx-$Project-$Environment" $endpoint = "$($PublicBaseUrl.TrimEnd('/'))/mcp/$Project/$Environment" $existing = & $CodexExecutable mcp get $name 2>$null if ($LASTEXITCODE -eq 0) { if (-not $Force) { throw "Codex MCP server '$name' already exists. Re-run with -Force to replace it." } & $CodexExecutable mcp remove $name | Out-Null if ($LASTEXITCODE -ne 0) { throw "Could not remove existing Codex MCP server '$name'." } } $arguments = @( 'mcp', 'add', $name, '--', $PythonExecutable, $bridge, '--endpoint', $endpoint, '--client-id', $HubClientId, '--token-endpoint', $TokenEndpoint, '--oauth-client-id', $OAuthClientId, '--oauth-username', $OAuthUsername, '--client-secret-file', $resolvedSecret, '--scope', 'openid profile' ) & $CodexExecutable @arguments | Out-Null if ($LASTEXITCODE -ne 0) { throw "Could not add Codex MCP server '$name'." } & $CodexExecutable mcp get $name if ($LASTEXITCODE -ne 0) { throw "Codex MCP server '$name' was not readable after installation." } Write-Output "Configured $name for $endpoint. Restart Codex before first use."