{
  "markdown": "# mcp-assist-memory\n\nA **generic, project-agnostic** memory / coordination / artifact server for\nmulti-agent and multi-surface work. One FastAPI process serves a **30-tool MCP**\nover Streamable HTTP, backed by **Postgres (+ pgvector)**, deployed standalone on\na **Replit Reserved VM**.\n\nThis is **Tier 1** of the [reusability contract](./REUSABILITY.md): every project\nreuses it as-is. It carries **zero domain terms** — project identity lives in\nnamespace *values*, never in tool names, tables, columns, or code.\n\n### Capabilities at a glance\n\n- **30-tool MCP** over Streamable HTTP (memory, handoff, session, artifact,\n  coordination, feedback, admin).\n- **Trust-boundary spine (Plan v2)** — actor-scoped exactly-once writes with\n  visible dedup, read-back-verified acks (`verified_persisted`), standardized\n  error payloads with remedies, write-time screening + quarantine, provenance\n  tiers (`origin`, model attribution, `derived_from` lineage), trust decay\n  (`needs_reverification`), PHI-safe `tool_events` telemetry, per-namespace\n  variant profiles, and an `observation_log` feedback channel.\n- **Namespace-scoped multi-tenancy** — every per-project query filters on `namespace`.\n- **Resilient to transient DB drops** — the server transparently retries genuine\n  connection losses (Neon scale-down / PgBouncer recycle, SQLSTATE `57P01`/`08xxx`)\n  on a fresh pooled connection, and validates connections at checkout, so callers\n  no longer have to retry. Retries are idempotency-gated, so they never double-write.\n- **Prompt-injection resistance layer** — values are sanitized on write (forged\n  markers are escaped one-way to `[[UNTRUSTED_DATA]]`, never reconstructed on read),\n  instruction-shaped writes are screened and quarantined (visible in the write ack;\n  `include_quarantined: true` opts reads back in), and reads come back wrapped in\n  `<<<UNTRUSTED_DATA>>>` markers; `storage.sanitize.unwrap_value` recovers the raw\n  value when a consumer needs it (e.g. to `json.loads`). Honest framing: these are\n  layers, not proofs — deterministic screens and wrappers are bypassable by an\n  adaptive attacker; adversarial evaluation is pending (see Phase 10 backlog in\n  `DECISION-PROTOCOL.md`).\n- **Content-addressed artifacts** (sha256, global dedup), 50 MB cap, ranged reads.\n- **Per-surface rotatable tokens** (web vs. desktop-cli) managed from a password-gated `/admin` dashboard.\n\n## The 30 tools\n\n| Group | Tools |\n|---|---|\n| memory | `memory_save` `memory_get` `memory_list` `memory_history` `memory_delete` `memory_search` |\n| handoff | `handoff_save` `handoff_load` `handoff_list` |\n| session | `session_create` `session_append_event` `session_get` `session_list` `session_events` |\n| artifact | `artifact_put` `artifact_get` `artifact_list` |\n| coordination | `coord_health` `coord_drift_scan` `coord_reconcile` `coord_curate` |\n| gate | `intent_open` `skill_define` `gate_close_outcome` `gate_cache_status` |\n| sequence | `session_bootstrap` `namespace_init` `recall` |\n| feedback | `observation_log` |\n| admin | `stats` |\n\n**Sequences** are the paths an agent should take. Correct use of this server has\nalways been a *sequence* — learn which database answered, resolve the profile,\ncheck what is stale, then act — and that ordering used to live in tool\ndescriptions and skills, i.e. it was enforced by a model remembering advice\nmid-task, and a skipped step produced no signal whatsoever. The three sequence\ntools run their steps server-side in a fixed order and return `steps_run`, so\nthe ordering is something the server did rather than something a caller got\nright. They are strictly additive: every primitive they are built from still\nworks exactly as before, for surgical use.\n\n`recall` and `memory_search` now share ONE retrieval guard\n(`storage/retrieval.py`): the same absolute floor and relative alpha the Intent\nGate applies. Previously `memory_search` had no floor at all while `intent_open`\nhad both, so the same store answered the same question differently depending on\nwhich tool the caller reached for. They still differ in what they do with the\nverdict — `memory_search` annotates every row and drops nothing, `recall`\nfilters by default and reports the rejected counts — but they can no longer\ndisagree about what counts as a match.\n\nThe **Intent Gate** (`claude/intent-gate/INTENT_GATE_CHARTER.md`) is a\nper-namespace opt-in (`variant_profiles.profile.intent_gate: \"on\"`): mutating\ntools gain a deterministic Tier-0 pre-flight (two-phase preview/confirm for\nsupersession + delete, dependency-freshness flags, compact `gate` ack block),\n`intent_open` adds the Tier-1 memory-similarity critic, and Tier 2 (LLM\nreasoning over the direct Anthropic API — never MCP sampling) ships behind\n`tier2: \"on\"` (default off). Default-profile namespaces are byte-identical to\nthe pre-gate server.\n\n`/healthz` (liveness) and the `/admin` token dashboard are served separately (not\nMCP tools).\n\n## Tenancy — namespace is the project boundary\n\n**`namespace` == project == tenant.** One namespace per project (e.g.\n`acme-billing`), with conventional sub-scopes by key prefix (`coord/…`,\n`knowledge/…`). Every per-project tool takes a required `namespace` and **every\nquery filters on it** — there are no implicit cross-project reads. The `session`\nand `session_event` tables carry `namespace` too, so episodic memory is scoped\nlike everything else.\n\n**Artifacts are the deliberate exception:** they are content-addressed (sha256)\nand dedup globally, so they are not tenant-scoped — the hash is the capability.\n\n### Honest limit (and the v2 fix)\n\nThe per-surface tokens scope **which client surface** connects, not **which\nproject** it may touch: any holder of any active token can pass any namespace, so\nnamespace remains a **soft** boundary — real isolation for honest clients, not\nenforced against a misbehaving one.\n\n> **v2 auth roadmap — per-project tokens/roles.** A token scoped to\n> `acme-billing` must not be able to read or write `other-project`. Until then,\n> treat the namespace boundary as a convention enforced by client configuration,\n> not by the server. (See REUSABILITY.md → \"namespace is the tenant boundary\".)\n\n## Auth & the /admin dashboard\n\nMCP tokens are stored in Postgres (`admin_auth_tokens`) and **rotatable from\n`/admin`** without a redeploy. There is **one active token per surface**:\n\n| surface | label | how the client sends it |\n| --- | --- | --- |\n| claude.ai web connector | `web` | `?token=<token>` in the URL (the web connector can't send headers) |\n| Claude Desktop **and** the Claude Code CLI | `desktop-cli` | `Authorization: Bearer <token>` |\n| Cursor | `cursor` | `Authorization: Bearer <token>` (via `.cursor/mcp.json`) |\n\nThe gate accepts **any** active token, so each surface can be **rotated or\nrevoked independently** — rotating `web` never disturbs `desktop-cli` or\n`cursor`. The\n`/admin` page shows one card per surface with a ready-to-paste URL/command and\nits own rotate button.\n\n`MCP_AUTH_TOKEN` seeds the **`web`** token on initial boot (so an existing\nclaude.ai connector keeps working); `desktop-cli` and `cursor` are\nauto-generated. After first boot the dashboard is the source of truth.\n\n- `/admin` is password-gated by **`ADMIN_PASSWORD`** (signed, HttpOnly session\n  cookie, CSRF-protected). Without it the dashboard refuses logins.\n- The only routes not behind the bearer gate are `GET /healthz`, the streamed\n  `GET /artifact/{sha256}`, and `/admin` (which self-authenticates).\n\n**Stateless transport.** `/mcp` runs in stateless HTTP mode\n(`http_app(stateless_http=True)`): every request is self-contained, with no\nin-memory session affinity. Client sessions therefore survive VM\nrestarts/redeploys, and the three surfaces share no server-side session state.\n\n## Architecture\n\n- One `AsyncConnectionPool` created in the FastAPI `lifespan` (`app.py`), injected\n  via `deps`. Nothing else opens a connection. The pool is built with\n  `check=AsyncConnectionPool.check_connection`, so a connection terminated\n  server-side while idle is validated and discarded on checkout, never handed to a\n  caller.\n- One `config.py` (`pydantic-settings`) — the **only** place secrets are read.\n- `StorageBackend` ABC (`storage/base.py`) implemented by `PostgresBackend`; the\n  18 tools map 1:1 onto it.\n- **Transparent reconnect:** reads and idempotent writes retry on a connection\n  drop (`_retry_on_disconnect`), so e.g. `OperationalError: terminating connection\n  due to administrator command` (SQLSTATE 57P01, Neon scale-down / PgBouncer) is\n  retried on a fresh pooled connection instead of surfacing to the caller. Only\n  genuine disconnects (`08xxx` / `57P0x` / an already-closed connection) are\n  retried — other operational errors (lock timeout, too-many-connections) surface\n  unchanged. Writes retry only when a replay is safe: `artifact_put`\n  (content-addressed) always, and `memory_save`/`handoff_save`/`memory_delete`\n  only when given an `event_id` (exactly-once). The `session_*` writes also retry,\n  with an explicit tradeoff: a drop in the narrow commit-ack window means\n  `session_create` may orphan an empty, unreferenced session row, and\n  `session_append_event` is **at-least-once** (a replay can append one duplicate\n  event). For an append-only session log that beats failing the call outright; a\n  save with no `event_id` still runs once and surfaces the error.\n- Write-path `sanitize` strips forged delimiters/control chars; reads wrap values\n  in `<<<UNTRUSTED_DATA>>>` markers (lethal-trifecta defense). **Note:** the\n  `value` (and session-event `payload`) fields come back wrapped, so a consumer\n  that needs the raw value — e.g. to `json.loads` a value that was a JSON string —\n  must strip the markers first. Use `storage.sanitize.unwrap_value` (or\n  `strip_untrusted` for a single string); the wrapping stays applied on every read.\n- Bounded lifespan readiness (no unbounded `pool.wait()`), 50 MB artifact cap,\n  ranged blob reads, idempotent `event_id` writes, idempotent blob backfill.\n- **Semantic recall (`memory_search`):** when a `VOYAGE_API_KEY` is set, every\n  `memory_save` embeds the entry (Voyage, `voyage-3.5-lite`, 1024-dim) into a\n  nullable `embedding vector(1024)` column with an HNSW cosine index, and\n  `memory_search` ranks live entries by meaning (`embedding <=> query`), then\n  backfills keyword/substring matches up to `limit`. Embedding is **best-effort**:\n  it runs before a connection is taken and never blocks (or fails) a write, and\n  with no key the column stays NULL and search degrades to pure keyword — the\n  pre-Phase-3 behavior. Every leg filters on `namespace` first (no cross-project\n  recall). After enabling a key on an existing DB, embed old rows once with\n  `python scripts/backfill_embeddings.py` (idempotent, only touches NULL rows).\n- **Recall tuning (`hnsw.ef_search`):** the HNSW index is approximate, so a larger\n  store can miss relevant hits unless its query-time recall parameter is tuned.\n  `memory_search` sets `hnsw.ef_search` per-statement (transaction-local, semantic\n  leg only) from `HNSW_EF_SEARCH` (default `100`; pgvector's own default is `40`).\n  Higher = better recall, slightly slower search; it must be `>=` the search limit\n  to take effect. Small stores return the same rows regardless, so the default is\n  safe to leave alone — raise it (e.g. `200`) if a large tenant reports missing\n  results, lower it toward `40` to shave latency. Very large tenants can also tune\n  the index **build** parameters (`m`, `ef_construction`); see\n  `migrations/0002_embeddings.sql` (changing those requires recreating the index).\n- **Coordination reconciler (`coord_reconcile`):** when GitHub access is available,\n  a `claim` (with `meta.repo` + `meta.pr`/`meta.branch`) is resolved against live\n  GitHub — is PR #N merged? what is branch X's head? — and stamped with an\n  append-only verdict; without access it stays `unverifiable` (never a wrong\n  `current`). Access is sourced in priority order: an explicit `GITHUB_TOKEN`\n  (read-only repo + PRs), else — on Replit — the **connected GitHub account** via\n  the Replit connector (token fetched fresh per cache-window so it survives OAuth\n  refresh), else disabled. Resolution is **best-effort**: a network/API failure\n  yields `unverifiable`, never a blocked write. `GITHUB_WEBHOOK_SECRET` enables\n  `POST /webhook/github` to reconcile affected claims on push / pull_request.\n- **Memory curator (`coord_curate`):** when `ANTHROPIC_API_KEY` is set, a finished\n  session can be consolidated write-side: `coord_curate(namespace, session_id)` reads\n  the session's execution trace plus similar existing memories, asks the model what is\n  worth persisting, and applies the resulting `ADD`/`UPDATE`/`MERGE`/`SUPERSEDE`/`NOOP`\n  operations deterministically. Every op passes a fail-closed PHI gate first, claims\n  without provenance (`meta.repo` + `meta.pr`/`branch`) are downgraded to notes,\n  supersession sets a validity boundary (history is kept, never deleted), and writes are\n  idempotent (deterministic `event_id`) so re-running a session never double-writes. It\n  is **best-effort**: without the key the curator is disabled and `coord_curate` is a\n  clean no-op (`{curator_enabled: false, operations: []}`), and any model/parse failure\n  yields zero operations — never a wrong write. `dry_run=True` returns the proposed\n  operations without writing. `CURATOR_MODEL` and `CURATOR_MAX_OUTPUT_TOKENS` tune it.\n  Curated rows also carry a second `hyde_embedding`, so `memory_search` can match a\n  future *question* (HyDE leg) as well as the stored statement.\n\n## Run locally\n\n```bash\ncp .env.example .env          # DATABASE_URL + MCP_AUTH_TOKEN + ADMIN_PASSWORD\nmake install                  # pip install -c constraints.txt -e \".[test]\"\nmake migrate                  # apply migrations/0001_init.sql\nmake run                      # uvicorn app:app\ncurl localhost:8000/healthz   # {\"status\":\"ok\",\"db\":\"ok\"}\n# token: open http://localhost:8000/admin and sign in with ADMIN_PASSWORD\n```\n\n## Tests (real Postgres)\n\nThe suite runs against a **real** Postgres and skips cleanly if `DATABASE_URL` is\nunset. The neutral test project is **`proj-test`** (never a real project name).\n\n```bash\nDATABASE_URL=... make test\n```\n\nCI (`.github/workflows/test.yml`) spins an **ephemeral Neon branch** per run,\nmigrates it, runs `pytest`, and deletes the branch. Set repo secrets\n`NEON_API_KEY` and `NEON_PROJECT_ID` to enable it.\n\n## Smoke test (the connector handshake)\n\n`scripts/smoke_mcp.py` performs the exact handshake a Claude connector does —\n`initialize` + `tools/list` over `/mcp` with a valid token — and asserts HTTP 200\nwith the full **30-tool** surface, plus the guard rails (no/bad token ⇒ 401,\n`/healthz` db ok). It exists so a transport/auth/host regression (like the\nfastmcp 3.4.3 421) can never ship silently again.\n\n- **Blocks a bad build:** the in-process half runs in CI via `pytest`\n  (`tests/test_smoke_mcp.py`) against the ephemeral Neon branch — a broken\n  handshake, gate, or tool count fails the build before it can deploy.\n- **Flags an unhealthy live deploy:** run it against the deployed URL after a\n  deploy. It exits non-zero on any failed check:\n\n  ```bash\n  SMOKE_BASE_URL=https://<your-vm> SMOKE_TOKEN=<active token> make smoke\n  ```\n\n  `.github/workflows/smoke.yml` runs this against a live URL on manual dispatch\n  (or a 6-hour schedule); set repo secrets `SMOKE_BASE_URL` and `SMOKE_TOKEN` to\n  enable it (it no-ops cleanly when unset).\n\n## Deploy on Replit (Reserved VM)\n\n1. In **Secrets**, set `DATABASE_URL` (pooled endpoint), `MCP_AUTH_TOKEN`, and\n   `ADMIN_PASSWORD` (plus optional Phase-3 keys).\n2. Deploy as a **Reserved VM** (`deploymentTarget = \"vm\"`) — *not* Autoscale;\n   the durability gate needs the process to persist.\n3. The deploy `run` step runs `python scripts/migrate.py` then starts uvicorn.\n4. Open `https://<your-vm>/admin`, sign in, and copy/rotate the token. Point each\n   client at `https://<your-vm>/mcp`.\n\n### Pinned dependencies (deterministic builds)\n\n`pyproject.toml` declares loose ranges, but the deploy build, `post-merge.sh`, and\n`make install` all pass `-c constraints.txt`, so **prod installs the exact versions\nverified in dev**. This is the guardrail against the class of failure that caused the\nprod `/mcp` 421 outage (an unpinned build silently resolving a newer `fastmcp`).\n\nTo **intentionally upgrade** a dependency (so pins don't rot):\n\n```bash\npip install -U <pkg>          # or `pip install -e .` to re-resolve a widened range\nmake test                     # AND exercise /mcp locally\nmake lock                     # regenerate constraints.txt from the verified env\n# then redeploy — prod now installs the newly verified set\n```\n\nNever hand-edit versions in `constraints.txt`; always regenerate with `make lock`\n(`scripts/lock-deps.sh`). The file header documents the same procedure.\n\n### Postgres / Neon\n\nUse the **pooled** connection string for the running service; psycopg is\nconfigured with `prepare_threshold=None` for PgBouncer transaction pooling. Tests\nand `scripts/migrate.py` use a **direct** endpoint (the test pool keeps prepared\nstatements on).\n\n## Boundary\n\nThis repo is **Tier 1 only**. Canvas-specific MCP tools, FHIR logic, and SDK\nknowledge live in the separate `canvas-sdk-tools` repo — never here.\n\n## Blob migration (filesystem → bytea)\n\n```bash\npython scripts/backfill_artifacts.py /path/to/old/blobstore\n```\nIdempotent (dedup by sha256), streams each file, skips/reports anything over the\n50 MB cap, and verifies a random sample by checksum readback.\n",
  "bytes": 17536,
  "sha": "4985c0008be9cf82d39d82f5779c4bf29c497a64f1c98a5a71317d0ca3d29c79",
  "repo_slug": "aliomranih/mcp-assist-memory",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_aliomranih_mcp_assist_memory_057d5c3d/readme"
}