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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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