Search Notes
search_notesSearch notes by keyword in title and body. Customize result count and pagination offset.
Instructions
Search notes by keyword in title and body.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Search keyword | |
| limit | No | Max results to return (default: 50) | |
| offset | No | Number of matching results to skip (for pagination) |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| total | Yes | ||
| returned | Yes | ||
| offset | Yes | ||
| notes | Yes |
Implementation Reference
- src/notes/tools.ts:157-206 (handler)Registers the search_notes tool handler; receives query, limit, offset, runs the JXA script, filters shared access, and returns structured results.
"search_notes", { title: "Search Notes", description: "Search notes by keyword in title and body. Returns matching notes with a 200-char preview.", inputSchema: { query: z.string().min(1).max(500).describe("Search keyword"), limit: z.number().int().min(1).max(500).optional().default(50).describe("Max results to return (default: 50)"), offset: z .number() .int() .min(0) .optional() .default(0) .describe("Number of matching results to skip (for pagination)"), }, outputSchema: { total: z.number(), returned: z.number(), offset: z.number(), notes: z.array( z.object({ id: z.string(), name: z.string(), folder: z.string(), preview: z.string(), creationDate: z.string(), modificationDate: z.string(), }), ), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async ({ query, limit, offset }) => { try { const result = await runJxa<{ total: number; returned: number; offset: number; notes: SearchResult[] }>( searchNotesScript(query, limit, offset), ); result.notes = filterSharedAccess(result.notes, config, "notes"); result.returned = result.notes.length; return okUntrustedStructured(result); } catch (e) { return errJxaFor("search notes", e); } }, ); - src/notes/tools.ts:158-193 (schema)Input/output schema and annotations for search_notes tool. Input: query (required), limit (default 50), offset (default 0). Output: notes array with preview (200-char snippet).
{ title: "Search Notes", description: "Search notes by keyword in title and body. Returns matching notes with a 200-char preview.", inputSchema: { query: z.string().min(1).max(500).describe("Search keyword"), limit: z.number().int().min(1).max(500).optional().default(50).describe("Max results to return (default: 50)"), offset: z .number() .int() .min(0) .optional() .default(0) .describe("Number of matching results to skip (for pagination)"), }, outputSchema: { total: z.number(), returned: z.number(), offset: z.number(), notes: z.array( z.object({ id: z.string(), name: z.string(), folder: z.string(), preview: z.string(), creationDate: z.string(), modificationDate: z.string(), }), ), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, - src/notes/scripts.ts:71-119 (helper)JXA script generator for search_notes. Two-phase search: Phase 1 matches by title only (fast), Phase 2 matches by body plaintext (slower). Returns notes with 200-char preview. Supports pagination via limit/offset.
export function searchNotesScript(query: string, limit: number, offset: number = 0): string { return ` ${JXA_BUILD_NOTE} const Notes = Application('Notes'); const names = Notes.notes.name(); const ids = Notes.notes.id(); const creationDates = Notes.notes.creationDate(); const modificationDates = Notes.notes.modificationDate(); const containers = Notes.notes.container(); const shareds = Notes.notes.shared(); const q = '${esc(query)}'.toLowerCase(); const result = []; const nameMatched = new Set(); let matched = 0; const skip = ${offset}; // Phase 1: Search by title only (cheap — no plaintext fetch) for (let i = 0; i < names.length; i++) { if (names[i].toLowerCase().includes(q)) { nameMatched.add(i); if (matched >= skip && result.length < ${limit}) { const pt = Notes.notes[i].plaintext(); result.push({...buildNote(i, ids, names, containers, creationDates, modificationDates, shareds), preview: pt.substring(0, 200)}); } matched++; if (result.length >= ${limit}) break; } } // Phase 2: Search body content (expensive — per-note plaintext, stops at limit) if (result.length < ${limit}) { for (let i = 0; i < names.length; i++) { if (nameMatched.has(i)) continue; if (matched < skip) { const pt = Notes.notes[i].plaintext(); if (pt.toLowerCase().includes(q)) { matched++; } continue; } const pt = Notes.notes[i].plaintext(); if (pt.toLowerCase().includes(q)) { result.push({...buildNote(i, ids, names, containers, creationDates, modificationDates, shareds), preview: pt.substring(0, 200)}); matched++; if (result.length >= ${limit}) break; } } } // Note: totalMatched is a lower bound — the loop exits early once the page is full, // so there may be more matches beyond what was counted. JSON.stringify({total: names.length, totalMatched: matched, offset: skip, returned: result.length, notes: result}); `; } - src/notes/tools.ts:45-47 (schema)Type interface for search results: extends NoteListItem with a preview string field.
interface SearchResult extends NoteListItem { preview: string; } - src/notes/tools.ts:93-156 (registration)Tool registration call for search_notes within the registerNoteTools function.
server.registerTool( "list_notes", { title: "List Notes", description: "List all notes with title, folder, and dates. Optionally filter by folder name. Supports pagination via limit/offset.", inputSchema: { folder: z.string().max(500).optional().describe("Filter by folder name"), limit: z .number() .int() .min(1) .max(1000) .optional() .default(200) .describe("Max number of notes to return (default: 200)"), offset: z .number() .int() .min(0) .optional() .default(0) .describe("Number of notes to skip for pagination (default: 0)"), }, outputSchema: { total: z.number(), offset: z.number(), returned: z.number(), notes: z.array( z.object({ id: z.string(), name: z.string(), folder: z.string(), // `shared` is always emitted by the JXA runtime (via Shareable) and // is useful signal for clients (e.g. to render a "shared" badge). // Declared here so the outputSchema drift guard can run in strict mode. shared: z.boolean(), creationDate: z.string(), modificationDate: z.string(), }), ), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async ({ folder, limit, offset }) => { try { const result = await runJxa<{ total: number; offset: number; returned: number; notes: NoteListItem[] }>( listNotesScript(limit, offset, folder), ); result.notes = filterSharedAccess(result.notes, config, "notes"); result.returned = result.notes.length; return okLinkedStructured("list_notes", result); } catch (e) { return errJxaFor("list notes", e); } }, ); server.registerTool(