From 29b9d755e215e89c77df862e9a54ec36bc0d2a8d Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 01:14:37 +0000 Subject: [PATCH] fix(mcp): restore overwrite param schema for external MCP clients (#818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AliasChoices on `overwrite: bool | None` in write_note caused FastMCP to generate a broken JSON schema where external clients (e.g. Claude Code) saw the parameter as type null-only. The boolean value was silently dropped, so `overwrite=True` arrived as None at the function body. Revert overwrite to a plain `bool | None = None` parameter. FastMCP now generates the correct `anyOf: [boolean, null]` schema, and external clients can send `overwrite=true` as intended. The `directory` alias (folder/dir/path) is unaffected — AliasChoices on a required `str` parameter does not exhibit this schema generation issue. Add regression test: test_write_note_overwrite_canonical_via_mcp verifies that overwrite=True reaches the function body through the MCP protocol. Co-authored-by: Paul Hernandez Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- src/basic_memory/mcp/tools/write_note.py | 9 ++++----- .../mcp/test_param_aliases_integration.py | 20 ++++++++++++------- 2 files changed, 17 insertions(+), 12 deletions(-) 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 ---