Skip to content

Stamp source_client server-side from MCP clientInfo — explicit MCP saves are surface-indistinguishable (NULL provenance) #274

Description

@cipher813

Stamp source_client server-side from MCP clientInfo — explicit MCP saves are surface-indistinguishable (NULL provenance)

Problem

documents.source_client is populated only when the caller passes it: the hooks pass claude-code-hook, the mirror passes mnemon-mirror, the CLI passes cli — but a plain memory_save MCP tool call leaves it NULL. Live-vault audit 2026-07-18: of the 100 most recent decision docs, 59 are NULL — meaning an explicit save from claude.ai chat, Claude Desktop, Cursor, or a Claude Code session are mutually indistinguishable. This made a real surface-coverage investigation ("has any claude.ai chat session ever written to mnemon?") unanswerable from the data.

Fix (the robust one — no delta)

Derive provenance at the transport layer instead of trusting an optional model-supplied param:

  1. In the MCP server path, capture the client identity from the initialize handshake's clientInfo.name/version (FastMCP exposes the session/context object to tool handlers) and/or, on the remote HTTP path, the authenticated OAuth client identity (auth.py — DCR-registered client) as a fallback.
  2. In memory_save (src/mnemon/server.py) and the profile-save path (source_client="mcp-profile" today), when the caller does NOT pass source_client, stamp the derived value (e.g. mcp:claude-ai, mcp:claude-code, mcp:cursor). An explicitly passed value still wins (hooks/mirror/CLI keep their existing tags; the transitional Profile-snippet source_client: "claude-ai" from claude.ai chat surface never writes to mnemon — decide autonomous-save posture (Profile snippet) or document explicit-only #273 keeps working).
  3. Backward compatible: pure column-value change, no schema migration; existing NULL rows stay NULL (optionally note in the issue-closing comment that history before the cutover is unattributable).

Acceptance criteria (closes when)

  • A memory_save call from a claude.ai chat session with no explicit source_client lands with a non-NULL derived value distinguishable from a Claude Code MCP session's, verified via memory_timeline.
  • Unit test covering: explicit param wins; derived stamp applied when absent; graceful None when handshake metadata is unavailable (stdio clients that omit clientInfo).
  • README.md tools table / docstring for memory_save updated to describe the derivation.

Anchors

  • src/mnemon/server.pymemory_save (source_client param), profile_update (source_client="mcp-profile").
  • src/mnemon/store.pyDocument.source_client, HOOK_SOURCE_CLIENTS (search-layer boost keys on these values — verify the new mcp:* values do NOT accidentally join the hook-boost set).
  • src/mnemon/server_remote.py / auth.py — remote HTTP session + OAuth client identity for the fallback path.
  • src/mnemon/api.py memory_save — REST path should get the same default-stamping treatment (or an explicit rest default).

Gotchas

  • search.py Layer 4 demotes results whose source_client is in HOOK_SOURCE_CLIENTS — new derived values must be chosen so explicit saves keep full ranking weight.
  • clientInfo.name strings are client-controlled free text; normalize (lowercase, prefix mcp:) and never branch security behavior on them — provenance/observability only.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions