"""Repo -> project binding MCP tools. Let the operator map the working git repository to a Scribe project so the SessionStart hook auto-loads that project's context — without pinning a project id in plugin config (which would force a single project for every repo). Run `bind_repo` once per repo; the binding is keyed on the normalized git remote, so it survives re-clones and ssh/https URL differences. """ from __future__ import annotations from scribe.mcp._context import current_user_id from scribe.services import projects as projects_svc from scribe.services import repo_bindings as repo_bindings_svc async def bind_repo(repo_url: str, project_id: int, ref: str = "") -> dict: """Bind a git repository to a Scribe project for session-start context. After this, any session started in that repo auto-loads the project's context (the SessionStart hook sends the repo's remote; the server resolves it here). Idempotent — re-binding the same repo updates the target project. The binding is also what the shape ledger reads (refresh_pattern_coverage): `ref` names the branch it follows. Default (""): the repo's default branch — which means the ledger only sees work after a merge. A dev-first project (rule 1: dev is home) should bind with ref="dev" so classification follows the push, not the merge. Re-binding with ref="" keeps the standing ref; pass ref="-" to clear it back to the default branch. Args: repo_url: the repo's git remote (e.g. the output of `git remote get-url origin` — ssh or https form, both work). project_id: the Scribe project this repo represents. ref: branch the ledger follows ("" = leave as is / default branch on a new binding; "-" = clear to the default branch). """ uid = current_user_id() project = await projects_svc.get_project(uid, project_id) if project is None: raise ValueError(f"project {project_id} not found") ref_arg = None if not ref else ("" if ref.strip() == "-" else ref) binding = await repo_bindings_svc.set_binding(uid, repo_url, project_id, ref_arg) follows = binding.ref or "the default branch" return { "repo_key": binding.repo_key, "project_id": binding.project_id, "project_title": project.title, "ref": binding.ref, "message": ( f"Bound `{binding.repo_key}` -> {project.title} (id {project.id}); " f"the ledger follows {follows}." ), } async def list_repo_bindings() -> dict: """List every repo -> project binding for the current user.""" uid = current_user_id() bindings = await repo_bindings_svc.list_bindings(uid) return {"bindings": [b.to_dict() for b in bindings]} async def unbind_repo(repo_url: str) -> dict: """Remove a repo's binding. Sessions there fall back to standing rules only. Args: repo_url: the repo's git remote (same form used to bind it). """ uid = current_user_id() removed = await repo_bindings_svc.delete_binding(uid, repo_url) key = repo_bindings_svc.normalize_repo_key(repo_url) return { "removed": removed, "repo_key": key, "message": ( f"Unbound `{key}`." if removed else f"No binding found for `{key}`." ), } def register(mcp) -> None: for fn in (bind_repo, list_repo_bindings, unbind_repo): mcp.tool(name=fn.__name__)(fn)