Skip to content

config: ~/.memtomem/config.json silently overrides MEMTOMEM_* env vars, contradicting all .mcp.json docs #248

@memtomem

Description

@memtomem

Summary

~/.memtomem/config.json (written by mm init) silently overrides any MEMTOMEM_* env var at server startup. Every integration doc we ship tells users to configure per-client settings like MEMTOMEM_INDEXING__MEMORY_DIRS / MEMTOMEM_STORAGE__SQLITE_PATH in an .mcp.json / claude_desktop_config.json / mcp_config.json env block — but for any field persisted to config.json by the init wizard, those env entries are ignored. Users get the wizard-saved value regardless.

This is the opposite of the convention most pydantic-settings-based tools follow (env wins over file) and is undocumented.

Repro

  1. mm init → wizard writes ~/.memtomem/config.json with indexing.memory_dirs = ["/tmp/memories"] (or whatever the user picked).
  2. Register the server with a per-project env override, e.g.:
    // .mcp.json
    {
      "mcpServers": {
        "memtomem": {
          "command": "uvx",
          "args": ["--from", "memtomem", "memtomem-server"],
          "env": {
            "MEMTOMEM_INDEXING__MEMORY_DIRS": "[\"/Users/me/Drive/memtomem-memories\"]"
          }
        }
      }
    }
  3. Restart the MCP client, call mem_status → the server reports /tmp/memories, not the Drive path. The env override was silently dropped.

Root cause

Startup sequence — packages/memtomem/src/memtomem/server/component_factory.py:41-47:

config = config or Mem2MemConfig()      # (1) pydantic-settings binds MEMTOMEM_* env
load_config_overrides(config)           # (2) ~/.memtomem/config.json setattr()s on top
ensure_auto_discovered_dirs(config)     # (3) append-only auto-discovery

load_config_overrides (packages/memtomem/src/memtomem/config.py:542-588) iterates every section/key in config.json and unconditionally setattr(section_obj, key, value), so any field present in the file clobbers the env-var-bound value. There is no "env wins" path.

Impact — docs vs reality

Env-var overrides in MCP env blocks are the documented way to configure per-client paths. Pages that currently promise this works:

  • docs/guides/mcp-clients.md — Cursor / Windsurf / Claude Desktop / Gemini CLI / Antigravity sections all show MEMTOMEM_INDEXING__MEMORY_DIRS examples
  • docs/guides/integrations/claude-desktop.md:41-62 — both macOS and Windows blocks
  • docs/guides/integrations/claude-code.md (after docs(mcp): document Claude Code install scopes (local/project/user) #247) — .mcp.json example
  • docs/guides/getting-started.md:180,195
  • docs/guides/cloud-sync.md:8 — explicitly tells users to point MEMTOMEM_INDEXING__MEMORY_DIRS at a cloud-synced folder
  • docs/guides/use-cases.md:54,74

For any user who ran mm init first (the recommended path), none of these examples work as documented for the fields the wizard persists (memory_dirs, sqlite_path, embedding.*, etc.).

Two real user scenarios this breaks:

  • Per-project memory_dirs: users with multiple MCP clients pointed at the same memtomem-server with different .mcp.json env blocks — all collapse to the wizard's single path.
  • Cloud-sync onboarding (cloud-sync.md): the doc tells users to set MEMTOMEM_INDEXING__MEMORY_DIRS to their Drive folder; post-mm init this is a no-op.

Separately but related: the bare-string-vs-JSON-array gotcha for list[Path] env vars (mcp-clients.md:274-278) becomes a secondary footgun — users who notice their env has no effect will try to "fix" it by unquoting the array, and then get a pydantic-settings crash instead of the same silent override.

Options for resolution

Not prescribing a fix — listing the alternatives for discussion:

  1. Invert precedence: env wins over config.json. Most intuitive, matches pydantic-settings default semantics. Breaking change for anyone currently relying on config.json as the authoritative source after a mm config set followed by a stale env var — but in practice that combo seems rare (env vars in .mcp.json are long-lived; users don't typically leave stale ones around).
  2. Only persist wizard fields to config.json if no matching env var is set — shifts the precedence decision to write-time instead of load-time. Less invasive at runtime but makes mm init non-deterministic (depends on env at wizard time).
  3. Merge semantics for list fields (memory_dirs = env ∪ config.json). Solves the per-project case but adds a new mental model users have to learn. Doesn't help scalar fields like sqlite_path.
  4. Keep current semantics, document them loudly. Update every MEMTOMEM_* env-var example in docs with a "⚠ only takes effect if this key is absent from ~/.memtomem/config.json" warning, and add a mm config unset <key> CLI for users who want to fall back to env. Minimal code change, maximal docs churn.
  5. Config isolation by scope — support MEMTOMEM_CONFIG_PATH env var so per-project .mcp.json can point at a project-local config.json. Enables real per-project configs but grows surface area.

Any of 1–3 also needs a migration / release-notes entry since mm init-driven setups would change behavior.

Acceptance criteria (once a direction is picked)

  • Behavior matches what docs claim (or docs are updated to match behavior, explicitly and per-page)
  • tests/test_config.py (or a new test) pins the chosen precedence so it can't silently flip again
  • mcp-clients.md / cloud-sync.md / integrations/*.md / getting-started.md examples are verified end-to-end against the chosen behavior before PR merge (docs-as-tests)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdocumentationImprovements or additions to documentation

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions