Skip to content

Commit 2514015

Browse files
fix(mcp): restore write_note overwrite schema for external clients (#818)
AliasChoices on optional bool | None made FastMCP emit a null-only JSON schema, so Claude Code and other external MCP clients dropped overwrite=true. Revert to a plain parameter; directory aliases unchanged. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: rudi193-cmd <rudi193@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 60ec672 commit 2514015

2 files changed

Lines changed: 34 additions & 13 deletions

File tree

src/basic_memory/mcp/tools/write_note.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,7 @@ async def write_note(
3535
tags: list[str] | str | None = None,
3636
note_type: str = "note",
3737
metadata: Annotated[dict | None, BeforeValidator(coerce_dict)] = None,
38-
# Force/replace are the file-write idioms models default to.
39-
overwrite: Annotated[
40-
bool | None,
41-
Field(default=None, validation_alias=AliasChoices("overwrite", "force", "replace")),
42-
] = None,
38+
overwrite: bool | None = None,
4339
output_format: Literal["text", "json"] = "text",
4440
context: Context | None = None,
4541
) -> str | dict:

test-int/mcp/test_param_aliases_integration.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -310,31 +310,40 @@ async def test_write_note_accepts_directory_aliases(mcp_server, app, test_projec
310310

311311

312312
@pytest.mark.asyncio
313-
async def test_write_note_accepts_overwrite_aliases(mcp_server, app, test_project):
314-
"""`force`/`replace` should map to `overwrite`."""
313+
async def test_write_note_overwrite_canonical_via_mcp(mcp_server, app, test_project):
314+
"""Canonical overwrite=True must reach the handler (#818 regression)."""
315315
async with Client(mcp_server) as client:
316-
# First create
317316
await client.call_tool(
318317
"write_note",
319318
{
320319
"project": test_project.name,
321-
"title": "Overwrite Alias Note",
320+
"title": "Overwrite Canonical Note",
322321
"directory": "overwrite-test",
323322
"content": "v1",
324323
},
325324
)
326-
# Overwrite using `force` alias
325+
blocked = await client.call_tool(
326+
"write_note",
327+
{
328+
"project": test_project.name,
329+
"title": "Overwrite Canonical Note",
330+
"directory": "overwrite-test",
331+
"content": "v2",
332+
},
333+
)
334+
assert "# Error: Note already exists" in blocked.content[0].text
335+
327336
result = await client.call_tool(
328337
"write_note",
329338
{
330339
"project": test_project.name,
331-
"title": "Overwrite Alias Note",
340+
"title": "Overwrite Canonical Note",
332341
"directory": "overwrite-test",
333342
"content": "v2",
334-
"force": True, # alias for overwrite
343+
"overwrite": True,
335344
},
336345
)
337-
assert "Updated note" in result.content[0].text or "Created note" in result.content[0].text
346+
assert "# Updated note" in result.content[0].text
338347

339348

340349
# --- move_note aliases ---
@@ -538,3 +547,19 @@ async def test_aliases_not_advertised_in_schema(mcp_server, app):
538547
assert canonical in props, f"{tool_name}: canonical '{canonical}' missing"
539548
for alias in must_not_have:
540549
assert alias not in props, f"{tool_name}: alias '{alias}' leaked into schema"
550+
551+
# #818: AliasChoices on optional bool broke external-client JSON schema (null-only).
552+
overwrite_schema = tools["write_note"].inputSchema["properties"]["overwrite"]
553+
schema_types: set[str] = set()
554+
if "type" in overwrite_schema:
555+
raw = overwrite_schema["type"]
556+
if isinstance(raw, str):
557+
schema_types.add(raw)
558+
else:
559+
schema_types.update(raw)
560+
for option in overwrite_schema.get("anyOf", ()):
561+
if "type" in option:
562+
schema_types.add(option["type"])
563+
assert "boolean" in schema_types, (
564+
f"write_note overwrite must expose boolean in schema, got {overwrite_schema}"
565+
)

0 commit comments

Comments
 (0)