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#7—replace+createTargetIfMissingcauses 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:
-
The
patch_contenttool passes the heading name as a leaf-only target (e.g.,"Action Items") -
markdown-patchrequires fully-qualified hierarchical paths using\x1f(unit separator) as delimiter (e.g.,"Project Title\x1fAction Items") -
For H1 headings, the leaf name equals the full path, so it works
-
For H2+ headings nested under an H1, the leaf-only lookup fails because
markdown-patchexpects the parent path -
When the lookup fails, the behavior depends on
createTargetIfMissing— eitherinvalid-targeterror (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:
-
Read the full file
-
Delete it
-
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:
-
get_file_contentsto read current state -
delete_fileto remove the file -
append_contentto a new file at the same path with the full modified content