z230
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# MCP server: Trilium — v1.0
|
||||
|
||||
**Soubor:** `mcp_trilium_v1.0.py`
|
||||
**Datum:** 2026-06-09
|
||||
**Úč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
|
||||
<https://trilium.buzalka.cz>. 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: <token>`.
|
||||
- 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).
|
||||
|
||||
## 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 |
|
||||
| `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` |
|
||||
|
||||
### Bezpečnost zápisu
|
||||
Dle zvyklostí ostatních zdejších MCP serverů (preview → confirm):
|
||||
- `create_note` / `append_note` / `upload_file` — bez tření (hlavní účel)
|
||||
- `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) |
|
||||
|
||||
> **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.0.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.0.py # stdio MCP server (běžný režim)
|
||||
python mcp_trilium_v1.0.py --selftest # rychlý test create/append/read/delete
|
||||
```
|
||||
|
||||
## Příklady použití (z pohledu LLM klienta)
|
||||
|
||||
- *„zapiš poznámku …"* → `create_note(title=…, 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.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.
|
||||
@@ -0,0 +1,463 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
==============================================================================
|
||||
MCP server: TRILIUM (zápis poznámek do Trilium Notes přes ETAPI)
|
||||
Verze: 1.0
|
||||
Datum: 2026-06-09
|
||||
Autor: vladimir.buzalka
|
||||
|
||||
Úč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: <token>.
|
||||
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.0.py (stdio MCP server)
|
||||
python mcp_trilium_v1.0.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 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")
|
||||
|
||||
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 = <br>).
|
||||
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("<p>" + html.escape(para).replace("\n", "<br>") + "</p>")
|
||||
return "".join(parts) or "<p></p>"
|
||||
|
||||
|
||||
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 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"<binární obsah typu {meta.get('type')}/{meta.get('mime')}; použij stažení v UI>"
|
||||
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()
|
||||
Reference in New Issue
Block a user