513 lines
21 KiB
Python
513 lines
21 KiB
Python
#!/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: <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.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 = <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 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 — <creator>" (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 <COMPUTERNAME>".
|
||
"""
|
||
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"<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()
|