Compare commits

...

1 Commits

Author SHA1 Message Date
claude[bot] 951d5d1ac4 fix: allow null optional fields in edit_note MCP tool
MCP clients (e.g. openclaw-basic-memory) may send explicit null values
for unused optional fields such as find_text, section, and
expected_replacements. Previously, expected_replacements was typed as
`int = 1` (non-optional), so FastMCP's JSON Schema validation rejected
null payloads before the function body ran.

Fix: change expected_replacements to Optional[int] = None and resolve
the effective value (defaulting to 1) inside the function body.
find_text and section were already Optional[str] so they handle null
correctly once schema validation passes.

Adds a regression test covering the exact payload shape from issue #606.

Fixes #606

Co-authored-by: bm-clawd <bm-clawd@users.noreply.github.com>
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-25 15:28:09 +00:00
2 changed files with 45 additions and 4 deletions
+8 -4
View File
@@ -135,7 +135,7 @@ async def edit_note(
workspace: Optional[str] = None,
section: Optional[str] = None,
find_text: Optional[str] = None,
expected_replacements: int = 1,
expected_replacements: Optional[int] = None,
output_format: Literal["text", "json"] = "text",
context: Context | None = None,
) -> str | dict:
@@ -219,6 +219,10 @@ async def edit_note(
async with get_project_client(project, workspace, context) as (client, active_project):
logger.info("MCP tool call", tool="edit_note", identifier=identifier, operation=operation)
# Null optional fields are equivalent to absent — treat None as the default.
# This handles MCP clients that send explicit null values for unused optional fields.
effective_replacements = expected_replacements if expected_replacements is not None else 1
# Validate operation
valid_operations = ["append", "prepend", "find_replace", "replace_section"]
if operation not in valid_operations:
@@ -254,8 +258,8 @@ async def edit_note(
edit_data["section"] = section
if find_text:
edit_data["find_text"] = find_text
if expected_replacements != 1: # Only send if different from default
edit_data["expected_replacements"] = str(expected_replacements)
if effective_replacements != 1: # Only send if different from default
edit_data["expected_replacements"] = str(effective_replacements)
# Call the PATCH endpoint
result = await knowledge_client.patch_entity(entity_id, edit_data, fast=False)
@@ -339,5 +343,5 @@ async def edit_note(
"error": str(e),
}
return _format_error_response(
str(e), operation, identifier, find_text, expected_replacements, active_project.name
str(e), operation, identifier, find_text, effective_replacements, active_project.name
)
+37
View File
@@ -456,3 +456,40 @@ async def test_edit_note_preserves_permalink_when_frontmatter_missing(client, te
assert f"permalink: {test_project.name}/test/test-note" in second_result
assert f"[Session: Using project '{test_project.name}']" in second_result
# The edit should succeed without validation errors
@pytest.mark.asyncio
async def test_edit_note_append_with_null_optional_fields(client, test_project):
"""Regression test for issue #606: null optional fields should be silently ignored.
MCP clients may send explicit null values for unused optional fields (e.g.
find_text=null, section=null, expected_replacements=null). Before this fix,
expected_replacements being typed as `int` caused FastMCP schema validation to
reject null, resulting in an "invalid payload" error even for simple append/prepend
operations that don't use those fields.
"""
# Create initial note
await write_note(
project=test_project.name,
title="Null Fields Test",
directory="test",
content="# Null Fields Test\nOriginal content.",
)
# Simulate an MCP client that sends explicit null for all optional fields.
# expected_replacements=None (null), find_text=None, section=None should all be
# treated as absent rather than rejected.
result = await edit_note(
project=test_project.name,
identifier="test/null-fields-test",
operation="append",
content="\n## Appended Section\nContent added via null-field call.",
find_text=None,
section=None,
expected_replacements=None,
)
assert isinstance(result, str)
assert "Edited note (append)" in result
assert f"project: {test_project.name}" in result
assert "file_path: test/Null Fields Test.md" in result