From 17632bea27da38b63edab41fec2673b4867329b0 Mon Sep 17 00:00:00 2001 From: bowei Date: Wed, 22 Jul 2026 03:55:54 +0800 Subject: [PATCH] =?UTF-8?q?sync:=20Shanghai-U9=202026-07-22=20tools-sync?= =?UTF-8?q?=20MCP=E5=8F=8C=E5=90=91=E5=90=8C=E6=AD=A5=E8=84=9A=E6=9C=AC+MA?= =?UTF-8?q?TLAB=E4=B8=93=E9=A1=B9=E8=8A=82=EF=BC=9B=E6=92=A4=E5=87=BA?= =?UTF-8?q?=E8=AF=AF=E5=85=A5=E5=BA=93=E7=9A=84=5F=5Fpycache=5F=5F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__pycache__/agents.cpython-311.pyc | Bin 4953 -> 0 bytes .../scripts/__pycache__/graph.cpython-311.pyc | Bin 900 -> 0 bytes .../__pycache__/workflow.cpython-311.pyc | Bin 15457 -> 0 bytes tools-sync/SKILL.md | 100 ++++++- tools-sync/references/skills_env_template.md | 9 +- tools-sync/scripts/sync_mcp_registrations.ps1 | 267 ++++++++++++++++++ .../tests/test_sync_mcp_registrations.ps1 | 56 ++++ 7 files changed, 423 insertions(+), 9 deletions(-) delete mode 100644 langgraph-plan-review/scripts/__pycache__/agents.cpython-311.pyc delete mode 100644 langgraph-plan-review/scripts/__pycache__/graph.cpython-311.pyc delete mode 100644 langgraph-plan-review/scripts/__pycache__/workflow.cpython-311.pyc create mode 100644 tools-sync/scripts/sync_mcp_registrations.ps1 create mode 100644 tools-sync/tests/test_sync_mcp_registrations.ps1 diff --git a/langgraph-plan-review/scripts/__pycache__/agents.cpython-311.pyc b/langgraph-plan-review/scripts/__pycache__/agents.cpython-311.pyc deleted file mode 100644 index 44ceaa2921dae8ccf8fcddb0b13ea034e0d1f3ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4953 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 Gf~fz?!vcT+ diff --git a/langgraph-plan-review/scripts/__pycache__/workflow.cpython-311.pyc b/langgraph-plan-review/scripts/__pycache__/workflow.cpython-311.pyc deleted file mode 100644 index e61694ac762fefc43aea5dcbb9a52e744d890bef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15457 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 V?3|gjQ{C{5Ll*j>wNS##{|(M(3fTYv diff --git a/tools-sync/SKILL.md b/tools-sync/SKILL.md index f152447..4f1ce92 100644 --- a/tools-sync/SKILL.md +++ b/tools-sync/SKILL.md @@ -1,6 +1,6 @@ --- name: tools-sync -description: 在本机统一同步并配置 Claude Code 与 Codex 的 skills、MCP server 和开发环境:将 ~\.claude\skills 和 ~\.codex\skills 与 git.rpchannel.com 上的 bowei/skills-win 仓库保持同步,审计 skills 中的硬编码参数并迁移到本机 ~\skills_env.md,校验 langgraph conda 环境,并确保 E:\MCP_design 下所有 MCP 都已在 Claude Code 和 Codex 中注册。当用户提到"同步skills"、"skills同步"、"tools sync"、"tools-sync"、"同步codex"、"同步MCP"、"环境同步"、"skills-win"、"把skills推上去/拉下来"、"新电脑配置claude/codex环境"、"检查MCP注册"、"skills_env"时,必须使用此技能,即使用户没有明确说"skill"二字。 +description: 在本机统一同步并配置 Claude Code 与 Codex 的 skills、MCP server 和开发环境:将 ~\.claude\skills 和 ~\.codex\skills 与 git.rpchannel.com 上的 bowei/skills-win 仓库保持同步,审计 skills 中的硬编码参数并迁移到本机 ~\skills_env.md,校验 langgraph conda 环境,双向发现并补齐 Claude Code 与 Codex 的用户 MCP 注册,同时确保 E:\MCP_design 下所有 MCP 均已注册。当用户提到"同步skills"、"skills同步"、"tools sync"、"tools-sync"、"同步codex"、"同步MCP"、"环境同步"、"skills-win"、"把skills推上去/拉下来"、"新电脑配置claude/codex环境"、"检查MCP注册"、"skills_env"时,必须使用此技能,即使用户没有明确说"skill"二字。 --- # tools-sync:Claude Code / Codex 工具链同步 @@ -116,10 +116,41 @@ commit + push 一次(junction 模型下 codex 侧自动同步,无需额外 失败(环境不存在或包缺失)→ 调用 `langgraph-vivado-install` skill 完成安装, 装好后把实际的 python 路径回填到 `~\skills_env.md`。 -## 阶段 5 — MCP server 注册校验(Claude Code + Codex 两边) +## 阶段 5 — MCP server 双向发现、注册与校验(Claude Code ↔ Codex) -MCP 的清单真源是 `E:\MCP_design\mcp_list.md`(路径见 skills_env.md 的 -`MCP_DESIGN_ROOT`),每行一个 MCP 工程目录。对清单里的每一项: +先运行双向审计脚本。它读取 Claude Code 用户级 `~\.claude.json` 的 `mcpServers` 和 +Codex 用户级 `~\.codex\config.toml` 的 `[mcp_servers.*]`,对用户可配置 MCP 做并集: + +```powershell +# 只审计;发现差集或漂移时退出码为 2,不修改配置 +powershell -File \scripts\sync_mcp_registrations.ps1 + +# 双向补齐单边缺失项;同名漂移默认只报告,不覆盖 +powershell -File \scripts\sync_mcp_registrations.ps1 -Apply + +# 经核实后才可选择一侧作为同名漂移项的权威配置 +powershell -File \scripts\sync_mcp_registrations.ps1 -Apply -ConflictAuthority Claude +# 或:-ConflictAuthority Codex +``` + +脚本必须遵守以下规则: + +- Claude 独有的用户 MCP 补到 Codex;Codex 独有的用户 MCP 补到 Claude,实现双向同步。 +- 云端账号连接器、插件动态 MCP、Codex 自带运行时/插件 MCP 不属于可移植用户配置, + 必须排除,不能复制到另一侧。额外的机器特有项可用 `-ExcludeName ` 排除。 +- 用户级配置才自动双向同步。Claude 的项目级 `.mcp.json` / local scope 不自动提升成 + Codex 全局配置;如确需提升,先向用户说明作用域扩大,再手工处理。 +- 同名 MCP 要规范化比较 transport、command、args、env、URL 和 headers。 + Codex stdio 专用的 `PYTHONIOENCODING=utf-8`、`PYTHONUTF8=1` 是允许的平台差异, + 比较时忽略;Claude → Codex 时自动补上,Codex → Claude 时自动移除。 +- 同名配置漂移时默认只报告,不能猜哪侧正确。结合 `~\skills_env.md`、工程实际路径和 + 用户意图核实后,才可显式指定 `-ConflictAuthority Claude` 或 `Codex` 覆盖另一侧。 +- 修改 Codex 配置前脚本必须创建带时间戳的备份。含无法无损转换的 HTTP headers、 + bearer-token 设置等配置时应停止该项并报告,不得静默丢字段。 + +然后用 `E:\MCP_design\mcp_list.md`(路径见 skills_env.md 的 `MCP_DESIGN_ROOT`)做 +第二层完整性校验。每行一个 MCP 工程目录;它能发现“工程存在但两侧都没注册”的项目, +这是仅比较两个注册表的并集无法发现的。对清单里的每一项: 1. **Claude Code 侧**:`claude mcp list` 或读 `~\.claude.json` 的 `mcpServers`, 确认存在对应条目。缺失的用 `claude mcp add ...` 注册,命令、解释器 @@ -132,9 +163,62 @@ MCP 的清单真源是 `E:\MCP_design\mcp_list.md`(路径见 skills_env.md 的 确认进程能起来、没有立刻报 import/路径错误(stdio MCP 起来后会静默等 输入,这就算通过,Ctrl-C 掉即可)。 -注意两侧配置语义相同但格式不同(JSON vs TOML),同一个 MCP 在两边的 -command/args/env 应当一致;发现两边漂移(比如只有一边改了 VIVADO_PATH)时, -以 skills_env.md 记录的值为准改齐两边。 +注意两侧配置语义相同但格式不同(JSON vs TOML)。先由双向脚本发现差集和漂移,再用 +`mcp_list.md` 查漏,最后以 skills_env.md 和实际可启动结果判定正确值。不要因为某项当前 +只在一侧出现,就假设另一侧没有安装需求。 + +### MATLAB / Simulink Agentic Toolkit 专项检查与安装 + +每次执行 MCP 同步时,还要专项检查 MathWorks Agentic Toolkits。它们不一定位于 +`MCP_DESIGN_ROOT`,因此不能只依赖 `mcp_list.md`。MATLAB 与 Simulink 共用一个名为 +`matlab` 的 MCP server;Simulink 能力通过 `--extension-file=.../simulink/tools/tools.json` +加载,不应错误地要求再注册一个独立的 `simulink` MCP。 + +检查以下项目,任一缺失均视为未完整安装: + +1. 本机存在 MATLAB;MATLAB Toolkit 要求 R2020b+,Simulink Toolkit 要求 R2023a+ + 且已安装 Simulink。实际 MATLAB 根目录记录到 `~\skills_env.md` 的 `MATLAB_ROOT`。 +2. `~\.matlab\agentic-toolkits\bin\matlab-mcp-server.exe` 存在且 `--version` 可运行; + `config.json` 的 `toolkits` 同时包含 `matlab` 与 `simulink`。 +3. Claude `~\.claude.json` 与 Codex `~\.codex\config.toml` 均有用户级 `matlab` MCP, + command 指向同一二进制,args 同时包含 `--matlab-session-mode=existing` 和正确的 + `--extension-file`。Claude 条目应有 `alwaysLoad=true`。 +4. Codex 的 `[mcp_servers.matlab]` 还必须有 `tool_timeout_sec = 600` 和 Windows 所需的 + `env_vars = ["WINDIR"]`;缺少 `WINDIR` 时 Simulink 子进程可能无法正常工作。 +5. `~\.agents\skills` 中存在指向两套 Toolkit 的 junction;Claude Code 的 MATLAB / + Simulink plugin 或等效 skill 注册存在。 + +缺失时使用 MathWorks 官方统一安装器,不使用旧版 agent-driven 安装法: + +```powershell +$installer = Join-Path $env:TEMP "mathworks-agentic-installer\agenticToolkitInstaller.mltbx" +Invoke-WebRequest -UseBasicParsing ` + "https://github.com/matlab/simulink-agentic-toolkit/releases/latest/download/agenticToolkitInstaller.mltbx" ` + -OutFile $installer + +# MATLAB 中先安装 installer,再分别执行 install 和 configure;Prompt=false 可无交互运行。 +matlab.addons.toolbox.installToolbox(""); +setupAgenticToolkit("install", Toolkit=["matlab","simulink"], ... + SessionMode="existing", Prompt=false); +setupAgenticToolkit("configure", Agents=["claude-code","codex"], ... + Scope="global", SessionMode="existing", Prompt=false); +``` + +注意事项: + +- `install` 与 `configure` 是两个独立动作,两者都必须成功;仅看到 Toolkit 文件落盘不代表 + MCP 已注册。每次修改 agent 配置前先备份 Claude JSON 与 Codex TOML。 +- 如果新 MATLAB 进程找不到 `setupAgenticToolkit`,先确认 “MathWorks Agentic Toolkit + Setup” Add-On 已启用;必要时把其实际 Toolbox 安装目录 `addpath` 后再执行。如果 + R2025b 的 Add-On 管理器能列出 Toolbox 却无法启用,则将 “MathWorks Agentic Toolkit + Setup” 与 “MATLAB MCP Server Toolbox” 两个官方 Toolbox 根目录加入 path 并 + `savepath`,再用全新 MATLAB 进程通过 `which setupAgenticToolkit`、 + `which shareMATLABSession` 验证。 +- 安装后重新运行双向审计脚本;`matlab` 必须显示为 Matched。再直接解析两边配置,确认 + Simulink extension、Codex timeout 与 `WINDIR` 未被规范化比较遗漏。 +- 使用 Simulink 工具前,每个 MATLAB 会话运行 + `addpath("~/.matlab/agentic-toolkits/simulink"); satk_initialize`,然后重启 Claude Code / + Codex 会话。MCP 健康检查在没有共享 MATLAB 会话时可能显示连接失败,这不等于未安装。 ## 阶段 6 — 输出同步报告 @@ -150,7 +234,7 @@ command/args/env 应当一致;发现两边漂移(比如只有一边改了 VI - 遗留待改: <清单> ## 环境 - langgraph env: OK / 已重装 / 失败原因 -- MCP 注册: <两侧各缺了什么、补了什么、抽查结果> +- MCP 注册: ## 需要用户决定的事项 - <冲突、密码泄露、codex 独有 skill 去留……没有则写"无"> ``` diff --git a/tools-sync/references/skills_env_template.md b/tools-sync/references/skills_env_template.md index 499169d..b0f2d2d 100644 --- a/tools-sync/references/skills_env_template.md +++ b/tools-sync/references/skills_env_template.md @@ -30,7 +30,9 @@ CONDA_LANGGRAPH_PYTHON = C:\Users\\.conda\envs\langgraph\python.exe VIVADO_ROOT = VIVADO_PATH = <上一行 + \bin\vivado.bat> KICAD_PYTHONPATH = C:\Program Files\KiCad\10.0\bin\Lib\site-packages -MATLAB_MCP_EXE = C:\Users\\.matlab\agentic-toolkits\bin\matlab-mcp-server.exe +MATLAB_ROOT = +MATLAB_AGENTIC_ROOT = C:\Users\\.matlab\agentic-toolkits +MATLAB_MCP_EXE = {MATLAB_AGENTIC_ROOT}\bin\matlab-mcp-server.exe ``` ## 板卡 / 仪器 IP @@ -48,6 +50,9 @@ MCP_DESIGN_ROOT = E:\MCP_design 每个 MCP 在 Claude Code(`~\.claude.json` 的 `mcpServers`)和 Codex (`~\.codex\config.toml` 的 `[mcp_servers.*]`)中应使用**同一份** command/args/env。 +同步时还要对两侧用户级注册表取并集,确保任一侧新增的可移植 MCP 都同步到另一侧; +`mcp_list.md` 继续用于发现“两侧都漏装”的本地工程。托管连接器、插件动态 MCP、 +Codex 内置运行时 MCP 不进入这个并集。 标准配置如下(路径变量引用上文条目): | name | command | args | env | @@ -60,6 +65,8 @@ MCP_DESIGN_ROOT = E:\MCP_design | matlab | {MATLAB_MCP_EXE} | --matlab-session-mode=existing --extension-file=...\simulink\tools\tools.json | | Codex 侧所有 stdio MCP 额外加 `PYTHONIOENCODING=utf-8`、`PYTHONUTF8=1`。 +MathWorks `matlab` MCP 是特例:Codex 还要设置 `tool_timeout_sec=600` 与 +`env_vars=["WINDIR"]`;Claude 侧要设置 `alwaysLoad=true`。MATLAB 与 Simulink 共用该条目。 ## 本机状态备注(同步时更新) diff --git a/tools-sync/scripts/sync_mcp_registrations.ps1 b/tools-sync/scripts/sync_mcp_registrations.ps1 new file mode 100644 index 0000000..b87d912 --- /dev/null +++ b/tools-sync/scripts/sync_mcp_registrations.ps1 @@ -0,0 +1,267 @@ +# tools-sync: audit and reconcile user-configured MCP registrations between +# Claude Code and Codex. Managed connectors, plugins, and Codex runtime MCPs are +# intentionally excluded because they are owned by their host application. + +[CmdletBinding()] +param( + [string]$ClaudeConfig = "$env:USERPROFILE\.claude.json", + [string]$CodexConfig = "$env:USERPROFILE\.codex\config.toml", + [switch]$Apply, + [ValidateSet("None", "Claude", "Codex")] + [string]$ConflictAuthority = "None", + [string[]]$ExcludeName = @(), + [string]$PythonCommand = "python" +) + +$ErrorActionPreference = "Stop" +$script:CodexBackupMade = $false +$script:Failures = @() +$CodexOnlyEnv = @("PYTHONIOENCODING", "PYTHONUTF8") + +function ConvertTo-StringMap($Object) { + $result = [ordered]@{} + if ($null -eq $Object) { return $result } + foreach ($property in $Object.psobject.Properties | Sort-Object Name) { + $result[$property.Name] = [string]$property.Value + } + return $result +} + +function ConvertFrom-DictionaryToStringMap($Dictionary) { + $result = [ordered]@{} + if ($null -eq $Dictionary) { return $result } + foreach ($key in @($Dictionary.Keys) | Sort-Object) { + $result[[string]$key] = [string]$Dictionary[$key] + } + return $result +} + +function New-CanonicalMcp([string]$Name, [string]$Transport, [string]$Command, + [object[]]$CommandArgs, $EnvMap, [string]$Url, + $Headers, [string]$BearerTokenEnvVar, [string]$Source) { + [pscustomobject][ordered]@{ + name = $Name + transport = $Transport + command = if ($Command) { $Command } else { "" } + args = @($CommandArgs | ForEach-Object { [string]$_ }) + env = $EnvMap + url = if ($Url) { $Url } else { "" } + headers = $Headers + bearer_token_env_var = if ($BearerTokenEnvVar) { $BearerTokenEnvVar } else { "" } + source = $Source + } +} + +function Get-ClaudeMcps { + if (-not (Test-Path -LiteralPath $ClaudeConfig)) { + throw "Claude Code config not found: $ClaudeConfig" + } + $root = Get-Content -Raw -LiteralPath $ClaudeConfig | ConvertFrom-Json + $result = @{} + if ($null -eq $root.mcpServers) { return $result } + foreach ($property in $root.mcpServers.psobject.Properties) { + $name = $property.Name + $cfg = $property.Value + $transport = if ($cfg.type -in @("http", "sse")) { [string]$cfg.type } else { "stdio" } + $result[$name] = New-CanonicalMcp $name $transport ([string]$cfg.command) @($cfg.args) ` + (ConvertTo-StringMap $cfg.env) ([string]$cfg.url) ` + (ConvertTo-StringMap $cfg.headers) "" "Claude" + } + return $result +} + +function Get-CodexMcps { + if (-not (Test-Path -LiteralPath $CodexConfig)) { + throw "Codex config not found: $CodexConfig" + } + $python = @' +import json, sys, tomllib +with open(sys.argv[1], "rb") as f: + data = tomllib.load(f) +print(json.dumps(data.get("mcp_servers", {}), ensure_ascii=False)) +'@ + $parseOutput = @($python | & $PythonCommand - $CodexConfig 2>&1) + if ($LASTEXITCODE -ne 0) { + $detail = ($parseOutput | ForEach-Object { "$_" }) -join "`n" + throw "Failed to parse Codex TOML with $PythonCommand`: $detail" + } + $json = $parseOutput -join "`n" + $root = $json | ConvertFrom-Json + $result = @{} + foreach ($property in $root.psobject.Properties) { + $name = $property.Name + $cfg = $property.Value + $transport = if ($cfg.url) { "http" } else { "stdio" } + $result[$name] = New-CanonicalMcp $name $transport ([string]$cfg.command) @($cfg.args) ` + (ConvertTo-StringMap $cfg.env) ([string]$cfg.url) ` + (ConvertTo-StringMap $cfg.http_headers) ([string]$cfg.bearer_token_env_var) "Codex" + } + return $result +} + +function Test-CodexManaged($Mcp) { + if ($Mcp.name -eq "node_repl") { return $true } + $command = [string]$Mcp.command + return ($command -match '(?i)[\\/]\.codex[\\/](runtimes|plugins[\\/]cache)[\\/]') +} + +function Get-PortableMcps($Map, [string]$Side) { + $result = @{} + foreach ($name in $Map.Keys) { + if ($ExcludeName -contains $name) { continue } + $mcp = $Map[$name] + if ($Side -eq "Codex" -and (Test-CodexManaged $mcp)) { continue } + $result[$name] = $mcp + } + return $result +} + +function Get-ComparableJson($Mcp) { + $envMap = [ordered]@{} + foreach ($key in @($Mcp.env.Keys) | Sort-Object) { + if ($CodexOnlyEnv -contains $key) { continue } + $envMap[$key] = [string]$Mcp.env[$key] + } + $headers = [ordered]@{} + foreach ($key in @($Mcp.headers.Keys) | Sort-Object) { + $headers[$key] = [string]$Mcp.headers[$key] + } + $value = [ordered]@{ + transport = $Mcp.transport + command = $Mcp.command + args = @($Mcp.args) + env = $envMap + url = $Mcp.url + headers = $headers + bearer_token_env_var = $Mcp.bearer_token_env_var + } + return ($value | ConvertTo-Json -Depth 8 -Compress) +} + +function Backup-CodexConfig { + if ($script:CodexBackupMade) { return } + $stamp = Get-Date -Format "yyyyMMdd-HHmmss" + $backup = "$CodexConfig.bak-$stamp" + Copy-Item -LiteralPath $CodexConfig -Destination $backup + $script:CodexBackupMade = $true + Write-Host "backup: $backup" +} + +function Invoke-External([string]$Label, [string]$Exe, [string[]]$Arguments) { + & $Exe @Arguments + if ($LASTEXITCODE -ne 0) { throw "$Label failed (exit $LASTEXITCODE)" } +} + +function Add-ToCodex($Mcp) { + if ($Mcp.transport -eq "stdio") { + Backup-CodexConfig + $arguments = @("mcp", "add", $Mcp.name) + $envMap = [ordered]@{} + foreach ($key in $Mcp.env.Keys) { $envMap[$key] = $Mcp.env[$key] } + $envMap["PYTHONIOENCODING"] = "utf-8" + $envMap["PYTHONUTF8"] = "1" + foreach ($key in $envMap.Keys) { $arguments += @("--env", "$key=$($envMap[$key])") } + $arguments += "--" + $arguments += $Mcp.command + $arguments += @($Mcp.args) + Invoke-External "codex add $($Mcp.name)" "codex" $arguments + return + } + if ($Mcp.headers.Count -gt 0) { + throw "HTTP headers cannot be translated safely by 'codex mcp add': $($Mcp.name)" + } + Backup-CodexConfig + Invoke-External "codex add $($Mcp.name)" "codex" @("mcp", "add", $Mcp.name, "--url", $Mcp.url) +} + +function Add-ToClaude($Mcp) { + if ($Mcp.transport -eq "stdio") { + $envMap = [ordered]@{} + foreach ($key in $Mcp.env.Keys) { + if ($CodexOnlyEnv -notcontains $key) { $envMap[$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 } + } + $json = $cfg | ConvertTo-Json -Depth 8 -Compress + Invoke-External "claude add $($Mcp.name)" "claude" @("mcp", "add-json", "--scope", "user", $Mcp.name, $json) +} + +function Remove-FromClaude([string]$Name) { + Invoke-External "claude remove $Name" "claude" @("mcp", "remove", "--scope", "user", $Name) +} + +function Remove-FromCodex([string]$Name) { + Backup-CodexConfig + Invoke-External "codex remove $Name" "codex" @("mcp", "remove", $Name) +} + +function Try-Action([string]$Description, [scriptblock]$Action) { + try { + & $Action + Write-Host "OK: $Description" + } catch { + $script:Failures += "$Description`: $($_.Exception.Message)" + Write-Warning $script:Failures[-1] + } +} + +$claude = Get-PortableMcps (Get-ClaudeMcps) "Claude" +$codex = Get-PortableMcps (Get-CodexMcps) "Codex" +$onlyClaude = @($claude.Keys | Where-Object { -not $codex.ContainsKey($_) } | Sort-Object) +$onlyCodex = @($codex.Keys | Where-Object { -not $claude.ContainsKey($_) } | Sort-Object) +$drift = @($claude.Keys | Where-Object { + $codex.ContainsKey($_) -and (Get-ComparableJson $claude[$_] ) -ne (Get-ComparableJson $codex[$_]) +} | Sort-Object) +$matched = @($claude.Keys | Where-Object { + $codex.ContainsKey($_) -and (Get-ComparableJson $claude[$_] ) -eq (Get-ComparableJson $codex[$_]) +} | Sort-Object) + +foreach ($name in @($claude.Keys | Where-Object { $codex.ContainsKey($_) } | Sort-Object)) { + Write-Verbose "$name Claude canonical: $(Get-ComparableJson $claude[$name])" + Write-Verbose "$name Codex canonical: $(Get-ComparableJson $codex[$name])" +} + +Write-Host "=== MCP bidirectional audit ===" +Write-Host "Claude only : $($onlyClaude -join ', ')" +Write-Host "Codex only : $($onlyCodex -join ', ')" +Write-Host "Drift : $($drift -join ', ')" +Write-Host "Matched : $($matched -join ', ')" + +if ($Apply) { + foreach ($name in $onlyClaude) { + Try-Action "copied $name from Claude to Codex" { Add-ToCodex $claude[$name] } + } + foreach ($name in $onlyCodex) { + Try-Action "copied $name from Codex to Claude" { Add-ToClaude $codex[$name] } + } + foreach ($name in $drift) { + if ($ConflictAuthority -eq "Claude") { + Try-Action "replaced Codex $name from Claude authority" { + Remove-FromCodex $name + Add-ToCodex $claude[$name] + } + } elseif ($ConflictAuthority -eq "Codex") { + Try-Action "replaced Claude $name from Codex authority" { + Remove-FromClaude $name + Add-ToClaude $codex[$name] + } + } else { + Write-Warning "not overwriting drifted MCP '$name'; choose -ConflictAuthority Claude or Codex" + } + } +} + +if ($script:Failures.Count -gt 0) { + Write-Host "=== failures ===" + $script:Failures | ForEach-Object { Write-Host "- $_" } + exit 1 +} + +if (-not $Apply -and ($onlyClaude.Count + $onlyCodex.Count + $drift.Count -gt 0)) { exit 2 } +Write-Host "=== MCP audit complete ===" diff --git a/tools-sync/tests/test_sync_mcp_registrations.ps1 b/tools-sync/tests/test_sync_mcp_registrations.ps1 new file mode 100644 index 0000000..a9c838b --- /dev/null +++ b/tools-sync/tests/test_sync_mcp_registrations.ps1 @@ -0,0 +1,56 @@ +$ErrorActionPreference = "Stop" +$scriptUnderTest = Join-Path (Split-Path $PSScriptRoot -Parent) "scripts\sync_mcp_registrations.ps1" +$testDir = Join-Path ([IO.Path]::GetTempPath()) ("tools-sync-mcp-test-" + [guid]::NewGuid().ToString("N")) +New-Item -ItemType Directory -Path $testDir | Out-Null +$claudeConfig = Join-Path $testDir "claude.json" +$codexConfig = Join-Path $testDir "config.toml" + +try { + $claudeText = @' +{ + "mcpServers": { + "claude-only": {"type":"stdio","command":"python","args":["a.py"],"env":{"A":"1"}}, + "same": {"type":"stdio","command":"python","args":["same.py"],"env":{}}, + "drifted": {"type":"stdio","command":"python","args":["claude.py"],"env":{}} + } +} +'@ + [IO.File]::WriteAllText($claudeConfig, $claudeText, [Text.UTF8Encoding]::new($false)) + + $codexText = @' +[mcp_servers.codex-only] +command = "python" +args = ["b.py"] + +[mcp_servers.same] +command = "python" +args = ["same.py"] +env = { PYTHONIOENCODING = "utf-8", PYTHONUTF8 = "1" } + +[mcp_servers.drifted] +command = "python" +args = ["codex.py"] + +[mcp_servers.node_repl] +command = "C:\\Users\\tester\\.codex\\runtimes\\node_repl.exe" +'@ + [IO.File]::WriteAllText($codexConfig, $codexText, [Text.UTF8Encoding]::new($false)) + + $oldErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = "Continue" + $output = & powershell -NoProfile -ExecutionPolicy Bypass -File $scriptUnderTest ` + -ClaudeConfig $claudeConfig -CodexConfig $codexConfig -Verbose 2>&1 | Out-String + $code = $LASTEXITCODE + $ErrorActionPreference = $oldErrorActionPreference + if ($code -ne 2) { throw "expected audit exit 2, got $code`n$output" } + if ($output -notmatch 'Claude only\s*:\s*claude-only') { throw "Claude-only difference not detected`n$output" } + if ($output -notmatch 'Codex only\s*:\s*codex-only') { throw "Codex-only difference not detected`n$output" } + if ($output -notmatch 'Drift\s*:\s*drifted') { throw "drift not detected`n$output" } + if ($output -notmatch 'Matched\s*:\s*same') { throw "Codex-only UTF-8 env should not cause drift`n$output" } + if ($output -match 'Codex only\s*:.*node_repl') { throw "managed Codex MCP was not excluded`n$output" } + Write-Host "PASS: bidirectional MCP audit fixtures" +} finally { + Remove-Item -LiteralPath $claudeConfig -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $codexConfig -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $testDir -Force -ErrorAction SilentlyContinue +}