"""Snippet MCP tools: record reusable functions/components for later recall. A snippet is a Note with note_type='snippet' (see services/snippets.py). Because snippets are ordinary embedded notes, once recorded they surface through the same semantic search + title-first auto-inject as everything else — so a reusable thing recorded once can be recalled before it's re-written as a one-off. The tools wrap services/snippets.py, mirroring the note/process tools (dedup gate on create, System association passthrough). """ from __future__ import annotations from scribe.mcp._context import current_user_id from scribe.services import access as access_svc from scribe.services import dedup as dedup_svc from scribe.services import snippets as snippets_svc from scribe.services import systems as systems_svc async def list_snippets( q: str = "", tag: str = "", limit: int = 50, project_id: int = 0, ) -> dict: """List recorded snippets (reusable functions/components). Args: q: Free-text search across name + body (optional). Matches on meaning as well as wording, so describe what you need the code to DO. tag: Filter to a single tag, e.g. a language like "python" (optional). limit: Max results (1-100). project_id: Narrow to one project. 0 (default) searches every project — usually what you want, since a helper you need here may well have been written somewhere else. Returns {"snippets": [{id, title, tags, preview}], "total": int}. The title reads "name — when to reach for it"; open one in full with get_snippet(id). An entry marked `shared: true` with an `owner` belongs to someone else — one person's suggestion, not settled practice here. Weigh it on its merits and attribute it when you use it. Searching (passing `q`) also reaches snippets shared directly with the operator; browsing without a query deliberately does not, so those stay out of ambient results until asked for. """ uid = current_user_id() items, total = await snippets_svc.list_snippets( uid, q=q or None, tag=tag, limit=max(1, min(limit, 100)), project_id=project_id or None, ) return { "snippets": await access_svc.label_shared_items(uid, items), "total": total, } async def create_snippet( name: str, code: str, language: str = "", signature: str = "", when_to_use: str = "", repo: str = "", path: str = "", symbol: str = "", locations: list[dict] | None = None, tags: list[str] | None = None, project_id: int = 0, system_ids: list[int] | None = None, force: bool = False, ) -> dict: """Record a reusable function/component so future sessions can RECALL it instead of writing a fresh one-off. Reach for this the moment you build (or notice) something reusable: a helper, a hook, a component, a pattern worth repeating. Recording it once makes it surface automatically when a similar problem comes up later. Before writing a new utility, search first — a snippet may already exist. Args: name: Short name of the function/component, e.g. "useDebouncedRef". code: The code itself (required). language: Language/format, e.g. "python", "vue", "sql". Becomes a tag and the code-fence language. signature: One-line signature/interface, e.g. "debounce(fn, ms) -> fn". when_to_use: One line on when to reach for it — this becomes part of the title, so it's what a recall menu shows. Keep it sharp. repo/path/symbol: Canonical location of the reference implementation. locations: Several locations at once, as [{"repo","path","symbol"}, ...], when you already know the thing lives in more than one place. Takes precedence over the single repo/path/symbol shorthand. tags: Extra plain-string tags (language + "snippet" are added for you). project_id: Associate with a project (0 = no project). Snippets surface proactively within their project; search finds them across projects. system_ids: Ids of the project's Systems to associate this snippet with. force: Bypass the near-duplicate gate (see below). Returns the created snippet (including a parsed `snippet` field), OR — when a near-duplicate snippet already exists and force is false — {"duplicate": true, "existing_id": ..., "message": ...} and nothing is created. When that happens and it really is the same reusable thing found in another place, prefer merge_snippets(existing_id, [new...]) — or record then merge — to unify them into ONE canonical record (which then carries every call site as a location), rather than forcing a second copy with force=true. """ if not (name or "").strip() or not (code or "").strip(): raise ValueError("create_snippet requires a non-empty name and code") uid = current_user_id() title = snippets_svc.compose_title(name, when_to_use) body = snippets_svc.compose_body( code=code, language=language, signature=signature, when_to_use=when_to_use, repo=repo, path=path, symbol=symbol, locations=locations, ) if not force: dup = await dedup_svc.find_duplicate_note( uid, title, body, project_id=project_id or None, is_task=False, note_type=snippets_svc.SNIPPET_NOTE_TYPE, ) if dup is not None: return dedup_svc.duplicate_response(dup, "snippet") note = await snippets_svc.create_snippet( uid, name=name, code=code, language=language, signature=signature, when_to_use=when_to_use, repo=repo, path=path, symbol=symbol, locations=locations, tags=tags, project_id=project_id or None, ) if system_ids: await systems_svc.set_record_systems(uid, note.id, system_ids) data = snippets_svc.snippet_to_dict(note) if system_ids: data["systems"] = [ s.to_dict() for s in await systems_svc.list_record_systems(uid, note.id) ] return data async def get_snippet(snippet_id: int) -> dict: """Fetch a snippet by id — the full record: code, signature, location, and a parsed `snippet` field of its structured parts. If the record belongs to someone else it carries `shared: true` with the `owner` and your `permission`. Read that as ONE PERSON'S SUGGESTION, not as established practice here: judge it on its merits, say whose it is when you reference it, and don't adopt it as the house pattern without checking. """ uid = current_user_id() note = await snippets_svc.get_snippet(uid, snippet_id) if note is None: raise ValueError(f"snippet {snippet_id} not found") data = snippets_svc.snippet_to_dict(note) data.update(await access_svc.describe_provenance(uid, note)) return data async def update_snippet( snippet_id: int, name: str | None = None, code: str | None = None, language: str | None = None, signature: str | None = None, when_to_use: str | None = None, repo: str | None = None, path: str | None = None, symbol: str | None = None, locations: list[dict] | None = None, tags: list[str] | None = None, project_id: int = 0, system_ids: list[int] | None = None, ) -> dict: """Update a snippet. Only the fields you pass change. An omitted field is left alone; an EMPTY STRING clears it — so a stale signature, a wrong "when to use", or an obsolete location can be removed, not just overwritten. A snippet that surfaces in recall with wrong details is worse than none, so correcting downward has to be possible. Args: locations: Replace the whole location set, as [{"repo","path","symbol"}, ...]. Pass [] to clear every location. The single repo/path/symbol args instead overlay onto the FIRST location, leaving the rest. tags: Replaces the extra-tag set (language + "snippet" are re-derived). project_id: 0 leaves it unchanged, -1 detaches it from its project, a positive id moves it. """ uid = current_user_id() if project_id == 0: project = snippets_svc.UNSET elif project_id < 0: project = None else: project = project_id note = await snippets_svc.update_snippet( uid, snippet_id, name=name, code=code, language=language, signature=signature, when_to_use=when_to_use, repo=repo, path=path, symbol=symbol, locations=locations, tags=tags, project_id=project, ) if note is None: raise ValueError(f"snippet {snippet_id} not found") if system_ids is not None: await systems_svc.set_record_systems(uid, snippet_id, system_ids) data = snippets_svc.snippet_to_dict(note) if system_ids is not None: data["systems"] = [ s.to_dict() for s in await systems_svc.list_record_systems(uid, snippet_id) ] return data async def delete_snippet(snippet_id: int) -> dict: """Retire a snippet you recorded — it moves to the trash and is recoverable. Reach for this when a snippet is wrong, obsolete, or was never worth keeping. A recorded snippet is offered as prior art on every matching turn, so a bad one costs more than a missing one. If instead it's a duplicate of something that should survive, prefer merge_snippets — that keeps the call sites. """ uid = current_user_id() if not await snippets_svc.delete_snippet(uid, snippet_id): raise ValueError(f"snippet {snippet_id} not found") return {"deleted": True, "id": snippet_id} async def merge_snippets(target_id: int, source_ids: list[int]) -> dict: """Unify duplicate/variant snippets INTO one canonical record — the cure for the same reusable thing recorded as several one-offs. Keeps the target as the canonical: its name, when-to-use, signature, language and code win. The sources' locations and extra tags are folded in — so the survivor ends up carrying EVERY call site as a location (which is itself the "this is duplicated N times" signal) — and the source records are moved to the trash (recoverable). The survivor is re-embedded so recall stops surfacing the now-merged duplicates. Args: target_id: The snippet to keep (the canonical record). source_ids: Snippet ids to fold into the target and retire. Ids that aren't your snippets are skipped; target_id in the list is ignored. Returns the merged canonical snippet (with a parsed `snippet` field) plus `merged_ids` — the source ids actually merged and trashed. """ uid = current_user_id() ids = [s for s in (source_ids or []) if s != target_id] if not ids: raise ValueError("merge_snippets requires at least one other source_id") result = await snippets_svc.merge_snippets(uid, target_id, ids) if result is None: raise ValueError(f"snippet {target_id} not found") note, merged_ids = result data = snippets_svc.snippet_to_dict(note) data["merged_ids"] = merged_ids return data def register(mcp) -> None: for fn in ( list_snippets, create_snippet, get_snippet, update_snippet, delete_snippet, merge_snippets, ): mcp.tool(name=fn.__name__)(fn)