HEX
Server: Apache/2.4.46 (Win64) OpenSSL/1.1.1j PHP/8.4.25
System: Windows NT DESKTOP-4TAV2RJ 10.0 build 19045 (Windows 10) AMD64
User: fred (0)
PHP: 8.4.25
Disabled: NONE
Upload Files
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