search_and_replace_note
Replace text in TriliumNext notes using search patterns. Update note content by specifying search and replacement text, with regex support and version safety checks.
Instructions
Search and replace content within a single note. When someone wants to replace text in a note, first call get_note to get the current content and hash, then use this function to make the changes. This ensures you're working with the latest version of their note.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| noteId | Yes | ID of the note to perform search and replace on | |
| searchPattern | Yes | What to search for in the note. | |
| replacePattern | Yes | What to replace it with. For regex: supports patterns like '$1' for captured groups. | |
| useRegex | No | Whether to use regex patterns (default: true). | |
| searchFlags | No | Search options. Defaults to 'gi' (global, case-insensitive). Remove 'i' for exact case matching. | gi |
| expectedHash | Yes | ⚠️ REQUIRED: Content hash from get_note response. Always get the note content first to obtain this hash. | |
| revision | No | Whether to create a backup before replacing (default: true for safety). |
Implementation Reference
- src/modules/noteManager.ts:689-841 (handler)Core implementation of the search_and_replace_note tool. Fetches note content, validates expectedHash to prevent conflicts, performs search and replace using regex or literal matching, validates new content against note type rules, optionally creates a revision, and updates the note via Trilium API.export async function handleSearchReplaceNote( args: NoteOperation, axiosInstance: any ): Promise<NoteSearchReplaceResponse> { const { noteId, searchPattern, replacePattern, useRegex = true, searchFlags = 'g', revision = true, expectedHash } = args; if (!noteId) { throw new Error("noteId is required for search_and_replace operation."); } if (!searchPattern) { throw new Error("searchPattern is required for search_and_replace operation."); } if (!replacePattern) { throw new Error("replacePattern is required for search_and_replace operation."); } if (!expectedHash) { throw new Error("expectedHash is required for search_and_replace operation. You must call get_note first to retrieve the current blobId."); } let revisionCreated = false; try { // Step 1: Get current note state and content const currentNote = await axiosInstance.get(`/notes/${noteId}`); const currentContent = await axiosInstance.get(`/notes/${noteId}/content`, { responseType: 'text' }); // Step 2: Hash validation const currentBlobId = currentNote.data.blobId; if (currentBlobId !== expectedHash) { return { noteId, message: `CONFLICT: Note has been modified by another user. ` + `Current blobId: ${currentBlobId}, expected: ${expectedHash}. ` + `Please get the latest note content and retry.`, matchesFound: 0, replacementsMade: 0, revisionCreated: false, conflict: true, searchPattern, replacePattern, useRegex }; } const noteType = currentNote.data.type; const originalContent = currentContent.data; // Step 3: Execute search and replace const { newContent, replacements } = executeSearchReplace( originalContent, searchPattern, replacePattern, useRegex, searchFlags ); // Step 4: Handle no matches case if (replacements === 0) { return { noteId, message: `No matches found for pattern "${searchPattern}" in note ${noteId}. No changes made.`, matchesFound: 0, replacementsMade: 0, revisionCreated: false, conflict: false, searchPattern, replacePattern, useRegex }; } // Step 5: Validate new content based on note type const validationResult = await validateContentForNoteType( newContent, noteType as NoteType, originalContent ); if (!validationResult.valid) { return { noteId, message: `CONTENT_TYPE_MISMATCH: ${validationResult.error}`, matchesFound: replacements, replacementsMade: 0, revisionCreated: false, conflict: false, searchPattern, replacePattern, useRegex }; } // Use validated/corrected content const finalContent = validationResult.content; // Step 6: Create revision if requested if (revision) { try { await axiosInstance.post(`/notes/${noteId}/revision`); revisionCreated = true; } catch (error) { console.error(`Warning: Failed to create revision for note ${noteId}:`, error); // Continue with update even if revision creation fails } } // Step 7: Update content const contentResponse = await axiosInstance.put(`/notes/${noteId}/content`, finalContent, { headers: { "Content-Type": "text/plain" } }); if (contentResponse.status !== 204) { throw new Error(`Unexpected response status: ${contentResponse.status}`); } // Step 8: Return success response const correctionMsg = (finalContent !== newContent) ? " (content auto-corrected)" : ""; const revisionMsg = revisionCreated ? " (revision created)" : " (no revision)"; return { noteId, message: `Search and replace completed successfully for note ${noteId}. Found ${replacements} match(es) and made ${replacements} replacement(s).${correctionMsg}${revisionMsg}`, matchesFound: replacements, replacementsMade: replacements, revisionCreated, conflict: false, searchPattern, replacePattern, useRegex }; } catch (error) { if ((error as any).response?.status === 404) { throw new Error(`Note ${noteId} not found`); } throw error; } }
- Input schema definition for the search_and_replace_note tool, including parameters like noteId, searchPattern, replacePattern, useRegex, expectedHash, etc., with descriptions and validation rules.name: "search_and_replace_note", description: "Search and replace content within a single note. When someone wants to replace text in a note, first call get_note to get the current content and hash, then use this function to make the changes. This ensures you're working with the latest version of their note.", inputSchema: { type: "object", properties: { noteId: { type: "string", description: "ID of the note to perform search and replace on" }, searchPattern: { type: "string", description: "What to search for in the note.", }, replacePattern: { type: "string", description: "What to replace it with. For regex: supports patterns like '$1' for captured groups.", }, useRegex: { type: "boolean", description: "Whether to use regex patterns (default: true).", default: true }, searchFlags: { type: "string", description: "Search options. Defaults to 'gi' (global, case-insensitive). Remove 'i' for exact case matching.", default: "gi" }, expectedHash: { type: "string", description: "⚠️ REQUIRED: Content hash from get_note response. Always get the note content first to obtain this hash.", }, revision: { type: "boolean", description: "Whether to create a backup before replacing (default: true for safety).", default: true } }, required: ["noteId", "searchPattern", "replacePattern", "expectedHash"] } },
- src/index.ts:105-106 (registration)Registration of the search_and_replace_note tool in the MCP server's CallToolRequest handler switch statement, dispatching calls to the noteHandler's request handler function.case "search_and_replace_note": return await handleSearchReplaceNoteRequest(request.params.arguments, this.axiosInstance, this);
- src/modules/noteHandler.ts:310-367 (handler)MCP-specific request handler for search_and_replace_note. Performs permission checks, validates input parameters, constructs the NoteOperation object, calls the core handleSearchReplaceNote function, and formats the response in MCP content format.export async function handleSearchReplaceNoteRequest( args: any, axiosInstance: any, permissionChecker: PermissionChecker ): Promise<{ content: Array<{ type: string; text: string }> }> { if (!permissionChecker.hasPermission("WRITE")) { throw new McpError(ErrorCode.InvalidRequest, "Permission denied: Not authorized to modify notes."); } // Validate that expectedHash is provided (required for data integrity) if (!args.expectedHash) { throw new McpError( ErrorCode.InvalidParams, "Missing required parameter 'expectedHash'. You must call get_note first to retrieve the current blobId (content hash) before performing search and replace. This ensures data integrity by preventing overwriting changes made by other users." ); } // Validate required parameters if (!args.searchPattern) { throw new McpError( ErrorCode.InvalidParams, "Missing required parameter 'searchPattern'. The pattern to search for is required." ); } if (!args.replacePattern) { throw new McpError( ErrorCode.InvalidParams, "Missing required parameter 'replacePattern'. The replacement pattern is required." ); } try { const noteOperation: NoteOperation = { noteId: args.noteId, searchPattern: args.searchPattern, replacePattern: args.replacePattern, useRegex: args.useRegex !== false, // Default to true searchFlags: args.searchFlags || 'g', revision: args.revision !== false, // Default to true for safety expectedHash: args.expectedHash }; const result = await handleSearchReplaceNote(noteOperation, axiosInstance); return { content: [{ type: "text", text: result.message }] }; } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error)); } }
- src/modules/noteManager.ts:179-206 (helper)Helper function that executes the actual search and replace operation on note content using RegExp.replace, supporting both regex and escaped literal patterns.function executeSearchReplace( content: string, searchPattern: string, replacePattern: string, useRegex: boolean = true, flags: string = 'g' ): { newContent: string; replacements: number } { try { let newContent = content; let replacements = 0; if (useRegex) { // Regex-based replacement const regex = new RegExp(searchPattern, flags); replacements = (content.match(regex) || []).length; newContent = content.replace(regex, replacePattern); } else { // Literal string replacement const searchRegex = new RegExp(escapeRegExp(searchPattern), flags); replacements = (content.match(searchRegex) || []).length; newContent = content.replace(searchRegex, replacePattern); } return { newContent, replacements }; } catch (error) { throw new Error(`Search and replace failed: ${error instanceof Error ? error.message : String(error)}`); } }