Skip to content

Instantly share code, notes, and snippets.

@nickdavis
Created August 2, 2026 09:21
Show Gist options
  • Select an option

  • Save nickdavis/784179df488a2d9fd2a4d917e201ea03 to your computer and use it in GitHub Desktop.

Select an option

Save nickdavis/784179df488a2d9fd2a4d917e201ea03 to your computer and use it in GitHub Desktop.
Restore Codex MCP Tools in RepoPrompt CE 1.1.1+ without copying the full Codex home

Restore Codex MCP Tools in RepoPrompt CE 1.1.1+

Tested with RepoPrompt CE 1.1.1 and its bundled Codex 0.145.0 on macOS. This also applies to later RepoPrompt CE releases while they continue to run Codex with a separate CODEX_HOME. A future release may provide a built-in import/sync feature, so check the app's release notes first.

Symptom

MCP servers work in Codex CLI or the Codex desktop app, but RepoPrompt CE's Tools → MCP Servers list shows only RepoPrompt CE and a few app-specific entries.

Why it happens

RepoPrompt CE intentionally keeps its Codex state separate from the normal Codex home:

Normal Codex:    ~/.codex
RepoPrompt CE:   ~/Library/Application Support/RepoPrompt CE/Codex/Release/home

RepoPrompt CE reads MCP definitions from its own config.toml; it does not automatically merge the [mcp_servers.*] tables from ~/.codex/config.toml.

This isolation is useful. The two homes contain different sessions, databases, locks, installation state, app-specific configuration, and RepoPrompt integration settings. Do not copy or symlink the whole ~/.codex directory or either complete Codex home.

Safe fix: merge only MCP tables

Use RepoPrompt CE's config as the base and add only missing keys under [mcp_servers] from the normal Codex config.

Preserve all RepoPrompt CE-owned settings, especially:

  • its complete RepoPromptCE MCP definition;
  • Code Mode and other feature settings;
  • project trust and tool-output settings;
  • app-specific MCPs that do not exist in the normal Codex config.

Do not import plugins, skills, prompts, auth files, secrets, sessions, databases, caches, browser state, or other top-level configuration.

The companion merge_repoprompt_ce_mcp.py script performs a missing-key-only merge. Existing RepoPrompt CE values always win. It defaults to a dry run, creates a timestamped backup before applying, writes atomically, and keeps the target mode at 0600.

Set up a temporary environment:

python3 -m venv /tmp/rpce-mcp-merge-venv
/tmp/rpce-mcp-merge-venv/bin/pip install tomlkit

Download both files from this gist, then preview the merge:

/tmp/rpce-mcp-merge-venv/bin/python merge_repoprompt_ce_mcp.py

The script prints key paths only, never values. Review that list. Save your work and quit RepoPrompt CE before applying:

/tmp/rpce-mcp-merge-venv/bin/python merge_repoprompt_ce_mcp.py --apply

Reopen RepoPrompt CE. Newly imported third-party MCPs may appear disabled in the Tools permission panel; enable only the servers needed for the current task.

Verify the catalog

On Apple Silicon, RepoPrompt CE 1.1.1's bundled binary is normally here:

RPCE_HOME="$HOME/Library/Application Support/RepoPrompt CE/Codex/Release/home"
RPCE_CODEX="/Applications/RepoPrompt CE.app/Contents/Resources/BundledRuntimes/Codex/aarch64-apple-darwin/bin/codex"
CODEX_HOME="$RPCE_HOME" "$RPCE_CODEX" mcp list --json

If that binary path differs, locate the bin/codex executable beneath:

/Applications/RepoPrompt CE.app/Contents/Resources/BundledRuntimes/Codex/

The expected server set is the union of:

  1. the normal Codex [mcp_servers.*] definitions; and
  2. RepoPrompt CE-owned definitions that were already present.

Do not compare against somebody else's server count; MCP catalogs are user-specific.

OAuth-backed MCPs

Copying a server definition does not necessarily copy its authorization. Do not copy Codex databases, auth.json, Keychain secrets, token files, or the whole ~/.mcp-auth directory.

For a connector using mcp-remote, both Codex environments should use the same connector-scoped auth directory when the connector is intentionally shared. If the normal Codex server already has an environment table like this, the merge script adds the missing table to RepoPrompt CE without replacing its server command:

[mcp_servers.google_drive_personal.env]
MCP_REMOTE_CONFIG_DIR = "/Users/YOUR_USERNAME/.mcp-auth/google_drive_personal"

Use an absolute path; replace YOUR_USERNAME. Keep each connector in its own directory.

On the first protected read, one browser approval may be legitimate. After approval:

  1. confirm the OAuth callback listener exists on the connector's configured port during authorization;
  2. confirm completed token state appears only in the connector-scoped directory;
  3. perform a second harmless read and verify that it does not request authorization again.

If approval redirects to localhost with ERR_CONNECTION_REFUSED, or the browser repeatedly asks for consent, stop approving. That indicates a callback/listener or state-coordination failure, not missing Google permissions. Inspect the exact connector process and callback port. Never use broad commands such as pkill node, and never delete all of ~/.mcp-auth.

Some deferred-auth servers need a connector-specific wrapper or upstream fix to start the callback listener when the first protected tool call returns 401. A table merge alone cannot repair a broken OAuth implementation.

Rollback

Quit RepoPrompt CE, restore the timestamped config.toml.pre-mcp-merge-* backup printed by the script, verify mode 0600, and reopen the app. The merge does not modify the normal Codex config.

Security checklist

  • Never publish either real config.toml; MCP tables may contain commands, account identifiers, environment values, or secret locations.
  • Never publish OAuth URLs, tokens, verifier values, client files, debug logs, or callback query strings.
  • Keep backups and generated configs mode 0600.
  • Merge configuration, not runtime state.
#!/usr/bin/env python3
"""Merge missing Codex MCP settings into RepoPrompt CE's isolated config."""
from __future__ import annotations
import argparse
import copy
import datetime as dt
import os
from pathlib import Path
import shutil
import tempfile
from typing import Any, MutableMapping
try:
import tomlkit
except ImportError as exc: # pragma: no cover - friendly CLI failure
raise SystemExit(
"tomlkit is required. Install it in a temporary virtual environment; "
"see README.md."
) from exc
DEFAULT_SOURCE = Path.home() / ".codex" / "config.toml"
DEFAULT_TARGET = (
Path.home()
/ "Library"
/ "Application Support"
/ "RepoPrompt CE"
/ "Codex"
/ "Release"
/ "home"
/ "config.toml"
)
def parse_config(path: Path) -> Any:
if not path.is_file() or path.is_symlink():
raise SystemExit(f"Expected a regular config file: {path}")
try:
return tomlkit.parse(path.read_text(encoding="utf-8"))
except Exception as exc:
raise SystemExit(f"Could not parse {path}: {exc}") from exc
def merge_missing(
destination: MutableMapping[str, Any],
source: MutableMapping[str, Any],
prefix: tuple[str, ...],
added: list[str],
) -> None:
"""Recursively add missing keys; never replace destination values."""
for key, source_value in source.items():
path = (*prefix, str(key))
if key not in destination:
destination[key] = copy.deepcopy(source_value)
added.append(".".join(path))
continue
destination_value = destination[key]
if isinstance(destination_value, MutableMapping) and isinstance(
source_value, MutableMapping
):
merge_missing(destination_value, source_value, path, added)
def atomic_write(target: Path, rendered: str) -> Path:
stamp = dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
backup = target.with_name(f"{target.name}.pre-mcp-merge-{stamp}")
if backup.exists():
raise SystemExit(f"Refusing to replace existing backup: {backup}")
shutil.copy2(target, backup)
os.chmod(backup, 0o600)
temporary_name: str | None = None
try:
with tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
dir=target.parent,
prefix=f".{target.name}.merge-",
delete=False,
) as temporary:
temporary.write(rendered)
temporary.flush()
os.fsync(temporary.fileno())
temporary_name = temporary.name
os.chmod(temporary_name, 0o600)
os.replace(temporary_name, target)
temporary_name = None
finally:
if temporary_name is not None:
Path(temporary_name).unlink(missing_ok=True)
return backup
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE)
parser.add_argument("--target", type=Path, default=DEFAULT_TARGET)
parser.add_argument(
"--apply",
action="store_true",
help="Create a backup and atomically update the target; default is dry-run.",
)
args = parser.parse_args()
source_path = args.source.expanduser().resolve()
target_path = args.target.expanduser().resolve()
if source_path == target_path:
raise SystemExit("Source and target must be different files")
source = parse_config(source_path)
target = parse_config(target_path)
source_mcp = source.get("mcp_servers")
target_mcp = target.get("mcp_servers")
if not isinstance(source_mcp, MutableMapping):
raise SystemExit(f"No [mcp_servers] table found in {source_path}")
if not isinstance(target_mcp, MutableMapping):
raise SystemExit(f"No [mcp_servers] table found in {target_path}")
added: list[str] = []
merge_missing(target_mcp, source_mcp, ("mcp_servers",), added)
rendered = tomlkit.dumps(target)
tomlkit.parse(rendered) # Refuse to write output that cannot be reparsed.
if not added:
print("No missing MCP keys found; target left unchanged.")
return 0
print("Missing key paths that would be added:")
for path in sorted(added):
print(f" {path}")
if not args.apply:
print("Dry run only. Re-run with --apply after reviewing the key paths.")
return 0
backup = atomic_write(target_path, rendered)
print(f"Updated: {target_path}")
print(f"Backup: {backup}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment