Skip to main content
Glama

Insert at Section

insert_at_section

Insert 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

TableJSON Schema
NameRequiredDescriptionDefault
pathYesVault-relative path to the note.
sectionYesHeading path identifying the section.
contentYesContent to insert. A trailing newline is normalized.
positionNoInsert before the heading line, immediately after the heading, or at the end of the section body.append

Implementation Reference

  • 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)}`);
      }
    },
  • 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)}`);
        }
      },
    );
  • 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."),
    },
  • 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;
    }
  • 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;
    }
Behavior3/5

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

Annotations provide minimal behavioral hints (readOnlyHint=false, destructiveHint=false). The description adds that the tool does not replace content and explains position behavior, but does not disclose error states (e.g., missing section) or side effects. It does not contradict 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?

The description is concise—three sentences front-loaded with purpose. Every sentence adds value, efficiently explaining the position parameter's behavior without redundant or extraneous words.

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?

Given the lack of an output schema, the description does not mention return values or error conditions (e.g., whether the section must exist). It adequately covers the core operation but lacks completeness regarding edge cases and integration with sibling tools.

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

Parameters5/5

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

Schema coverage is 100%, but the description adds significant value by elaborating on the 'position' parameter: it explains each enum value in detail ('before inserts above the heading', 'after-heading' at top of section body, 'append' at end before next heading), which is more informative than the schema description.

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 'Insert content into a specific section without replacing it.' It specifies the verb (insert), resource (content into a specific section), and distinguishes from replace operations. The three position options are explained, differentiating it from sibling tools like append_to_note and prepend_to_note.

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

Usage Guidelines4/5

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

The description provides context for when to use the tool: 'Use to add a new bullet or paragraph without rewriting the section.' While it doesn't explicitly state when not to use or list alternatives, the purpose is clear and implies this is for non-destructive insertion.

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/rps321321/obsidian-mcp-pro'

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