Skip to main content
Glama

GitHub Repository Creator

by flickleafy
setup.py•17.9 kB
#!/usr/bin/env python3 """ Copyright (C) 2025 flickleafy This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. setup.py - Setup and VS Code MCP integration for mcp-github-repo-creator Features: - Create Python virtualenv and install requirements - Integrate MCP server into VS Code mcp.json config (safe JSONC merge) - Backup VS Code mcp.json before changes and maintain a manifest - Revert functionality to restore configs from backups Usage: python setup.py # Install + integrate python setup.py --revert # Revert changes using the latest manifest Options: --venv-dir VENV Path to virtual environment directory (default: ./venv) --requirements FILE Requirements file (default: ./requirements.txt) --no-extensions Skip installing VS Code Copilot extensions --dry-run Show planned changes without writing --manifest FILE Custom manifest path to use for revert --keep-backups Keep backups after successful revert --include-missing Also create mcp.json for VS Code variants that don't have one yet """ from __future__ import annotations import argparse import datetime as _dt import json import os import platform import re import shutil import subprocess import sys import venv as _venv from pathlib import Path from typing import Dict, List, Optional, Tuple VERSION = "1.0.0" def log(msg: str) -> None: print(msg) def run(cmd: List[str], check: bool = True) -> subprocess.CompletedProcess: return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=check) def ensure_venv(venv_dir: Path) -> Tuple[Path, Path]: if not venv_dir.exists(): log(f"Creating virtual environment at {venv_dir}...") builder = _venv.EnvBuilder(with_pip=True) builder.create(str(venv_dir)) else: log(f"Virtual environment already exists at {venv_dir}") if platform.system() == "Windows": python_exe = venv_dir / "Scripts" / "python.exe" bin_dir = venv_dir / "Scripts" else: python_exe = venv_dir / "bin" / "python" bin_dir = venv_dir / "bin" if not python_exe.exists(): raise RuntimeError(f"Python executable not found in venv: {python_exe}") return python_exe, bin_dir def pip_install(python_exe: Path, args: List[str]) -> None: cmd = [str(python_exe), "-m", "pip"] + args res = run(cmd, check=False) if res.returncode != 0: log(res.stdout) raise RuntimeError(f"pip command failed: {' '.join(cmd)}") def detect_mcp_config_candidates() -> List[Path]: home = Path.home() candidates: List[Path] = [] if platform.system() == "Darwin": candidates += [ home / "Library/Application Support/Code/User/mcp.json", home / "Library/Application Support/Code - Insiders/User/mcp.json", home / "Library/Application Support/VSCodium/User/mcp.json", ] else: xdg = Path(os.environ.get("XDG_CONFIG_HOME", str(home / ".config"))) candidates += [ xdg / "Code/User/mcp.json", xdg / "Code - Insiders/User/mcp.json", xdg / "Code - OSS/User/mcp.json", xdg / "VSCodium/User/mcp.json", # Flatpak variants home / ".var/app/com.visualstudio.code/config/Code/User/mcp.json", home / ".var/app/com.visualstudio.code.insiders/config/Code - Insiders/User/mcp.json", home / ".var/app/com.visualstudio.code-oss/config/Code - OSS/User/mcp.json", home / ".var/app/com.vscodium.codium/config/VSCodium/User/mcp.json", ] return candidates def which_all(names: List[str]) -> List[str]: out: List[str] = [] for n in names: if shutil.which(n): out.append(n) return out def strip_jsonc(text: str) -> str: # Remove // line comments text = re.sub(r"(^|\s)//.*?$", "", text, flags=re.MULTILINE) # Remove /* block comments */ text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL) # Remove trailing commas in objects/arrays text = re.sub(r",\s*(\]|\})", r"\1", text) return text def load_mcp_config(path: Path) -> Tuple[Dict, bool]: """Load VS Code MCP config (mcp.json) handling comments. Returns (data, ok). If ok is False, the file existed but could not be parsed safely. """ if not path.exists(): return {"servers": {}, "inputs": []}, True try: text = path.read_text(encoding="utf-8") if not text.strip(): return {"servers": {}, "inputs": []}, True try: return json.loads(strip_jsonc(text)), True except Exception: # last resort: strict JSON return json.loads(text), True except Exception as e: log(f"Warning: Could not parse MCP config at {path}: {e}") return {}, False def load_settings(path: Path) -> Tuple[Dict, bool]: """Load VS Code settings.json handling comments. Returns (data, ok). If ok is False, the file existed but could not be parsed safely. """ if not path.exists(): return {}, True try: text = path.read_text(encoding="utf-8") if not text.strip(): return {}, True try: return json.loads(strip_jsonc(text)), True except Exception: # last resort: strict JSON return json.loads(text), True except Exception as e: log(f"Warning: Could not parse settings at {path}: {e}") return {}, False def write_mcp_config(path: Path, data: Dict, dry_run: bool = False) -> None: """Atomically write mcp.json.""" path.parent.mkdir(parents=True, exist_ok=True) content = json.dumps(data, indent=2, ensure_ascii=False) + "\n" if dry_run: log(f"[dry-run] Would write {path}") return tmp = path.with_suffix(path.suffix + ".tmp") tmp.write_text(content, encoding="utf-8") os.replace(tmp, path) def write_settings(path: Path, data: Dict, dry_run: bool = False) -> None: """Atomically write settings.json.""" path.parent.mkdir(parents=True, exist_ok=True) content = json.dumps(data, indent=2, ensure_ascii=False) + "\n" if dry_run: log(f"[dry-run] Would write {path}") return tmp = path.with_suffix(path.suffix + ".tmp") tmp.write_text(content, encoding="utf-8") os.replace(tmp, path) def backup_settings(path: Path, backups_root: Path, existed_before: bool, dry_run: bool = False) -> Optional[Path]: backups_root.mkdir(parents=True, exist_ok=True) timestamp = _dt.datetime.now().strftime("%Y%m%d-%H%M%S") safe_name = re.sub(r"[^A-Za-z0-9_.-]", "_", str(path)) backup_path = backups_root / f"{safe_name}.{timestamp}.bak" if not existed_before: # Record intent but no file to copy if dry_run: log(f"[dry-run] Would record non-existent original for {path}") return None if dry_run: log(f"[dry-run] Would backup {path} -> {backup_path}") return backup_path shutil.copy2(path, backup_path) return backup_path def merge_for_mcp(data: Dict, repo_dir: Path, venv_python: Path, venv_bin: Path) -> Dict: # Ensure servers section exists if "servers" not in data: data["servers"] = {} if "inputs" not in data: data["inputs"] = [] # Add or update the github-repo-creator server cfg = { "type": "stdio", "command": str(venv_python), "args": ["server.py"], "cwd": str(repo_dir), "env": { "PATH": f"{venv_bin}{os.pathsep}${{env:PATH}}" } } data["servers"]["github-repo-creator"] = cfg return data def merge_for_mcp_old(data: Dict, repo_dir: Path, venv_python: Path, venv_bin: Path) -> Dict: # Ensure Copilot MCP enable flags cop = data.get("github.copilot.enable") if not isinstance(cop, dict): cop = {} cop["*"] = True cop["mcp"] = True data["github.copilot.enable"] = cop # Ensure MCP server entry servers = data.get("mcp.servers") if not isinstance(servers, dict): servers = {} cfg = { "command": str(venv_python), "args": ["server.py"], "cwd": str(repo_dir), "env": { "PATH": f"{venv_bin}{os.pathsep}${{env:PATH}}" } } servers["github-repo-creator"] = cfg data["mcp.servers"] = servers return data def install_extensions(skip: bool = False) -> None: if skip: return clis = which_all(["code", "code-insiders", "codium", "code-oss"]) if not clis: log("VS Code CLI not found; skipping extension installation.") return for cli in clis: try: log(f"Installing Copilot extensions via {cli} (if missing)...") run([cli, "--install-extension", "GitHub.copilot"], check=False) run([cli, "--install-extension", "GitHub.copilot-chat"], check=False) except Exception as e: log(f"Warning: Could not install extensions with {cli}: {e}") def repo_root() -> Path: return Path(__file__).resolve().parent def default_manifest_paths(root: Path) -> Tuple[Path, Path]: backups_dir = root / ".mcp-vscode-backups" manifest_latest = backups_dir / "manifest-latest.json" return backups_dir, manifest_latest def integrate(args) -> None: rroot = repo_root() venv_dir = Path(args.venv_dir).resolve() req_file = Path(args.requirements).resolve() if not req_file.exists(): log(f"Warning: requirements file not found at {req_file}. Continuing without dependency install.") # Create / ensure venv venv_python, venv_bin = ensure_venv(venv_dir) # Upgrade pip and install deps try: log("Upgrading pip...") pip_install(venv_python, ["install", "--upgrade", "pip"]) except Exception as e: log(f"Warning: pip upgrade failed: {e}") if req_file.exists(): log(f"Installing dependencies from {req_file}...") pip_install(venv_python, ["install", "-r", str(req_file)]) # Optional helper, though script doesn't need it to parse JSONC try: pip_install(venv_python, ["install", "--upgrade", "json5"]) # best effort except Exception: pass # Integrate with VS Code MCP configuration candidates = detect_mcp_config_candidates() backups_dir, manifest_latest = default_manifest_paths(rroot) manifest_entries = [] any_updated = False for path in candidates: parent_exists = path.parent.exists() existed_before = path.exists() if not existed_before and not args.include_missing: # Only modify configs that already exist or have parent directory, to guarantee 1:1 backup->change safety. if parent_exists: log(f"Creating new MCP config: {path}") else: log(f"Skipping (no VS Code directory): {path}") continue # Backup existing file (if present) backup_path = backup_settings(path, backups_dir, existed_before, dry_run=args.dry_run) # Load, merge, write data, ok = load_mcp_config(path) if not ok: log(f"Skipped unparseable MCP config file without changes: {path}") # Record in manifest only if we intended to change it; since we skipped, no write happened manifest_entries.append({ "settings_path": str(path), "backup_path": str(backup_path) if backup_path else None, "existed_before": existed_before, "timestamp": _dt.datetime.now().isoformat(), "script_version": VERSION, "skipped_unparseable": True, }) continue merged = merge_for_mcp(dict(data), rroot, venv_python, venv_bin) if merged != data: any_updated = True write_mcp_config(path, merged, dry_run=args.dry_run) log(f"Updated VS Code MCP config: {path}") else: log(f"No changes needed for: {path}") manifest_entries.append({ "settings_path": str(path), "backup_path": str(backup_path) if backup_path else None, "existed_before": existed_before, "timestamp": _dt.datetime.now().isoformat(), "script_version": VERSION, }) if manifest_entries and not args.dry_run: backups_dir.mkdir(parents=True, exist_ok=True) ts = _dt.datetime.now().strftime("%Y%m%d-%H%M%S") manifest_path = backups_dir / f"manifest-{ts}.json" manifest_path.write_text(json.dumps({"entries": manifest_entries}, indent=2), encoding="utf-8") manifest_latest.write_text(json.dumps({"ref": str(manifest_path)}), encoding="utf-8") log(f"Saved manifest: {manifest_path}") log(f"Updated pointer: {manifest_latest}") install_extensions(skip=args.no_extensions) if any_updated: log("VS Code MCP integration complete. If VS Code is running, please restart it.") else: log("No VS Code MCP configs were changed. Ensure VS Code is installed and run once to create user directory.") def resolve_manifest_path(args) -> Optional[Path]: if args.manifest: p = Path(args.manifest).expanduser().resolve() return p if p.exists() else None backups_dir, manifest_latest = default_manifest_paths(repo_root()) if manifest_latest.exists(): try: ref = json.loads(manifest_latest.read_text(encoding="utf-8")).get("ref") p = Path(ref) if p.exists(): return p except Exception: pass # Fallback: pick newest manifest-*.json if backups_dir.exists(): manifests = sorted(backups_dir.glob("manifest-*.json"), reverse=True) if manifests: return manifests[0] return None def revert(args) -> None: manifest_path = resolve_manifest_path(args) if not manifest_path: log("No manifest found to revert. Nothing to do.") return try: manifest = json.loads(manifest_path.read_text(encoding="utf-8")) except Exception as e: log(f"Failed to read manifest {manifest_path}: {e}") return entries = manifest.get("entries", []) restored = 0 deleted = 0 for item in entries: settings_path = Path(item.get("settings_path", "")) backup_path = Path(item["backup_path"]) if item.get("backup_path") else None existed_before = bool(item.get("existed_before")) if existed_before: if backup_path and backup_path.exists(): settings_path.parent.mkdir(parents=True, exist_ok=True) if args.dry_run: log(f"[dry-run] Would restore {backup_path} -> {settings_path}") else: shutil.copy2(backup_path, settings_path) restored += 1 else: log(f"Backup missing for {settings_path}; skipping restore.") else: # The file was created by setup; remove it if settings_path.exists(): if args.dry_run: log(f"[dry-run] Would delete {settings_path}") else: try: settings_path.unlink() except Exception: pass deleted += 1 log(f"Revert complete. Restored: {restored}, Deleted new files: {deleted}") if not args.keep_backups and not args.dry_run: # Cleanup backups and pointer backups_dir, manifest_latest = default_manifest_paths(repo_root()) try: if manifest_latest.exists(): manifest_latest.unlink() # Remove all backups (be conservative: only our directory) if backups_dir.exists(): shutil.rmtree(backups_dir) log("Removed backups and manifest pointer.") except Exception as e: log(f"Warning: Failed to clean backups: {e}") def parse_args(argv: List[str]) -> argparse.Namespace: p = argparse.ArgumentParser(description="Setup MCP GitHub Repository Creator and integrate with VS Code") p.add_argument("--venv-dir", default="venv", help="Virtual environment directory") p.add_argument("--requirements", default="requirements.txt", help="Requirements file path") p.add_argument("--no-extensions", action="store_true", help="Skip installing VS Code Copilot extensions") p.add_argument("--dry-run", action="store_true", help="Show planned changes without writing") p.add_argument("--manifest", help="Manifest file to use for revert") p.add_argument("--keep-backups", action="store_true", help="Keep backups after revert") p.add_argument("--revert", action="store_true", help="Revert MCP configs to the backed up versions") p.add_argument("--include-missing", action="store_true", help="Also create mcp.json for variants without one") return p.parse_args(argv) def main(argv: List[str]) -> int: args = parse_args(argv) log(f"MCP GitHub Repo Creator setup v{VERSION}") if args.revert: revert(args) return 0 integrate(args) return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/flickleafy/mcp-github-repo-creator'

If you have feedback or need assistance with the MCP directory API, please join our Discord server