Skip to main content
Glama

Update Note

update_note
DestructiveIdempotent

Replace the body of an existing note with new HTML content using its ID.

Instructions

Replace the entire body of an existing note.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
idYesNote ID (x-coredata:// format)
bodyYesNew HTML body to replace existing content

Implementation Reference

  • The tool handler for 'update_note' — registers the tool with schema, permissions guard, and JXA execution.
    server.registerTool(
      "update_note",
      {
        title: "Update Note",
        description:
          "Replace the entire body of an existing note. WARNING: This overwrites all content. Read the note first if you need to preserve parts of it. Attachments may be lost.",
        inputSchema: {
          id: z.string().max(500).describe("Note ID (x-coredata:// format)"),
          body: z.string().max(50000).describe("New HTML body to replace existing content"),
        },
        annotations: {
          readOnlyHint: false,
          destructiveHint: true,
          idempotentHint: true,
          openWorldHint: false,
        },
      },
      async ({ id, body }) => {
        try {
          const blocked = await guardShared(id, config, "update_note");
          if (blocked) return errPermission(blocked);
          const result = await runJxa<MutationResult>(updateNoteScript(id, body));
          return ok(result);
        } catch (e) {
          return errJxaFor("update note", e);
        }
      },
    );
  • Input schema for 'update_note' — requires 'id' (string, max 500) and 'body' (string, max 50000) parameters.
      inputSchema: {
        id: z.string().max(500).describe("Note ID (x-coredata:// format)"),
        body: z.string().max(50000).describe("New HTML body to replace existing content"),
      },
      annotations: {
        readOnlyHint: false,
        destructiveHint: true,
        idempotentHint: true,
        openWorldHint: false,
      },
    },
  • JXA helper that builds the AppleScript/JavaScript for Automation script to update a note's body by ID.
    export function updateNoteScript(id: string, body: string): string {
      return `
        const Notes = Application('Notes');
        const note = Notes.notes.byId('${esc(id)}');
        note.body = '${esc(body)}';
        JSON.stringify({id: note.id(), name: note.name()});
      `;
    }
  • Registration of 'update_note' within the registerNoteTools function which is called during server setup.
    export function registerNoteTools(server: McpServer, config: AirMcpConfig): void {
      // --- Layer 1: CRUD ---
    
      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(
        "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);
          }
        },
      );
    
      server.registerTool(
        "read_note",
        {
          title: "Read Note",
          description: "Read the full content of a specific note by its ID. Returns HTML body and plaintext.",
          inputSchema: {
            id: z.string().max(500).describe("Note ID (x-coredata:// format)"),
          },
          outputSchema: {
            id: z.string(),
            name: z.string(),
            body: z.string(),
            plaintext: z.string(),
            creationDate: z.string(),
            modificationDate: z.string(),
            folder: z.string(),
            shared: z.boolean(),
            passwordProtected: z.boolean(),
          },
          annotations: {
            readOnlyHint: true,
            destructiveHint: false,
            idempotentHint: true,
            openWorldHint: false,
          },
        },
        async ({ id }) => {
          try {
            const result = await runJxa<NoteDetail>(readNoteScript(id));
            const blocked = await guardSharedAccess(result.shared, config, "notes", "read_note", { id });
            if (blocked) return errPermission(blocked);
            return okUntrustedStructured(result);
          } catch (e) {
            return errJxaFor("read note", e);
          }
        },
      );
    
      server.registerTool(
        "create_note",
        {
          title: "Create Note",
          description:
            "Create a new note with HTML body. The first line of the body becomes the note title automatically. Optionally specify a target folder.",
          inputSchema: {
            body: z.string().max(50000).describe("Note content in HTML (e.g. '<h1>Title</h1><p>Body text</p>')"),
            folder: z.string().max(500).optional().describe("Target folder name"),
          },
          annotations: {
            readOnlyHint: false,
            destructiveHint: false,
            idempotentHint: false,
            openWorldHint: false,
          },
        },
        async ({ body, folder }) => {
          try {
            const script = config.includeShared ? createNoteSharedScript(body, folder) : createNoteScript(body, folder);
            const result = await runJxa<MutationResult>(script);
            return okLinked("create_note", result);
          } catch (e) {
            return errJxaFor("create note", e);
          }
        },
      );
    
      server.registerTool(
        "update_note",
        {
          title: "Update Note",
          description:
            "Replace the entire body of an existing note. WARNING: This overwrites all content. Read the note first if you need to preserve parts of it. Attachments may be lost.",
          inputSchema: {
            id: z.string().max(500).describe("Note ID (x-coredata:// format)"),
            body: z.string().max(50000).describe("New HTML body to replace existing content"),
          },
          annotations: {
            readOnlyHint: false,
            destructiveHint: true,
            idempotentHint: true,
            openWorldHint: false,
          },
        },
        async ({ id, body }) => {
          try {
            const blocked = await guardShared(id, config, "update_note");
            if (blocked) return errPermission(blocked);
            const result = await runJxa<MutationResult>(updateNoteScript(id, body));
            return ok(result);
          } catch (e) {
            return errJxaFor("update note", e);
          }
        },
      );
    
      server.registerTool(
        "delete_note",
        {
          title: "Delete Note",
          description: "Delete a note by ID. The note is moved to Recently Deleted and permanently removed after 30 days.",
          inputSchema: {
            id: z.string().max(500).describe("Note ID (x-coredata:// format)"),
          },
          annotations: {
            readOnlyHint: false,
            destructiveHint: true,
            idempotentHint: true,
            openWorldHint: false,
          },
        },
        async ({ id }) => {
          try {
            const blocked = await guardShared(id, config, "delete_note");
            if (blocked) return errPermission(blocked);
            const result = await runJxa<DeleteResult>(deleteNoteScript(id));
            return ok(result);
          } catch (e) {
            return errJxaFor("delete note", e);
          }
        },
      );
    
      server.registerTool(
        "list_folders",
        {
          title: "List Folders",
          description: "List all folders across all accounts with note counts.",
          inputSchema: {},
          outputSchema: {
            folders: z.array(
              z.object({
                id: z.string(),
                name: z.string(),
                account: z.string(),
                noteCount: z.number(),
                shared: z.boolean(),
              }),
            ),
          },
          annotations: {
            readOnlyHint: true,
            destructiveHint: false,
            idempotentHint: true,
            openWorldHint: false,
          },
        },
        async () => {
          try {
            const result = await runJxa<FolderItem[]>(listFoldersScript());
            return okStructured({ folders: filterSharedAccess(result, config, "notes") });
          } catch (e) {
            return errJxaFor("list folders", e);
          }
        },
      );
    
      server.registerTool(
        "create_folder",
        {
          title: "Create Folder",
          description: "Create a new folder. Optionally specify which account to create it in.",
          inputSchema: {
            name: z.string().max(500).describe("Folder name"),
            account: z.string().max(500).optional().describe("Account name (e.g. 'iCloud'). Defaults to primary account."),
          },
          annotations: {
            readOnlyHint: false,
            destructiveHint: false,
            idempotentHint: true,
            openWorldHint: false,
          },
        },
        async ({ name, account }) => {
          try {
            const result = await runJxa<MutationResult>(createFolderScript(name, account));
            return ok(result);
          } catch (e) {
            return errJxaFor("create folder", e);
          }
        },
      );
    
      server.registerTool(
        "move_note",
        {
          title: "Move Note",
          description:
            "Move a note to a different folder. NOTE: Apple Notes has no native move command, so this copies the note body to the target folder and deletes the original. The note will get a new ID and creation date. Attachments (images) will be lost.",
          inputSchema: {
            id: z.string().max(500).describe("Note ID to move"),
            folder: z.string().max(500).describe("Target folder name"),
          },
          annotations: {
            readOnlyHint: false,
            destructiveHint: true,
            idempotentHint: false,
            openWorldHint: false,
          },
        },
        async ({ id, folder }) => {
          try {
            const blocked = await guardShared(id, config, "move_note");
            if (blocked) return errPermission(blocked);
            const result = await runJxa<MutationResult>(moveNoteScript(id, folder));
            return ok(result);
          } catch (e) {
            return errJxaFor("move note", e);
          }
        },
      );
    
      // --- Layer 2: Bulk ---
    
      server.registerTool(
        "scan_notes",
        {
          title: "Scan Notes",
          description:
            "Bulk scan notes returning metadata and a text preview for each. Supports pagination via offset. Optionally filter by folder. Use this to get an overview before organizing.",
          inputSchema: {
            folder: z.string().max(500).optional().describe("Filter by folder name. Omit to scan all notes."),
            limit: z
              .number()
              .int()
              .min(1)
              .max(LIMITS.NOTES_BULK_SCAN)
              .optional()
              .default(100)
              .describe("Max number of notes to return (default: 100)"),
            offset: z
              .number()
              .int()
              .min(0)
              .optional()
              .default(0)
              .describe("Number of notes to skip for pagination (default: 0)"),
            previewLength: z
              .number()
              .int()
              .min(1)
              .max(5000)
              .optional()
              .default(300)
              .describe("Preview text length in characters (default: 300)"),
          },
          outputSchema: {
            total: z.number(),
            offset: z.number(),
            returned: z.number(),
            notes: z.array(
              z.object({
                id: z.string(),
                name: z.string(),
                folder: z.string(),
                creationDate: z.string(),
                modificationDate: z.string(),
                preview: z.string(),
                charCount: z.number(),
                shared: z.boolean(),
              }),
            ),
          },
          annotations: {
            readOnlyHint: true,
            destructiveHint: false,
            idempotentHint: true,
            openWorldHint: false,
          },
        },
        async ({ folder, limit, offset, previewLength }) => {
          try {
            const result = await runJxa<ScanResult>(scanNotesScript(limit, previewLength, offset, folder));
            result.notes = filterSharedAccess(result.notes, config, "notes");
            result.returned = result.notes.length;
            return okUntrustedLinkedStructured("scan_notes", result);
          } catch (e) {
            return errJxaFor("scan notes", e);
          }
        },
      );
    
      server.registerTool(
        "compare_notes",
        {
          title: "Compare Notes",
          description:
            "Retrieve full plaintext content of 2-5 notes at once for comparison. Use this after scan_notes to safely compare potentially duplicate or similar notes before deciding what to keep, merge, or delete.",
          inputSchema: {
            ids: z.array(z.string()).min(2).max(5).describe("Array of 2-5 note IDs to compare"),
          },
          annotations: {
            readOnlyHint: true,
            destructiveHint: false,
            idempotentHint: true,
            openWorldHint: false,
          },
        },
        async ({ ids }) => {
          try {
            const result = await runJxa<CompareResult[]>(compareNotesScript(ids));
            const shared = result.filter((n) => n.shared);
            if (shared.length > 0) {
              // Check first shared note to determine if access is allowed
              const blocked = await guardSharedAccess(true, config, "notes", "compare_notes", { ids });
              if (blocked) return errPermission(blocked);
            }
            return okUntrusted(result);
          } catch (e) {
            return errJxaFor("compare notes", e);
          }
        },
      );
    
      server.registerTool(
        "bulk_move_notes",
        {
          title: "Bulk Move Notes",
          description:
            "Move multiple notes to a target folder at once. Same limitations as move_note apply to each successful move (new ID, body-only copy — Notes JXA cannot preserve creationDate/modificationDate or attachments on the copy). " +
            "Use `dryRun: true` to preview which notes would move + what meta would be lost without making any change. " +
            "Default `stopOnError: true` halts on the first failure to keep the source/target in a recoverable mid-state — pass `false` for best-effort partial completion. Returns per-note results plus aggregate counts.",
          inputSchema: {
            ids: z.array(z.string().max(500)).min(1).max(100).describe("Array of note IDs to move (max 100)"),
            folder: z.string().max(500).describe("Target folder name"),
            dryRun: z
              .boolean()
              .optional()
              .default(false)
              .describe("Preview only — list what would move + meta that would be lost. No notes modified."),
            stopOnError: z
              .boolean()
              .optional()
              .default(true)
              .describe(
                "Halt on first failure (default true) so the move stays at a recoverable mid-state. Set false for best-effort partial completion.",
              ),
          },
          annotations: {
            readOnlyHint: false,
            destructiveHint: true,
            idempotentHint: false,
            openWorldHint: false,
          },
        },
        async ({ ids, folder, dryRun, stopOnError }) => {
          try {
            if (!config.includeShared) {
              const { sharedIds } = await runJxa<{ sharedIds: string[] }>(guardSharedBulkScript(ids));
              if (sharedIds.length > 0) {
                const blocked = await guardSharedAccess(true, config, "notes", "bulk_move_notes", { ids, folder });
                if (blocked) return errPermission(blocked);
              }
            }
            const result = await runJxa<unknown>(bulkMoveNotesScript(ids, folder, { dryRun, stopOnError }));
            return ok(result);
          } catch (e) {
            return errJxaFor("bulk move notes", e);
          }
        },
      );
    }
  • Tool graph link: read_note results suggest 'update_note' as a next action with noteId template.
    { tool: "update_note", description: "Edit this note", args: { noteId: "{{id}}" } },
Behavior4/5

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

Annotations indicate destructiveHint=true and idempotentHint=true. The description adds context by stating 'entire body' (not partial), which clarifies the destructive nature. No contradictions with annotations.

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

Conciseness5/5

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

A single, precise sentence of 8 words with no redundancy. Every word is necessary and informative.

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

Completeness3/5

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

The description is adequate for a simple update operation but lacks information about return values (e.g., updated note or success status) and error handling (e.g., invalid ID). No output schema exists to compensate.

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?

Input schema covers both parameters with descriptions (100% coverage). The description does not add extra meaning beyond what the schema provides, so baseline score of 3 applies.

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 'Replace the entire body of an existing note' clearly states the action (replace), the resource (body of a note), and differentiates from siblings like delete_note, move_note, or read_note.

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 specify when to use this tool versus alternatives like bulk_move_notes or edit_shortcut. Usage is implied (when you need to change the note body), but no explicit guidance on exclusions or prerequisites.

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