feat(vault_read_note): add outline and section-scoped reads#130
Conversation
vault_read_note returned the whole note body, so large notes (a long
Kanban board, CLAUDE.local.md) blow the tool-result token budget and
truncate. The write side already targets sections by heading
(vault_patch_note); this closes the read/write asymmetry.
Two new optional, mutually-exclusive modes:
- outline: true — returns the heading tree as JSON ({ level, text, bytes }
per heading), a cheap structure fetch.
- heading (+ heading_level) — returns just that section (heading line +
body, child headings included), mirroring vault_patch_note's targeting.
Extracts the section-span parser (parseHeadings/findHeading) from
vault-patcher.ts into a shared heading-parser.ts so read and patch resolve
sections identically — no second parser. Pure move; patcher behavior and
tests unchanged.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 6 minutes and 17 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughA new shared ChangesHeading Parser Extraction and Note Read Modes
Sequence DiagramsequenceDiagram
participant Client as MCP Client
participant Handler as VAULT_READ_NOTE Handler
participant vaultFs as vault-filesystem
participant headingParser as heading-parser
Client->>Handler: { path, outline: true }
Handler->>vaultFs: readNoteOutline(path)
vaultFs->>headingParser: parseHeadings(lines)
headingParser-->>vaultFs: HeadingInfo[]
vaultFs-->>Handler: HeadingOutline[]
Handler-->>Client: outline JSON
Client->>Handler: { path, heading: "Section A", heading_level: 2 }
Handler->>vaultFs: readNoteSection(path, "Section A", 2)
vaultFs->>headingParser: parseHeadings(lines)
headingParser-->>vaultFs: HeadingInfo[]
vaultFs->>headingParser: findHeading(headings, "Section A", 2)
headingParser-->>vaultFs: HeadingInfo
vaultFs-->>Handler: section markdown text
Handler-->>Client: section markdown
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/vault-mcp/tool-definitions.ts`:
- Around line 146-160: The tool schemas allow heading_level to be specified
independently and accept empty string values for heading, both of which bypass
section mode validation and fall back to full-note reads. For each tool
definition in src/vault-mcp/tool-definitions.ts at lines 146-160 (anchor),
181-189, and 207-217 (siblings), add schema validation to ensure heading_level
can only be used when heading is present and non-empty, and to reject empty
string values for heading. Use Zod's refine or superRefine method on the
parameter object to add a constraint that validates heading_level is only set
when heading is a non-empty string, so section mode is properly enforced before
dispatch.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 20c2778f-298a-430c-96f3-aaa487eea2df
📒 Files selected for processing (8)
AGENTS.mdsrc/vault-mcp/__tests__/tool-definitions.test.tssrc/vault-mcp/tool-definitions.tssrc/vault-mcp/vault-operations/__tests__/heading-parser.test.tssrc/vault-mcp/vault-operations/__tests__/vault-filesystem.test.tssrc/vault-mcp/vault-operations/heading-parser.tssrc/vault-mcp/vault-operations/vault-filesystem.tssrc/vault-mcp/vault-operations/vault-patcher.ts
CodeRabbit flagged two silent fall-throughs in the new section-read
modes: heading_level passed without heading was ignored, and heading: ""
was treated as falsy and fell back to a full-note read — both contrary
to the fail-loud contract the tool documents.
- Mutual-exclusivity check now treats heading !== undefined as section
mode (so an empty heading is caught, not skipped).
- Reject heading_level supplied without heading ("heading_level requires
a heading").
- Dispatch on heading !== undefined so an empty heading reaches
findHeading and surfaces its "heading cannot be empty" error.
Kept the validation in the handler rather than tightening the Zod schema
(.min(1)/.trim()) so vault_read_note's heading schema stays identical to
vault_patch_note's — schema parity is the point of this change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
| Prefer vault_search when you don't know the path. Prefer vault_get_memory for ${config.memoryDir}/ files (returns content without properties). To edit a section you've read, use vault_patch_note (same heading targeting). | ||
|
|
||
| Returns: Raw markdown string (default), or JSON object of properties (when properties_only: true).`, | ||
| Section boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included in the parent section. This matches vault_patch_note exactly, so "read section X" and "edit section X" resolve to the same span. |
There was a problem hiding this comment.
We don't need to specify it matches patch note exactly. This is a tool description and unnecessary tokens.
…tion The tool description ships to the model on every call, so the "matches vault_patch_note exactly" sentence and the "(same heading targeting)" / "Mirrors vault_patch_note's heading targeting" asides were unnecessary tokens that don't help a caller use the tool. Removed all three; the behavioral guidance (section boundaries, when-to-use pointer to vault_patch_note for editing) is kept. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
Add .min(1) to the heading input schema so an empty heading is rejected at parse time rather than only at the handler. The handler guards (heading_level-requires-heading, and findHeading's "heading cannot be empty" for whitespace-only input) remain as defense-in-depth. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
Restore heading-schema parity with vault_read_note: vault_patch_note's heading was missing .min(1) (omitting it stays valid for file-level append/prepend, since it's .optional() — only an empty string is now rejected at parse time). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
| if ( | ||
| [ | ||
| properties_only === true, | ||
| outline === true, |
There was a problem hiding this comment.
Let's make this a clearly named car vs inline
Pull the inline `[...].filter(Boolean).length` mutual-exclusivity check out into a `selectedModeCount` const so the guard reads clearly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
| text: heading.text, | ||
| // Section span = heading line through bodyEndLine (the same span a section | ||
| // read returns), so the size hint matches what reading it would cost. | ||
| bytes: Buffer.byteLength( |
There was a problem hiding this comment.
Why not put this in a var and log it?
Pull the per-section byte computation onto a named `sectionText` var and log the summed `totalBytes` alongside the heading count, so the outline read's payload size is observable. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
| ) | ||
| } | ||
|
|
||
| // An empty heading reaches readNoteSection and surfaces findHeading's |
There was a problem hiding this comment.
Don't we have a min value check now?
The heading schema now enforces min(1), so an empty heading is rejected before the handler. Reword the section-routing comment to reflect that the !== undefined check just selects section vs full read. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
heading is either undefined or a non-empty string (schema min(1)), so a plain truthy `if (heading)` is equivalent to `!== undefined` and reads cleaner. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
Mirror the obsidian-headless-sync-docker fork's config: disable the poem, sequence diagrams, and review-status sections so the recurring walkthrough comment (and its webhook payload) stays lean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
Every read mode had an example except heading_level. Add one showing heading + heading_level together for the disambiguation case. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
Why
vault_read_noteonly ever returned the entire note body. For large notes (a long Kanban board, a longCLAUDE.local.md) that exceeds the tool-result token budget and truncates. The write side already targets sections by heading (vault_patch_note), so an agent could surgically edit## Activewithout reading## Done— yet couldn't read## Activewithout pulling the whole file. This closes that read/write asymmetry.What
Two new optional, mutually-exclusive params on
vault_read_note:outline: true— returns the heading tree as JSON ({ level, text, bytes }per heading), no bodies. A cheap structure fetch: read the outline, then drill into the one section you need.heading(+ optionalheading_level) — returns just that section: the heading line plus its body, through the next heading of the same or higher level (child headings included). Case-sensitive exact match, mirroringvault_patch_note's targeting.properties_onlyis unchanged; default (no mode) still returns the full note. The handler rejects combining more than one mode.How
The section-span parser (
parseHeadings/findHeading) is extracted fromvault-patcher.tsinto a new sharedheading-parser.ts, so read and patch resolve a section to the exact same span — "read section X" and "edit section X" line up, and there's no second parser to drift. The move is behaviour-preserving; the patcher's existing tests pass unchanged.Notes on semantics
bytes) includes its nested child sections — consistent with what a section read of that heading returns.%%settings block (e.g. a Kanban board's settings) is excluded from the last section, reusing the patcher's existing boundary logic.Tests
heading-parser.test.ts— direct unit tests for the extracted parser (H1–H6 spans, child inclusion, fenced-code headings ignored, trailing%%exclusion, not-found/ambiguous/empty errors).vault-filesystem.test.ts—readNoteOutline(level/text/bytes, no-headings →[], frontmatter excluded, not-found) andreadNoteSection(heading + body, child inclusion,heading_leveldisambiguation, trailing%%exclusion, not-found/ambiguous).tool-definitions.test.ts— handler rejects combining modes.657 tests pass;
lint,build, andprettier:checkclean.AGENTS.mdstructure map updated to listheading-parser.ts; tool count unchanged (params added, no new tool).Generated by Claude Code
Summary by CodeRabbit
New Features
Tests
Refactor