triliumnext-mcp 0.3.10-beta.2 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.js +3 -0
- package/build/modules/attributeListHandler.js +34 -0
- package/build/modules/attributeListManager.js +147 -0
- package/build/modules/noteManager.js +37 -20
- package/build/modules/searchManager.js +53 -1
- package/build/modules/toolDefinitions.js +35 -13
- package/build/utils/contentRules.js +10 -2
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { handleCreateNoteRequest, handleUpdateNoteRequest, handleDeleteNoteReque
|
|
|
9
9
|
import { handleSearchNotesRequest } from "./modules/searchHandler.js";
|
|
10
10
|
import { handleResolveNoteRequest } from "./modules/resolveHandler.js";
|
|
11
11
|
import { handleManageAttributes, handleReadAttributes } from "./modules/attributeHandler.js";
|
|
12
|
+
import { handleListAttributesRequest } from "./modules/attributeListHandler.js";
|
|
12
13
|
const TRILIUM_API_URL = process.env.TRILIUM_API_URL;
|
|
13
14
|
const TRILIUM_API_TOKEN = process.env.TRILIUM_API_TOKEN;
|
|
14
15
|
const PERMISSIONS = process.env.PERMISSIONS || "READ;WRITE";
|
|
@@ -77,6 +78,8 @@ class TriliumServer {
|
|
|
77
78
|
return await handleReadAttributes(request.params.arguments, this.axiosInstance, this);
|
|
78
79
|
case "manage_attributes":
|
|
79
80
|
return await handleManageAttributes(request.params.arguments, this.axiosInstance, this);
|
|
81
|
+
case "list_attributes":
|
|
82
|
+
return await handleListAttributesRequest(request.params.arguments, this.axiosInstance, this);
|
|
80
83
|
default:
|
|
81
84
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
82
85
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attribute List Handler Module
|
|
3
|
+
* Centralized request handling for attribute listing operations
|
|
4
|
+
*/
|
|
5
|
+
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { handleListAttributes } from "./attributeListManager.js";
|
|
7
|
+
/**
|
|
8
|
+
* Handle list_attributes tool requests
|
|
9
|
+
*/
|
|
10
|
+
export async function handleListAttributesRequest(args, axiosInstance, permissionChecker) {
|
|
11
|
+
if (!permissionChecker.hasPermission("READ")) {
|
|
12
|
+
throw new McpError(ErrorCode.InvalidRequest, "Permission denied: Not authorized to list attributes.");
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const listOperation = {
|
|
16
|
+
noteId: args.noteId,
|
|
17
|
+
hierarchyLevel: args.hierarchyLevel || 'immediate',
|
|
18
|
+
limit: args.limit || 50
|
|
19
|
+
};
|
|
20
|
+
const result = await handleListAttributes(listOperation, axiosInstance);
|
|
21
|
+
return {
|
|
22
|
+
content: [{
|
|
23
|
+
type: "text",
|
|
24
|
+
text: JSON.stringify(result, null, 2)
|
|
25
|
+
}]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error instanceof McpError) {
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attribute List Management Module
|
|
3
|
+
* Handles attribute listing operations using search_notes internally
|
|
4
|
+
*/
|
|
5
|
+
import { handleSearchNotes } from './searchManager.js';
|
|
6
|
+
import { logVerboseInput, logVerboseOutput } from '../utils/verboseUtils.js';
|
|
7
|
+
/**
|
|
8
|
+
* Handle list attributes operation using search_notes internally
|
|
9
|
+
*/
|
|
10
|
+
export async function handleListAttributes(args, axiosInstance) {
|
|
11
|
+
logVerboseInput('handleListAttributes', args);
|
|
12
|
+
// Build search criteria based on hierarchy navigation
|
|
13
|
+
const searchCriteria = buildHierarchySearchCriteria(args);
|
|
14
|
+
// Use search_notes internally to find notes
|
|
15
|
+
const searchOperation = {
|
|
16
|
+
searchCriteria: searchCriteria,
|
|
17
|
+
limit: args.limit || 100
|
|
18
|
+
};
|
|
19
|
+
const searchResults = await handleSearchNotes(searchOperation, axiosInstance);
|
|
20
|
+
// Extract and organize attributes from search results
|
|
21
|
+
const attributes = extractAttributesFromResults(searchResults.results, args);
|
|
22
|
+
// Generate summary
|
|
23
|
+
const summary = generateAttributeSummary(attributes, args);
|
|
24
|
+
const result = {
|
|
25
|
+
attributes: attributes,
|
|
26
|
+
summary: summary
|
|
27
|
+
};
|
|
28
|
+
logVerboseOutput('handleListAttributes', result);
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build search criteria for hierarchy navigation
|
|
33
|
+
*/
|
|
34
|
+
function buildHierarchySearchCriteria(args) {
|
|
35
|
+
const criteria = [];
|
|
36
|
+
const hierarchyLevel = args.hierarchyLevel || 'immediate';
|
|
37
|
+
if (hierarchyLevel === 'immediate') {
|
|
38
|
+
// For immediate level, search for direct parents and children
|
|
39
|
+
criteria.push({
|
|
40
|
+
property: 'parents.noteId',
|
|
41
|
+
type: 'noteProperty',
|
|
42
|
+
op: '=',
|
|
43
|
+
value: args.noteId
|
|
44
|
+
});
|
|
45
|
+
criteria.push({
|
|
46
|
+
property: 'children.noteId',
|
|
47
|
+
type: 'noteProperty',
|
|
48
|
+
op: '=',
|
|
49
|
+
value: args.noteId,
|
|
50
|
+
logic: 'OR'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// For all levels, search for ancestors and descendants
|
|
55
|
+
criteria.push({
|
|
56
|
+
property: 'ancestors.noteId',
|
|
57
|
+
type: 'noteProperty',
|
|
58
|
+
op: '=',
|
|
59
|
+
value: args.noteId
|
|
60
|
+
});
|
|
61
|
+
criteria.push({
|
|
62
|
+
property: 'descendants.noteId',
|
|
63
|
+
type: 'noteProperty',
|
|
64
|
+
op: '=',
|
|
65
|
+
value: args.noteId,
|
|
66
|
+
logic: 'OR'
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// Exclude the anchor note itself
|
|
70
|
+
criteria.push({
|
|
71
|
+
property: 'noteId',
|
|
72
|
+
type: 'noteProperty',
|
|
73
|
+
op: '!=',
|
|
74
|
+
value: args.noteId,
|
|
75
|
+
logic: 'AND'
|
|
76
|
+
});
|
|
77
|
+
return criteria;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Extract and organize attributes from search results
|
|
81
|
+
*/
|
|
82
|
+
function extractAttributesFromResults(searchResults, args) {
|
|
83
|
+
const attributes = [];
|
|
84
|
+
for (const note of searchResults) {
|
|
85
|
+
if (note.attributes && Array.isArray(note.attributes)) {
|
|
86
|
+
for (const attribute of note.attributes) {
|
|
87
|
+
const attributeInfo = {
|
|
88
|
+
noteId: note.noteId,
|
|
89
|
+
noteTitle: note.title,
|
|
90
|
+
noteType: note.type,
|
|
91
|
+
attributeId: attribute.attributeId,
|
|
92
|
+
attributeType: attribute.type,
|
|
93
|
+
attributeName: attribute.name,
|
|
94
|
+
attributeValue: attribute.value || '',
|
|
95
|
+
position: attribute.position || 10,
|
|
96
|
+
isInheritable: attribute.isInheritable || false,
|
|
97
|
+
hierarchyPath: buildHierarchyPath(note, args)
|
|
98
|
+
};
|
|
99
|
+
attributes.push(attributeInfo);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return attributes;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Build hierarchy path for attribute context
|
|
107
|
+
*/
|
|
108
|
+
function buildHierarchyPath(note, args) {
|
|
109
|
+
const hierarchyLevel = args.hierarchyLevel || 'immediate';
|
|
110
|
+
const noteTitle = note.title || 'Unknown';
|
|
111
|
+
if (hierarchyLevel === 'immediate') {
|
|
112
|
+
return `Related to ${args.noteId}: ${noteTitle}`;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
return `Hierarchy-connected to ${args.noteId}: ${noteTitle}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Generate summary of attributes found
|
|
120
|
+
*/
|
|
121
|
+
function generateAttributeSummary(attributes, args) {
|
|
122
|
+
const totalAttributes = attributes.length;
|
|
123
|
+
if (totalAttributes === 0) {
|
|
124
|
+
return `No attributes found`;
|
|
125
|
+
}
|
|
126
|
+
// Group by attribute type
|
|
127
|
+
const labelCount = attributes.filter(attr => attr.attributeType === 'label').length;
|
|
128
|
+
const relationCount = attributes.filter(attr => attr.attributeType === 'relation').length;
|
|
129
|
+
// Group by note
|
|
130
|
+
const uniqueNotes = new Set(attributes.map(attr => attr.noteId)).size;
|
|
131
|
+
let summary = `Found ${totalAttributes} attributes across ${uniqueNotes} notes`;
|
|
132
|
+
if (labelCount > 0 || relationCount > 0) {
|
|
133
|
+
summary += ` (${labelCount} labels, ${relationCount} relations)`;
|
|
134
|
+
}
|
|
135
|
+
// Add hierarchy context
|
|
136
|
+
const hierarchyLevel = args.hierarchyLevel || 'immediate';
|
|
137
|
+
if (hierarchyLevel === 'immediate') {
|
|
138
|
+
summary += ` in immediate hierarchy (parents and children)`;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
summary += ` in full hierarchy (ancestors and descendants)`;
|
|
142
|
+
}
|
|
143
|
+
// Add unique attribute names count
|
|
144
|
+
const uniqueAttributeNames = new Set(attributes.map(attr => attr.attributeName)).size;
|
|
145
|
+
summary += ` with ${uniqueAttributeNames} unique attribute types`;
|
|
146
|
+
return summary;
|
|
147
|
+
}
|
|
@@ -129,35 +129,52 @@ async function checkDuplicateTitleInDirectory(parentNoteId, title, axiosInstance
|
|
|
129
129
|
* Handle create note operation
|
|
130
130
|
*/
|
|
131
131
|
export async function handleCreateNote(args, axiosInstance) {
|
|
132
|
-
const { parentNoteId, title, type, content: rawContent, mime, attributes
|
|
132
|
+
const { parentNoteId, title, type, content: rawContent, mime, attributes } = args;
|
|
133
133
|
// Validate required parameters
|
|
134
134
|
if (!parentNoteId || !title || !type) {
|
|
135
135
|
throw new Error("parentNoteId, title, and type are required for create operation.");
|
|
136
136
|
}
|
|
137
|
-
// Check for duplicate title in the same directory
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
nextSteps: `Please specify your choice by calling create_note again with your preferred action. To update the existing note, use update_note with noteId: ${duplicateCheck.duplicateNoteId}`
|
|
151
|
-
};
|
|
152
|
-
}
|
|
137
|
+
// Check for duplicate title in the same directory
|
|
138
|
+
const duplicateCheck = await checkDuplicateTitleInDirectory(parentNoteId, title, axiosInstance);
|
|
139
|
+
if (duplicateCheck.found) {
|
|
140
|
+
return {
|
|
141
|
+
message: `Found existing note with title "${title}" in this directory. Please choose how to proceed:`,
|
|
142
|
+
duplicateFound: true,
|
|
143
|
+
duplicateNoteId: duplicateCheck.duplicateNoteId,
|
|
144
|
+
choices: {
|
|
145
|
+
skip: "Skip creation - do nothing",
|
|
146
|
+
updateExisting: "Update existing - replace content of existing note with new content"
|
|
147
|
+
},
|
|
148
|
+
nextSteps: `Please specify your choice by calling create_note again with a different title, or use update_note with noteId: ${duplicateCheck.duplicateNoteId}`
|
|
149
|
+
};
|
|
153
150
|
}
|
|
154
151
|
// Process content to ETAPI format
|
|
155
152
|
// Content is optional - if not provided, default to empty string
|
|
156
153
|
const content = rawContent || "";
|
|
157
154
|
// Extract template relation for content validation
|
|
158
155
|
const templateRelation = extractTemplateRelation(attributes);
|
|
156
|
+
// Clean template relation for consistent processing
|
|
157
|
+
const cleanedTemplateRelation = templateRelation ? templateRelation.trim() : '';
|
|
158
|
+
// Auto-correct note type for container templates
|
|
159
|
+
let correctedType = type;
|
|
160
|
+
if (templateRelation) {
|
|
161
|
+
// List of container templates that require 'book' type
|
|
162
|
+
const containerTemplates = [
|
|
163
|
+
'board', '_template_board',
|
|
164
|
+
'grid view', '_template_grid_view',
|
|
165
|
+
'list view', '_template_list_view',
|
|
166
|
+
'geo map', '_template_geo_map',
|
|
167
|
+
'calendar', '_template_calendar'
|
|
168
|
+
];
|
|
169
|
+
const isContainerTemplate = cleanedTemplateRelation &&
|
|
170
|
+
containerTemplates.includes(cleanedTemplateRelation.toLowerCase());
|
|
171
|
+
if (isContainerTemplate && type !== 'book') {
|
|
172
|
+
logVerbose("handleCreateNote", `Auto-correcting note type from '${type}' to 'book' for ${templateRelation} template`);
|
|
173
|
+
correctedType = 'book';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
159
176
|
// Validate content with template-aware rules
|
|
160
|
-
const contentValidation = await validateContentForNoteType(content,
|
|
177
|
+
const contentValidation = await validateContentForNoteType(content, correctedType, undefined, templateRelation);
|
|
161
178
|
if (!contentValidation.valid) {
|
|
162
179
|
return {
|
|
163
180
|
message: `CONTENT_VALIDATION_ERROR: ${contentValidation.error}`,
|
|
@@ -168,7 +185,7 @@ export async function handleCreateNote(args, axiosInstance) {
|
|
|
168
185
|
// Use validated content (may have been auto-corrected)
|
|
169
186
|
const validatedContent = contentValidation.content;
|
|
170
187
|
// Process content to ETAPI format
|
|
171
|
-
const processed = await processContentArray(validatedContent,
|
|
188
|
+
const processed = await processContentArray(validatedContent, correctedType);
|
|
172
189
|
if (processed.error) {
|
|
173
190
|
throw new Error(`Content processing error: ${processed.error}`);
|
|
174
191
|
}
|
|
@@ -177,7 +194,7 @@ export async function handleCreateNote(args, axiosInstance) {
|
|
|
177
194
|
const noteData = {
|
|
178
195
|
parentNoteId,
|
|
179
196
|
title,
|
|
180
|
-
type,
|
|
197
|
+
type: correctedType,
|
|
181
198
|
content: processedContent
|
|
182
199
|
};
|
|
183
200
|
// Add MIME type if specified
|
|
@@ -5,6 +5,55 @@
|
|
|
5
5
|
import { buildSearchQuery } from "./searchQueryBuilder.js";
|
|
6
6
|
import { trimNoteResults } from "../utils/noteFormatter.js";
|
|
7
7
|
import { createSearchDebugInfo } from "../utils/verboseUtils.js";
|
|
8
|
+
/**
|
|
9
|
+
* Filters out parent notes from search results when performing hierarchy searches
|
|
10
|
+
* This prevents the parent folder itself from appearing in results when searching for its children/descendants
|
|
11
|
+
*
|
|
12
|
+
* @param searchResults - The raw search results from Trilium API
|
|
13
|
+
* @param searchArgs - The original search arguments used to detect hierarchy searches
|
|
14
|
+
* @returns Filtered search results with parent notes excluded
|
|
15
|
+
*/
|
|
16
|
+
function filterParentNotes(searchResults, searchArgs) {
|
|
17
|
+
// If no search criteria, return results as-is (no filtering needed)
|
|
18
|
+
if (!searchArgs.searchCriteria || !Array.isArray(searchArgs.searchCriteria)) {
|
|
19
|
+
return searchResults;
|
|
20
|
+
}
|
|
21
|
+
// Check if any search criteria involves hierarchy navigation (parents, children, ancestors)
|
|
22
|
+
const hasHierarchySearch = searchArgs.searchCriteria.some(criteria => {
|
|
23
|
+
if (criteria.type !== 'noteProperty')
|
|
24
|
+
return false;
|
|
25
|
+
const property = criteria.property;
|
|
26
|
+
return property.startsWith('parents.') ||
|
|
27
|
+
property.startsWith('children.') ||
|
|
28
|
+
property.startsWith('ancestors.');
|
|
29
|
+
});
|
|
30
|
+
// If no hierarchy search, return results as-is
|
|
31
|
+
if (!hasHierarchySearch) {
|
|
32
|
+
return searchResults;
|
|
33
|
+
}
|
|
34
|
+
// Extract target note IDs from hierarchy search criteria
|
|
35
|
+
const targetNoteIds = new Set();
|
|
36
|
+
searchArgs.searchCriteria.forEach(criteria => {
|
|
37
|
+
if (criteria.type === 'noteProperty' && criteria.value) {
|
|
38
|
+
const property = criteria.property;
|
|
39
|
+
// Handle different hierarchy property patterns
|
|
40
|
+
if (property.endsWith('.noteId') && criteria.op === '=') {
|
|
41
|
+
targetNoteIds.add(criteria.value);
|
|
42
|
+
}
|
|
43
|
+
// For other hierarchy properties, we might need to resolve note IDs from titles
|
|
44
|
+
// This is a simplified approach - in practice, you'd need to resolve titles to IDs
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
// If no target note IDs found, return results as-is
|
|
48
|
+
if (targetNoteIds.size === 0) {
|
|
49
|
+
return searchResults;
|
|
50
|
+
}
|
|
51
|
+
// Filter out notes that match the target note IDs (these are the parents we want to exclude)
|
|
52
|
+
const filteredResults = searchResults.filter(note => {
|
|
53
|
+
return !targetNoteIds.has(note.noteId);
|
|
54
|
+
});
|
|
55
|
+
return filteredResults;
|
|
56
|
+
}
|
|
8
57
|
/**
|
|
9
58
|
* Handle search notes operation
|
|
10
59
|
*/
|
|
@@ -26,7 +75,10 @@ export async function handleSearchNotes(args, axiosInstance) {
|
|
|
26
75
|
// Prepare verbose debug info if enabled
|
|
27
76
|
const verboseInfo = createSearchDebugInfo(query, args);
|
|
28
77
|
let searchResults = response.data.results || [];
|
|
29
|
-
|
|
78
|
+
// Apply parent filtering for hierarchy searches to exclude parent notes from results
|
|
79
|
+
// This implements the documented behavior: "Parent note filtering automatically applied to avoid showing parent in children/descendant lists"
|
|
80
|
+
const filteredResults = filterParentNotes(searchResults, args);
|
|
81
|
+
const trimmedResults = trimNoteResults(filteredResults);
|
|
30
82
|
return {
|
|
31
83
|
results: trimmedResults,
|
|
32
84
|
debugInfo: verboseInfo
|
|
@@ -9,7 +9,7 @@ export function createWriteTools() {
|
|
|
9
9
|
return [
|
|
10
10
|
{
|
|
11
11
|
name: "create_note",
|
|
12
|
-
description: "Create a new note in TriliumNext with duplicate title detection. When a note with the same title already exists in the same directory, you'll be presented with choices: skip creation
|
|
12
|
+
description: "Create a new note in TriliumNext with duplicate title detection. When a note with the same title already exists in the same directory, you'll be presented with choices: skip creation or update the existing note. ONLY use this tool when the user explicitly requests note creation (e.g., 'create a note', 'make a new note'). DO NOT use this tool proactively or when the user is only asking questions about their notes. TIP: For code notes, content is plain text (no HTML processing).",
|
|
13
13
|
inputSchema: {
|
|
14
14
|
type: "object",
|
|
15
15
|
properties: {
|
|
@@ -29,7 +29,7 @@ export function createWriteTools() {
|
|
|
29
29
|
},
|
|
30
30
|
content: {
|
|
31
31
|
type: "string",
|
|
32
|
-
description: "Content of the note (optional). Content requirements by note type: TEXT notes require HTML content (plain text auto-wrapped in <p> tags, e.g., '<p>Hello world</p>', '<strong>bold</strong>'); CODE/MERMAID notes require plain text ONLY (HTML tags rejected, e.g., 'def fibonacci(n):'); ⚠️ OMIT CONTENT for: 1) WEBVIEW notes (use #webViewSrc label instead), 2) Container templates (Board, Calendar, Grid View, List View, Table, Geo Map), 3)
|
|
32
|
+
description: "Content of the note (optional). Content requirements by note type: TEXT notes require HTML content (plain text auto-wrapped in <p> tags, e.g., '<p>Hello world</p>', '<strong>bold</strong>'); CODE/MERMAID notes require plain text ONLY (HTML tags rejected, e.g., 'def fibonacci(n):'); ⚠️ OMIT CONTENT for: 1) WEBVIEW notes (use #webViewSrc label instead), 2) Container templates (Board, Calendar, Grid View, List View, Table, Geo Map), 3) SYSTEM notes: RENDER, SEARCH, RELATION_MAP, NOTE_MAP, BOOK - these must be EMPTY to work properly. When omitted, note will be created with empty content.\n\n📋 WORKFLOW FOR SPECIAL NOTE TYPES:\n• RENDER notes: Create empty → create child HTML code note → add ~renderNote relation to child\n• CALENDAR/BOARD/GRID/LIST/TABLE/GEO templates: Create empty with ~template relation → add child notes with proper labels\n• TEXT SNIPPET templates: Create with ~template relation + content\n• Most other types: Add content directly during creation"
|
|
33
33
|
},
|
|
34
34
|
mime: {
|
|
35
35
|
type: "string",
|
|
@@ -37,7 +37,7 @@ export function createWriteTools() {
|
|
|
37
37
|
},
|
|
38
38
|
attributes: {
|
|
39
39
|
type: "array",
|
|
40
|
-
description: "Optional attributes to create with the note (labels and relations). Enables one-step note creation with metadata. ⚠️ CRITICAL: Always add template relations during create_note when possible!\n\n📋 TEMPLATE RELATIONS (add during create_note):\n• ~template = 'Board' (kanban task boards)\n• ~template = 'Calendar' (calendar event displays)\n• ~template = 'Grid View' (grid layouts)\n• ~template = 'List View' (list layouts)\n• ~template = 'Table' (table structures)\n• ~template = 'Geo Map' (geographic maps)\n• ~template = 'Text Snippet' (reusable text)\n\n⚠️ SPECIAL CASES requiring separate steps:\n• ~renderNote = '<noteId>' (RENDER notes - requires existing child note ID)\n• Custom relations pointing to specific notes (use note IDs after creation)\n\n📋 LABELS & OTHER RELATIONS:\n• Labels: #tag format (e.g., #important, #project)\n• Relations: ~connection format (e.g., ~author, ~publisher)\n• Template relations use human-readable names (auto-translated to system IDs)\n\n⚠️ TEMPLATE RESTRICTIONS: Container templates MUST be empty
|
|
40
|
+
description: "Optional attributes to create with the note (labels and relations). Enables one-step note creation with metadata. ⚠️ CRITICAL: Always add template relations during create_note when possible!\n\n📋 TEMPLATE RELATIONS (add during create_note):\n• ~template = 'Board' (kanban task boards)\n• ~template = 'Calendar' (calendar event displays)\n• ~template = 'Grid View' (grid layouts)\n• ~template = 'List View' (list layouts)\n• ~template = 'Table' (table structures)\n• ~template = 'Geo Map' (geographic maps)\n• ~template = 'Text Snippet' (reusable text)\n\n⚠️ SPECIAL CASES requiring separate steps:\n• ~renderNote = '<noteId>' (RENDER notes - requires existing child note ID)\n• Custom relations pointing to specific notes (use note IDs after creation)\n\n📋 LABELS & OTHER RELATIONS:\n• Labels: #tag format (e.g., #important, #project)\n• Relations: ~connection format (e.g., ~author, ~publisher)\n• Template relations use human-readable names (auto-translated to system IDs)\n\n⚠️ TEMPLATE RESTRICTIONS: Container templates MUST be book note type with empty content - add content as child notes",
|
|
41
41
|
items: {
|
|
42
42
|
type: "object",
|
|
43
43
|
properties: {
|
|
@@ -68,11 +68,6 @@ export function createWriteTools() {
|
|
|
68
68
|
required: ["type", "name"]
|
|
69
69
|
}
|
|
70
70
|
},
|
|
71
|
-
forceCreate: {
|
|
72
|
-
type: "boolean",
|
|
73
|
-
description: "Bypass duplicate title check and create note even if a note with the same title already exists in the same directory. Use this when you want to intentionally create duplicate notes.",
|
|
74
|
-
default: false
|
|
75
|
-
},
|
|
76
71
|
},
|
|
77
72
|
required: ["parentNoteId", "title", "type"],
|
|
78
73
|
},
|
|
@@ -187,7 +182,7 @@ export function createReadTools() {
|
|
|
187
182
|
return [
|
|
188
183
|
{
|
|
189
184
|
name: "get_note",
|
|
190
|
-
description: "Get a note and its content by ID. Perfect for when someone wants to see what's in a note, extract specific information, or prepare for search and replace operations. Getting the full content lets you see the context and
|
|
185
|
+
description: "Get a note and its content by ID. Perfect for when someone wants to see what's in a note, extract specific information, or prepare for search and replace operations. Getting the full content lets you see the context and extract information accurately. For extracting information from multiple notes: use search_notes to find relevant notes, then use get_note (with useRegex=false for LLM analysis, or useRegex=true for simple pattern matching).",
|
|
191
186
|
inputSchema: {
|
|
192
187
|
type: "object",
|
|
193
188
|
properties: {
|
|
@@ -197,16 +192,16 @@ export function createReadTools() {
|
|
|
197
192
|
},
|
|
198
193
|
searchPattern: {
|
|
199
194
|
type: "string",
|
|
200
|
-
description: "Optional pattern to search for within the note.
|
|
195
|
+
description: "Optional pattern to search for within the note. ⚠️ IMPORTANT: When users request to extract specific types of information (URLs, emails, dates, code snippets, etc.), automatically generate appropriate regex patterns based on the extraction request. Use useRegex=true for pattern matching and searchFlags='gi' for comprehensive results.",
|
|
201
196
|
},
|
|
202
197
|
useRegex: {
|
|
203
198
|
type: "boolean",
|
|
204
|
-
description: "Whether to use regex patterns (
|
|
205
|
-
default:
|
|
199
|
+
description: "Whether to use regex patterns for searchPattern. Set to true for simple pattern matching, or false to let LLM read and analyze full content for more accurate extraction (recommended for complex cases like URLs, emails with context).",
|
|
200
|
+
default: false
|
|
206
201
|
},
|
|
207
202
|
searchFlags: {
|
|
208
203
|
type: "string",
|
|
209
|
-
description: "Search options. Defaults to 'gi' (find all matches, case-insensitive).",
|
|
204
|
+
description: "Search options. Defaults to 'gi' (find all matches, case-insensitive). Use 'gi' for comprehensive extraction of all patterns.",
|
|
210
205
|
default: "gi"
|
|
211
206
|
},
|
|
212
207
|
},
|
|
@@ -322,6 +317,33 @@ export function createReadAttributeTools() {
|
|
|
322
317
|
},
|
|
323
318
|
required: ["noteId"]
|
|
324
319
|
}
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: "list_attributes",
|
|
323
|
+
description: "List attributes from note hierarchy using search_notes internally. Explore attributes across immediate hierarchy (parents and children) or full hierarchy (ancestors and descendants). Returns comprehensive attribute information including note context, attribute details, and hierarchy relationships. Perfect for understanding template usage, label patterns, and relation networks across your note structure.",
|
|
324
|
+
inputSchema: {
|
|
325
|
+
type: "object",
|
|
326
|
+
properties: {
|
|
327
|
+
noteId: {
|
|
328
|
+
type: "string",
|
|
329
|
+
description: "Anchor note ID for hierarchy navigation"
|
|
330
|
+
},
|
|
331
|
+
hierarchyLevel: {
|
|
332
|
+
type: "string",
|
|
333
|
+
enum: ["immediate", "all"],
|
|
334
|
+
description: "Hierarchy navigation depth: 'immediate' (direct parents and children only) or 'all' (include ancestors and descendants)",
|
|
335
|
+
default: "immediate"
|
|
336
|
+
},
|
|
337
|
+
limit: {
|
|
338
|
+
type: "number",
|
|
339
|
+
description: "Maximum number of results to return",
|
|
340
|
+
default: 50,
|
|
341
|
+
minimum: 1,
|
|
342
|
+
maximum: 200
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
required: ["noteId"]
|
|
346
|
+
}
|
|
325
347
|
}
|
|
326
348
|
];
|
|
327
349
|
}
|
|
@@ -206,8 +206,16 @@ export async function validateContentForNoteType(content, noteType, currentConte
|
|
|
206
206
|
const textContent = content.trim();
|
|
207
207
|
// Get template-aware content rules
|
|
208
208
|
const rules = getTemplateContentRules(noteType, templateRelation);
|
|
209
|
-
//
|
|
210
|
-
if (!rules.
|
|
209
|
+
// Special handling for container templates: empty content is always valid
|
|
210
|
+
if (!textContent && rules.enforceEmpty) {
|
|
211
|
+
return {
|
|
212
|
+
valid: true,
|
|
213
|
+
content: "",
|
|
214
|
+
corrected: false
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
// Check if content is allowed at all (only for non-empty content)
|
|
218
|
+
if (!rules.allowContent && textContent) {
|
|
211
219
|
return {
|
|
212
220
|
valid: false,
|
|
213
221
|
content,
|