Apple Notes MCP has solid tool naming (all verb-first), clear descriptions (average ~180 chars), and complete Zod schemas with parameter types and descriptions. Error handling returns friendly messages with actionable guidance. However, output schemas are not explicitly documented, responses are JSON stringified without formal structure declarations. Parameters lack format constraints (e.g., note titles accept any string, folders are unconstrained). No tool annotations (readOnlyHint, destructiveHint, idempotentHint) despite having clear read/write/delete semantics. Pagination is absent from list_notes and search_notes despite potentially large result sets. The update_note tool's delete-then-create approach creates a two-step idempotency risk not flagged in the description.
Create a new note in Apple Notes from Markdown content. The first line becomes the title. Optionally specify a target folder (defaults to 'Notes').
Delete a note from Apple Notes by title. Optionally specify folder to disambiguate. The note is moved to Recently Deleted.
Get the full content of a note by its title, returned as Markdown. Optionally specify folder to disambiguate.
List notes from Apple Notes. Returns title, folder, dates, pinned status, snippet. Optionally filter by folder name and limit results.
Search Apple Notes by keyword. Searches in title and snippet. Returns matching notes with metadata.
Update an existing note in Apple Notes. Deletes the old note and creates a new one with the given Markdown content, preserving the original folder.
No tool annotations (readOnlyHint, destructiveHint, idempotentHint) despite clear semantic distinctions. list_notes, search_notes, and get_note are read-only; create_note and delete_note are destructive; update_note is non-idempotent. LLMs cannot infer these properties from names alone and may retry destructive operations or treat read-only tools as safe to call repeatedly without checking results.
Output schemas not formally documented. All tools return {content: [{type: 'text', text: JSON.stringify(...)}]} without declaring the structure of the JSON payload (e.g., list_notes returns an array of {title, folder, dates, pinned, snippet}, but this is inferred from description, not schema). LLMs cannot plan downstream operations or validate expected fields.
Inferred effective spec: <=2025-11-25.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 62 | <=2025-11-25 | v2 |
No pagination support in list_notes or search_notes. Both tools accept an optional 'limit' parameter but do not document the total count, next cursor, or whether results are truncated. Large result sets will be truncated silently or blow the context window. Best practice: include total_count and next_cursor in response, cap default limit at 20.
update_note description does not acknowledge idempotency risk. The implementation deletes the old note and creates a new one, which is NOT idempotent, if the create fails after delete, the note is lost. LLMs may retry assuming safety. Solution: clarify 'non-idempotent' in description, or implement atomic swap with rollback.
Parameters lack format and constraint documentation. 'title' in get_note, create_note, update_note, and delete_note accept any string with no length limits, special character restrictions, or examples. 'folder' is similarly unconstrained. LLMs may pass titles with newlines, special chars, or extremely long strings that fail silently or cause unexpected behavior.
Error messages are friendly but not always actionable. Example: friendlyError(e) in catch blocks may return opaque messages. When get_note fails (note not found), the error should suggest 'Try search_notes() to find the note by keyword' or 'Available folders: ...' rather than generic text.