sync: Shanghai-U9 2026-07-22 tools-sync MCP双向同步脚本+MATLAB专项节;撤出误入库的__pycache__
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
+92
-8
@@ -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 <skill目录>\scripts\sync_mcp_registrations.ps1
|
||||
|
||||
# 双向补齐单边缺失项;同名漂移默认只报告,不覆盖
|
||||
powershell -File <skill目录>\scripts\sync_mcp_registrations.ps1 -Apply
|
||||
|
||||
# 经核实后才可选择一侧作为同名漂移项的权威配置
|
||||
powershell -File <skill目录>\scripts\sync_mcp_registrations.ps1 -Apply -ConflictAuthority Claude
|
||||
# 或:-ConflictAuthority Codex
|
||||
```
|
||||
|
||||
脚本必须遵守以下规则:
|
||||
|
||||
- Claude 独有的用户 MCP 补到 Codex;Codex 独有的用户 MCP 补到 Claude,实现双向同步。
|
||||
- 云端账号连接器、插件动态 MCP、Codex 自带运行时/插件 MCP 不属于可移植用户配置,
|
||||
必须排除,不能复制到另一侧。额外的机器特有项可用 `-ExcludeName <name>` 排除。
|
||||
- 用户级配置才自动双向同步。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 <name> ...` 注册,命令、解释器
|
||||
@@ -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("<installer绝对路径>");
|
||||
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 注册: <Claude 独有、Codex 独有、同名漂移、已补齐、排除项、清单查漏、MATLAB/Simulink 专项检查、抽查结果>
|
||||
## 需要用户决定的事项
|
||||
- <冲突、密码泄露、codex 独有 skill 去留……没有则写"无">
|
||||
```
|
||||
|
||||
@@ -30,7 +30,9 @@ CONDA_LANGGRAPH_PYTHON = C:\Users\<user>\.conda\envs\langgraph\python.exe
|
||||
VIVADO_ROOT = <Vivado 安装根目录,如 C:\Xilinx\Vivado\2018.1 —— 必须实测,不同机器盘符不同>
|
||||
VIVADO_PATH = <上一行 + \bin\vivado.bat>
|
||||
KICAD_PYTHONPATH = C:\Program Files\KiCad\10.0\bin\Lib\site-packages
|
||||
MATLAB_MCP_EXE = C:\Users\<user>\.matlab\agentic-toolkits\bin\matlab-mcp-server.exe
|
||||
MATLAB_ROOT = <MATLAB 安装根目录,如 D:\MATLAB\R2025b>
|
||||
MATLAB_AGENTIC_ROOT = C:\Users\<user>\.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 共用该条目。
|
||||
|
||||
## 本机状态备注(同步时更新)
|
||||
|
||||
|
||||
@@ -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 ==="
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user