codex-mcp-eraser 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codex_mcp_eraser/__init__.py +3 -0
- codex_mcp_eraser/__main__.py +8 -0
- codex_mcp_eraser/cli.py +153 -0
- codex_mcp_eraser-0.0.1.dist-info/METADATA +43 -0
- codex_mcp_eraser-0.0.1.dist-info/RECORD +8 -0
- codex_mcp_eraser-0.0.1.dist-info/WHEEL +5 -0
- codex_mcp_eraser-0.0.1.dist-info/entry_points.txt +2 -0
- codex_mcp_eraser-0.0.1.dist-info/top_level.txt +1 -0
codex_mcp_eraser/cli.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Command-line interface for safely removing Codex MCP servers and hooks."""
|
|
2
|
+
import argparse
|
|
3
|
+
import datetime as dt
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import tomllib
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
HEADER = re.compile(r'^\s*\[\[?\s*(.*?)\s*\]\]?\s*(?:#.*)?$')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def toml_path(header):
|
|
17
|
+
"""Divide TOML table paths while respecting quoted segments."""
|
|
18
|
+
result, part, quote, escape = [], '', None, False
|
|
19
|
+
for char in header:
|
|
20
|
+
if escape:
|
|
21
|
+
part += char
|
|
22
|
+
escape = False
|
|
23
|
+
elif quote == '"' and char == '\\':
|
|
24
|
+
part += char
|
|
25
|
+
escape = True
|
|
26
|
+
elif quote:
|
|
27
|
+
if char == quote:
|
|
28
|
+
quote = None
|
|
29
|
+
else:
|
|
30
|
+
part += char
|
|
31
|
+
elif char in ('"', "'"):
|
|
32
|
+
quote = char
|
|
33
|
+
elif char == '.':
|
|
34
|
+
result.append(part.strip())
|
|
35
|
+
part = ''
|
|
36
|
+
else:
|
|
37
|
+
part += char
|
|
38
|
+
if quote:
|
|
39
|
+
raise ValueError('Cabecera TOML con comillas incompletas')
|
|
40
|
+
result.append(part.strip())
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def transform(content):
|
|
45
|
+
original = tomllib.loads(content)
|
|
46
|
+
lines = content.splitlines(keepends=True)
|
|
47
|
+
keep, section, removed = [], (), set()
|
|
48
|
+
# Table headers are the only safe way to selectively edit these settings.
|
|
49
|
+
for line in lines:
|
|
50
|
+
match = HEADER.match(line.rstrip('\r\n'))
|
|
51
|
+
if match:
|
|
52
|
+
section = tuple(toml_path(match.group(1)))
|
|
53
|
+
drop = bool(section and section[0] in ('mcp_servers', 'hooks'))
|
|
54
|
+
if drop:
|
|
55
|
+
removed.add(section[0])
|
|
56
|
+
else:
|
|
57
|
+
keep.append(line)
|
|
58
|
+
result = ''.join(keep)
|
|
59
|
+
updated = tomllib.loads(result)
|
|
60
|
+
# Stop without changes if an unsupported representation remains.
|
|
61
|
+
if 'mcp_servers' in updated or 'hooks' in updated:
|
|
62
|
+
raise ValueError('Hay MCP/hooks fuera de tablas TOML editables; se requiere revisión manual')
|
|
63
|
+
if any(key in original for key in ('mcp_servers', 'hooks')) and not removed:
|
|
64
|
+
raise ValueError('No se localizaron tablas de MCP/hooks para eliminar con seguridad')
|
|
65
|
+
return result, removed
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def atomic_write(path, content):
|
|
69
|
+
stat = path.stat()
|
|
70
|
+
with tempfile.NamedTemporaryFile('w', encoding='utf-8', dir=path.parent,
|
|
71
|
+
prefix='.codex-reset-', delete=False) as tmp:
|
|
72
|
+
tmp.write(content)
|
|
73
|
+
tmp_path = Path(tmp.name)
|
|
74
|
+
try:
|
|
75
|
+
os.chmod(tmp_path, stat.st_mode & 0o777)
|
|
76
|
+
os.replace(tmp_path, path)
|
|
77
|
+
finally:
|
|
78
|
+
if tmp_path.exists():
|
|
79
|
+
tmp_path.unlink()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _run():
|
|
83
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
84
|
+
parser.add_argument('--dry-run', action='store_true', help='Solo muestra los cambios')
|
|
85
|
+
parser.add_argument('--apply', action='store_true', help='Aplica cambios tras confirmar')
|
|
86
|
+
parser.add_argument('--yes', action='store_true', help='Omite la confirmación (requiere --apply)')
|
|
87
|
+
parser.add_argument('--project', type=Path, help='Incluye .codex/ del proyecto indicado')
|
|
88
|
+
parser.add_argument('--codex-home', type=Path,
|
|
89
|
+
default=Path(os.environ.get('CODEX_HOME', str(Path.home() / '.codex'))),
|
|
90
|
+
help='Directorio de configuración del usuario de Codex')
|
|
91
|
+
args = parser.parse_args()
|
|
92
|
+
if args.apply and args.dry_run:
|
|
93
|
+
parser.error('--apply y --dry-run son incompatibles')
|
|
94
|
+
if args.yes and not args.apply:
|
|
95
|
+
parser.error('--yes requiere --apply')
|
|
96
|
+
|
|
97
|
+
dirs = [args.codex_home.expanduser().resolve()]
|
|
98
|
+
if args.project:
|
|
99
|
+
dirs.append((args.project.expanduser().resolve() / '.codex'))
|
|
100
|
+
targets = []
|
|
101
|
+
print('Modo:', 'APLICAR' if args.apply else 'DRY-RUN (sin cambios)')
|
|
102
|
+
for directory in dict.fromkeys(dirs):
|
|
103
|
+
config = directory / 'config.toml'
|
|
104
|
+
hooks = directory / 'hooks.json'
|
|
105
|
+
if config.is_symlink() or hooks.is_symlink():
|
|
106
|
+
raise RuntimeError(f'Enlaces simbólicos no admitidos: {directory}')
|
|
107
|
+
if config.is_file():
|
|
108
|
+
content = config.read_text(encoding='utf-8')
|
|
109
|
+
changed, sections = transform(content)
|
|
110
|
+
if changed != content:
|
|
111
|
+
targets.append((config, changed))
|
|
112
|
+
print(f' EDITAR {config}: quitar tablas {", ".join(sorted(sections))}')
|
|
113
|
+
if hooks.is_file():
|
|
114
|
+
targets.append((hooks, None))
|
|
115
|
+
print(f' RETIRAR {hooks}: hooks de usuario/proyecto')
|
|
116
|
+
if not targets:
|
|
117
|
+
print('No hay cambios en las ubicaciones revisadas.')
|
|
118
|
+
return 0
|
|
119
|
+
if not args.apply:
|
|
120
|
+
print('Para ejecutar: codex-mcp-eraser --apply')
|
|
121
|
+
return 0
|
|
122
|
+
if not args.yes:
|
|
123
|
+
answer = input('Escribe RESTABLECER para continuar: ')
|
|
124
|
+
if answer != 'RESTABLECER':
|
|
125
|
+
print('Cancelado, sin cambios.')
|
|
126
|
+
return 1
|
|
127
|
+
stamp = dt.datetime.now().strftime('%Y%m%d-%H%M%S-%f')
|
|
128
|
+
# Keep backups under Codex home, away from external credentials.
|
|
129
|
+
backup = dirs[0] / 'backups' / f'mcp-hooks-reset-{stamp}'
|
|
130
|
+
backup.mkdir(parents=True, mode=0o700, exist_ok=False)
|
|
131
|
+
os.chmod(backup, 0o700)
|
|
132
|
+
for i, (path, _) in enumerate(targets):
|
|
133
|
+
dst = backup / f'{i:02d}-{path.name}'
|
|
134
|
+
shutil.copy2(path, dst)
|
|
135
|
+
os.chmod(dst, 0o600)
|
|
136
|
+
print(f' RESPALDO {path} -> {dst}')
|
|
137
|
+
for path, content in targets:
|
|
138
|
+
if content is None:
|
|
139
|
+
path.unlink()
|
|
140
|
+
else:
|
|
141
|
+
atomic_write(path, content)
|
|
142
|
+
print(f'Listo. Copia de seguridad: {backup}')
|
|
143
|
+
print('Reinicia Codex. Los hooks/MCP de plugins o configuración administrada no se modificaron.')
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main():
|
|
148
|
+
"""Run the CLI and convert operational errors into a concise exit status."""
|
|
149
|
+
try:
|
|
150
|
+
return _run()
|
|
151
|
+
except (OSError, ValueError, RuntimeError, tomllib.TOMLDecodeError) as exc:
|
|
152
|
+
print(f'ERROR: {exc}', file=sys.stderr)
|
|
153
|
+
return 2
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codex-mcp-eraser
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Safely remove user-configured MCP servers and hooks from Codex CLI
|
|
5
|
+
Classifier: Environment :: Console
|
|
6
|
+
Classifier: Operating System :: OS Independent
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# codex-mcp-eraser
|
|
16
|
+
|
|
17
|
+
A Python command-line tool that removes user-configured MCP server tables and
|
|
18
|
+
hooks from Codex CLI configuration. It does not modify plugin settings or
|
|
19
|
+
managed policies.
|
|
20
|
+
|
|
21
|
+
Requires Python 3.11 or later.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
python -m pip install codex-mcp-eraser
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Use
|
|
30
|
+
|
|
31
|
+
The default is a dry run. Review the paths and changes before applying them:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
codex-mcp-eraser
|
|
35
|
+
codex-mcp-eraser --apply
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Use `--yes` with `--apply` to skip the confirmation prompt. `--project PATH`
|
|
39
|
+
also checks that project's `.codex/` directory. `--codex-home PATH` overrides
|
|
40
|
+
the user configuration directory; otherwise `CODEX_HOME` or `~/.codex` is used.
|
|
41
|
+
|
|
42
|
+
Before changing or removing files, the tool creates a permission-restricted
|
|
43
|
+
backup under the Codex home directory.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
codex_mcp_eraser/__init__.py,sha256=dwvLl3zJkCCUUSEQCXR6j6qpxK_PNjdePo4pqLCymik,89
|
|
2
|
+
codex_mcp_eraser/__main__.py,sha256=SCMXvbQ9yMHeNPEOCvQhybPG6lLGuwaqiQjN3wVeWlc,123
|
|
3
|
+
codex_mcp_eraser/cli.py,sha256=M6O_7-8zvoyRXBO2mzyBcJfw5krfhnShX9JRlmU2Ub8,5816
|
|
4
|
+
codex_mcp_eraser-0.0.1.dist-info/METADATA,sha256=G0Jn-6A15CpHF3G1TElvgk3XgQIBSOdWkZi15vf_alM,1314
|
|
5
|
+
codex_mcp_eraser-0.0.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
codex_mcp_eraser-0.0.1.dist-info/entry_points.txt,sha256=qvI2QuqHQICIXdwYNc4Nwv_zPdkz7cXI9xVqg2BFXwA,63
|
|
7
|
+
codex_mcp_eraser-0.0.1.dist-info/top_level.txt,sha256=v3IUS2m0NAmC2aSIKQnAJRc8ab5N5rUR02ZuyfidsQw,17
|
|
8
|
+
codex_mcp_eraser-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
codex_mcp_eraser
|