diff --git a/src/basic_memory/mcp/tools/write_note.py b/src/basic_memory/mcp/tools/write_note.py index cb8bd172..3019fc67 100644 --- a/src/basic_memory/mcp/tools/write_note.py +++ b/src/basic_memory/mcp/tools/write_note.py @@ -35,11 +35,10 @@ async def write_note( tags: list[str] | str | None = None, note_type: str = "note", metadata: Annotated[dict | None, BeforeValidator(coerce_dict)] = None, - # Force/replace are the file-write idioms models default to. - overwrite: Annotated[ - bool | None, - Field(default=None, validation_alias=AliasChoices("overwrite", "force", "replace")), - ] = None, + # Simple bool so FastMCP generates a correct boolean schema for external clients. + # AliasChoices caused external clients (e.g. Claude Code) to receive a broken schema + # where overwrite appeared as type null-only, silently dropping the value (issue #818). + overwrite: bool | None = None, output_format: Literal["text", "json"] = "text", context: Context | None = None, ) -> str | dict: diff --git a/test-int/mcp/test_param_aliases_integration.py b/test-int/mcp/test_param_aliases_integration.py index d3500235..e3a4763b 100644 --- a/test-int/mcp/test_param_aliases_integration.py +++ b/test-int/mcp/test_param_aliases_integration.py @@ -310,31 +310,37 @@ async def test_write_note_accepts_directory_aliases(mcp_server, app, test_projec @pytest.mark.asyncio -async def test_write_note_accepts_overwrite_aliases(mcp_server, app, test_project): - """`force`/`replace` should map to `overwrite`.""" +async def test_write_note_overwrite_canonical_via_mcp(mcp_server, app, test_project): + """Canonical overwrite=True must reach the function body when sent via MCP protocol. + + Regression test for issue #818: PR #766 introduced AliasChoices on the overwrite + parameter which caused external MCP clients (e.g. Claude Code) to receive a broken + JSON schema where overwrite appeared as type null-only. The boolean value was silently + dropped, so the note could never be replaced even with overwrite=True. + """ async with Client(mcp_server) as client: # First create await client.call_tool( "write_note", { "project": test_project.name, - "title": "Overwrite Alias Note", + "title": "Overwrite Canonical Note", "directory": "overwrite-test", "content": "v1", }, ) - # Overwrite using `force` alias + # Overwrite using the canonical overwrite parameter result = await client.call_tool( "write_note", { "project": test_project.name, - "title": "Overwrite Alias Note", + "title": "Overwrite Canonical Note", "directory": "overwrite-test", "content": "v2", - "force": True, # alias for overwrite + "overwrite": True, }, ) - assert "Updated note" in result.content[0].text or "Created note" in result.content[0].text + assert "Updated note" in result.content[0].text # --- move_note aliases ---