Insert at Section
insert_at_sectionInsert content into a specific section of an Obsidian note at a chosen position (before heading, after heading, or append) to add bullets or paragraphs without rewriting the section.
Instructions
Insert content into a specific section without replacing it. position controls where: 'before' inserts above the heading, 'after-heading' inserts immediately under the heading line (at the top of the section body), 'append' inserts at the end of the section's body just before the next heading. Use to add a new bullet or paragraph without rewriting the section.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | Vault-relative path to the note. | |
| section | Yes | Heading path identifying the section. | |
| content | Yes | Content to insert. A trailing newline is normalized. | |
| position | No | Insert before the heading line, immediately after the heading, or at the end of the section body. | append |
Implementation Reference
- src/tools/sections.ts:105-141 (handler)The async handler for the 'insert_at_section' tool. Receives path, section, content, and position; calls updateNote to atomically read-modify-write the note. Depending on position ('before', 'after-heading', 'append'), it performs string slicing to insert content before the heading, immediately after the heading line, or at the end of the section body. Uses findSection() from the sections library to locate the target section.
async ({ path: notePath, section, content, position }) => { try { const headingPath = splitHeadingPath(section); if (headingPath.length === 0) return errorResult("section must not be empty"); let resolvedHeading = ""; await updateNote(vaultPath, notePath, (existing) => { const found = findSection(existing, headingPath); if (!found) { throw new Error(`Section not found: "${section}"`); } resolvedHeading = found.heading.text; if (position === "after-heading") { return insertAfterHeading(existing, found, content); } if (position === "before") { const before = existing.slice(0, found.start); const after = existing.slice(found.start); const trailing = content.endsWith("\n") ? "" : "\n"; return before + content + trailing + after; } // append: insert at end of section body, before the next heading const before = existing.slice(0, found.end); const after = existing.slice(found.end); let payload = content; // Make sure there's a leading newline if the section body didn't // already end on one (so we don't fuse with the prior line). if (!before.endsWith("\n")) payload = "\n" + payload; if (!payload.endsWith("\n")) payload += "\n"; return before + payload + after; }); return textResult(`Inserted ${content.length} bytes into "${resolvedHeading}" (${position}) in ${notePath}`); } catch (err) { log.error("insert_at_section failed", { tool: "insert_at_section", err: err as Error }); return errorResult(`Error inserting at section: ${sanitizeError(err)}`); } }, - src/tools/sections.ts:83-142 (registration)The 'insert_at_section' tool registration via server.registerTool(), including its title, description, annotations, and Zod-based input schema (path, section, content, position enum with default 'append'). This is where the tool name is officially registered with the MCP server.
server.registerTool( "insert_at_section", { title: "Insert at Section", description: "Insert content into a specific section without replacing it. `position` controls where: 'before' inserts above the heading, 'after-heading' inserts immediately under the heading line (at the top of the section body), 'append' inserts at the end of the section's body just before the next heading. Use to add a new bullet or paragraph without rewriting the section.", annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, }, inputSchema: { path: z.string().min(1).describe("Vault-relative path to the note."), section: z.string().min(1).describe("Heading path identifying the section."), content: z.string().describe("Content to insert. A trailing newline is normalized."), position: z .enum(["before", "after-heading", "append"]) .default("append") .describe("Insert before the heading line, immediately after the heading, or at the end of the section body."), }, }, async ({ path: notePath, section, content, position }) => { try { const headingPath = splitHeadingPath(section); if (headingPath.length === 0) return errorResult("section must not be empty"); let resolvedHeading = ""; await updateNote(vaultPath, notePath, (existing) => { const found = findSection(existing, headingPath); if (!found) { throw new Error(`Section not found: "${section}"`); } resolvedHeading = found.heading.text; if (position === "after-heading") { return insertAfterHeading(existing, found, content); } if (position === "before") { const before = existing.slice(0, found.start); const after = existing.slice(found.start); const trailing = content.endsWith("\n") ? "" : "\n"; return before + content + trailing + after; } // append: insert at end of section body, before the next heading const before = existing.slice(0, found.end); const after = existing.slice(found.end); let payload = content; // Make sure there's a leading newline if the section body didn't // already end on one (so we don't fuse with the prior line). if (!before.endsWith("\n")) payload = "\n" + payload; if (!payload.endsWith("\n")) payload += "\n"; return before + payload + after; }); return textResult(`Inserted ${content.length} bytes into "${resolvedHeading}" (${position}) in ${notePath}`); } catch (err) { log.error("insert_at_section failed", { tool: "insert_at_section", err: err as Error }); return errorResult(`Error inserting at section: ${sanitizeError(err)}`); } }, ); - src/tools/sections.ts:95-103 (schema)Input schema for 'insert_at_section' defining the four parameters: path (string), section (string), content (string), and position (enum: 'before', 'after-heading', 'append' with default 'append').
inputSchema: { path: z.string().min(1).describe("Vault-relative path to the note."), section: z.string().min(1).describe("Heading path identifying the section."), content: z.string().describe("Content to insert. A trailing newline is normalized."), position: z .enum(["before", "after-heading", "append"]) .default("append") .describe("Insert before the heading line, immediately after the heading, or at the end of the section body."), }, - src/lib/sections.ts:228-238 (helper)The insertAfterHeading() helper function used by the 'after-heading' position. It inserts content immediately after the heading line (bodyStart offset) and before the existing body content.
export function insertAfterHeading( content: string, section: Section, inserted: string, ): string { const before = content.slice(0, section.bodyStart); const after = content.slice(section.bodyStart); let payload = inserted; if (!payload.endsWith("\n")) payload += "\n"; return before + payload + after; } - src/lib/sections.ts:150-184 (helper)The findSection() helper function that locates a section by a heading path (array of heading names) using case-insensitive, whitespace-tolerant matching. Used by the insert_at_section handler to find the target section.
export function findSection(content: string, headingPath: readonly string[]): Section | null { if (headingPath.length === 0) return null; const headings = parseHeadings(content); if (headings.length === 0) return null; const targets = headingPath.map(normalizeHeadingText); // Walk linearly. Track the "open path" of headings as we go. const openPath: { level: number; index: number }[] = []; for (let i = 0; i < headings.length; i++) { const h = headings[i]; while (openPath.length > 0 && openPath[openPath.length - 1].level >= h.level) { openPath.pop(); } openPath.push({ level: h.level, index: i }); if (openPath.length < targets.length) continue; // Compare the last `targets.length` opened headings (ancestor-first) to // the requested path. Allows the path to begin at any depth — this is // the behavior most users expect, and matches single-element fallback // automatically. const slice = openPath.slice(openPath.length - targets.length); let match = true; for (let k = 0; k < targets.length; k++) { if (normalizeHeadingText(headings[slice[k].index].text) !== targets[k]) { match = false; break; } } if (!match) continue; return buildSection(content, headings, i); } return null; }