Changes
diff --git a/skills/trilium/README.md b/skills/trilium/README.md
new file mode 100644
index 0000000..10a5313
--- /dev/null
+++ b/skills/trilium/README.md
@@ -0,0 +1,5 @@
+# Trilium Skill & MCP
+
+This directory contains the agent skill and MCP server to interact
+with a Trilium Notes server. The MCP server is written in Python, and
+the only external dependency is `requests`.
diff --git a/skills/trilium/SKILL.md b/skills/trilium/SKILL.md
new file mode 100644
index 0000000..b88aed6
--- /dev/null
+++ b/skills/trilium/SKILL.md
@@ -0,0 +1,46 @@
+---
+name: trilium
+description: Interface with Trilium Notes to search, read, create, and update notes. Use when the user wants to manage their personal knowledge base, retrieve information from Trilium, or save new thoughts and snippets.
+---
+
+# Trilium Notes Skill
+
+This skill enables seamless interaction with Trilium Notes, a hierarchical note-taking application. It allows you to search for notes, retrieve their content and metadata, and create or update notes directly from the CLI.
+
+## Core Capabilities
+
+### 1. Searching Notes
+Use `search_notes` to find notes using Trilium's powerful search syntax.
+- **Simple search**: `search_notes(query="React")`
+- **Attribute search**: `search_notes(query="#todo")`
+- **Full-text search**: `search_notes(query="content *= 'project plan'")`
+
+### 2. Reading Notes
+Retrieve the content and structure of your knowledge base.
+- **Get content**: Use `get_note_content(noteId="...")` to read the HTML content of a note.
+- **Get metadata**: Use `get_note_metadata(noteId="...")` to see tags, attributes, and creation dates.
+- **List children**: Use `get_note_children(noteId="...")` to explore the hierarchy. The root note ID is `root`.
+
+### 3. Modifying Notes
+Keep your notes up to date or capture new information.
+- **Create note**: Use `create_note(title="...", content="...", parentNoteId="root")` to add new entries.
+- **Update content**: Use `update_note_content(noteId="...", content="...")` to modify existing notes.
+
+## Workflow Examples
+
+### Researching a Topic
+1. **Search**: `search_notes(query="Machine Learning")` to find relevant notes.
+2. **Read**: `get_note_content(noteId="...")` for the most promising results.
+3. **Explore**: `get_note_children(noteId="...")` if the note is a folder/parent.
+
+### Capturing Meeting Notes
+1. **Find Parent**: `search_notes(query="Meetings")` to find where to put the new note.
+2. **Create**: `create_note(title="Sync 2024-05-20", content="<p>Discussed project roadmap...</p>", parentNoteId="...")`.
+
+### Updating a Todo List
+1. **Search**: `search_notes(query="title = 'Todo List'")`.
+2. **Read**: `get_note_content(noteId="...")` to see current state.
+3. **Update**: `update_note_content(noteId="...", content="...")` with the new list.
+
+## Notes on HTML Content
+Trilium stores notes as HTML. When creating or updating notes, ensure the content is wrapped in appropriate HTML tags (e.g., `<p>`, `<ul>`, `<li>`) for best rendering in the Trilium UI.
diff --git a/skills/trilium/mcp/design_doc.md b/skills/trilium/mcp/design_doc.md
new file mode 100644
index 0000000..846aa67
--- /dev/null
+++ b/skills/trilium/mcp/design_doc.md
@@ -0,0 +1,85 @@
+# Technical Design Document: Trilium MCP Server
+
+## 1. Introduction
+The Trilium MCP Server provides a Model Context Protocol (MCP) interface to Trilium Notes, allowing LLMs to search, read, and manipulate notes within a Trilium instance. This implementation is built in Python with minimal dependencies and communicates over STDIO.
+
+## 2. Requirements
+- Python 3.x
+- `requests` library for HTTP communication with Trilium.
+- No external MCP framework (e.g., `mcp-python-sdk`).
+- Communication via JSON-RPC 2.0 over STDIO.
+
+## 3. Architecture
+
+### 3.1. MCP Protocol Layer
+Handles the low-level JSON-RPC communication:
+- **Main Loop:** Reads from `sys.stdin` line-by-line.
+- **JSON-RPC Parser:**
+ - Validates `jsonrpc`, `id`, `method`.
+ - Maps `initialize`, `initialized`, `tools/list`, `tools/call` to specific functions.
+- **ResponseWriter:**
+ - Formats results as `{"jsonrpc": "2.0", "id": <id>, "result": <result>}`.
+ - Formats errors as `{"jsonrpc": "2.0", "id": <id>, "error": {"code": <code>, "message": <message>}}`.
+ - Writes to `sys.stdout` followed by a newline and `sys.stdout.flush()`.
+- **Error Handling:** Standard codes:
+ - `-32700`: Parse error
+ - `-32600`: Invalid Request
+ - `-32601`: Method not found
+ - `-32602`: Invalid params
+ - `-32603`: Internal error
+
+### 3.2. Trilium API Client
+A thin wrapper around `requests` to interact with the Trilium ETAPI:
+- **Authentication:** Uses the Trilium ETAPI token (passed via environment variable `TRILIUM_TOKEN`).
+- **Base URL:** Configuration via `TRILIUM_URL`.
+- **Endpoints:**
+ - `GET /etapi/notes/{noteId}`: Fetch note metadata and content.
+ - `GET /etapi/notes/{noteId}/children`: List child notes.
+ - `POST /etapi/notes`: Create new notes.
+ - `PATCH /etapi/notes/{noteId}`: Update note content/metadata.
+ - `GET /etapi/notes-search`: Search for notes using Trilium's search syntax.
+
+### 3.3. Tools Implementation
+The following MCP tools will be exposed via `tools/list` and `tools/call`:
+- `search_notes(query: string)`:
+ - Calls `GET /etapi/notes-search?query={query}`.
+ - Returns a list of note metadata (noteId, title, type).
+- `get_note_content(noteId: string)`:
+ - Calls `GET /etapi/notes/{noteId}/content`.
+ - Returns the raw content of the note (likely HTML).
+- `get_note_metadata(noteId: string)`:
+ - Calls `GET /etapi/notes/{noteId}`.
+ - Returns full note attributes, including tags and properties.
+- `create_note(parentNoteId: string, title: string, content: string, type: string = "text")`:
+ - Calls `POST /etapi/notes` with given parameters.
+ - Defaults `parentNoteId` to `root` if not provided.
+- `update_note_content(noteId: string, content: string)`:
+ - Calls `PATCH /etapi/notes/{noteId}/content` (or similar endpoint).
+ - Updates the content of an existing note.
+- `get_note_children(noteId: string)`:
+ - Calls `GET /etapi/notes/{noteId}/children`.
+ - Returns a list of children for the specified note.
+
+## 4. Implementation Details
+
+### 4.1. Message Handling
+The server will follow the MCP lifecycle:
+1. **Initialize:** Receive `initialize` request, return server capabilities (tools).
+2. **Initialized:** Receive `notifications/initialized`.
+3. **Operation:** Handle `list_tools` and `call_tool`.
+
+### 4.2. File Structure
+- `server.py`: Main entry point, JSON-RPC loop.
+- `trilium_client.py`: Trilium ETAPI interaction logic.
+- `handlers.py`: Mapping of MCP tool calls to Trilium client calls.
+
+## 5. Security & Configuration
+- **Environment Variables:**
+ - `TRILIUM_URL`: URL of the Trilium instance (e.g., `http://localhost:8080`).
+ - `TRILIUM_TOKEN`: ETAPI token for authentication.
+- **Note on Safety:** Destructive operations (delete) will be omitted in the initial version to prevent accidental data loss.
+
+## 6. Verification Plan
+- **Unit Tests:** Mock the `requests` calls and test the Trilium client logic.
+- **Integration Tests:** Use a local Trilium instance to verify tool functionality.
+- **MCP Validation:** Manually test the JSON-RPC interface using a tool like `mcp-inspector`.
diff --git a/skills/trilium/mcp/main.py b/skills/trilium/mcp/main.py
new file mode 100644
index 0000000..bb4b8ae
--- /dev/null
+++ b/skills/trilium/mcp/main.py
@@ -0,0 +1,32 @@
+import os
+import sys
+from src.trilium_client import TriliumClient
+from src.handlers import ToolHandlers
+from src.server import MCPServer
+
+def main():
+ # Load configuration from environment variables
+ url = os.environ.get("TRILIUM_URL", "").strip()
+ token = os.environ.get("TRILIUM_TOKEN", "").strip()
+
+ if not url or not token:
+ sys.stderr.write("Error: TRILIUM_URL and TRILIUM_TOKEN environment variables must be set.\n")
+ sys.exit(1)
+
+ # Initialize components
+ client = TriliumClient(url, token)
+ handlers = ToolHandlers(client)
+ server = MCPServer(handlers)
+
+ # Start the JSON-RPC loop
+ try:
+ server.run()
+ except KeyboardInterrupt:
+ pass
+ except Exception:
+ import traceback
+ sys.stderr.write(f"Critical error:\n{traceback.format_exc()}\n")
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
diff --git a/skills/trilium/mcp/prd.md b/skills/trilium/mcp/prd.md
new file mode 100644
index 0000000..e718fb0
--- /dev/null
+++ b/skills/trilium/mcp/prd.md
@@ -0,0 +1,14 @@
+# Trilium MCP Server
+
+An MCP server in Python to interact with Trilium Notes server. The
+Trilium ETAPI doc: https://docs.triliumnotes.org/rest-api/etapi/
+
+* The MCP server should be STDIO-based
+* Minimal dependencies. Do not use any existing MCP framework library
+* Use requests to communicate with the Trilium server
+* Put python files in a sub-directory. In the project’s root directory
+ there can be a `main.py` as the entry point.
+
+## Remarks
+
+* The ID of the root note is `root`.
diff --git a/skills/trilium/mcp/requirements.txt b/skills/trilium/mcp/requirements.txt
new file mode 100644
index 0000000..f229360
--- /dev/null
+++ b/skills/trilium/mcp/requirements.txt
@@ -0,0 +1 @@
+requests
diff --git a/skills/trilium/mcp/src/__init__.py b/skills/trilium/mcp/src/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/skills/trilium/mcp/src/handlers.py b/skills/trilium/mcp/src/handlers.py
new file mode 100644
index 0000000..188fd2f
--- /dev/null
+++ b/skills/trilium/mcp/src/handlers.py
@@ -0,0 +1,106 @@
+from typing import Dict, Any, List
+from .trilium_client import TriliumClient
+
+
+class ToolHandlers:
+ def __init__(self, client: TriliumClient):
+ self.client = client
+
+ def listTools(self) -> List[Dict[str, Any]]:
+ """Return the list of tools available through the MCP server."""
+ return [
+ {
+ "name": "search_notes",
+ "description": "Search for notes in Trilium using its search syntax.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "query": {"type": "string", "description": "Trilium search query."}
+ },
+ "required": ["query"]
+ }
+ },
+ {
+ "name": "get_note_content",
+ "description": "Fetch the raw content (HTML) of a specific note.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "noteId": {"type": "string", "description": "Unique ID of the note."}
+ },
+ "required": ["noteId"]
+ }
+ },
+ {
+ "name": "get_note_metadata",
+ "description": "Fetch full metadata for a note including attributes and tags.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "noteId": {"type": "string", "description": "Unique ID of the note."}
+ },
+ "required": ["noteId"]
+ }
+ },
+ {
+ "name": "get_note_children",
+ "description": "Fetch a list of child notes for a given parent note.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "noteId": {"type": "string", "description": "Unique ID of the parent note."}
+ },
+ "required": ["noteId"]
+ }
+ },
+ {
+ "name": "create_note",
+ "description": "Create a new note in Trilium.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "parentNoteId": {"type": "string", "description": "ID of parent note (default 'root')."},
+ "title": {"type": "string", "description": "Title of the note."},
+ "content": {"type": "string", "description": "HTML content for the note."},
+ "type": {"type": "string", "description": "Type of note (e.g., 'text').", "default": "text"}
+ },
+ "required": ["title", "content"]
+ }
+ },
+ {
+ "name": "update_note_content",
+ "description": "Update the content of an existing note.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "noteId": {"type": "string", "description": "ID of the note to update."},
+ "content": {"type": "string", "description": "New HTML content."}
+ },
+ "required": ["noteId", "content"]
+ }
+ }
+ ]
+
+ def callTool(self, name: str, params: Dict[str, Any]) -> Any:
+ """Dispatcher for tool calls."""
+ if name == "search_notes":
+ return self.client.searchNotes(params["query"])
+ elif name == "get_note_content":
+ return self.client.getNoteContent(params["noteId"])
+ elif name == "get_note_metadata":
+ return self.client.getNoteMetadata(params["noteId"])
+ elif name == "get_note_children":
+ return self.client.getNoteChildren(params["noteId"])
+ elif name == "create_note":
+ parent_note_id = params.get("parentNoteId", "root")
+ return self.client.createNote(
+ parent_note_id,
+ params["title"],
+ params["content"],
+ params.get("type", "text")
+ )
+ elif name == "update_note_content":
+ self.client.updateNoteContent(params["noteId"], params["content"])
+ return f"Successfully updated note content for {params['noteId']}."
+ else:
+ raise ValueError(f"Unknown tool: {name}")
diff --git a/skills/trilium/mcp/src/server.py b/skills/trilium/mcp/src/server.py
new file mode 100644
index 0000000..c85378f
--- /dev/null
+++ b/skills/trilium/mcp/src/server.py
@@ -0,0 +1,98 @@
+import json
+import sys
+from typing import Dict, Any, Optional, List
+from .handlers import ToolHandlers
+
+
+class MCPServer:
+ def __init__(self, handlers: ToolHandlers):
+ self.handlers = handlers
+
+ def sendResponse(self, result: Any = None, error: Any = None, id: Optional[Any] = None) -> None:
+ """Send formatted JSON-RPC 2.0 response to stdout."""
+ response = {"jsonrpc": "2.0", "id": id}
+ if error:
+ response["error"] = error
+ else:
+ response["result"] = result
+ sys.stdout.write(json.dumps(response) + "\n")
+ sys.stdout.flush()
+
+ def sendError(self, code: int, message: str, id: Optional[Any] = None) -> None:
+ """Helper to send JSON-RPC errors."""
+ self.sendResponse(error={"code": code, "message": message}, id=id)
+
+ def run(self) -> None:
+ """Main loop reading JSON-RPC messages from stdin."""
+ for line in sys.stdin:
+ if not line.strip():
+ continue
+ try:
+ request = json.loads(line)
+ except json.JSONDecodeError:
+ self.sendError(-32700, "Parse error")
+ continue
+
+ # Standard JSON-RPC validation
+ if not isinstance(request, dict) or request.get("jsonrpc") != "2.0":
+ self.sendError(-32600, "Invalid Request", id=request.get("id"))
+ continue
+
+ method = request.get("method")
+ params = request.get("params", {})
+ req_id = request.get("id")
+
+ try:
+ self.handleRequest(method, params, req_id)
+ except Exception as e:
+ # Catch-all for tool execution errors or internal issues
+ self.sendError(-32603, f"Internal error: {str(e)}", id=req_id)
+
+ def handleRequest(self, method: str, params: Dict[str, Any], req_id: Any) -> None:
+ """Dispatcher for MCP methods."""
+ if method == "initialize":
+ # Respond with server capabilities
+ self.sendResponse(
+ result={
+ "protocolVersion": "2024-11-05",
+ "capabilities": {
+ "tools": {}
+ },
+ "serverInfo": {
+ "name": "trilium-mcp-server",
+ "version": "0.1.0"
+ }
+ },
+ id=req_id
+ )
+ elif method == "notifications/initialized":
+ # Ignore acknowledgment for now
+ pass
+ elif method == "tools/list":
+ # List all tools and their schemas
+ tools = self.handlers.listTools()
+ self.sendResponse(result={"tools": tools}, id=req_id)
+ elif method == "tools/call":
+ # Call a specific tool with params
+ tool_name = params.get("name")
+ tool_params = params.get("arguments", {})
+ if not tool_name:
+ self.sendError(-32602, "Invalid params: Missing tool name", id=req_id)
+ return
+
+ result = self.handlers.callTool(tool_name, tool_params)
+
+ # Format the result correctly for MCP
+ mcp_result = self.formatToolResult(result)
+ self.sendResponse(result={"content": mcp_result}, id=req_id)
+ else:
+ self.sendError(-32601, f"Method not found: {method}", id=req_id)
+
+ def formatToolResult(self, result: Any) -> List[Dict[str, str]]:
+ """Format arbitrary result into MCP TextContent list."""
+ if isinstance(result, (dict, list)):
+ text = json.dumps(result, indent=2)
+ else:
+ text = str(result)
+
+ return [{"type": "text", "text": text}]
diff --git a/skills/trilium/mcp/src/trilium_client.py b/skills/trilium/mcp/src/trilium_client.py
new file mode 100644
index 0000000..460e271
--- /dev/null
+++ b/skills/trilium/mcp/src/trilium_client.py
@@ -0,0 +1,85 @@
+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."""
+ # ETAPI pattern to list notes with a specific parent
+ response = requests.get(
+ f"{self.url}/etapi/notes",
+ params={"parentNoteId": note_id},
+ 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
+ }
+ response = requests.post(
+ f"{self.url}/etapi/notes",
+ 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/html")
+ )
+ response.raise_for_status()
diff --git a/skills/trilium/mcp/test_server.py b/skills/trilium/mcp/test_server.py
new file mode 100644
index 0000000..626750e
--- /dev/null
+++ b/skills/trilium/mcp/test_server.py
@@ -0,0 +1,99 @@
+import unittest
+from unittest.mock import MagicMock, patch
+import json
+import io
+import sys
+from src.trilium_client import TriliumClient
+from src.handlers import ToolHandlers
+from src.server import MCPServer
+
+class TestTriliumMCP(unittest.TestCase):
+ def setUp(self):
+ self.mock_client = MagicMock()
+ self.handlers = ToolHandlers(self.mock_client)
+ self.server = MCPServer(self.handlers)
+
+ def testInitialize(self):
+ # Mock stdin with initialize request
+ stdin_content = json.dumps({
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "initialize",
+ "params": {}
+ }) + "\n"
+
+ with patch('sys.stdin', io.StringIO(stdin_content)):
+ with patch('sys.stdout', io.StringIO()) as mock_stdout:
+ self.server.run()
+ output = mock_stdout.getvalue().strip()
+ response = json.loads(output)
+
+ self.assertEqual(response["id"], 1)
+ self.assertIn("capabilities", response["result"])
+ self.assertIn("tools", response["result"]["capabilities"])
+
+ def testListTools(self):
+ # Mock stdin with tools/list request
+ stdin_content = json.dumps({
+ "jsonrpc": "2.0",
+ "id": 2,
+ "method": "tools/list",
+ "params": {}
+ }) + "\n"
+
+ with patch('sys.stdin', io.StringIO(stdin_content)):
+ with patch('sys.stdout', io.StringIO()) as mock_stdout:
+ self.server.run()
+ output = mock_stdout.getvalue().strip()
+ response = json.loads(output)
+
+ self.assertEqual(response["id"], 2)
+ self.assertGreater(len(response["result"]["tools"]), 0)
+ # Verify search_notes tool exists
+ tool_names = [t["name"] for t in response["result"]["tools"]]
+ self.assertIn("search_notes", tool_names)
+
+ def testCallSearchNotes(self):
+ # Setup mock return value for client
+ self.mock_client.searchNotes.return_value = [{"noteId": "abc", "title": "Test Note"}]
+
+ stdin_content = json.dumps({
+ "jsonrpc": "2.0",
+ "id": 3,
+ "method": "tools/call",
+ "params": {
+ "name": "search_notes",
+ "arguments": {"query": "test"}
+ }
+ }) + "\n"
+
+ with patch('sys.stdin', io.StringIO(stdin_content)):
+ with patch('sys.stdout', io.StringIO()) as mock_stdout:
+ self.server.run()
+ output = mock_stdout.getvalue().strip()
+ response = json.loads(output)
+
+ self.assertEqual(response["id"], 3)
+ content = response["result"]["content"][0]["text"]
+ self.assertIn("abc", content)
+ self.assertIn("Test Note", content)
+
+ def testInvalidMethod(self):
+ stdin_content = json.dumps({
+ "jsonrpc": "2.0",
+ "id": 4,
+ "method": "non_existent_method",
+ "params": {}
+ }) + "\n"
+
+ with patch('sys.stdin', io.StringIO(stdin_content)):
+ with patch('sys.stdout', io.StringIO()) as mock_stdout:
+ self.server.run()
+ output = mock_stdout.getvalue().strip()
+ response = json.loads(output)
+
+ self.assertEqual(response["id"], 4)
+ self.assertEqual(response["error"]["code"], -32601)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/skills/trilium/references/api_reference.md b/skills/trilium/references/api_reference.md
new file mode 100644
index 0000000..5b8bc5d
--- /dev/null
+++ b/skills/trilium/references/api_reference.md
@@ -0,0 +1,30 @@
+# Trilium Search Syntax Reference
+
+Trilium uses a powerful search syntax. Here are the most common patterns:
+
+## Basic Search
+- `word1 word2` - Search for notes containing both `word1` and `word2`.
+- `"exact phrase"` - Search for the exact phrase.
+
+## Attribute Search
+- `#tag` - Find notes with tag `tag`.
+- `#tag=value` - Find notes with tag `tag` having value `value`.
+- `#dateCreated >= '2024-01-01'` - Find notes created since 2024.
+- `!#tag` - Find notes without the tag `tag`.
+
+## Logical Operators
+- `word1 AND word2` - Logical AND.
+- `word1 OR word2` - Logical OR.
+- `NOT word1` - Logical NOT.
+
+## Field Search
+- `title *= 'part of title'` - Title contains the string.
+- `content *= 'part of content'` - Content contains the string.
+- `type = 'text'` - Filter by note type.
+
+## Advanced Examples
+- `note.dateCreated >= 2024-05-01 AND note.title *= 'Sync'`
+- `#status = 'todo' AND #priority = 'high'`
+- `parent.title = 'Project X' AND #todo`
+
+For more details, see the official Trilium [Search documentation](https://github.com/zadam/trilium/wiki/Search).