From 7c56f125ade057d56f5803997d67b833efceeb08 Mon Sep 17 00:00:00 2001 From: Master Date: Sat, 4 Jul 2026 08:14:44 +0200 Subject: [PATCH] feat: tools remove_relation + move_relation (gestion des aretes du graphe) - trilium_api.py : remove_relation_safe (retrait via attributeId, idempotent), move_relation_safe (reconnexion = add nouvelle puis remove ancienne, ordre sur) - mcp_server.py : 2 tools MCP exposes (22 tools au total) Comble le manque revele par la fusion render_engine.py : le systeme savait creer une relation mais pas la retirer ni la reconnecter. Rend triviales les corrections de mesusage et les fusions de hub. Teste de bout en bout. --- mcp_server.py | 38 +++++++++++++++++++++++++++++++++++++- trilium_api.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/mcp_server.py b/mcp_server.py index 526406e..85e3273 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -24,6 +24,7 @@ from trilium_api import ( create_note, get_note_id, get_note, get_note_content, get_children, update_note_content, search_by_label, set_label, get_label_value, find_note_by_title, delete_note_safe, move_note_safe, add_relation_safe, + remove_relation_safe, move_relation_safe, ) load_dotenv() @@ -248,6 +249,33 @@ TOOLS = [ "required": ["source_id", "nom", "cible_id"], }, }, + { + "name": "remove_relation", + "description": "Retire une relation existante : source ~nom cible. Idempotent (si absente, ne fait rien). Utile pour corriger un mesusage ou lors d une fusion de notes.", + "inputSchema": { + "type": "object", + "properties": { + "source_id": {"type": "string"}, + "nom": {"type": "string"}, + "cible_id": {"type": "string"}, + }, + "required": ["source_id", "nom", "cible_id"], + }, + }, + { + "name": "move_relation", + "description": "Reconnecte une relation d une cible vers une autre en une operation : cree source ~nom nouvelle_cible puis retire source ~nom ancienne_cible. Geste de fusion de notes (reconnexion sure : cree avant de retirer).", + "inputSchema": { + "type": "object", + "properties": { + "source_id": {"type": "string"}, + "nom": {"type": "string"}, + "ancienne_cible": {"type": "string"}, + "nouvelle_cible": {"type": "string"}, + }, + "required": ["source_id", "nom", "ancienne_cible", "nouvelle_cible"], + }, + }, { "name": "create_entite", "description": "Cree une entite de la couche connaissance/technique. type : concept, composantLogiciel ou service. Range automatiquement dans le bon dossier. definition (optionnelle) n est posee que pour les concepts (les fait remonter dans le briefing). Garde-fou anti-doublon sur le titre.", @@ -548,6 +576,14 @@ def tool_add_relation(source_id, nom, cible_id): ok, message = add_relation_safe(source_id, nom, cible_id) return {"success": ok, "message": message} +def tool_remove_relation(source_id, nom, cible_id): + ok, message = remove_relation_safe(source_id, nom, cible_id) + return {"success": ok, "message": message} + +def tool_move_relation(source_id, nom, ancienne_cible, nouvelle_cible): + ok, message = move_relation_safe(source_id, nom, ancienne_cible, nouvelle_cible) + return {"success": ok, "message": message} + def tool_move_note(note_id, ancien_parent_id, nouveau_parent_id): ok, message = move_note_safe(note_id, ancien_parent_id, nouveau_parent_id) return {"success": ok, "message": message} @@ -582,7 +618,7 @@ DISPATCH = { "close_session": tool_close_session, "get_skills": tool_get_skills, "add_skill": tool_add_skill, "move_note": tool_move_note, - "add_relation": tool_add_relation, "create_entite": tool_create_entite, + "add_relation": tool_add_relation, "remove_relation": tool_remove_relation, "move_relation": tool_move_relation, "create_entite": tool_create_entite, "update_note": tool_update_note, "get_note": tool_get_note, "get_children": tool_get_children, } diff --git a/trilium_api.py b/trilium_api.py index d73b43e..ed5ddcb 100644 --- a/trilium_api.py +++ b/trilium_api.py @@ -227,6 +227,40 @@ def add_relation_safe(source_id, nom, cible_id): return (True, "Relation creee: %s ~%s %s" % (source_id, nom, cible_id)) +def remove_relation_safe(source_id, nom, cible_id): + """Retire la relation source ~nom-> cible si elle existe (via son attributeId). + Idempotent : si absente, ne fait rien et le signale. + Retourne (True, message) ou (False, message).""" + src = get_note(source_id) + if not src: + return (False, "Note source introuvable: %s" % source_id) + aid = None + for a in src.get("attributes", []): + if a.get("type") == "relation" and a.get("name") == nom and a.get("value") == cible_id: + aid = a.get("attributeId") + break + if aid is None: + return (True, "Relation absente (rien a retirer): %s ~%s %s" % (source_id, nom, cible_id)) + try: + _ea().delete_attribute(aid) + except Exception as e: + return (False, "Echec retrait relation: %s" % e) + return (True, "Relation retiree: %s ~%s %s" % (source_id, nom, cible_id)) + + +def move_relation_safe(source_id, nom, ancienne_cible, nouvelle_cible): + """Reconnecte une relation : cree source ~nom-> nouvelle_cible puis retire + source ~nom-> ancienne_cible. Geste de fusion en une operation. + Retourne (True, message) ou (False, message).""" + ok_add, msg_add = add_relation_safe(source_id, nom, nouvelle_cible) + if not ok_add: + return (False, "Echec (creation nouvelle cible) : %s" % msg_add) + ok_rm, msg_rm = remove_relation_safe(source_id, nom, ancienne_cible) + if not ok_rm: + return (False, "Nouvelle cible creee MAIS echec retrait ancienne : %s" % msg_rm) + return (True, "Relation deplacee: %s ~%s de %s vers %s" % (source_id, nom, ancienne_cible, nouvelle_cible)) + + def _est_editable(note): """Nouveau critere de garde-fou : une note est editable/supprimable si elle porte un label projet (note de projet, propriete du projet), ou si son type