Skip to content

Registered tool signatures drift from handler schemas: remember lacks supersedes_id/write_class, wiki_write lacks memory_ids — promotion loop cannot be closed over MCP #98

Description

@mbe14

Environment: Cortex plugin 4.13.1, Windows 11, Claude Code as MCP client (validates tool calls against the declared input schema; additionalProperties violations are rejected client-side).

Summary

The client-visible input schemas for remember and wiki_write are derived by FastMCP from the registered wrapper signatures — and those signatures have drifted from the handlers' own schemas and from the instructions your grooming planners emit. Net effect: the documented lesson-promotion loop cannot be closed over MCP, and get_grooming_health reports promotion: last_run=null, stale=true forever, even after every candidate has been processed.

The drift, file by file

1. remember — missing supersedes_id and write_class

  • Registered signature (mcp_server/tool_registry_memory.py:93-101, and the rooted variant at :65-72):

    async def tool_remember(
        content: str,
        tags: list[str] | None = None,
        directory: str | None = None,
        domain: str | None = None,
        source: str | None = None,
        force: bool = False,
        agent_topic: str | None = None,
    ) -> dict:

    No supersedes_id, no write_class. Your own comment at tool_registry_memory.py:58-61 states that FastMCP derives the input schema from this signature — so clients never see these params.

  • Internal handler schema does declare both: mcp_server/handlers/remember_schema.py:182 (write_class, enum auto/deliberate/derived/mechanical) and :226 (supersedes_id).

  • Even if a permissive client passed them, the wrapper forwards a hand-built dict (tool_registry_memory.py:103-115) containing only content/tags/directory/domain/source/force/agent_topic — the params are silently dropped before the handler.

  • Meanwhile lesson_promotion's instructions tell the in-session LLM to call remember(content=..., supersedes_id=<memory_id>, tags=[..., 'promoted:<kind>'], write_class='deliberate', force=true) to close the loop, and curate_distill's job prompts require write_class='deliberate'. Neither call is possible.

2. wiki_write — missing memory_ids

  • Registered signature (mcp_server/tool_registry_wiki.py:58-63): path, content, mode, tags. The forwarded dict (:67-72) also omits memory_ids.
  • The handler declares memory_ids in its schema (mcp_server/handlers/wiki_write.py:132), reads it (:310), and _sync_page_and_cite (:173) writes the wiki.citations rows from it — this is the documented path by which a curate_wiki job's memory cluster becomes durable provenance. Unreachable over MCP: citations_written is always 0.

3. (minor, same family) curate_distill's job prompts instruct source='distillation', but remember_schema.py:175-181 restricts source to enum [session, tool, user, consolidation, import]. The registered signature (free string) happens to accept it — the three definitions disagree three ways.

Observed impact (live, 4.13.1)

Processed a full grooming pass by hand: all curate_wiki jobs authored, curate_distill dossiers distilled/skipped, all 3 lesson_promotion candidates resolved (one promoted to a wiki page). Afterwards get_grooming_health still reports:

"promotion": {"backlog_count": 3, "last_run_at": null, "stale": true}

because none of the loop-closing writes (wiki_write(memory_ids=...), remember(supersedes_id=...)) can be expressed through the declared schemas.

Suggested fix

  1. Add the missing parameters to the registered signatures and forward them: supersedes_id: int | None = None, write_class: str | None = None on tool_remember; memory_ids: list[int] | None = None on tool_wiki_write.
  2. Longer term: generate the registered signatures (or at least a parity test) from the handlers' schema["inputSchema"]["properties"], so a param added to a handler schema cannot silently miss the MCP surface again. A one-assertion test — set(handler schema properties) ⊆ set(wrapper signature params) per tool — would have caught all three of these.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions