Skip to content

Commit aa635b8

Browse files
phernandezclaude
andcommitted
fix: accept null for expected_replacements in edit_note (#606)
MCP clients may send explicit `null` for unused optional fields. `expected_replacements: int = 1` caused FastMCP's JSON Schema validation to reject null before the function body ran. Changed to `Optional[int] = None` with an effective default resolved inside the function body. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 1856d4b commit aa635b8

2 files changed

Lines changed: 40 additions & 4 deletions

File tree

src/basic_memory/mcp/tools/edit_note.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ async def edit_note(
135135
workspace: Optional[str] = None,
136136
section: Optional[str] = None,
137137
find_text: Optional[str] = None,
138-
expected_replacements: int = 1,
138+
expected_replacements: Optional[int] = None,
139139
output_format: Literal["text", "json"] = "text",
140140
context: Context | None = None,
141141
) -> str | dict:
@@ -216,6 +216,9 @@ async def edit_note(
216216
search_notes() first to find the correct identifier. The tool provides detailed
217217
error messages with suggestions if operations fail.
218218
"""
219+
# Resolve effective default: allow MCP clients to send null for optional int field
220+
effective_replacements = expected_replacements if expected_replacements is not None else 1
221+
219222
async with get_project_client(project, workspace, context) as (client, active_project):
220223
logger.info("MCP tool call", tool="edit_note", identifier=identifier, operation=operation)
221224

@@ -254,8 +257,8 @@ async def edit_note(
254257
edit_data["section"] = section
255258
if find_text:
256259
edit_data["find_text"] = find_text
257-
if expected_replacements != 1: # Only send if different from default
258-
edit_data["expected_replacements"] = str(expected_replacements)
260+
if effective_replacements != 1: # Only send if different from default
261+
edit_data["expected_replacements"] = str(effective_replacements)
259262

260263
# Call the PATCH endpoint
261264
result = await knowledge_client.patch_entity(entity_id, edit_data, fast=False)
@@ -339,5 +342,5 @@ async def edit_note(
339342
"error": str(e),
340343
}
341344
return _format_error_response(
342-
str(e), operation, identifier, find_text, expected_replacements, active_project.name
345+
str(e), operation, identifier, find_text, effective_replacements, active_project.name
343346
)

tests/mcp/test_tool_edit_note.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,39 @@ async def test_edit_note_find_replace_empty_find_text(client, test_project):
414414
# Should contain helpful guidance about the error
415415

416416

417+
@pytest.mark.asyncio
418+
async def test_edit_note_append_with_null_optional_fields(client, test_project):
419+
"""Regression test: MCP clients may send explicit null for unused optional fields.
420+
421+
When an MCP client sends find_text=None, section=None, expected_replacements=None
422+
for an append operation, the tool should accept them without validation errors.
423+
"""
424+
# Create initial note
425+
await write_note(
426+
project=test_project.name,
427+
title="Null Fields Test",
428+
directory="test",
429+
content="# Null Fields Test\nOriginal content.",
430+
)
431+
432+
# Call edit_note with explicit None for all optional fields (simulates MCP null)
433+
result = await edit_note(
434+
project=test_project.name,
435+
identifier="test/null-fields-test",
436+
operation="append",
437+
content="\nAppended content.",
438+
find_text=None,
439+
section=None,
440+
expected_replacements=None,
441+
)
442+
443+
assert isinstance(result, str)
444+
assert "Edited note (append)" in result
445+
assert f"project: {test_project.name}" in result
446+
assert "file_path: test/Null Fields Test.md" in result
447+
assert f"[Session: Using project '{test_project.name}']" in result
448+
449+
417450
@pytest.mark.asyncio
418451
async def test_edit_note_preserves_permalink_when_frontmatter_missing(client, test_project):
419452
"""Test that editing a note preserves the permalink when frontmatter doesn't contain one.

0 commit comments

Comments
 (0)