import requests
from typing import Dict, Any, List
class TriliumClient:
def __init__(self, url: str, token: str):
self.url = url.rstrip("/")
self.token = token
# ETAPI requires "Authorization: Bearer <token>"
self.auth_header = {
"Authorization": f"Bearer {self.token}",
"User-Agent": "TriliumMCP/1.0"
}
def _get_headers(self, content_type: str = None) -> Dict[str, str]:
headers = self.auth_header.copy()
if content_type:
headers["Content-Type"] = content_type
return headers
def searchNotes(self, query: str) -> List[Dict[str, Any]]:
"""Search for notes using Trilium's search syntax."""
response = requests.get(
f"{self.url}/etapi/notes",
params={"search": query},
headers=self._get_headers()
)
response.raise_for_status()
return response.json()
def getNoteContent(self, note_id: str) -> str:
"""Fetch raw content of a note."""
# Note: /etapi/notes/{noteId}/content returns raw HTML
response = requests.get(
f"{self.url}/etapi/notes/{note_id}/content",
headers=self._get_headers()
)
response.raise_for_status()
return response.text
def getNoteMetadata(self, note_id: str) -> Dict[str, Any]:
"""Fetch full note metadata and attributes."""
response = requests.get(
f"{self.url}/etapi/notes/{note_id}",
headers=self._get_headers()
)
response.raise_for_status()
return response.json()
def getNoteChildren(self, note_id: str) -> List[Dict[str, Any]]:
"""List children of a specified note."""
# ancestorNoteId and ancestorDepth=eq1 is the supported way to list direct children
# the search parameter is mandatory, so we use a generic query that matches all notes
params = {
"search": "note.title *= ''",
"ancestorNoteId": note_id,
"ancestorDepth": "eq1"
}
response = requests.get(
f"{self.url}/etapi/notes",
params=params,
headers=self._get_headers()
)
response.raise_for_status()
return response.json()
def createNote(self, parent_note_id: str, title: str, content: str, type: str = "text") -> Dict[str, Any]:
"""Create a new note under a specified parent."""
payload = {
"parentNoteId": parent_note_id,
"title": title,
"content": content,
"type": type
}
# Note: Using /etapi/create-note instead of /etapi/notes for compatibility
response = requests.post(
f"{self.url}/etapi/create-note",
json=payload,
headers=self._get_headers("application/json")
)
response.raise_for_status()
return response.json()
def createAttribute(self, note_id: str, type: str, name: str, value: str, is_inheritable: bool = False) -> Dict[str, Any]:
"""Create a new attribute (label or relation) for a note."""
payload = {
"noteId": note_id,
"type": type,
"name": name,
"value": value,
"isInheritable": is_inheritable
}
response = requests.post(
f"{self.url}/etapi/attributes",
json=payload,
headers=self._get_headers("application/json")
)
response.raise_for_status()
return response.json()
def updateNoteContent(self, note_id: str, content: str) -> None:
"""Update content of an existing note."""
# ETAPI uses PUT /etapi/notes/{noteId}/content for updating content
response = requests.put(
f"{self.url}/etapi/notes/{note_id}/content",
data=content.encode('utf-8'),
headers=self._get_headers("text/plain")
)
response.raise_for_status()