"""Typed-entity MCP tools: person, place, list. These are notes with a non-'note' note_type and type-specific JSON metadata stored in the entity_meta column. Three tools per type — list, create, update. For get and delete, use get_note / delete_note (typed entities share the Note model). The wrappers translate typed-field kwargs into the entity_meta dict shape that KnowledgeView.vue and services/knowledge.py expect. Lists: a list entity stores its items in entity_meta["list_items"] as a list of {text, checked} dicts. The create/update tools take a simpler `items` list of plain strings for ergonomics; checked-state is reset to False on each call. """ from __future__ import annotations from scribe.mcp._context import current_user_id from scribe.services import knowledge as knowledge_svc from scribe.services import notes as notes_svc async def _list_by_type(note_type: str, q: str, tag: str, limit: int) -> dict: """Common list query for a typed entity.""" uid = current_user_id() items, total = await knowledge_svc.query_knowledge( user_id=uid, note_type=note_type, tags=[tag] if tag else [], sort="modified", q=q or None, limit=max(1, min(limit, 100)), offset=0, ) # Map "items" key to a type-specific key for caller clarity. plural = {"person": "persons", "place": "places", "list": "lists"}[note_type] return {plural: items, "total": total} async def _create_entity(note_type: str, name: str, entity_meta: dict, tags: list[str] | None = None) -> dict: """Create a typed note with the given metadata.""" uid = current_user_id() note = await notes_svc.create_note( uid, title=name, note_type=note_type, entity_meta=entity_meta or None, tags=tags, ) return note.to_dict() async def _update_entity(entity_id: int, note_type: str, name: str, meta_updates: dict) -> dict: """Merge updates into entity_meta and (optionally) update the title.""" uid = current_user_id() note = await notes_svc.get_note(uid, entity_id) if note is None or note.note_type != note_type: raise ValueError(f"{note_type} {entity_id} not found") new_meta = dict(note.entity_meta or {}) new_meta.update(meta_updates) fields: dict = {"entity_meta": new_meta} if name: fields["title"] = name updated = await notes_svc.update_note(uid, entity_id, **fields) return updated.to_dict() # ─── Person ────────────────────────────────────────────────────────────────── _PERSON_FIELDS = ("relationship", "email", "phone", "birthday", "organization", "address") async def list_persons(q: str = "", tag: str = "", limit: int = 25) -> dict: """List people in the user's knowledge base. Args: q: Free-text search across name + body (optional). tag: Filter to a single tag (optional). limit: Max results (1-100). """ return await _list_by_type("person", q, tag, limit) async def create_person( name: str, relationship: str = "", email: str = "", phone: str = "", birthday: str = "", organization: str = "", address: str = "", tags: list[str] | None = None, ) -> dict: """Create a person in the user's knowledge base. Args: name: Person's name (required). relationship: How the user knows them (e.g. "colleague", "friend"). email / phone / birthday (YYYY-MM-DD) / organization / address: optional. tags: Plain-string tags, no # prefix. """ meta = {f: v for f, v in ( ("relationship", relationship), ("email", email), ("phone", phone), ("birthday", birthday), ("organization", organization), ("address", address), ) if v} return await _create_entity("person", name, meta, tags) async def update_person( person_id: int, name: str = "", relationship: str = "", email: str = "", phone: str = "", birthday: str = "", organization: str = "", address: str = "", ) -> dict: """Update a person. Only explicitly provided fields are changed. To clear a field, pass an explicit space character (this preserves the fable-mcp empty-string-means-omit convention). """ meta_updates = {f: v for f, v in ( ("relationship", relationship), ("email", email), ("phone", phone), ("birthday", birthday), ("organization", organization), ("address", address), ) if v} return await _update_entity(person_id, "person", name, meta_updates) # ─── Place ─────────────────────────────────────────────────────────────────── async def list_places(q: str = "", tag: str = "", limit: int = 25) -> dict: """List places (cafes, offices, addresses) in the user's knowledge base.""" return await _list_by_type("place", q, tag, limit) async def create_place( name: str, address: str = "", phone: str = "", hours: str = "", website: str = "", category: str = "", tags: list[str] | None = None, ) -> dict: """Create a place in the user's knowledge base. Args: name: Place name (required). address / phone / hours / website / category: optional. tags: Plain-string tags, no # prefix. """ meta = {f: v for f, v in ( ("address", address), ("phone", phone), ("hours", hours), ("website", website), ("category", category), ) if v} return await _create_entity("place", name, meta, tags) async def update_place( place_id: int, name: str = "", address: str = "", phone: str = "", hours: str = "", website: str = "", category: str = "", ) -> dict: """Update a place. Only explicitly provided fields are changed.""" meta_updates = {f: v for f, v in ( ("address", address), ("phone", phone), ("hours", hours), ("website", website), ("category", category), ) if v} return await _update_entity(place_id, "place", name, meta_updates) # ─── List ──────────────────────────────────────────────────────────────────── async def list_lists(q: str = "", tag: str = "", limit: int = 25) -> dict: """List checklists in the user's knowledge base.""" return await _list_by_type("list", q, tag, limit) async def create_list( name: str, category: str = "", items: list[str] | None = None, tags: list[str] | None = None, ) -> dict: """Create a checklist (a list-type entity). Args: name: List name (required). category: Optional category label (e.g. "shopping", "packing"). items: Initial item texts. All items start unchecked. tags: Plain-string tags, no # prefix. """ meta: dict = {} if category: meta["category"] = category if items: meta["list_items"] = [{"text": t, "checked": False} for t in items] return await _create_entity("list", name, meta, tags) async def update_list( list_id: int, name: str = "", category: str = "", items: list[str] | None = None, ) -> dict: """Update a checklist. Args: list_id: ID of the list to update. name: New title (optional). category: New category (optional). items: REPLACES the entire item set with these texts (all reset to unchecked). Omit (None) to leave items unchanged. Pass an empty list to clear all items. """ meta_updates: dict = {} if category: meta_updates["category"] = category if items is not None: meta_updates["list_items"] = [ {"text": t, "checked": False} for t in items ] return await _update_entity(list_id, "list", name, meta_updates) def register(mcp) -> None: for fn in ( list_persons, create_person, update_person, list_places, create_place, update_place, list_lists, create_list, update_list, ): mcp.tool(name=fn.__name__)(fn)