diff --git a/.mcp.json b/.mcp.json index 8c91b83..dff505e 100644 --- a/.mcp.json +++ b/.mcp.json @@ -37,7 +37,7 @@ }, "trilium": { "command": "python", - "args": ["U:\\PythonProject\\Janssen\\TrilliumMCP\\mcp_trilium_v1.0.py"], + "args": ["U:\\PythonProject\\Janssen\\TrilliumMCP\\mcp_trilium_v1.1.py"], "cwd": "U:\\PythonProject\\Janssen\\TrilliumMCP" } } diff --git a/Evernote/en_backup.db b/Evernote/en_backup.db new file mode 100644 index 0000000..7f71dbd Binary files /dev/null and b/Evernote/en_backup.db differ diff --git a/Evernote/evernote-backup b/Evernote/evernote-backup new file mode 160000 index 0000000..94193e6 --- /dev/null +++ b/Evernote/evernote-backup @@ -0,0 +1 @@ +Subproject commit 94193e68ebd7553fbe6c9ad6d752ffb795fbdd5c diff --git a/TrilliumMCP/mcp_trilium_v1.0.md b/TrilliumMCP/TRASH/mcp_trilium_v1.0.md similarity index 100% rename from TrilliumMCP/mcp_trilium_v1.0.md rename to TrilliumMCP/TRASH/mcp_trilium_v1.0.md diff --git a/TrilliumMCP/mcp_trilium_v1.0.py b/TrilliumMCP/TRASH/mcp_trilium_v1.0.py similarity index 100% rename from TrilliumMCP/mcp_trilium_v1.0.py rename to TrilliumMCP/TRASH/mcp_trilium_v1.0.py diff --git a/TrilliumMCP/mcp_trilium_v1.1.md b/TrilliumMCP/mcp_trilium_v1.1.md new file mode 100644 index 0000000..0853e62 --- /dev/null +++ b/TrilliumMCP/mcp_trilium_v1.1.md @@ -0,0 +1,110 @@ +# MCP server: Trilium — v1.1 + +**Soubor:** `mcp_trilium_v1.1.py` +**Datum:** 2026-06-11 +**Účel:** umožnit libovolnému MCP klientovi (Claude Code v jiné session, Claude +Chat, Cowork, …) **číst a hlavně psát** poznámky a soubory do Trilia běžícího na +. Slouží k rychlému předávání poznámek do prostředí, +kam se jinak nedá kopírovat (JNJ remote PC apod.). + +## Jak to funguje + +- Komunikuje s Trilium přes **ETAPI** (REST), autentizace hlavičkou + `Authorization: `. +- Používá **jen Python stdlib** (`urllib`) — žádné externí HTTP knihovny. +- Jediná závislost je balík `mcp` (FastMCP), který už je v prostředí ostatních + zdejších MCP serverů. +- Výchozí cílová složka je **root > Claude** (`NeoXOIw0uBK2`), která má nastaveno + `#sorted=dateCreated` + `#sortDirection=desc`, takže **nové poznámky jdou + automaticky nahoru** (newest-first). +- Summary diskuzí jdou do **root > ClaudeSummaries** (`gwBGibRzpN8d`), složky na + stejné úrovni jako Claude (hned pod ní), rovněž newest-first. + +## Nástroje (tools) + +| Tool | Co dělá | Pozn. | +|------|---------|-------| +| `ping` | health check + verze + default složka | zavolej první | +| `create_note` | nová textová poznámka | plain text → HTML; default rodič = Claude | +| `create_summary` | **summary diskuze do ClaudeSummaries** | viz pravidla níže — **nové v1.1** | +| `append_note` | připíše text na konec poznámky | nedestruktivní | +| `set_note_content` | **přepíše** obsah poznámky | vyžaduje `confirmed=True` | +| `create_folder` | nová podsložka | volitelně newest-first + na začátek | +| `upload_file` | nahraje lokální soubor jako poznámku `file` | ověří SHA-256 | +| `read_note` | přečte obsah + metadata | | +| `list_children` | vypíše potomky (default Claude) | | +| `search_notes` | fulltext / atributové hledání | Trilium search syntax | +| `delete_note` | **smaže** poznámku (+ potomky) | vyžaduje `confirmed=True` | + +### Pravidla pro `create_summary` (nové v1.1) + +1. **První řádek poznámky doplní tool automaticky**: + `YYYY-MM-DD HH:MM — ` (kdo a kdy summary vytvořil), creator default + `ClaudeCode z počítače ` (např. „ClaudeCode z počítače Z230"). + Klient tedy NEMÁ začínat content vlastní hlavičkou s datem. +2. **Rozsah summary**: tak podrobné, aby šlo kdykoliv (i za půl roku) navázat na + práci bez znalosti původní konverzace — kontext/cíl, co se udělalo a jak + (postupy, příkazy, triky), výsledky, co zbývá, cesty k souborům/skriptům/ID. +3. Title = datum + téma (např. „2026-06-11 Úklid disku C"). + +### Bezpečnost zápisu +Dle zvyklostí ostatních zdejších MCP serverů (preview → confirm): +- `create_note` / `create_summary` / `append_note` / `upload_file` — bez tření +- `set_note_content` (přepis) a `delete_note` — vrátí nejdřív **náhled**; + reálná akce až s `confirmed=True`. + +### Diakritika +Text se posílá v **UTF-8** jako `text/plain; charset=utf-8`. Ověřeno selftestem +(`První řádek`, `—`, `ěščřž` se ukládají i čtou správně). + +## Konfigurace (volitelné přepsání přes proměnné prostředí) + +| Proměnná | Default | +|----------|---------| +| `TRILIUM_URL` | `https://trilium.buzalka.cz` | +| `TRILIUM_ETAPI_TOKEN` | (zabudovaný token) | +| `TRILIUM_DEFAULT_PARENT` | `NeoXOIw0uBK2` (složka Claude) | +| `TRILIUM_SUMMARY_PARENT` | `gwBGibRzpN8d` (složka ClaudeSummaries) | + +> **Bezpečnost:** token je pro pohodlí zabudovaný přímo ve skriptu (stejně jako +> hesla v ostatních zdejších MCP serverech). Lze ho kdykoli **revoknout** v +> Triliu (Options → ETAPI) a/nebo nahradit proměnnou `TRILIUM_ETAPI_TOKEN` +> (viz `.env.example`). Token dává plný přístup k poznámkám přes API. + +## Registrace v `.mcp.json` + +```json +"trilium": { + "command": "python", + "args": ["U:\\PythonProject\\Janssen\\TrilliumMCP\\mcp_trilium_v1.1.py"], + "cwd": "U:\\PythonProject\\Janssen\\TrilliumMCP" +} +``` + +Pro jiné klienty (Claude Desktop / Cowork) přidej obdobný záznam do jejich +konfigurace MCP serverů (stejný `command` + `args`). + +## Spuštění a test + +```bash +python mcp_trilium_v1.1.py # stdio MCP server (běžný režim) +python mcp_trilium_v1.1.py --selftest # rychlý test create/append/read/delete +``` + +## Příklady použití (z pohledu LLM klienta) + +- *„zapiš poznámku …"* → `create_note(title=…, content=…)` +- *„ulož summary téhle diskuze"* → `create_summary(title="2026-06-11 Téma", content=…)` +- *„přidej k té poznámce …"* → `append_note(note_id=…, text=…)` +- *„nahraj tenhle PDF do Trilia"* → `upload_file(file_path=…)` +- *„co je ve složce Claude"* → `list_children()` +- *„najdi poznámku o protokolu"* → `search_notes(query="protokol")` + +## Historie verzí +- **v1.1** (2026-06-11) — nový tool `create_summary`: ukládá summary diskuzí do + root > ClaudeSummaries (`gwBGibRzpN8d`), automatická hlavička „datum čas — + creator", pravidlo podrobnosti (navázat na práci kdykoliv); env + `TRILIUM_SUMMARY_PARENT`. +- **v1.0** (2026-06-09) — první verze: ping, create/append/set_content, + create_folder, upload_file, read/list/search, delete. Selftest OK proti + TriliumNext 0.103.0. diff --git a/TrilliumMCP/mcp_trilium_v1.1.py b/TrilliumMCP/mcp_trilium_v1.1.py new file mode 100644 index 0000000..1fa25dd --- /dev/null +++ b/TrilliumMCP/mcp_trilium_v1.1.py @@ -0,0 +1,512 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +============================================================================== +MCP server: TRILIUM (zápis poznámek do Trilium Notes přes ETAPI) +Verze: 1.1 +Datum: 2026-06-11 +Autor: vladimir.buzalka + +Změny v1.1: + + tool create_summary — ukládá summary diskuze do složky ClaudeSummaries + (root > ClaudeSummaries, hned pod složkou Claude). Vynucuje pravidla: + 1. první řádek poznámky = "DATUM ČAS — kdo summary vytvořil" + (např. "2026-06-11 09:30 — ClaudeCode z počítače Z230") + 2. summary musí být dost podrobné, aby šlo kdykoliv navázat na práci + (kontext, co se udělalo, jak, výsledky, co zbývá, cesty k souborům) + +Účel: + Umožnit libovolnému MCP klientovi (Claude Code v jiné session, Claude + Chat, Cowork, …) ČÍST a hlavně PSÁT poznámky do Trilia běžícího na + https://trilium.buzalka.cz. Slouží k rychlému předávání poznámek/souborů + do prostředí, kam se jinak nedá kopírovat (např. JNJ remote PC). + +Komunikace: + Trilium ETAPI (REST), autentizace hlavičkou Authorization: . + Používá jen Python stdlib (urllib) — žádné externí HTTP knihovny. + +Konvence řazení: + Výchozí rodič je složka "Claude" (root > Claude), která má nastaveno + #sorted=dateCreated + #sortDirection=desc => nové poznámky jdou NAHORU + automaticky, není třeba řešit pozice. + +Bezpečnost zápisu (dle zvyklostí ostatních zdejších MCP serverů): + - create_note / append_note ... bez tření (hlavní účel serveru) + - set_note_content (přepis) ... vyžaduje confirmed=True + - delete_note ... vyžaduje confirmed=True + +Spuštění: + python mcp_trilium_v1.1.py (stdio MCP server) + python mcp_trilium_v1.1.py --selftest (rychlý test create/read/append/delete) + +Registrace v .mcp.json jako "trilium" (viz doprovodné .md). +============================================================================== +""" + +from __future__ import annotations + +import os +import sys +import json +import html +import hashlib +import datetime +import mimetypes +import urllib.parse +import urllib.request +import urllib.error +from typing import Optional + +from mcp.server.fastmcp import FastMCP + +# --- konfigurace ------------------------------------------------------------ +BASE_URL = os.environ.get("TRILIUM_URL", "https://trilium.buzalka.cz").rstrip("/") +ETAPI = BASE_URL + "/etapi" +# Token lze přepsat proměnnou prostředí TRILIUM_ETAPI_TOKEN; jinak default níže. +TOKEN = os.environ.get( + "TRILIUM_ETAPI_TOKEN", + "WoPH9O8hn2y6_r6pQSjpOVSmuL0os2hIQsLBHDOawebOx8l+MUc8v+GE=", +) +# Výchozí složka, kam se zapisuje (root > Claude), newest-first. +CLAUDE_NOTE_ID = os.environ.get("TRILIUM_DEFAULT_PARENT", "NeoXOIw0uBK2") +# Složka pro summary diskuzí (root > ClaudeSummaries, hned pod Claude), newest-first. +SUMMARY_NOTE_ID = os.environ.get("TRILIUM_SUMMARY_PARENT", "gwBGibRzpN8d") + +HTTP_TIMEOUT = 20 + + +def log(msg: str) -> None: + print(msg, file=sys.stderr, flush=True) + + +# --- nízkoúrovňové ETAPI volání --------------------------------------------- +def _req(method: str, path: str, *, json_body=None, raw_body: Optional[bytes] = None, + raw_ctype: Optional[str] = None, expect_bytes: bool = False): + """Jedno ETAPI volání. Vrací dict/list/str (JSON nebo text), nebo bytes + (expect_bytes=True). Vyhazuje RuntimeError s tělem chyby při HTTP >= 400.""" + body = None + ctype = None + if json_body is not None: + body = json.dumps(json_body, ensure_ascii=False).encode("utf-8") + ctype = "application/json; charset=utf-8" + elif raw_body is not None: + body = raw_body + ctype = raw_ctype or "application/octet-stream" + + req = urllib.request.Request(ETAPI + path, data=body, method=method) + req.add_header("Authorization", TOKEN) + if ctype: + req.add_header("Content-Type", ctype) + + try: + with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT) as resp: + data = resp.read() + if expect_bytes: + return data + text = data.decode("utf-8", errors="replace") + if text.strip().startswith(("{", "[")): + return json.loads(text) + return text + except urllib.error.HTTPError as e: + detail = e.read().decode("utf-8", errors="replace") + raise RuntimeError(f"ETAPI {method} {path} -> HTTP {e.code}: {detail}") from None + except urllib.error.URLError as e: + raise RuntimeError(f"ETAPI {method} {path} -> spojení selhalo: {e.reason}") from None + + +def _text_to_html(text: str, is_html: bool) -> str: + """Plain text -> bezpečné HTML (prázdné řádky = odstavce, \\n =
). + Pokud is_html=True, obsah se bere tak, jak je.""" + if is_html: + return text + text = text.replace("\r\n", "\n").replace("\r", "\n") + parts = [] + for para in text.split("\n\n"): + if para.strip() == "": + continue + parts.append("

" + html.escape(para).replace("\n", "
") + "

") + return "".join(parts) or "

" + + +def _put_text_content(note_id: str, html_content: str) -> None: + """Uloží HTML obsah textové poznámky (UTF-8, ověřený způsob: text/plain;charset).""" + _req("PUT", f"/notes/{note_id}/content", + raw_body=html_content.encode("utf-8"), + raw_ctype="text/plain; charset=utf-8") + + +def _note_summary(note: dict) -> dict: + """Zhuštěné info o poznámce do tool response.""" + return { + "noteId": note.get("noteId"), + "title": note.get("title"), + "type": note.get("type"), + "mime": note.get("mime"), + "parentNoteIds": note.get("parentNoteIds"), + "childNoteIds": note.get("childNoteIds"), + "dateCreated": note.get("dateCreated"), + "dateModified": note.get("dateModified"), + "url": f"{BASE_URL}/#root/{note.get('noteId')}", + } + + +# --- startup check ---------------------------------------------------------- +try: + _info = _req("GET", "/app-info") + log(f"Trilium OK ({BASE_URL}) — appVersion {_info.get('appVersion')}") +except Exception as e: # noqa: BLE001 + log(f"Trilium ETAPI nedostupné: {e}") + sys.exit(1) + + +# --- MCP -------------------------------------------------------------------- +mcp = FastMCP("trilium") + + +@mcp.tool() +def ping() -> dict: + """Health check Trilium ETAPI. Vrátí verzi aplikace, DB a default složku. + Zavolej jako první pro ověření, že je server dostupný a token platí.""" + try: + info = _req("GET", "/app-info") + return { + "status": "ok", + "base_url": BASE_URL, + "appVersion": info.get("appVersion"), + "dbVersion": info.get("dbVersion"), + "default_parent": CLAUDE_NOTE_ID, + "default_parent_url": f"{BASE_URL}/#root/{CLAUDE_NOTE_ID}", + } + except Exception as e: # noqa: BLE001 + return {"status": "error", "error": str(e)} + + +@mcp.tool() +def create_note( + title: str, + content: str = "", + parent_note_id: Optional[str] = None, + is_html: bool = False, +) -> dict: + """Vytvoří NOVOU textovou poznámku. Hlavní nástroj pro předávání poznámek. + + title: název poznámky (zobrazí se ve stromu) + content: text poznámky; prostý text se převede na HTML (prázdný + řádek = nový odstavec). Pokud už posíláš HTML, nastav is_html=True. + parent_note_id: kam ji zařadit. Default = složka "Claude" (root > Claude), + která řadí potomky nejnovější-nahoře automaticky. + is_html: True pokud content už JE HTML. + + Vrací noteId a URL nové poznámky. + """ + try: + parent = parent_note_id or CLAUDE_NOTE_ID + html_content = _text_to_html(content, is_html) + res = _req("POST", "/create-note", json_body={ + "parentNoteId": parent, + "title": title, + "type": "text", + "content": html_content, + }) + note = res["note"] + log(f"create_note: {note['noteId']} '{title}' pod {parent}") + return {"status": "ok", **_note_summary(note)} + except Exception as e: # noqa: BLE001 + return {"status": "error", "error": str(e)} + + +@mcp.tool() +def create_summary(title: str, content: str, creator: Optional[str] = None) -> dict: + """Uloží SUMMARY diskuze/práce do složky ClaudeSummaries (root > ClaudeSummaries). + + PRAVIDLA PRO SUMMARY (dodržuj při sestavování obsahu): + 1. První řádek poznámky doplní tento tool automaticky ve tvaru + "YYYY-MM-DD HH:MM — " (kdo a kdy summary vytvořil). + NEZAČÍNEJ proto content vlastní hlavičkou s datem. + 2. Summary piš tak podrobně, aby šlo KDYKOLIV (i za půl roku) navázat + na práci bez znalosti původní konverzace. Vždy uveď: + - kontext/cíl (co se řešilo a proč), + - co se udělalo a JAK (postupy, příkazy, triky, řešené problémy), + - výsledky (čísla, stavy, co je ověřeno), + - co zbývá / další kroky, + - cesty k souborům, skriptům, ID záznamů, odkazy. + + title: název poznámky — datum + téma (např. "2026-06-11 Úklid disku C"). + content: text summary (prostý text; prázdný řádek = nový odstavec). + creator: kdo summary vytváří; default "ClaudeCode z počítače ". + """ + try: + who = creator or f"ClaudeCode z počítače {os.environ.get('COMPUTERNAME', 'neznámý')}" + stamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M") + header = f"{stamp} — {who}" + html_content = _text_to_html(header + "\n\n" + content, is_html=False) + res = _req("POST", "/create-note", json_body={ + "parentNoteId": SUMMARY_NOTE_ID, + "title": title, + "type": "text", + "content": html_content, + }) + note = res["note"] + log(f"create_summary: {note['noteId']} '{title}' ({who})") + return {"status": "ok", "header": header, **_note_summary(note)} + except Exception as e: # noqa: BLE001 + return {"status": "error", "error": str(e)} + + +@mcp.tool() +def append_note(note_id: str, text: str, is_html: bool = False) -> dict: + """Připíše text na KONEC existující textové poznámky (nedestruktivní). + + note_id: ID poznámky, ke které připisujeme + text: co připsat; prostý text -> HTML (is_html=True pokud posíláš HTML) + """ + try: + meta = _req("GET", f"/notes/{note_id}") + if meta.get("type") != "text": + return {"status": "error", + "error": f"Poznámka {note_id} není typu 'text' (je '{meta.get('type')}')."} + current = _req("GET", f"/notes/{note_id}/content") + if not isinstance(current, str): + current = json.dumps(current, ensure_ascii=False) + addition = _text_to_html(text, is_html) + _put_text_content(note_id, (current or "") + addition) + log(f"append_note: {note_id} (+{len(text)} znaků)") + return {"status": "ok", "noteId": note_id, + "url": f"{BASE_URL}/#root/{note_id}"} + except Exception as e: # noqa: BLE001 + return {"status": "error", "error": str(e)} + + +@mcp.tool() +def set_note_content(note_id: str, content: str, is_html: bool = False, + confirmed: bool = False) -> dict: + """PŘEPÍŠE celý obsah textové poznámky (DESTRUKTIVNÍ — smaže stávající text). + + Vyžaduje confirmed=True. Při confirmed=False jen vrátí náhled: délku + stávajícího obsahu a co by se zapsalo — to ukaž uživateli a teprve po + schválení zavolej znovu s confirmed=True. + """ + try: + meta = _req("GET", f"/notes/{note_id}") + if meta.get("type") != "text": + return {"status": "error", + "error": f"Poznámka {note_id} není typu 'text' (je '{meta.get('type')}')."} + new_html = _text_to_html(content, is_html) + if not confirmed: + current = _req("GET", f"/notes/{note_id}/content") + cur_len = len(current) if isinstance(current, str) else 0 + return { + "status": "preview", + "noteId": note_id, + "title": meta.get("title"), + "current_content_length": cur_len, + "new_content_preview": new_html[:500], + "note": "Přepis smaže stávající obsah. Ukaž uživateli a zavolej znovu s confirmed=True.", + } + _put_text_content(note_id, new_html) + log(f"set_note_content: {note_id} přepsáno") + return {"status": "ok", "noteId": note_id, + "url": f"{BASE_URL}/#root/{note_id}"} + except Exception as e: # noqa: BLE001 + return {"status": "error", "error": str(e)} + + +@mcp.tool() +def create_folder(title: str, parent_note_id: str = "root", + newest_first: bool = True, place_on_top: bool = False) -> dict: + """Vytvoří podsložku (poznámku, pod kterou se vnořují další). + + title: název složky + parent_note_id: rodič (default "root") + newest_first: nastaví #sorted=dateCreated + #sortDirection=desc, takže + potomci se řadí nejnovější-nahoře (jako složka Claude) + place_on_top: posune složku na úplný začátek u svého rodiče (pozice 0) + """ + try: + res = _req("POST", "/create-note", json_body={ + "parentNoteId": parent_note_id, + "title": title, + "type": "text", + "content": "", + }) + note = res["note"] + nid = note["noteId"] + if newest_first: + _req("POST", "/attributes", json_body={ + "noteId": nid, "type": "label", "name": "sorted", + "value": "dateCreated", "isInheritable": False}) + _req("POST", "/attributes", json_body={ + "noteId": nid, "type": "label", "name": "sortDirection", + "value": "desc", "isInheritable": False}) + if place_on_top: + branch_id = res["branch"]["branchId"] + _req("PATCH", f"/branches/{branch_id}", json_body={"notePosition": 0}) + log(f"create_folder: {nid} '{title}' pod {parent_note_id}") + return {"status": "ok", "newest_first": newest_first, + "place_on_top": place_on_top, **_note_summary(note)} + except Exception as e: # noqa: BLE001 + return {"status": "error", "error": str(e)} + + +@mcp.tool() +def upload_file(file_path: str, parent_note_id: Optional[str] = None, + title: Optional[str] = None) -> dict: + """Nahraje LOKÁLNÍ soubor (z disku stroje, kde běží tento MCP) jako + samostatnou poznámku typu 'file'. Po nahrání ověří integritu přes SHA-256. + + file_path: absolutní cesta k souboru na tomto stroji + parent_note_id: kam (default složka Claude) + title: název poznámky (default = jméno souboru) + """ + try: + if not os.path.isfile(file_path): + return {"status": "error", "error": f"Soubor neexistuje: {file_path}"} + parent = parent_note_id or CLAUDE_NOTE_ID + fname = os.path.basename(file_path) + with open(file_path, "rb") as fh: + blob = fh.read() + mime = mimetypes.guess_type(fname)[0] or "application/octet-stream" + sha_local = hashlib.sha256(blob).hexdigest() + + res = _req("POST", "/create-note", json_body={ + "parentNoteId": parent, "title": title or fname, + "type": "file", "mime": mime, "content": ""}) + nid = res["note"]["noteId"] + _req("PUT", f"/notes/{nid}/content", raw_body=blob, + raw_ctype="application/octet-stream") + _req("POST", "/attributes", json_body={ + "noteId": nid, "type": "label", "name": "originalFileName", + "value": fname, "isInheritable": False}) + + back = _req("GET", f"/notes/{nid}/content", expect_bytes=True) + sha_remote = hashlib.sha256(back).hexdigest() + log(f"upload_file: {nid} '{fname}' {len(blob)}B match={sha_local == sha_remote}") + return { + "status": "ok", + "noteId": nid, + "title": title or fname, + "mime": mime, + "size_bytes": len(blob), + "sha256": sha_local, + "integrity_ok": sha_local == sha_remote, + "url": f"{BASE_URL}/#root/{nid}", + } + except Exception as e: # noqa: BLE001 + return {"status": "error", "error": str(e)} + + +@mcp.tool() +def read_note(note_id: str) -> dict: + """Přečte obsah poznámky (text/HTML) + základní metadata.""" + try: + meta = _req("GET", f"/notes/{note_id}") + result = {"status": "ok", **_note_summary(meta)} + if meta.get("type") in ("text", "code", "mermaid"): + content = _req("GET", f"/notes/{note_id}/content") + result["content"] = content if isinstance(content, str) else json.dumps(content, ensure_ascii=False) + else: + result["content"] = f"" + return result + except Exception as e: # noqa: BLE001 + return {"status": "error", "error": str(e)} + + +@mcp.tool() +def list_children(note_id: Optional[str] = None) -> dict: + """Vypíše přímé potomky poznámky (default = složka Claude) v pořadí stromu.""" + try: + nid = note_id or CLAUDE_NOTE_ID + meta = _req("GET", f"/notes/{nid}") + children = [] + for child_branch in meta.get("childBranchIds", []): + br = _req("GET", f"/branches/{child_branch}") + ch = _req("GET", f"/notes/{br['noteId']}") + children.append({ + "noteId": ch.get("noteId"), + "title": ch.get("title"), + "type": ch.get("type"), + "notePosition": br.get("notePosition"), + "dateCreated": ch.get("dateCreated"), + }) + return {"status": "ok", "parent": nid, "parent_title": meta.get("title"), + "count": len(children), "children": children} + except Exception as e: # noqa: BLE001 + return {"status": "error", "error": str(e)} + + +@mcp.tool() +def search_notes(query: str, limit: int = 20, ancestor_note_id: Optional[str] = None) -> dict: + """Fulltextové / atributové vyhledávání poznámek (Trilium search syntax). + + query: např. 'protokol', '#book', 'note.title *=* report' + limit: max počet výsledků (default 20) + ancestor_note_id: omez hledání na podstrom (např. složka Claude) + """ + try: + params = {"search": query, "limit": str(max(1, min(limit, 200)))} + if ancestor_note_id: + params["ancestorNoteId"] = ancestor_note_id + qs = urllib.parse.urlencode(params) + res = _req("GET", f"/notes?{qs}") + results = res.get("results", []) if isinstance(res, dict) else [] + out = [{ + "noteId": n.get("noteId"), + "title": n.get("title"), + "type": n.get("type"), + "dateModified": n.get("dateModified"), + "url": f"{BASE_URL}/#root/{n.get('noteId')}", + } for n in results] + return {"status": "ok", "query": query, "count": len(out), "results": out} + except Exception as e: # noqa: BLE001 + return {"status": "error", "error": str(e)} + + +@mcp.tool() +def delete_note(note_id: str, confirmed: bool = False) -> dict: + """SMAŽE poznámku (a její potomky) — DESTRUKTIVNÍ. + + Vyžaduje confirmed=True. Při confirmed=False vrátí náhled (název + počet + potomků), který ukaž uživateli; teprve po schválení zavolej s confirmed=True. + """ + try: + meta = _req("GET", f"/notes/{note_id}") + if not confirmed: + return { + "status": "preview", + "noteId": note_id, + "title": meta.get("title"), + "type": meta.get("type"), + "child_count": len(meta.get("childNoteIds", [])), + "note": "Smazání je nevratné a zahrne i potomky. Ukaž uživateli a zavolej znovu s confirmed=True.", + } + _req("DELETE", f"/notes/{note_id}") + log(f"delete_note: {note_id} '{meta.get('title')}' smazáno") + return {"status": "ok", "deleted": note_id, "title": meta.get("title")} + except Exception as e: # noqa: BLE001 + return {"status": "error", "error": str(e)} + + +# --- selftest (mimo MCP) ---------------------------------------------------- +def selftest() -> None: + try: + sys.stdout.reconfigure(encoding="utf-8") + except Exception: + pass + print("== Trilium MCP selftest ==") + print("ping:", json.dumps(ping(), ensure_ascii=False)) + c = create_note("MCP selftest – ěščřž", "První řádek.\n\nDruhý odstavec — diakritika.") + print("create:", json.dumps(c, ensure_ascii=False)) + nid = c.get("noteId") + if nid: + print("append:", json.dumps(append_note(nid, "Připsaný řádek."), ensure_ascii=False)) + r = read_note(nid) + print("read.content:", r.get("content")) + print("delete:", json.dumps(delete_note(nid, confirmed=True), ensure_ascii=False)) + + +if __name__ == "__main__": + if "--selftest" in sys.argv: + selftest() + else: + log("MCP trilium server started (FastMCP)") + mcp.run()