- Change withLineNumbers to use numbered string format (Option B) - Remove expectedContent validation (unnecessary with ifMatch + line numbers) - Use force:true instead of skipVersionCheck for opt-out - Clarify breaking change impact
5.8 KiB
Plan: Fix update_sections Line Number Issue via MCP Server Changes
Problem Analysis
When using update_sections, line number errors occur because:
read_notedoesn't return line numbers - Returns content as a string, no line mappingifMatchis optional - No enforcement of version checking before editsversionIdinconsistent - Only returned whenparseFrontmatter: true
Root Cause
The Read tool shows line numbers (e.g., 1→content) but read_note does not. When using read_note and later calling update_sections, line numbers are guessed based on stale content.
Proposed Changes
Change 1: Add withLineNumbers Option to read_note
File: src/tools/note-tools.ts
Current behavior: Returns { content: "...", wordCount: N }
New behavior with withLineNumbers: true: Returns numbered lines using → prefix:
{
"content": "1→---\n2→title: Example\n3→---\n4→\n5→## Overview\n6→Some text here",
"totalLines": 6,
"versionId": "abc123",
"wordCount": 42
}
Implementation (add after existing options handling):
// If withLineNumbers requested, prefix each line with line number
if (options?.withLineNumbers && withContent) {
const lines = content.split('\n');
const numberedContent = lines
.map((line, idx) => `${idx + 1}→${line}`)
.join('\n');
const result = {
content: numberedContent,
totalLines: lines.length,
versionId: VersionUtils.generateVersionId(file),
wordCount: ContentUtils.countWords(content)
};
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
};
}
Schema update (in index.ts):
withLineNumbers: {
type: "boolean",
description: "If true, prefix each line with its line number (e.g., '1→content'). Use this when you need to make line-based edits with update_sections. Returns totalLines count and versionId for use with ifMatch parameter."
}
Change 2: Require ifMatch for update_sections
File: src/tools/note-tools.ts
Current behavior: ifMatch is optional - edits proceed without version check.
New behavior: ifMatch is required unless force: true is passed.
Method signature change:
async updateSections(
path: string,
edits: SectionEdit[],
ifMatch?: string, // Still optional in signature
validateLinks: boolean = true,
force?: boolean // NEW: explicit opt-out
): Promise<CallToolResult>
Validation logic (early in method, after path/edits validation):
// Require ifMatch unless force is true
if (!ifMatch && !force) {
return {
content: [{
type: "text",
text: JSON.stringify({
error: 'Version check required',
message: 'The ifMatch parameter is required to prevent overwriting concurrent changes. First call read_note with withLineNumbers:true to get the versionId, then pass it as ifMatch. To bypass this check, set force:true (not recommended).'
}, null, 2)
}],
isError: true
};
}
Schema update (in index.ts):
ifMatch: {
type: "string",
description: "Required: ETag/versionId for concurrency control. Get this from read_note response. Update only proceeds if file hasn't changed since read. Omit only with force:true."
},
force: {
type: "boolean",
description: "If true, skip version check and apply edits without ifMatch. Use only when you intentionally want to overwrite without checking for concurrent changes. Default: false"
}
Note: Keep required: ["path", "edits"] in schema - we enforce ifMatch in code to provide a helpful error message.
Change 3: Always Return versionId from read_note
File: src/tools/note-tools.ts
Current behavior: Only returns versionId when parseFrontmatter: true.
New behavior: Always include versionId in the response.
Current code (around line 88):
const result = {
content,
wordCount
};
Updated code:
const result = {
content,
wordCount,
versionId: VersionUtils.generateVersionId(file)
};
Files to Modify
| File | Changes |
|---|---|
src/tools/note-tools.ts |
Add withLineNumbers, add force parameter, always return versionId |
src/tools/index.ts |
Update schemas for read_note and update_sections |
Implementation Steps
-
Modify
readNoteinnote-tools.ts:- Add
withLineNumbersoption handling - Always return
versionIdwhen returning content
- Add
-
Modify
updateSectionsinnote-tools.ts:- Add
forceparameter - Add validation requiring
ifMatchunlessforce: true
- Add
-
Update tool schemas in
index.ts:- Add
withLineNumbersproperty toread_noteschema - Add
forceproperty toupdate_sectionsschema - Update
ifMatchdescription to indicate it's required
- Add
-
Update call site in
index.ts:- Pass
forceparameter through toupdateSections
- Pass
-
Write tests for new behaviors
-
Build and test in Obsidian
Verification
read_notewithwithLineNumbers: true→ returns numbered content,totalLines,versionIdread_notewithout options → returns content withversionId(new behavior)update_sectionswithoutifMatch→ returns error with helpful messageupdate_sectionswithforce: true→ proceeds without version checkupdate_sectionswith validifMatch→ proceeds normallyupdate_sectionswith staleifMatch→ returns version mismatch error
Breaking Change
Impact: Callers that omit ifMatch from update_sections will receive an error unless they explicitly pass force: true.
Mitigation: The error message explains how to fix the issue and mentions the force option for those who intentionally want to skip version checking.