Summary
The MCP transport documents X-hal0-Private: 1 as the way to scope memory_add writes to the caller's private:<client_id> namespace. In practice, writes still land in the shared dataset.
Reproduction
Initialize an MCP session against /mcp/memory/mcp with X-hal0-Private: 1 and no bearer (so client_id == "anonymous"), then call:
{
"name": "memory_add",
"arguments": {
"args": {"text": "private-test", "dataset": ""}
}
}
Inspect the resulting record: dataset == "shared", not "private:anonymous". The behaviour is identical with X-hal0-Private: 1 and without.
Confirmed across two records:
ca49afd2-... — sent with X-hal0-Private: 1
d724a86d-... — sent without
Both landed in shared. (Cleaned up after testing.)
Suggested fix area
/opt/hal0/src/hal0/api/mcp_mount.py:MCPAuthMiddleware parses the header into the request _caller contextvar, but
- by the time
/opt/hal0/src/hal0/memory/cognee_wrapper.py:_effective_write_dataset is called inside the FastMCP tool handler, the contextvar appears to have been dropped — likely because the Starlette BaseHTTPMiddleware task context doesn't propagate to the MCP session's anyio task group.
The fix is either to attach caller.private to the MCP session state explicitly during initialize, or to read the header out of request.headers directly in the tool handler.
Workaround
Always pass dataset: "private:hermes-agent" (or whatever your agent's namespace is) explicitly in every memory_add call. We've encoded this in our hermes config as a force_write_dataset per-server hint and documented it as a hard convention.
Environment
- hal0 v0.3.0a1, CT 105
- Discovered while wiring hermes-agent's
hal0-memory MCP client
Summary
The MCP transport documents
X-hal0-Private: 1as the way to scopememory_addwrites to the caller'sprivate:<client_id>namespace. In practice, writes still land in theshareddataset.Reproduction
Initialize an MCP session against
/mcp/memory/mcpwithX-hal0-Private: 1and no bearer (soclient_id == "anonymous"), then call:{ "name": "memory_add", "arguments": { "args": {"text": "private-test", "dataset": ""} } }Inspect the resulting record:
dataset == "shared", not"private:anonymous". The behaviour is identical withX-hal0-Private: 1and without.Confirmed across two records:
ca49afd2-...— sent withX-hal0-Private: 1d724a86d-...— sent withoutBoth landed in
shared. (Cleaned up after testing.)Suggested fix area
/opt/hal0/src/hal0/api/mcp_mount.py:MCPAuthMiddlewareparses the header into the request_callercontextvar, but/opt/hal0/src/hal0/memory/cognee_wrapper.py:_effective_write_datasetis called inside the FastMCP tool handler, the contextvar appears to have been dropped — likely because the StarletteBaseHTTPMiddlewaretask context doesn't propagate to the MCP session's anyio task group.The fix is either to attach
caller.privateto the MCP session state explicitly duringinitialize, or to read the header out ofrequest.headersdirectly in the tool handler.Workaround
Always pass
dataset: "private:hermes-agent"(or whatever your agent's namespace is) explicitly in everymemory_addcall. We've encoded this in our hermes config as aforce_write_datasetper-server hint and documented it as a hard convention.Environment
hal0-memoryMCP client