diff --git a/mcp-server/src/legal_mcp/tools/cases.py b/mcp-server/src/legal_mcp/tools/cases.py
index 4ce03db..7c07717 100644
--- a/mcp-server/src/legal_mcp/tools/cases.py
+++ b/mcp-server/src/legal_mcp/tools/cases.py
@@ -28,12 +28,17 @@ def _gitea_token() -> str:
return os.environ.get("GITEA_ACCESS_TOKEN") or os.environ.get("GITEA_TOKEN", "")
-async def _setup_gitea_remote(case_number: str, title: str, case_dir: Path) -> bool:
- """Create Gitea repo and configure git remote. Best-effort — returns False on failure."""
+async def _setup_gitea_remote(case_number: str, title: str, case_dir: Path) -> dict:
+ """Create Gitea repo and configure git remote.
+
+ Returns a dict with: ok (bool), url (str|None), error (str|None).
+ Never raises — failures are reported via the dict so callers can surface
+ them to the UI instead of silently swallowing them.
+ """
token = _gitea_token()
if not token:
logger.info("No GITEA_TOKEN — skipping Gitea repo creation for %s", case_number)
- return False
+ return {"ok": False, "url": None, "error": "no_token"}
try:
async with httpx.AsyncClient(verify=False, timeout=30) as client:
@@ -59,8 +64,9 @@ async def _setup_gitea_remote(case_number: str, title: str, case_dir: Path) -> b
repo = resp.json()
clone_url = repo.get("clone_url", "")
+ html_url = repo.get("html_url", "")
if not clone_url:
- return False
+ return {"ok": False, "url": None, "error": "no_clone_url"}
auth_url = clone_url.replace("https://", f"https://chaim:{token}@")
@@ -94,15 +100,20 @@ async def _setup_gitea_remote(case_number: str, title: str, case_dir: Path) -> b
cwd=case_dir, capture_output=True, text=True, env=git_env,
)
if push.returncode != 0:
- logger.warning("Gitea push failed for %s: %s", case_number, push.stderr)
- return False
+ stderr = push.stderr.strip()
+ logger.warning("Gitea push failed for %s: %s", case_number, stderr)
+ return {"ok": False, "url": html_url or None, "error": f"push_failed: {stderr[:200]}"}
logger.info("Gitea repo created and pushed for %s", case_number)
- return True
+ return {"ok": True, "url": html_url or None, "error": None}
+ except httpx.HTTPStatusError as exc:
+ msg = f"http_{exc.response.status_code}"
+ logger.warning("Gitea setup failed for %s: %s", case_number, msg)
+ return {"ok": False, "url": None, "error": msg}
except Exception as exc:
logger.warning("Gitea setup failed for %s: %s", case_number, exc)
- return False
+ return {"ok": False, "url": None, "error": f"{type(exc).__name__}: {exc}"[:200]}
async def case_create(
@@ -214,11 +225,10 @@ async def case_create(
except Exception:
pass # git not available — non-critical
- # Create Gitea repo and configure remote (best-effort)
- try:
- await _setup_gitea_remote(case_number, title, case_dir)
- except Exception:
- pass # Gitea not available — non-critical
+ # Create Gitea repo and configure remote — surface result so callers can
+ # show failures (e.g. stale token) and offer a retry button instead of
+ # silently producing a case with no remote.
+ case["gitea"] = await _setup_gitea_remote(case_number, title, case_dir)
return json.dumps(case, default=str, ensure_ascii=False, indent=2)
diff --git a/web-ui/src/components/cases/case-header.tsx b/web-ui/src/components/cases/case-header.tsx
index e2b70d4..c4c48ae 100644
--- a/web-ui/src/components/cases/case-header.tsx
+++ b/web-ui/src/components/cases/case-header.tsx
@@ -4,6 +4,7 @@ import { Badge } from "@/components/ui/badge";
import { StatusBadge } from "@/components/cases/status-badge";
import { SyncIndicator } from "@/components/cases/sync-indicator";
import { CaseArchiveAction } from "@/components/cases/case-archive-action";
+import { CreateRepoButton } from "@/components/cases/create-repo-button";
import {
PRACTICE_AREA_LABELS,
APPEAL_SUBTYPE_LABELS,
@@ -67,6 +68,7 @@ export function CaseHeader({ data }: { data?: CaseDetail }) {
archivedAt={data.archived_at}
/>
)}
+