"""Unknown tool arguments are rejected, never silently dropped (#2709). The failure this pins: FastMCP validates tool arguments with a pydantic model whose extra-field policy is "ignore", so `create_note(content=...)` — a plausible near-miss for `body=`, primed by add_task_log's `content` — ran successfully, stored `body: ""`, and left a record embedding/search cannot see. The call REPORTED SUCCESS. Two real notes were persisted body-less before the pattern was noticed. The fix is at the dispatch seam, not per-tool: StrictArgsFastMCP rejects any call carrying arguments the tool does not declare, with a did-you-mean when one is close. An error the caller sees once beats data half-written forever. """ import pytest from scribe.mcp.server import StrictArgsFastMCP def _echo_server() -> StrictArgsFastMCP: mcp = StrictArgsFastMCP("strict-test") @mcp.tool() def echo(text: str = "") -> str: return text return mcp async def test_declared_arguments_still_dispatch(): mcp = _echo_server() result = await mcp.call_tool("echo", {"text": "hi"}) assert "hi" in str(result) async def test_unknown_argument_is_an_error_not_a_silent_drop(): """The load-bearing property: the call must FAIL, because succeeding is what turned a typo into data loss.""" mcp = _echo_server() with pytest.raises(ValueError) as exc: await mcp.call_tool("echo", {"txt": "hi"}) msg = str(exc.value) assert "'txt'" in msg assert "did you mean 'text'?" in msg # difflib near-miss hint assert "Nothing was created or changed" in msg async def test_empty_arguments_pass(): mcp = _echo_server() assert await mcp.call_tool("echo", {}) is not None async def test_unknown_tool_keeps_the_upstream_error(): """The gate must not swallow or reshape 'no such tool' — that error path belongs to FastMCP and clients already understand it.""" mcp = _echo_server() with pytest.raises(Exception) as exc: await mcp.call_tool("no_such_tool", {"text": "hi"}) assert "no_such_tool" in str(exc.value) async def test_the_original_regression_create_note_with_content(): """Pin #2709 itself against the REAL server: `content=` on create_note must raise before dispatch — naming the bad argument and listing `body` among the accepted ones — instead of creating a body-less note.""" from scribe.mcp.server import build_mcp_server mcp = build_mcp_server() assert isinstance(mcp, StrictArgsFastMCP) # the guard is actually mounted with pytest.raises(ValueError) as exc: await mcp.call_tool( "create_note", {"title": "t", "content": "the body text"} ) msg = str(exc.value) assert "'content'" in msg assert "body" in msg