98 lines
3.8 KiB
PowerShell
98 lines
3.8 KiB
PowerShell
[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."
|