Skip to content

Commit 58fd3ae

Browse files
fix(tools): stop the MCP repo generator from overwriting evolved repos
create_mcp_repos.py was written as a one-time bootstrap and was never guarded against re-runs. Running it against an existing repo replaced server.py, deleted and recreated base/, rewrote the generated docs, committed the result as "initial commit", and pushed it to origin. History survived -- it is an ordinary commit on top, not a force-push -- but HEAD moved to the stale generated version. Measured 2026-08-10: all 13 target repos have evolved past their generated state, 75 commits and roughly 29 merged PRs between them. The blast radius was the whole MCP fleet, not a single repo. Existing repos are now skipped unless --force is passed, and a plain run exits 1 instead of reporting success, so re-running cannot look like a no-op. --dry-run reports the decision for every target and writes nothing. mcp-session-mgr is additionally marked canonical and is never regenerated even with --force. It is what settings.json runs, and it leads the in-engine copy by mcp 2.x support, an O_CREAT|O_EXCL claim-file mutex and a merged path-traversal fix -- all three of which regeneration would silently revert. Verified: a plain run writes nothing (repo HEADs and working trees identical before and after) and exits 1; --dry-run exits 0; the canonical guard holds under --force. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 1030325 commit 58fd3ae

1 file changed

Lines changed: 180 additions & 21 deletions

File tree

‎scripts/tools/create_mcp_repos.py‎

Lines changed: 180 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,26 @@
99
- Is private, with main as default branch
1010
1111
Usage:
12-
python scripts/create_mcp_repos.py
12+
python scripts/tools/create_mcp_repos.py --dry-run # report only, write nothing
13+
python scripts/tools/create_mcp_repos.py # bootstrap missing repos only
14+
python scripts/tools/create_mcp_repos.py --force # also overwrite existing repos
15+
16+
Safety:
17+
This script was written as a one-time bootstrap and is destructive when re-run.
18+
Writing into an existing repo replaces server.py and base/, rewrites the
19+
generated docs, commits the result as "initial commit", and pushes it to
20+
origin. History survives (it is an ordinary commit on top, not a force-push)
21+
but HEAD moves to the stale generated version.
22+
23+
Existing repos are therefore skipped unless --force is passed, and repos in
24+
CANONICAL_REPOS are never written to at all. See _guard_existing_repo.
1325
1426
Prerequisites:
1527
- gh CLI authenticated (gh auth status)
1628
- Git configured with user name/email
1729
"""
1830

31+
import argparse
1932
import shlex
2033
import shutil
2134
import subprocess
@@ -440,6 +453,23 @@ def display_path(*parts):
440453
# -- Shared utility files to copy into each server repo -----------------------
441454
SHARED_UTILS = ["mcp_errors.py", "input_validator.py", "rate_limiter.py"]
442455

456+
# -- Regeneration safety -------------------------------------------------------
457+
# Measured 2026-08-10: all 13 target repos have evolved past their generated
458+
# state -- 75 commits and roughly 29 merged PRs between them. Regeneration is
459+
# therefore a destructive operation against real work, not a refresh.
460+
#
461+
# CANONICAL_REPOS are never written to, with or without --force, because the
462+
# repo is ahead of the source this script would copy from. Removing a name from
463+
# this set means asserting that src/mcp/ is once again the better version.
464+
CANONICAL_REPOS = {
465+
# Authority for the session MCP surface. It is what settings.json runs, and
466+
# it leads the in-engine copy by mcp 2.x support, an O_CREAT|O_EXCL
467+
# claim-file mutex, and a merged path-traversal fix. Regenerating it from
468+
# src/mcp/session_mcp_server.py would silently revert all three.
469+
# See docs/policies/session-management-policy.md.
470+
"mcp-session-mgr",
471+
}
472+
443473
# -- .gitignore template -------------------------------------------------------
444474
GITIGNORE = """__pycache__/
445475
*.py[cod]
@@ -758,14 +788,90 @@ def write_file(path, content):
758788
path.write_text(content, encoding="utf-8")
759789

760790

791+
def _commit_count(repo_dir):
792+
"""Count commits on the current branch of a repo.
793+
794+
Args:
795+
repo_dir: Path to a directory that may or may not be a git repo.
796+
797+
Returns:
798+
int: Commit count, or 0 when the directory is not a readable git repo.
799+
"""
800+
result = subprocess.run(
801+
["git", "rev-list", "--count", "HEAD"],
802+
shell=False,
803+
cwd=str(repo_dir),
804+
capture_output=True,
805+
text=True,
806+
)
807+
if result.returncode != 0:
808+
return 0
809+
try:
810+
return int(result.stdout.strip())
811+
except ValueError:
812+
return 0
813+
814+
815+
def _guard_existing_repo(repo_name, repo_dir, force):
816+
"""Decide whether this run may write into a target repo.
817+
818+
Bootstrapping a repo that does not exist yet is always safe. Writing into
819+
one that does is not: it replaces server.py and base/, rewrites the
820+
generated docs, commits the result, and pushes it. The default is therefore
821+
to refuse, so that a routine run cannot silently revert downstream work.
822+
823+
Args:
824+
repo_name: Repo name, matched against CANONICAL_REPOS.
825+
repo_dir: Path the repo would be written to.
826+
force: True when the caller passed --force.
827+
828+
Returns:
829+
bool: True when the caller may proceed, False when it must skip.
830+
"""
831+
if repo_name in CANONICAL_REPOS:
832+
print(f" REFUSED: {repo_name} is canonical -- it leads src/mcp/ and is never regenerated.")
833+
print(" Remove it from CANONICAL_REPOS only if src/mcp/ is genuinely ahead.")
834+
return False
835+
836+
if not repo_dir.exists():
837+
return True
838+
839+
commits = _commit_count(repo_dir)
840+
if not force:
841+
print(f" REFUSED: {repo_dir.name} already exists ({commits} commits).")
842+
print(" Overwriting replaces server.py and base/, then commits and pushes.")
843+
print(" Pass --force if you intend to discard what is there.")
844+
return False
845+
846+
print(f" FORCED: overwriting existing {repo_dir.name} ({commits} commits will be superseded).")
847+
return True
848+
849+
761850
# -- Main ----------------------------------------------------------------------
762-
def create_repo(server):
851+
def create_repo(server, force=False, dry_run=False):
852+
"""Generate one MCP server repo, refusing to overwrite existing work.
853+
854+
Args:
855+
server: Entry from SERVERS describing the repo to generate.
856+
force: Allow writing into a repo that already exists.
857+
dry_run: Report the decision and write nothing.
858+
859+
Returns:
860+
bool: True when the repo was written, False when it was skipped.
861+
"""
763862
repo_name = server["repo"]
764863
repo_dir = WORKSPACE / repo_name
765864
print(f"\n{'='*60}")
766865
print(f"Creating: {repo_name}")
767866
print(f"{'='*60}")
768867

868+
if not _guard_existing_repo(repo_name, repo_dir, force):
869+
return False
870+
871+
if dry_run:
872+
print(f" DRY RUN: would generate into {repo_dir}")
873+
return False
874+
769875
# 1. Create directory
770876
if repo_dir.exists():
771877
print(" Directory already exists, skipping mkdir")
@@ -841,14 +947,29 @@ def create_repo(server):
841947
return True
842948

843949

844-
def create_mcp_base_repo():
845-
"""Create the shared mcp-base repo."""
950+
def create_mcp_base_repo(force=False, dry_run=False):
951+
"""Create the shared mcp-base repo, refusing to overwrite existing work.
952+
953+
Args:
954+
force: Allow writing into a repo that already exists.
955+
dry_run: Report the decision and write nothing.
956+
957+
Returns:
958+
bool: True when the repo was written, False when it was skipped.
959+
"""
846960
repo_name = "mcp-base"
847961
repo_dir = WORKSPACE / repo_name
848962
print(f"\n{'='*60}")
849963
print(f"Creating: {repo_name} (shared base package)")
850964
print(f"{'='*60}")
851965

966+
if not _guard_existing_repo(repo_name, repo_dir, force):
967+
return False
968+
969+
if dry_run:
970+
print(f" DRY RUN: would generate into {repo_dir}")
971+
return False
972+
852973
repo_dir.mkdir(parents=True, exist_ok=True)
853974

854975
# Copy base/ package as mcp_base/ package
@@ -1028,38 +1149,76 @@ def create_mcp_base_repo():
10281149

10291150

10301151
def main():
1152+
"""Generate the MCP server repos, skipping any that already exist.
1153+
1154+
Returns:
1155+
int: 0 when every target was written or the run was a dry run, 1 when
1156+
any target was refused or failed. A plain run against an already
1157+
bootstrapped workspace refuses everything and exits 1 on purpose,
1158+
so that re-running this script cannot look like a success.
1159+
"""
1160+
parser = argparse.ArgumentParser(description="Create the techdeveloper-org MCP server repos.")
1161+
parser.add_argument(
1162+
"--force",
1163+
action="store_true",
1164+
help="Overwrite repos that already exist. Discards downstream work; see the module docstring.",
1165+
)
1166+
parser.add_argument(
1167+
"--dry-run",
1168+
action="store_true",
1169+
help="Report what each target would do and write nothing.",
1170+
)
1171+
args = parser.parse_args()
1172+
10311173
print("Claude Workflow Engine -- MCP Repos Creator")
10321174
print(f"Organization: {ORG}")
10331175
print(f"Workspace: {WORKSPACE}")
1034-
print(f"Total repos to create: {len(SERVERS) + 1} (1 base + {len(SERVERS)} servers)")
1035-
1036-
# Create mcp-base first
1037-
create_mcp_base_repo()
1176+
print(f"Targets: {len(SERVERS) + 1} (1 base + {len(SERVERS)} servers)")
1177+
if args.dry_run:
1178+
print("Mode: DRY RUN -- nothing will be written")
1179+
elif args.force:
1180+
print("Mode: FORCE -- existing repos will be overwritten, committed and pushed")
1181+
else:
1182+
print("Mode: bootstrap only -- existing repos will be skipped")
10381183

1039-
# Create all server repos
1040-
success_count = 0
1184+
written = []
1185+
skipped = []
10411186
failed = []
1187+
1188+
if create_mcp_base_repo(force=args.force, dry_run=args.dry_run):
1189+
written.append("mcp-base")
1190+
else:
1191+
skipped.append("mcp-base")
1192+
10421193
for server in SERVERS:
10431194
try:
1044-
create_repo(server)
1045-
success_count += 1
1195+
if create_repo(server, force=args.force, dry_run=args.dry_run):
1196+
written.append(server["repo"])
1197+
else:
1198+
skipped.append(server["repo"])
10461199
except Exception as exc:
10471200
print(f" FAILED {server['repo']}: {exc}")
10481201
failed.append(server["repo"])
10491202

10501203
print(f"\n{'='*60}")
1051-
print(f"COMPLETE: {success_count + 1}/{len(SERVERS) + 1} repos created")
1204+
print(f"written: {len(written)} skipped: {len(skipped)} failed: {len(failed)}")
1205+
if skipped:
1206+
print(f"SKIPPED: {skipped}")
10521207
if failed:
10531208
print(f"FAILED: {failed}")
1054-
else:
1055-
print("All repos created successfully!")
10561209

1057-
# Print settings.json update instructions
1058-
print("\n--- settings.json paths to update ---")
1059-
for server in SERVERS:
1060-
new_path = str(WORKSPACE / server["repo"] / "server.py").replace("\\", "/")
1061-
print(f' "{server["repo"].replace("mcp-", "")}": "{new_path}"')
1210+
if written:
1211+
print("\n--- settings.json paths to update ---")
1212+
for server in SERVERS:
1213+
if server["repo"] not in written:
1214+
continue
1215+
new_path = str(WORKSPACE / server["repo"] / "server.py").replace("\\", "/")
1216+
print(f' "{server["repo"].replace("mcp-", "")}": "{new_path}"')
1217+
1218+
if args.dry_run:
1219+
return 0
1220+
return 1 if (skipped or failed) else 0
10621221

10631222

10641223
if __name__ == "__main__":
1065-
main()
1224+
sys.exit(main())

0 commit comments

Comments
 (0)