diff --git a/tools-sync/scripts/sync_mcp_registrations.ps1 b/tools-sync/scripts/sync_mcp_registrations.ps1 index b87d912..4ae2055 100644 --- a/tools-sync/scripts/sync_mcp_registrations.ps1 +++ b/tools-sync/scripts/sync_mcp_registrations.ps1 @@ -116,6 +116,16 @@ function Get-PortableMcps($Map, [string]$Side) { return $result } +function ConvertTo-PortablePath([string]$Value) { + # `claude mcp add-json` rejects Windows backslash paths ("Invalid configuration: + # : Invalid input"); forward slashes work on Windows and are what the CLI itself + # writes. Only rewrite strings that actually carry a drive-letter path, so flags + # and non-path args are left alone (`--extension-file=C:\..` is rewritten too). + if ($null -eq $Value) { return $Value } + if ($Value -match '[A-Za-z]:\\') { return ($Value -replace '\\', '/') } + return $Value +} + function Get-ComparableJson($Mcp) { $envMap = [ordered]@{} foreach ($key in @($Mcp.env.Keys) | Sort-Object) { @@ -126,10 +136,13 @@ function Get-ComparableJson($Mcp) { foreach ($key in @($Mcp.headers.Keys) | Sort-Object) { $headers[$key] = [string]$Mcp.headers[$key] } + # `C:\x\y` and `C:/x/y` are the same target on Windows -- the two CLIs write + # different separators, so compare on a single normalized form or every path + # arg reads as drift. $value = [ordered]@{ transport = $Mcp.transport - command = $Mcp.command - args = @($Mcp.args) + command = (ConvertTo-PortablePath $Mcp.command) + args = @(@($Mcp.args) | ForEach-Object { ConvertTo-PortablePath $_ }) env = $envMap url = $Mcp.url headers = $headers @@ -175,21 +188,32 @@ function Add-ToCodex($Mcp) { } function Add-ToClaude($Mcp) { + # Deliberately NOT `claude mcp add-json`: Windows PowerShell 5.1 strips the + # double quotes out of a JSON argument on its way to a native exe, so claude.exe + # sees malformed JSON and dies with "Invalid configuration: : Invalid input". + # The flag form takes plain tokens and is immune. (Separately, add-json rejects + # backslash paths even when quoting is clean -- hence ConvertTo-PortablePath.) if ($Mcp.transport -eq "stdio") { - $envMap = [ordered]@{} + $arguments = @("mcp", "add", $Mcp.name, "--scope", "user") foreach ($key in $Mcp.env.Keys) { - if ($CodexOnlyEnv -notcontains $key) { $envMap[$key] = $Mcp.env[$key] } + if ($CodexOnlyEnv -contains $key) { continue } + $arguments += @("--env", "$key=$($Mcp.env[$key])") } - $cfg = [ordered]@{ type = "stdio"; command = $Mcp.command; args = @($Mcp.args); env = $envMap } - } else { - if ($Mcp.bearer_token_env_var) { - throw "Codex bearer_token_env_var has no lossless Claude JSON translation: $($Mcp.name)" - } - $cfg = [ordered]@{ type = $Mcp.transport; url = $Mcp.url } - if ($Mcp.headers.Count -gt 0) { $cfg.headers = $Mcp.headers } + $arguments += "--" + $arguments += (ConvertTo-PortablePath $Mcp.command) + foreach ($arg in @($Mcp.args)) { $arguments += (ConvertTo-PortablePath $arg) } + Invoke-External "claude add $($Mcp.name)" "claude" $arguments + return } - $json = $cfg | ConvertTo-Json -Depth 8 -Compress - Invoke-External "claude add $($Mcp.name)" "claude" @("mcp", "add-json", "--scope", "user", $Mcp.name, $json) + if ($Mcp.bearer_token_env_var) { + throw "Codex bearer_token_env_var has no lossless Claude translation: $($Mcp.name)" + } + $arguments = @("mcp", "add", $Mcp.name, "--scope", "user", + "--transport", $Mcp.transport, $Mcp.url) + foreach ($key in $Mcp.headers.Keys) { + $arguments += @("--header", "$key`: $($Mcp.headers[$key])") + } + Invoke-External "claude add $($Mcp.name)" "claude" $arguments } function Remove-FromClaude([string]$Name) {