z230
This commit is contained in:
@@ -37,7 +37,7 @@
|
|||||||
},
|
},
|
||||||
"trilium": {
|
"trilium": {
|
||||||
"command": "python",
|
"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"
|
"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