Skip to main content
Glama

Search Notes

search_notes
Read-onlyIdempotent

Search notes by keyword in title and body. Customize result count and pagination offset.

Instructions

Search notes by keyword in title and body.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesSearch keyword
limitNoMax results to return (default: 50)
offsetNoNumber of matching results to skip (for pagination)

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
totalYes
returnedYes
offsetYes
notesYes

Implementation Reference

  • 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);
        }
      },
    );
  • 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,
      },
    },
  • 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});
      `;
    }
  • Type interface for search results: extends NoteListItem with a preview string field.
    interface SearchResult extends NoteListItem {
      preview: string;
    }
  • 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(
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, destructiveHint=false, and idempotentHint=true, so the description doesn't need to repeat these. The description adds the scope of search (title and body) but does not disclose other behavioral traits like pagination, ordering, or whether the search is case-sensitive. Given the safety coverage, a score of 3 is appropriate.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single sentence with no waste. It is appropriately concise for a simple search tool. However, a bit more detail (e.g., 'returns matching notes') could be added without sacrificing brevity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool has an output schema (present but not shown) and simple input parameters with full schema descriptions, the description covers the core functionality of searching by keyword in title and body. It does not mention ordering or case-sensitivity, but these are minor given the presence of output schema. The completeness is adequate for a straightforward search tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% description coverage for all three parameters (query, limit, offset). The tool description does not add any additional semantic meaning beyond what the schema already provides. The baseline score is 3 as specified.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the verb (search), resource (notes), and scope (by keyword in title and body). It distinguishes from sibling tools like list_notes (which returns all notes) and semantic_search (which uses semantic indexing), making the purpose specific and unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description does not provide any explicit guidance on when to use this tool versus alternatives like list_notes, scan_notes, or semantic_search. While the purpose is clear, the lack of when-not or alternative references leaves the agent without decision-making context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/heznpc/AirMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server