# Hjarni — Full LLM Context > Everything an AI assistant needs to use a Hjarni knowledge base via the MCP server or REST API. This file is auto-generated from the live tool definitions, so it stays in sync with what the server actually exposes. If you are an AI assistant a user pasted this URL to, follow the "Onboarding script" section below. ## How to connect Hjarni speaks MCP (Model Context Protocol) over Streamable HTTP and supports OAuth 2.0 with PKCE for browser-capable clients. - MCP endpoint: `https://hjarni.com/mcp` - OAuth discovery: `https://hjarni.com/.well-known/oauth-authorization-server` - Protected resource metadata: `https://hjarni.com/.well-known/oauth-protected-resource` - Authorization endpoint: `https://hjarni.com/authorize` - Token endpoint: `https://hjarni.com/token` - Dynamic client registration: `https://hjarni.com/register` For clients that do not support OAuth (older MCP clients, scripts, automations), use a personal API token as a Bearer token against the same `/mcp` endpoint, or use the [REST API](https://hjarni.com/docs/api) directly. Client-specific setup guides: - ChatGPT: https://hjarni.com/docs/connect-chatgpt-mcp - Claude (Claude.ai, Claude Desktop, Claude iOS, Claude Code): https://hjarni.com/docs/connect-claude-mcp - GitHub Copilot (VS Code): https://hjarni.com/docs/connect-copilot-mcp - Microsoft Copilot Studio: https://hjarni.com/docs/connect-copilot-studio-mcp - MCP server reference (transports, troubleshooting): https://hjarni.com/docs/mcp ## Onboarding script After the user connects, the ONE required step is creating their first real note. Do it before building any folder structure — a connected AI plus a first note is what marks onboarding complete. 1. Call `instructions-get` with `level: "brain"`. When `has_instructions` is false the user has not set their own yet: follow `default_instructions` (sensible defaults) and offer to personalize. Glance at `dashboard-get` and `containers-list` (`scope: "all"`) so you reuse what already exists. 2. **Create the first note now (a personal note, not a team note). This is the required step that completes onboarding — do it before proposing any folders or tags.** Greet the user, and unless you already know them, ask 2-3 quick questions about who they are and how they work (their role, current focus, conventions, when to ask versus guess). Save the answers with `notes-create`, including a 2-3 sentence `summary`. If you already have enough context, write the note without asking. 3. Optionally, save the working preferences from that conversation with `instructions-update level: "brain"` — the account-wide block that rides along with every future conversation. 4. Only AFTER the first note exists may you propose a starter folder hierarchy and tags. Ask which persona fits (developer, founder, researcher, writer, or traveler) and confirm before creating anything. Per-folder conventions go in `instructions-update level: "container"`. Always show your plan and prefer asking before mutating the knowledge base. Lead with the first note, then build out the rest if it is useful. Give every note you create, including ones you add while building out the brain, a 2-3 sentence summary. ## Core concepts - **Note**: a Markdown document with title, body, optional summary, tags, file attachments. Every edit is versioned. Notes can live in the personal space or inside a team. - **Container**: a folder. Containers nest. Each can have its own LLM instructions that cascade to descendants. - **Tag**: a cross-cutting label. Notes can have multiple tags. Tag names are lowercased. - **Wiki-link**: `[[id:Title]]` inside a note body. The numeric ID is the source of truth; the title is cosmetic and stays in sync. Creates a bidirectional NoteLink automatically on save. - **Summary**: a 2-3 sentence field on every note. Has a `summary_stale` flag set when the user edits the body directly — regenerate the summary when you encounter a stale one. - **Freshness**: every note returns a `freshness` object (`status` of `active`/`stale`/`retired`, `verified_at`, `review_after`, and a plain-language `hint`) so an aging note that may no longer be true is flagged, not silently trusted. Stale notes are still returned — flag them before relying on them, and mark them verified (`notes-update verified: true`) once the user confirms they're still accurate. - **LLM instructions**: plain-language rules attached to the brain (account), the personal root, a container, or a team. Read with `instructions-get`, modify with `instructions-update`. Container instructions inherit from ancestors and from the personal root — follow all levels, they stack. - **Team**: a shared space with its own notes, containers, and members. Caller must be a member to read or write. - **Inbox**: unorganized personal notes (no container assigned). Surface them during a weekly review and offer to file them. ## Conventions an AI assistant should follow - Search before creating. The note may already exist. - Keep notes short and focused — one idea per note. When a topic is complex, create multiple short notes in a container and link them with `[[id:Title]]` rather than writing one long note. - Always write a `summary` when creating a note. Always update the summary in the same call when you modify the body. Keep summaries to 2-3 sentences capturing the key takeaway. - When you encounter a note with `summary_stale: true`, regenerate the summary using `notes-update`. - Check each note's `freshness.status`. When it is `stale`, tell the user the note may be outdated before treating it as current fact; once they confirm it, mark it verified with `notes-update verified: true`. Use `notes-list stale: true` to surface notes that need re-confirming. - Reuse existing tags. Call `tags-list` first; only create new tags when none of the existing ones fit. - Before working in a container, check `has_llm_instructions` (returned on every container in list/search results). If true, call `instructions-get level: "container"` and follow every level of the inheritance chain. - When the user expresses a durable preference ("always tag meeting notes", "use lowercase titles"), proactively offer to save it at the appropriate level. Read current instructions first, then merge — do not overwrite. - For large file attachments, prefer `files-create_upload_url` (the user uploads directly via their browser) over `files-attach` (base64 through the conversation, wastes tokens). Use `files-attach_from_url` when the file is already at a public URL. ## Account-level instructions to LLM clients The following text is also served as the MCP server's `instructions` capability. It is the source of truth for in-session conventions: ``` Second Brain is a personal knowledge management system with notes organized in a hierarchy of containers (folders) and tagged for cross-cutting categorization. QUICK START (most common workflows): - Start each session: call `me`, then instructions-get (brain); see IDENTITY & ONBOARDING and LLM INSTRUCTIONS. - Find and read a saved note: search → notes-get. - Edit an existing note: search → notes-get → notes-update. - Save something new: notes-create. Title is required; add a body, and always a summary (see SUMMARIES). IDENTITY & ONBOARDING: - Call `me` once at the start of a conversation. It returns the user's first name, plan, team memberships, note quota, and onboarding state in a single call. - Use first_name to greet the user (when present) and to personalize replies. - If onboarding.completed is true, skip the onboarding script — the user is already set up. Otherwise walk through onboarding.missing_steps in order: * "connect_mcp" — the user hasn't connected an MCP client yet. * "first_note" — create their first real PERSONAL note (notes-create, not a team note); this is what completes setup. Lead with one note about who the user is and how they work. - If onboarding.next_action is present, follow it before anything else — it is the authoritative setup script. After that first personal note exists you may also build out folders, tags, and instructions from the conversation, but a real personal note is what completes setup — and give EVERY note a 2-3 sentence summary. - Onboarding completes on those two steps. Capturing brain instructions is an optional enhancement, not a required step: if onboarding.signals.has_custom_instructions is false, you may once offer to save the user's working preferences (instructions-update with level: "brain"), but don't block or nag on it. - Use plan to avoid suggesting Pro features (file attachments, public sharing) to free users, and limits.notes_used / limits.notes_limit to warn before hitting the quota. - Use the teams array to know which spaces are available before routing a note — e.g. ask "save this to or your personal brain?" when both apply. SEARCHING: - Use the search tool to search across notes, containers, and tags in one call. This is the fastest way to find anything. - Use the types parameter to narrow results to specific types: ["notes"], ["containers"], ["tags"], or any combination. Defaults to all three. - By default, note results span the user's personal notes AND all teams they belong to. Use the search_scope parameter to narrow the note results: "personal" for personal notes only, "team:" for a specific team, or "all" (default) for everything. search_scope applies to note results only; container and tag results are not scoped by it. - Team notes in search results include team_id and team_name fields so you can identify which team they belong to. - Search results for notes include a snippet showing the matching portion of the note body. - search and notes-list support multiple tags (AND logic) via the tags array, and include_nested to search within a container and all its sub-containers. - Use search with types: ["containers"] to find a container by name or description instead of paginating through containers-list. NOTE LINKING: - Notes support wiki-link syntax: [[id:Note Title]] where id is the note's numeric ID and Note Title is the display text. The ID is the source of truth for resolution; the title is cosmetic and kept in sync automatically when a note is renamed. - Wiki-links are the primary way to link notes. When a note is saved, [[id:Note Title]] references automatically create NoteLink records in the database. Removing a reference from the body automatically removes the link. - Use this to cross-reference related notes inline. For example: "See [[42:Meeting Notes 2024]] for context." If the referenced note exists, it becomes a clickable link and appears in the "Linked Notes" section of the note page. - The old [[Note Title]] format (without ID) is still supported as a fallback for existing notes that haven't been re-linked yet. - The links-manage tool is available for programmatic linking without modifying note body text. These create persistent links that are not affected by body edits. FILE ATTACHMENTS: - When a user wants to attach a file (photo, PDF, document, etc.) to a note, prefer files-create_upload_url over files-attach. This generates a one-time upload link that you share with the user. They upload the file directly via their browser, which avoids sending large file data through the conversation and saves tokens. - Flow: 1) Call files-create_upload_url with the note ID, 2) Share the returned upload_url with the user, 3) After the user uploads, call files-check_upload with the token to confirm. - Use files-attach_from_url when the file is already at a public URL. - Only use files-attach (base64) as a last resort for very small files. - To retrieve/download a file, use files-get_download_url with the note_id and file_id (from notes-get response). This returns a temporary signed URL the user can open in their browser. The download_url is also included in file metadata whenever files appear in responses (e.g., notes-get, files-check_upload). NOTE SIZE: - Keep notes short and focused — each note should cover one idea, topic, or reference. - When a topic is complex, create multiple short notes in a container and link them with wiki-links ([[id:Title]]) rather than writing one long note. - Short notes are easier to search, link, reuse, and update independently. SUMMARIES: - Every note has a summary field and a summary_stale flag. - When you create a note, always write a summary. - When you update a note's body, always update the summary too in the same call. - When you encounter a note with summary_stale: true, regenerate its summary using notes-update with the summary parameter. This flag means the user edited the note body directly and the summary is now outdated. - Use notes-list with summary_stale: true to find all notes needing summary updates. - Keep summaries to 2-3 sentences that capture the key takeaway. - If the note's container has LLM instructions, follow those conventions for the summary style. FRESHNESS (avoiding stale retrieval): - Every note carries a freshness object: status ('active' | 'stale' | 'retired'), verified_at (when a human last confirmed it's still true), review_after (an optional "treat as stale after this date" for time-boxed notes), and a plain-language hint. - 'stale' means the note may be outdated: it is past its review_after date, or no one has verified it in a while. Stale notes are STILL returned by search, notes-get, and notes-list — they are not hidden. But before presenting a stale note's content as current fact, flag that it may be outdated and, when it matters, confirm it with the user. - 'retired' means the note is archived: keep it for reference only. - When the user confirms a note is still accurate, mark it verified with notes-update (verified: true). When a note is only valid until some date (a quarterly plan, a temporary workaround), set review_after so it flags itself automatically once that date passes. - Use notes-list with stale: true to find notes that need re-confirming. ORGANIZING: - Always check the container hierarchy first using containers-list with scope "all" or search with types: ["containers"] to find the right container. - Use containers-get with include_tree: true to see a container's ancestors and children. - Check existing tags with tags-list and reuse them before creating new tags. - Confirm placement with the user when the right container is ambiguous; otherwise place it in the obvious container and tell the user where you put it. - Containers may declare a negative_space — what does NOT belong in them. Respect it when filing notes: if a note matches a folder's negative_space, route it elsewhere. - To move an existing note into a different folder, use notes-update with container_id set to the destination container's id (or null to move it to the inbox). Moving is part of notes-update; there is no separate "move" tool. - When a container has no LLM instructions (has_llm_instructions is false), especially one you just created or that the user is actively working in, proactively offer to set some up. Ask what kind of content will go there and suggest rules like note format (structured with headings vs. freeform), naming conventions, required tags, or summary style. Even brief rules ("use bullet points", "always include a source URL") make the container much more useful, because future AI interactions follow them automatically. SHARED CONTAINERS: - Other users can share specific containers with you. Shared containers and their notes appear alongside your personal content in containers-list, search, and search results. - Shared containers and notes include `shared: true` and `shared_by` (the owner's email) in their response data. Use these fields to distinguish shared content from your own. - You can read and edit notes in shared containers, and create new notes in them. - You CANNOT delete, archive, move, or favorite notes in shared containers — those actions are reserved for the owner. - You CANNOT modify shared containers themselves (rename, delete, change instructions). CONTEXT: - Every note in search/list results includes container_path (e.g., "Projects > Work > Frontend") so you can see where it lives in the hierarchy. - source_url is included in list/search results for quick identification. LLM INSTRUCTIONS: - After `me`, call instructions-get with level: "brain" to learn how the user wants you to use their Second Brain. These are user-written instructions that describe their personal conventions, preferences, and workflows. The response also includes personal_llm_instructions (personal root instructions) and lists any containers that have their own instructions. - If has_instructions is false, offer to help the user set up their brain instructions by asking about their preferences (e.g., note conventions, tagging strategies, tone). - There are multiple levels of LLM instructions: 1. Brain instructions (global) — apply across all spaces. Use instructions-update with level: "brain". 2. Personal root instructions — apply to personal notes/containers. Use instructions-get / instructions-update with level: "personal_root". 3. Container instructions — apply to a specific container and are inherited by sub-containers. Use instructions-get / instructions-update with level: "container". 4. Team instructions — apply to a team's notes. Use instructions-get / instructions-update with level: "team". - IMPORTANT: When creating, editing, or organizing notes in a container, always check if the container has LLM instructions. Every container in list/search results includes a has_llm_instructions flag. If true, call instructions-get with level: "container" to read them, or use containers-get to see the instructions inline. Container instructions define conventions for that area of the brain (e.g., note format, naming, tagging rules) and MUST be followed when working with notes in that container. - When working within a container, instructions-get with level: "container" returns the full inheritance chain: personal root instructions -> ancestor container instructions -> container instructions. Follow all levels — they stack. - When the user expresses a preference about how their brain should work (e.g., "always tag meeting notes", "use lowercase titles", "from now on, summarize notes"), proactively offer to save it at the appropriate level. Always read the current instructions first, then merge the new preference in — do not overwrite existing instructions. NOTES & TEAMS: - Use notes-create with team_id to create notes in a team. - Use notes-list with team_id to list notes in a team. - Use containers-list with team_id to list containers in a team. - Use notes-update with archived: true/false to archive/unarchive notes. - Use notes-update with favorited: true/false to favorite/unfavorite notes. - Use notes-update with container_id to move a note into a different container/folder (or null to move it to the inbox). SUGGESTING A TEAM: - `me` returns suggestions.create_team. When it is true, the user is a good candidate for a team: onboarded, working solo, engaged, not already invited. When it is false, do NOT bring teams up — never pitch a team to someone who is already on one or who the signal hasn't flagged. - Suggest only when it's genuinely relevant AND the moment fits — e.g. they're writing notes that read as shared team knowledge (runbooks, onboarding docs, conventions, meeting notes with colleagues), or they mention teammates, "we", or collaborating. Don't interrupt an unrelated task to pitch. - Keep it a one-line, low-pressure offer, not a sales pitch: a team gives shared notes and per-team instructions, invited teammates read and write the same brain, and it stays free until it outgrows the note allowance. - If they say yes, call teams-create (ref: "assistant_suggestion"), then offer to invite their teammates with teams-invite. - Offer at most once per conversation. If they decline, respect it and move on — don't bring it up again. NUDGES: - `me` may include a `nudge`: one server-picked, frequency-capped suggestion with a `key`, a `message` explaining what to do with it, and a `url` to share if the user is interested. Absent means there is nothing to mention — never invent one. - Follow the message's own etiquette: mention it at most once per conversation, only when a natural moment fits, and never interrupt or derail the user's actual task for it. - If the user declines, is annoyed, or asks not to be reminded, call nudges-dismiss with the nudge's key so it stops appearing everywhere — web included. Don't dismiss on mere silence; the server already caps how often an ignored nudge is shown. ``` ## Tools The MCP server exposes the following tools. Each tool's `inputSchema` lists required and optional parameters. Tools marked `destructive` may overwrite or delete data — confirm with the user first. Tools marked `read-only` are safe to call freely. ### `me` Get the connected user's profile, plan, onboarding state, team memberships, and note quota in a single call. Call this once at the start of a conversation so you can greet the user by first name, run the onboarding script only when needed, route notes to the right team space, and avoid suggesting Pro features to free users. Returns onboarding.completed (boolean) and onboarding.missing_steps (array of 'connect_mcp' | 'first_note'), which together tell you what, if any, setup is left. May include a `nudge` (key, message, url) — one frequency-capped suggestion; see the NUDGES section of the server instructions for how to handle it. Exposes the user's email address and plan — same data the user sees in account settings, but never billing or token metadata. No parameters required. - Title: Get current user - Flags: read-only, idempotent - Parameters: none ### `dashboard-get` Get an overview of the Second Brain: counts of notes, containers, tags, and inbox items, plus recent_notes (the 5 most recently created personal notes) and recent_changes (the 5 most recently edited notes across ALL spaces — personal, teams, and shared containers — newest edit first). Use recent_changes to orient at the start of a conversation on what changed lately everywhere. If everything is empty because the user hasn't saved anything yet, do not just report that: call `me` and follow onboarding.next_action to run their 60-second setup. No parameters required. - Title: Get Dashboard - Flags: read-only, idempotent - Parameters: none ### `search` Search across notes, containers, and tags in one call. Returns results grouped by type with pagination metadata (total_count, page, per_page, total_pages). Required: query (string). Optional: types (array, default all three), search_scope ('all'|'personal'|'team:'), scope ('active'|'archived'), container_id (integer, ignored when search_scope is 'all'), tags (array, AND logic), tag_ids (array, AND logic), include_nested (boolean), include_body (boolean, default false — when true each note includes its full body), created_after / created_before / updated_after / updated_before (ISO 8601 datetime filters on note timestamps), page (integer, default 1), per_page (integer, default 25, max 100). Note results include a snippet of the matching portion. If results are empty because the user hasn't saved anything yet, do not just report that: call `me` and follow onboarding.next_action to run their 60-second setup. - Title: Search - Flags: read-only, idempotent - Parameters: - `query` (string, required): Search query string (required) - `types` (array): Which types to search. Defaults to all three: ['notes', 'containers', 'tags'] - `search_scope` (string): Search scope: 'all' (default, personal + all teams), 'personal' (personal notes only), or 'team:' (specific team). Applies to note results. - `scope` (string): Search active or archived notes (default: active) - `container_id` (integer): Filter note results to this container (ignored when search_scope is 'all') - `tags` (array): Filter note results to notes with ALL these tags (by name) - `tag_ids` (array): Filter note results to notes with ALL these tags (by ID) - `include_nested` (boolean): Include notes from sub-containers when container_id is set (default: false) - `include_body` (boolean): Include the full note body on each note result (default: false) - `created_after` (string): Filter notes created on or after this ISO 8601 datetime (e.g. '2026-04-01T00:00:00Z') - `created_before` (string): Filter notes created before this ISO 8601 datetime - `updated_after` (string): Filter notes updated on or after this ISO 8601 datetime - `updated_before` (string): Filter notes updated before this ISO 8601 datetime - `page` (integer): Page number for note results (default: 1) - `per_page` (integer): Results per page for notes, max 100 (default: 25) ### `notes-list` List notes with optional filtering, sorting, and pagination. Returns paginated results. Optional: team_id (integer) to list team notes, scope ('active'|'archived'|'inbox'|'favorited'|'trashed'), container_id (integer) with include_nested (boolean), tags (array of strings, AND logic), tag_ids (array of integers, AND logic), summary_stale (boolean, filter to notes with outdated summaries), stale (boolean, filter to notes whose freshness is stale — past their review_after date or unverified for a while), sort ('recent'|'oldest'|'title'), page (integer, default 1), per_page (integer, max 100, default 25). container_id can be combined with team_id to list a specific team container. Example: list ruby-tagged notes in a container: {container_id: 5, tags: ['ruby']}. If the list is empty because the user hasn't saved anything yet, do not just report that: call `me` and follow onboarding.next_action to run their 60-second setup. - Title: List Notes - Flags: read-only, idempotent - Parameters: - `team_id` (integer): List notes in this team instead of personal notes - `scope` (string): Filter scope (default: active). 'inbox' and 'favorited' only for personal notes; 'trashed' lists the recoverable Trash — restore an entry with notes-restore. - `container_id` (integer): Filter by container ID - `tags` (array): Filter to notes with ALL these tags by name (AND logic). Example: ['ruby', 'rails'] - `tag_ids` (array): Filter to notes with ALL these tags by ID (AND logic) - `include_nested` (boolean): Include notes from sub-containers when container_id is set (default: false) - `summary_stale` (boolean): Filter to notes with outdated summaries (default: not filtered) - `stale` (boolean): Filter to notes whose freshness is stale — past their review_after date or unverified beyond the freshness window (default: not filtered) - `sort` (string): Sort order: 'recent' (updated_at desc, default), 'oldest' (updated_at asc), or 'title' (alphabetical) - `page` (integer): Page number (default: 1) - `per_page` (integer): Results per page, max 100 (default: 25) ### `notes-get` Get a single note by ID, including its full Markdown body, tags, container path, linked notes (outgoing), backlinks (incoming links from other notes), file attachments, and inherited LLM instructions. Required: id (integer). - Title: Get Note - Flags: read-only, idempotent - Parameters: - `id` (integer, required): Note ID (required) ### `notes-create` Create a new note. Required: title (string). Always include a 2-3 sentence summary too (required by convention, even though the schema only enforces title) so the note is useful to future LLM sessions. Optional: body (Markdown with [[id:Note Title]] wiki-links), summary, source_url, container_id, tag_list (comma-separated), team_id (to create in a team), review_after (ISO 8601 datetime, only set this for time-boxed notes that should flag themselves stale after a date). A new note starts verified (status 'active'). Example: {title: 'Meeting Notes', body: '## Agenda\n...', container_id: 5, tag_list: 'meetings, q4'}. - Title: Create Note - Flags: none - Parameters: - `title` (string, required): Note title (required) - `body` (string): Note body content (Markdown with [[id:Note Title]] wiki-links) - `summary` (string): A 2-3 sentence summary of the note. Always provide one. - `source_url` (string): Source URL reference - `container_id` (integer): Container ID to place the note in - `tag_list` (string): Comma-separated list of tags (e.g., 'ruby, rails, testing') - `team_id` (integer): Create note in this team instead of personal space - `review_after` (["string", "null"]): Optional ISO 8601 datetime after which the note should be treated as stale. Use for time-boxed notes (e.g. '2026-12-31'). ### `notes-update` Update a note: edit its content, move it to a different folder (set container_id, or null for the inbox), archive/favorite it, or change its tags. This is the tool for moving notes between folders; there is no separate move tool. Required: id (integer). Optional content (exactly one body-mutation mode at a time): title, body (full replace), append_body (appends to existing body), insert_after + insert_body (insert text immediately after a unique anchor snippet from the existing body), insert_before + insert_body (insert before a unique anchor), replace_find (+ optional replace_with) (replace a unique snippet; omit replace_with entirely to delete the snippet). Markdown-structure ops (heading/section/checklist aware — safer than eyeballing a unique snippet on long notes): replace_section + section_body (replace everything UNDER a heading, keeping the heading line); append_to_section + section_body (add content at the END of a section — the safe 'insert under heading' when you don't know its last line); rename_heading + new_heading (rename a heading in place, preserving its level unless new_heading carries its own '#'); check_item / uncheck_item (tick/untick a checklist item by its text, e.g. '- [ ] ship it'). Headings and checklist items must each match exactly one line. Anchor and find snippets must match exactly once; include enough surrounding context to disambiguate. Also optional: summary, source_url. Freshness: verified (boolean — pass true to mark the note re-confirmed as still true right now; only send this after the user has actually confirmed it), review_after (ISO 8601 datetime to time-box the note, or null to clear it). Organization: container_id (move note), archived (boolean, personal only), favorited (boolean). Tags: tag_list (full replace, comma-separated), add_tags, remove_tags. tag_list takes precedence over add_tags/remove_tags. Concurrent edit safety: pass expected_lock_version (the lock_version you saw when you last read the note via notes-get / notes-list / search) whenever you want a stale-write guard. If it doesn't match the current version, the update is rejected with the current state included so you can re-read and re-apply. Surgical edits (append_body / insert_after / insert_before / replace_find) are anchor-based and so don't *need* expected_lock_version for their body change — but if you also change title / summary / container_id alongside, those fields can still silently overwrite a newer save unless you supply expected_lock_version. Examples: insert under a heading {id: 42, append_to_section: 'Open Questions', section_body: '- Should we ship Friday?'}; replace a section {id: 42, replace_section: '## Status', section_body: 'Shipped 🎉'}; rename a heading {id: 42, rename_heading: 'TODO', new_heading: 'Done'}; tick a checklist item {id: 42, check_item: 'ship it'}; fix a typo {id: 42, replace_find: 'recieved', replace_with: 'received'}; safe full rewrite {id: 42, body: '...', expected_lock_version: 5}. Every edit is recorded as a named, revertable revision attributed to you — use notes-history to see who changed what, and notes-revert to undo a change. - Title: Update Note - Flags: destructive, idempotent - Parameters: - `id` (integer, required): Note ID (required) - `title` (string): New title - `body` (string): New body content — full replacement. Mutually exclusive with the other body-mutation modes. Pair with expected_lock_version for concurrent-edit safety. - `append_body` (string): Content to append to the existing body. Mutually exclusive with the other body-mutation modes. - `insert_after` (string): Anchor snippet from the existing body — insert_body is inserted immediately after the unique occurrence. Anchor must match exactly once; include surrounding context to disambiguate. - `insert_before` (string): Anchor snippet from the existing body — insert_body is inserted immediately before the unique occurrence. Anchor must match exactly once. - `insert_body` (string): Text to insert. Must be paired with either insert_after or insert_before. Mutually exclusive with the other body-mutation modes. - `replace_find` (string): Snippet to find in the existing body. Must match exactly once. Mutually exclusive with the other body-mutation modes. - `replace_with` (string): Optional replacement for replace_find. Omit it (or pass an empty string) to delete the matched snippet. - `replace_section` (string): Heading whose section content should be replaced (with or without leading '#', e.g. '## Status' or 'Status'). Replaces everything under the heading up to the next same-or-higher-level heading, keeping the heading line. Pair with section_body. Heading must match exactly one. - `append_to_section` (string): Heading to append content to (with or without leading '#'). Adds section_body at the END of that section — the safe way to 'insert under a heading'. Pair with section_body. Heading must match exactly one. - `section_body` (string): The content for replace_section / append_to_section. Required when either is given. - `rename_heading` (string): Heading to rename (with or without leading '#'). Pair with new_heading. Heading must match exactly one. - `new_heading` (string): New heading text for rename_heading. The original level is preserved unless this carries its own leading '#' markers. - `check_item` (string): Text of a checklist item to tick (set to '[x]'). Matched case- and whitespace-insensitively; a leading bullet/checkbox in the text is ignored. Must match exactly one item. - `uncheck_item` (string): Text of a checklist item to untick (set to '[ ]'). Same matching rules as check_item. - `summary` (string): New summary - `source_url` (string): New source URL - `verified` (boolean): Pass true to mark the note re-confirmed as still true as of now (the 'mark verified' affordance). Refreshes verified_at and clears 'stale' status. Only send after the user has confirmed the note is still accurate. - `review_after` (["string", "null"]): ISO 8601 datetime after which the note should be treated as stale (time-boxing), or null to clear it. - `container_id` (["integer", "null"]): Folder (container) id to move the note into; pass null to move it to the inbox (remove it from its folder) - `tag_list` (string): Full replacement comma-separated tag list (takes precedence over add_tags/remove_tags) - `add_tags` (string): Comma-separated tags to add to existing tags (ignored if tag_list is provided) - `remove_tags` (string): Comma-separated tags to remove from existing tags (ignored if tag_list is provided) - `archived` (boolean): Archive (true) or unarchive (false) the note. Personal notes only. - `favorited` (boolean): Favorite (true) or unfavorite (false) the note. Personal and team notes. - `expected_lock_version` (integer): Optional concurrent-edit guard. Pass the lock_version you saw when you last read the note; if it doesn't match the current version, the update is rejected and you should re-read and re-apply. Checked whenever supplied — covers title, summary, container_id, body, anything else. Surgical body edits without this param still work and remain anchor-safe; supply it any time you want a stale-write guard for the other fields too. ### `notes-delete` Move a note to Trash. This is RECOVERABLE — the note (with its body, attachments, and history) is restorable with notes-restore until its purge date (default 30 days); it is not an immediate permanent erase. Deleting the wrong note can be undone with notes-restore. Works on your own personal notes and on team notes where you have the editor role. You cannot delete notes in a shared container (only the owner can). Required: id (integer). - Title: Delete Note - Flags: destructive - Parameters: - `id` (integer, required): Note ID (required) ### `notes-restore` Restore a note that notes-delete moved to Trash, bringing back its body, tags, attachments, and history. Only works before the note's purge date. Required: id (integer) — the same note id you deleted. - Title: Restore Note - Flags: idempotent - Parameters: - `id` (integer, required): Note ID (required) ### `notes-history` Read a note's revision history and provenance: who wrote each version (you, the user, or which AI client), when, and what changed. Use it to attribute facts to their source and to see recent edits before making your own. Read-only. Required: id (integer). Optional: limit (default 20, max 100), before_seq (paginate to older revisions), include_body (boolean — reconstruct each version's full text), seq (integer — return only that revision, with its full reconstructed body). - Title: Note History - Flags: read-only, idempotent - Parameters: - `id` (integer, required): Note ID (required) - `limit` (integer): Max revisions, newest first (default 20, max 100) - `before_seq` (integer): Return revisions older than this seq (pagination) - `include_body` (boolean): Reconstruct and include each revision's full body (default false) - `seq` (integer): Return only this revision, including its reconstructed body ### `notes-revert` Revert a note's body to an earlier revision (find the seq via notes-history). This does NOT erase history — it appends a NEW revision whose body equals the chosen earlier one, attributed to you, so the revert can itself be undone. Use only when the user explicitly asks to undo a change. Optional expected_lock_version (from notes-get) rejects the revert if the note changed since you read it, instead of clobbering that change. Required: id (integer), seq (integer). - Title: Revert Note - Flags: destructive - Parameters: - `id` (integer, required): Note ID (required) - `seq` (integer, required): The revision seq to restore the body to (from notes-history) - `expected_lock_version` (integer): Optional concurrent-edit guard; pass the lock_version you last saw via notes-get ### `containers-list` List containers (folders) for organizing notes. Each container includes notes_count and children_count. Optional: team_id (integer) for team containers, scope ('roots' default|'all'|'archived'), page, per_page. Shared containers are automatically included when listing root-level personal containers. - Title: List Containers - Flags: read-only, idempotent - Parameters: - `team_id` (integer): List containers in this team instead of personal containers - `scope` (string): Filter scope (default: roots). 'archived' only for personal containers. - `page` (integer): Page number - `per_page` (integer): Results per page ### `containers-get` Get a single container by ID, including notes_count, children_count, description, negative_space (what does NOT belong in the folder), and LLM instructions if set. Optional: include_tree (boolean) to also get ancestor chain and children. Required: id (integer). - Title: Get Container - Flags: read-only, idempotent - Parameters: - `id` (integer, required): Container ID (required) - `include_tree` (boolean): Include ancestors and children arrays (default: false) ### `containers-create` Create a new container (folder) for organizing notes. Required: name (string). Optional: description (string), negative_space (string — what does NOT belong in this folder, so notes can be routed away from it), parent_id (integer or null) for nesting inside another container — null or omitted creates it at the top level, team_id (integer) to create the container in a team instead of personal space. When team_id is set, parent_id (if provided) must belong to the same team. After creating, consider setting up LLM instructions with instructions-update. - Title: Create Container - Flags: none - Parameters: - `name` (string, required): Container name (required) - `description` (string): Container description - `negative_space` (["string", "null"]): What does NOT belong in this folder (e.g. 'No meeting notes, those go in Meetings'). Helps route notes away from the wrong place. - `parent_id` (["integer", "null"]): Parent container ID for nesting, or null/omitted for a top-level container. Must belong to the same scope (personal or team) as the new container. - `team_id` (integer): Create container in this team instead of personal space ### `containers-update` Update an existing container — rename, change description or negative_space (what does NOT belong here), move to a different parent, or set display position. Works for both personal and team containers the current user can edit (team containers require the editor role). Required: id (integer). Optional: name, description, negative_space (pass null to clear it), parent_id (null for root, must be in the same scope), position (integer, lower = first). - Title: Update Container - Flags: destructive, idempotent - Parameters: - `id` (integer, required): Container ID (required) - `name` (string): New name - `description` (string): New description - `negative_space` (["string", "null"]): What does NOT belong in this folder, or null to clear it. - `parent_id` (["integer", "null"]): New parent container ID, or null to move the container to root level. Must belong to the same scope as the container. - `position` (integer): Display order position (lower numbers appear first) ### `containers-permissions` Set or clear a team member's role on a team container. Roles cascade to descendants unless a child has its own role. Caller must have admin role on the target container. Required: container_id (integer, must be a team container), user_id (integer, must be a member of the same team), role ('viewer' | 'editor' | 'admin' | 'inherit'). Use 'inherit' to delete an explicit role and fall back to the role inherited from an ancestor (or from team membership). Team owners are always admin and cannot be downgraded. Returns the user's resulting effective_role on that container. - Title: Set Container Permissions - Flags: destructive, idempotent - Parameters: - `container_id` (integer, required): Team container ID (required) - `user_id` (integer, required): Team member's user ID (required) - `role` (string, required): New role, or 'inherit' to clear an explicit role (required) ### `tags-list` List all tags with their notes_count. Paginated. Optional: page (integer), per_page (integer). - Title: List Tags - Flags: read-only, idempotent - Parameters: - `page` (integer): Page number - `per_page` (integer): Results per page ### `tags-create` Create a new tag. Check tags-list first to avoid duplicates. Required: name (string). Tag names are automatically lowercased. - Title: Create Tag - Flags: none - Parameters: - `name` (string, required): Tag name (required) ### `tags-manage` Rename, merge, or delete a tag — the cleanup tools for the tag list (this operates on the tag itself, unlike notes-update which only edits one note's tags). Required: action ('rename'|'merge'|'delete') and the tag to act on via name (string, case-insensitive) or tag_id (integer). For 'rename' also pass new_name. For 'merge' also pass the destination via target_name or target_id: every note on the source tag is moved onto the destination and the source tag is deleted — ideal for collapsing duplicates like 'machine learning' into 'machine-learning'. If target_name names a tag that doesn't exist yet, it's created, so you can merge straight into a clean canonical name without creating it first. 'delete' removes the tag from all its notes and deletes it; deleting a name that doesn't exist succeeds as a no-op (already_absent: true), so it's safe to retry. Merge and delete are destructive and cannot be undone; check tags-list first. - Title: Manage Tags - Flags: destructive - Parameters: - `action` (string, required): Action: 'rename', 'merge', or 'delete' (required) - `name` (string): Name of the tag to act on (case-insensitive). Provide this or tag_id. - `tag_id` (integer): ID of the tag to act on. Provide this or name. - `new_name` (string): New name for the tag (required for action 'rename'). - `target_name` (string): Name of the destination tag to merge into (for action 'merge'). Provide this or target_id. - `target_id` (integer): ID of the destination tag to merge into (for action 'merge'). Provide this or target_name. ### `feedback-submit` Submit feedback about Hjarni itself — confusing tool descriptions, missing capabilities, unexpected errors, friction, or praise. Use this when something about the MCP server, a tool, or the product behavior is worth flagging to the maintainers. Do NOT use this for the user's own notes or knowledge — those belong in notes-create. Required: category ('bug'|'confusing'|'missing_feature'|'friction'|'praise'|'other'), message (string, what's wrong and ideally what you'd expect instead). Optional: severity ('low'|'medium'|'high', default 'medium'), tool_name (the MCP tool the feedback is about, e.g. 'notes-update'), context (JSON-encoded string with any extra structured data — error excerpts, the arguments you tried, the workflow that broke). - Title: Submit Feedback - Flags: none - Parameters: - `category` (string, required): What kind of feedback this is (required) - `message` (string, required): The feedback itself. Be specific — describe what happened, what you expected, and (if relevant) what would have helped. (required, max 4000 chars) - `severity` (string): How impactful this is for users (default: medium) - `tool_name` (string): The MCP tool this feedback is about, if any (e.g. 'notes-update', 'search') - `context` (string): Optional extra context as a JSON-encoded string (e.g. error messages, arguments tried, related note IDs). Non-JSON strings are stored as plain text. ### `teams-list` List all teams the user is a member of, including members_count, notes_count, and containers_count for each team. No parameters required. - Title: List Teams - Flags: read-only, idempotent - Parameters: none ### `teams-get` Get team details including the 10 most recent notes. Required: id (integer). - Title: Get Team - Flags: read-only, idempotent - Parameters: - `id` (integer, required): Team ID (required) ### `teams-create` Create a new team owned by the user (they become the owner). Required: name (string). Optional: ref — set to "assistant_suggestion" ONLY when creating in response to the me → suggestions.create_team offer, so the creation is attributed to that surface; omit it when the user asked to create a team unprompted. No charge is incurred here; a per-seat charge is added only when an invited teammate accepts. A user can have only ONE free team: if they already own an unsubscribed team this returns an error telling them to upgrade that team first — relay it rather than retrying. After creating, invite teammates with teams-invite. - Title: Create Team - Flags: none - Parameters: - `name` (string, required): Team name (required) - `ref` (string): Attribution surface. Set to 'assistant_suggestion' only when acting on me.suggestions.create_team; otherwise omit. ### `teams-invite` Invite a teammate to a team by email (team owner only). Required: team_id (integer, from teams-list), email (string). Emails an invite; they must accept from that exact address, and a per-seat charge is added only when they accept. - Title: Invite Teammate - Flags: open-world - Parameters: - `team_id` (integer, required): Team ID (required; use teams-list to find it) - `email` (string, required): Email address to invite (required) ### `instructions-get` Get LLM instructions at the specified level. Call with level 'brain' early in conversations to learn user preferences. Optional: level ('brain'|'personal_root'|'container'|'team'), defaults to 'brain' if omitted or blank; the response echoes resolved_level and defaulted_level (true when the level was defaulted). Optional: id (integer, required for 'container' and 'team' levels). 'container' level returns the full inheritance chain (personal root -> ancestors -> container). - Title: Get Instructions - Flags: read-only, idempotent - Parameters: - `level` (string): Instruction level: 'brain' (global), 'personal_root', 'container', or 'team'. Defaults to 'brain' if omitted or blank. - `id` (integer): Container ID or Team ID (required for 'container' and 'team' levels) ### `instructions-update` Update LLM instructions at the specified level. Required: level ('brain'|'personal_root'|'container'|'team'), instructions (string). Optional: id (integer, required for 'container' and 'team'), mode ('replace' default|'append'). The 'container' level updates personal containers only; to set instructions for a team, use level 'team' (team owners only). In 'replace' mode (default), the provided text overwrites existing instructions. In 'append' mode, the text is appended to existing instructions with a newline separator. Always read current instructions first before replacing to avoid losing existing content. - Title: Update Instructions - Flags: destructive, idempotent - Parameters: - `level` (string, required): Instruction level to update (required) - `id` (integer): Container ID or Team ID (required for 'container' and 'team' levels) - `instructions` (string, required): The instructions text. In 'replace' mode (default), this overwrites existing instructions. In 'append' mode, this is appended to existing instructions. - `mode` (string): Update mode: 'replace' (default) overwrites existing instructions, 'append' adds to them ### `nudges-dismiss` Dismiss a nudge served in the `me` payload, on the user's behalf, so it stops appearing on every surface (including the web app). Call this ONLY when the user has actually declined the suggestion or asked not to be reminded — never on mere silence; ignored nudges are already frequency-capped server-side. Required: key (string, the `nudge.key` from `me`). Idempotent. - Title: Dismiss Nudge - Flags: idempotent - Parameters: - `key` (string, required): The nudge key from the `me` payload's nudge block (required) ### `links-manage` Create or remove a bidirectional link between two notes. Required: action ('link'|'unlink'), source_note_id (integer), target_note_id (integer). Prefer wiki-link syntax [[id:Title]] in note bodies for inline linking — use this tool for programmatic links without modifying body text. Unlinking is destructive and cannot be undone. - Title: Manage Note Links - Flags: destructive, idempotent - Parameters: - `action` (string, required): Action: 'link' to create a link, 'unlink' to remove it (required) - `source_note_id` (integer, required): First note ID (required) - `target_note_id` (integer, required): Second note ID (required) ### `files-attach` Attach a file to one of your personal notes via base64-encoded data (personal notes only; for team or shared notes use files-create_upload_url). Prefer files-create_upload_url for large files to save tokens. Required: note_id (integer), filename (string), data (base64 string). Optional: content_type (MIME type, default: application/octet-stream), description. - Title: Attach File to Note - Flags: none - Parameters: - `note_id` (integer, required): Note ID (required) - `filename` (string, required): Filename (e.g. report.pdf) (required) - `data` (string, required): Base64-encoded file contents (required) - `content_type` (string): MIME type (e.g. application/pdf). Defaults to application/octet-stream - `description` (string): Optional file description ### `files-attach_from_url` Fetch a file from a public URL and attach it to one of your personal notes (personal notes only; for team or shared notes use files-create_upload_url). Follows one redirect. Required: note_id (integer), url (string). Optional: filename (default: derived from URL), content_type (default: from HTTP response), description. - Title: Attach File from URL - Flags: open-world - Parameters: - `note_id` (integer, required): Note ID (required) - `url` (string, required): URL to fetch the file from (required) - `filename` (string): Override filename (default: derived from URL) - `content_type` (string): Override MIME type (default: from HTTP response) - `description` (string): Optional file description ### `files-remove` Permanently remove a file attachment from one of your personal notes (personal notes only). This action is irreversible. Required: note_id (integer), file_id (integer, from notes-get response). - Title: Remove File from Note - Flags: destructive - Parameters: - `note_id` (integer, required): Note ID (required) - `file_id` (integer, required): File ID from notes-get response (required) ### `files-create_upload_url` Generate a one-time upload URL for attaching a file to a note. Share this URL with the user so they can upload directly in their browser — saves tokens by avoiding base64 encoding. The link expires after 30 minutes. Use files-check_upload to verify completion. Required: note_id (integer). Optional: description. - Title: Create File Upload URL - Flags: none - Parameters: - `note_id` (integer, required): Note ID to attach the file to (required) - `description` (string): Optional file description ### `files-check_upload` Check the status of a file upload created by files-create_upload_url. Returns status: 'pending' (not uploaded yet), 'completed' (file attached, includes file metadata), or 'expired' (link timed out). Required: token (string, from files-create_upload_url response). - Title: Check File Upload - Flags: read-only, idempotent - Parameters: - `token` (string, required): Upload token from files-create_upload_url response (required) ### `files-get_download_url` Get a temporary download URL for a file attached to a note. Share the URL with the user to download in their browser. URL expires after a few minutes. Required: note_id (integer), file_id (integer, from notes-get response). - Title: Get File Download URL - Flags: read-only, idempotent - Parameters: - `note_id` (integer, required): Note ID (required) - `file_id` (integer, required): File ID from notes-get response (required) ## Prompts (slash commands) The MCP server also exposes named prompts. Clients that support `prompts/list` (Claude Desktop, Claude Code) surface these as slash commands. ### `/summarize_note` Summarize a note and suggest tags and links to related notes. - Arguments: - `note_id` (required): The ID of the note to summarize. ### `/weekly_review` Review notes created or updated in the past week, identify themes, and suggest organizational improvements. - Arguments: - `days`: Number of days to look back (default: 7). ### `/research_topic` Search the knowledge base for everything related to a topic, synthesize findings, and identify gaps in coverage. - Arguments: - `topic` (required): The topic to research across the knowledge base. ## REST API Every MCP tool has a corresponding REST endpoint. Useful when the user wants scripts or automations outside an LLM context, or when their client does not support MCP. - Base URL: `https://hjarni.com/api/v1` - Auth: `Authorization: Bearer ` header. Tokens come from the user's account settings or from an OAuth code exchange. - Full reference: https://hjarni.com/docs/api ## Pricing and limits - Free plan available; paid plans start at EUR 9/month. - File attachments and public folder links are Pro features. - See https://hjarni.com/pricing for current details. ## See also - Short version of this file: https://hjarni.com/llms.txt - Human-readable docs index: https://hjarni.com/docs - MCP server reference: https://hjarni.com/docs/mcp - What is MCP?: https://hjarni.com/docs/what-is-mcp - AI Glossary: https://hjarni.com/docs/glossary