z230
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Submodule
+1
Submodule Evernote/evernote-backup added at 94193e68eb
@@ -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
|
||||
<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).
|
||||
- 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 — <creator>` (kdo a kdy summary vytvořil), creator default
|
||||
`ClaudeCode z počítače <COMPUTERNAME>` (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.
|
||||
@@ -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: <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()
|
||||
Reference in New Issue
Block a user