File: C:/Users/fred/.codex/vendor_imports/skills/skills/.curated/migrate-to-codex/scripts/migrate/mcps.py
"""Convert Claude Code MCP/settings JSON into Codex config TOML.
Reads Claude settings plus `.mcp.json` / `.claude.json`, maps model and sandbox
settings when there is a known Codex equivalent, and renders MCP server entries
for `.codex/config.toml`. Header/env forms are partially normalized to Codex
`bearer_token_env_var`, `env_http_headers`, `http_headers`, `env_vars`, and
literal `env` tables.
"""
from __future__ import annotations
import json
import re
import shutil
from collections.abc import Mapping
from pathlib import Path
from migrate.common import (
CODEX_CONFIG_PATH,
MigrationReportItem,
json_object,
json_string,
json_string_tuple,
path_exists_with_exact_case,
)
from migrate.settings import CLAUDE_MCP_JSON_RELATIVE
from utils.util import TomlValue
ENV_VAR_RE = re.compile(r"\A\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-[^}]*)?\}\Z")
BEARER_ENV_VAR_RE = re.compile(
r"\ABearer\s+\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-[^}]*)?\}\Z"
)
def mcp_server_toml_table(
server_name: str,
server_config: Mapping[str, object],
enabled_servers: tuple[str, ...],
disabled_servers: frozenset[str],
) -> dict[str, TomlValue]:
table: dict[str, TomlValue] = {}
enabled = mcp_enabled_state(server_config)
if enabled is False:
table["enabled"] = False
elif enabled_servers and server_name not in enabled_servers:
table["enabled"] = False
elif server_name in disabled_servers:
table["enabled"] = False
if url := json_string(server_config.get("url")):
table["url"] = url
if command := json_string(server_config.get("command")):
table["command"] = command
if args := json_string_tuple(server_config.get("args")):
table["args"] = list(args)
if "headers" in server_config:
append_header_config(table, json_object(server_config["headers"]))
if "env" in server_config:
append_env_config(table, json_object(server_config["env"]))
return table
def mcp_report_items(
mcp_servers: tuple[tuple[str, dict[str, object]], ...],
) -> list[MigrationReportItem]:
report_items = [
MigrationReportItem(
"rewritten",
CODEX_CONFIG_PATH,
f"Converted {len(mcp_servers)} MCP server entries.",
)
]
for server_name, server_config in mcp_servers:
notes = mcp_manual_notes(server_name, server_config)
if notes:
report_items.append(
MigrationReportItem(
"manual_fix_required",
CODEX_CONFIG_PATH,
f"MCP server `{server_name}` needs review: {' '.join(notes)}",
)
)
return report_items
def append_header_config(
table: dict[str, TomlValue],
headers: Mapping[str, object],
) -> None:
static_headers: dict[str, str] = {}
env_headers: dict[str, str] = {}
for key, value in headers.items():
header_value = str(value)
bearer_match = BEARER_ENV_VAR_RE.match(header_value)
if key.lower() == "authorization" and bearer_match:
table["bearer_token_env_var"] = bearer_match.group(1)
continue
env_match = ENV_VAR_RE.match(header_value)
if env_match:
env_headers[key] = env_match.group(1)
continue
static_headers[key] = header_value
if static_headers:
table["http_headers"] = static_headers
if env_headers:
table["env_http_headers"] = env_headers
def append_env_config(
table: dict[str, TomlValue],
env: Mapping[str, object],
) -> None:
static_env: dict[str, str] = {}
env_vars: list[str] = []
for key, value in env.items():
env_value = str(value)
env_match = ENV_VAR_RE.match(env_value)
if env_match and env_match.group(1) == key:
env_vars.append(key)
continue
static_env[key] = env_value
if env_vars:
table["env_vars"] = env_vars
if static_env:
table["env"] = static_env
def mcp_manual_notes(
server_name: str,
server_config: Mapping[str, object],
) -> tuple[str, ...]:
notes: list[str] = []
source_type = json_string(server_config.get("type"))
if source_type and source_type not in {"http", "stdio"}:
notes.append(
f"Claude MCP `type: {source_type}` was not written to Codex config; verify that the generated `url` or `command` is a supported Codex transport."
)
unsupported_fields = unsupported_mcp_server_fields(server_config)
if unsupported_fields:
notes.append(
"Review unsupported Claude MCP fields manually: "
+ ", ".join(f"`{field_name}`" for field_name in unsupported_fields)
+ "."
)
return tuple(notes)
def mcp_enabled_state(server_config: Mapping[str, object]) -> bool | None:
if server_config.get("enabled") is False:
return False
if server_config.get("disabled") is True:
return False
return None
def unsupported_mcp_server_fields(
server_config: Mapping[str, object],
) -> tuple[str, ...]:
supported = {
"args",
"command",
"disabled",
"enabled",
"env",
"headers",
"name",
"scope",
"type",
"url",
}
return tuple(sorted(key for key in server_config if key not in supported))
def read_claude_mcp_servers(source_root: Path) -> tuple[tuple[str, dict[str, object]], ...]:
servers: list[tuple[str, dict[str, object]]] = []
for relative_path in CLAUDE_MCP_JSON_RELATIVE:
source_file = source_root / relative_path
if not path_exists_with_exact_case(source_file):
continue
mcp_config = json_object(json.loads(source_file.read_text()))
for server_name, server_config in json_object(mcp_config.get("mcpServers")).items():
servers.append((server_name, json_object(server_config)))
return tuple(servers)
def validate_mcp_commands(config: dict[str, object]) -> list[MigrationReportItem]:
mcp_servers = config.get("mcp_servers")
if not isinstance(mcp_servers, dict):
return []
report_items: list[MigrationReportItem] = []
for server_name, server_config in sorted(mcp_servers.items()):
if not isinstance(server_config, dict):
continue
command = server_config.get("command")
if not command:
continue
command_text = str(command)
if shutil.which(command_text):
report_items.append(
MigrationReportItem(
"ok",
CODEX_CONFIG_PATH,
f"MCP server `{server_name}` command `{command_text}` is on PATH.",
)
)
else:
report_items.append(
MigrationReportItem(
"warning",
CODEX_CONFIG_PATH,
f"MCP server `{server_name}` command `{command_text}` was not found on PATH.",
)
)
return report_items