Repository navigation
Description
Bug Description
The MCP recall and reflect tools take tags_match: str = "any" in hindsight_api/mcp_tools.py, in both the bank-id and the session-bank copy of each. FastMCP accepts any string, and _parse_tags_match falls back to "any" for anything it doesn't recognise. "any" also admits untagged memories. So a mistyped strict mode quietly widens a recall meant to stay inside one tag, and no error comes back.
The HTTP endpoint rejects the same value with a 422, and the MCP list_memories tool already types the parameter as TagsMatch.
Two things make this easy to hit from an agent:
- The tool schema shows
tags_matchas{"type": "string", "default": "any"}, with no enum. - Both tools' docstrings say
'any' (match any tag) or 'all' (match all tags). They never nameany_strict,all_strictorexact, though the MCP docs page lists all five. An agent that wants "only this tag" has to guess the spelling.
Steps to Reproduce
On main (9269b88), with a scratch server from hindsight-system-tests (embedded Postgres, stub LLM) and the tagged_bank fixture from test_06_tag_filtering.py (one memory tagged typescript, one music, one untagged):
Call MCP recall with query "What do we know about Alice?", tags: ["typescript"], and each tags_match below.
| transport | tags_match |
result |
|---|---|---|
MCP recall |
any_strict |
TypeScript fact only |
MCP recall |
any-strict |
TypeScript fact + untagged fact, no error |
MCP recall |
strict |
TypeScript fact + untagged fact, no error |
MCP recall |
all_strict |
TypeScript fact only |
MCP recall |
allstrict |
TypeScript fact + untagged fact, no error |
HTTP POST .../memories/recall |
any-strict |
422 Input should be 'any', 'all', 'any_strict', 'all_strict' or 'exact' |
Expected Behavior
An unknown tags_match is rejected over MCP, the same way HTTP rejects it, and the tool schema and description list the five valid modes.
Actual Behavior
An unknown value runs as any, so untagged memories come back in a recall meant to be tag-scoped. Nothing in the response shows the mode was ignored.
Version
main @ 9269b88
Possible fix
Type the four signatures as tags_match: TagsMatch = "any", as list_memories already does, and describe all five modes in the docstrings. FastMCP then rejects an unknown mode before the engine is called. I have this on a branch with tests (12 tests; 10 fail on main, all pass with the change), in case it's useful: https://github.com/JayOfTheKeyboard/hindsight/tree/fix/mcp-recall-reflect-tags-match
Two related things I noticed but didn't measure:
GET /v1/default/banks/{bank_id}/documentsand the graph endpoint also taketags_match: str, so by reading the code a typo there falls through toanytoo.recallregisters a fixedDEFAULT_MCP_RECALL_DESCRIPTION, so its Args docstring, including themax_tokenswording from fix(mcp): say what recall's max_tokens counts, and return compact JSON #4870, never reaches MCP clients.
Activity
koriyoshi2041 commentedon Oct 7, 2026
Confirmed the widening one layer below MCP on exact
main(9269b8841): both unknown spellings produce the same SQL predicate asany, whileany_strictexcludes untagged rows.The existing focused tag semantics tests are green (7/7), so this is the parser's deliberate fallback rather than a query-builder regression.
One scope detail for a team-owned fix: the four MCP signatures should be typed as proposed, but
GET /v1/default/banks/{bank_id}/documentsindependently still declarestags_match: strand passes it to the same builder. That endpoint should useTagsMatchtoo; otherwise MCP becomes strict while a typo on document listing continues to widen. I could not reproduce the same issue on the graph routes: their public query parameters are already typed asTagsMatchon current main.Focused checks:
JayOfTheKeyboard commentedon Oct 10, 2026
Thanks @koriyoshi2041 for confirming this on main and for finding the documents endpoint.
I've rebased the fix branch onto current main and added two commits:
GET /v1/default/banks/{bank_id}/documentsnow typestags_matchasTagsMatch, soany-strictorstrictgets a 422 instead of a 200 that includes untagged documents: JayOfTheKeyboard@b32c4a1GET /v1/default/banks/{bank_id}/graphhad the same problem. On main it still declarestags_match: str(defaultall_strict), andget_graph_datahas a comment noting that an unknown mode falls through toany. You're right that/entities/graphis typed, but the memory graph route isn't. It now gets a 422 too: JayOfTheKeyboard@5d3e721Each has a test that fails on main with the untagged row in the response and passes with the fix. The OpenAPI spec and the clients are regenerated, and the control plane's documents proxy narrows the value for the new TypeScript type.
I also checked the other places
tags_matchcomes in as a plain string. The mental model MCP tools validate it before use,list_mental_modelsandlist_directivesuse a Literal, and the Python client wrapper passes it through to routes that now validate it. So I don't think anything else widens.Branch: https://github.com/JayOfTheKeyboard/hindsight/tree/fix/mcp-recall-reflect-tags-match
koriyoshi2041 commentedon Oct 10, 2026
Checked the rebased branch at
5d3e7219. The widened scope is now closed at each public entry point I could identify:recall/reflect: invalid modes fail validation before the engine; schemas expose all five modes.GET /documentsandGET /graph: invalid modes return 422; validany_strictstill excludes untagged documents.git diff --checkis clean.Focused results:
The HTTP suite initially hit my local SOCKS environment while loading the embedding model; rerunning without proxy env completed cleanly. I don't see another plain-string public ingress left that reaches the permissive parser.
fix(tags): reject an unknown tags_match instead of widening it to "an…