patch_content with target_type: heading fails on all H2+ headings

patch_content with target_type: heading fails on all H2+ headings

Repository: MarkusPfundstein/mcp-obsidian Filed against: patch_content tool Related upstream issues:

  • jacksteamdev/obsidian-mcp-tools#71 — same root cause, different MCP wrapper

  • coddingtonbear/markdown-patch#7replace + createTargetIfMissing causes silent corruption

  • coddingtonbear/obsidian-local-rest-api#221 — leaf-only targets with no qualified path support


Summary

patch_content with target_type: heading returns Error 40080: The patch you provided could not be applied to the target content. invalid-target for all H2 (##) and deeper headings. H1 (#) headings and frontmatter fields work correctly.

This makes the heading-targeted patch operation unusable for structured notes that use H1 as the document title and H2 for sections (which is the standard Obsidian pattern).

Environment

  • MCP server: mcp-obsidian (MarkusPfundstein) via Claude.ai MCP integration

  • Obsidian: latest version as of May 2026

  • Obsidian Local REST API plugin: latest version as of May 2026

Reproduction

1. Create a test file with standard heading structure:

markdown

---
status: active
last_updated: 2026-05-26
---

# Project Title

> Some metadata block

## Summary
This is the summary content.

## Action Items
- [ ] Task one
- [ ] Task two

## Updates Log
### 2026-05-26
- Initial entry.

2. Try patching under an H2 heading — all of these fail:

Append to H2 (plain heading text):

json

{
  "filepath": "test/test-file.md",
  "operation": "append",
  "target_type": "heading",
  "target": "Action Items",
  "content": "\n- [ ] New task"
}

Result: Error 40080: invalid-target

Append to H2 (with ## prefix):

json

{
  "filepath": "test/test-file.md",
  "operation": "append",
  "target_type": "heading",
  "target": "## Action Items",
  "content": "\n- [ ] New task"
}

Result: Error 40080: invalid-target

Replace H2 content:

json

{
  "filepath": "test/test-file.md",
  "operation": "replace",
  "target_type": "heading",
  "target": "Summary",
  "content": "Updated summary content."
}

Result: Error 40080: invalid-target

Prepend to H2:

json

{
  "filepath": "test/test-file.md",
  "operation": "prepend",
  "target_type": "heading",
  "target": "Updates Log",
  "content": "### 2026-05-27\n- New entry.\n"
}

Result: Error 40080: invalid-target

3. H1 targeting works fine:

json

{
  "filepath": "test/test-file.md",
  "operation": "append",
  "target_type": "heading",
  "target": "Project Title",
  "content": "\nAppended under H1."
}

Result: Success ✓

4. Frontmatter targeting works fine:

json

{
  "filepath": "test/test-file.md",
  "operation": "replace",
  "target_type": "frontmatter",
  "target": "status",
  "content": "completed"
}

Result: Success ✓

Root Cause Analysis

Based on jacksteamdev/obsidian-mcp-tools#71, the underlying issue is in the markdown-patch library used by obsidian-local-rest-api:

  1. The patch_content tool passes the heading name as a leaf-only target (e.g., "Action Items")

  2. markdown-patch requires fully-qualified hierarchical paths using \x1f (unit separator) as delimiter (e.g., "Project Title\x1fAction Items")

  3. For H1 headings, the leaf name equals the full path, so it works

  4. For H2+ headings nested under an H1, the leaf-only lookup fails because markdown-patch expects the parent path

  5. When the lookup fails, the behavior depends on createTargetIfMissing — either invalid-target error (our case) or silent corruption (appending a duplicate heading at EOF)

Impact

This effectively breaks section-level editing for any structured note. The workaround is to:

  1. Read the full file

  2. Delete it

  3. Recreate it with the modified content via append_content

This works but is three tool calls instead of one, and risks data loss if the delete succeeds but the recreate fails.

Suggested Fix

Option A (MCP wrapper level): When target_type is heading and the target is not an H1, resolve the leaf heading to its fully-qualified path by parsing the file’s heading hierarchy before sending the PATCH request to the Obsidian REST API.

Option B (expose qualified paths): Accept and document fully-qualified heading paths in the target parameter (e.g., "Project Title\x1fAction Items" or "Project Title > Action Items"), allowing callers to disambiguate nested headings.

Option C (upstream): Fix markdown-patch to index root-level H2+ headings correctly without requiring a parent H1 path.

Workaround

For any file modification beyond frontmatter or H1-level content:

  1. get_file_contents to read current state

  2. delete_file to remove the file

  3. append_content to a new file at the same path with the full modified content