Releases: joshdougall/memory-mcp
Release list
v1.2.2
ANNOTATION SOURCE CHECK: if these notes appear on the release, the fix works.
Fixes the release-notes job added in 1.2.1, which published the squash commit
message instead of the annotated tag message.
actions/checkout fetches the commit SHA straight onto the tag ref with
--no-tags, so refs/tags/ is a lightweight tag and the annotated object
never arrives. fetch-depth 0 does not help, because it changes the depth and
not the refspec. %(contents:body) on a lightweight tag silently returns the
commit body, which is wrong but non-empty, so the previous check never noticed
and never reached its own fallback.
The job now fetches refs/tags/:refs/tags/ explicitly to force the
real object, branches on git cat-file -t rather than on emptiness so a
genuinely lightweight tag falls through to generated notes, and logs the
object type so a recurrence is visible rather than silent.
No functional change to the server. 1.2.1 and 1.2.2 are CI only; the server
is byte-identical to 1.2.0. 49 tests passing, lint clean.
v1.2.1
Makes releases self-recording.
release.yml built and pushed the image on every version tag but never created
a GitHub release entry, so the Releases page stopped at v1.0.1 while v1.1.0,
v1.1.1, v1.1.2 and v1.2.0 had all shipped to GHCR. The repository read as
abandoned since April, and 'gh release list' became a false negative for
whether a version had shipped, which caused at least one wrong conclusion
during development.
A github-release job now reads the annotated tag message, which is where this
project keeps its notes, falling back to generated notes for a lightweight
tag. It is idempotent, so re-running a workflow cannot fail on a duplicate,
and it is gated on the image build: a release entry pointing at a version with
no published image would be worse than no entry at all.
The four missing entries were backfilled by hand from their tag annotations.
No functional change to the server. 49 tests passing, lint clean.
v1.2.0
Operational hardening.
- Graceful shutdown: the HTTP server and the per-request MCP transports are
closed on SIGTERM/SIGINT instead of being dropped. - Timing-safe bearer token comparison, via crypto.timingSafeEqual rather
than ===. - release.yml now gates the image build on lint and tests. Previously a tag
built and published without running either. - Container healthcheck in docker-compose for the /health endpoint, using
node rather than curl since node:22-slim has no curl. - The server version is read from package.json instead of being hardcoded,
which fixes real drift: a deployment was observed reporting serverInfo
1.0.0 while running the 1.0.1 image. - MAX_ENTRIES_WARN default raised from 300 to 1000, matching what real
deployments were already overriding it to.
Also fixes the test configuration: 'timeout' is not a vitest option and was
silently ignored, so the suite had always run under the 5s default instead
of the intended 30s. It is now testTimeout, with hookTimeout raised
alongside it. Agent worktrees under .claude/ are excluded from test
discovery, so a bare npm test no longer runs one copy of the suite per
worktree against the same Valkey.
49 tests passing, lint clean.
v1.1.2
Makes memory_get atomic.
memory_get read the entry, checked it existed, then incremented hits in a
separate round trip. If the entry was deleted or its TTL expired in that
gap, HINCRBY recreated the missing key, leaving a mem: hash with only
hits, no title and no TTL. Because existence is defined by the title field
everywhere else, such a key was invisible to memory_get, memory_list,
memory_delete and prune_candidates, while the mem:* scan still counted it
toward memory_entries_total and the soft-cap warning. Nothing in the API
could remove it.
memory_get is now a single Lua EVAL that returns not_found when there is no
title and only increments otherwise, closing the window by construction and
dropping the call from two round trips to one.
The response also reports the value HINCRBY returned rather than
synthesising read + 1, which was wrong under concurrent reads: ten
simultaneous callers all reported hits 1 while the stored counter reached
ten.
Present in 1.0.0 through 1.1.1. Tracked as Forgejo issue #1.
47 tests passing, lint clean.
v1.1.1
Fixes 10 findings from a post-merge review of the if_version/operation_id
work in 1.1.0.
- every mutation (set, rollback, delete) is a single Lua EVAL sharing one
set of index and snapshot helpers; no JS-side index or snapshot writers - memrev and memver now share a TTL entry's lifetime: they expire with it,
and persist together when the TTL is removed. Delete still persists both,
so the tombstone and the ABA counter outlive any expiry. - reusing an operation_id for a different memory id returns
operation_id_mismatch instead of silently replaying the other result - memory_get surfaces read errors instead of reporting revision 0 from a
failed read - replayed writes carry the soft-cap warning
- tags validated: non-empty, no commas, which the CSV wire format cannot
represent
Verified on the merged tree: 44 tests passing, lint clean.
v1.1.0
Adds optional compare-and-set (if_version) and write idempotency
(operation_id) to memory_set. Both default to off, so existing callers
are unaffected.
The memory_set write path moves from a non-atomic ioredis pipeline to a
single Lua EVAL. Besides making the compare-and-set a guarantee rather
than a race, this fixes a pre-existing bug where concurrent writes
interleaved index maintenance and left an entry in tag index sets it no
longer belonged to. Upgrading stops new corruption but does not clean
existing stale memberships.
Adds the memory_conflict_total metric and
MEMORY_MCP_OPERATION_ID_TTL_SECONDS.
v1.0.1
Bug fixes and hardening.
Changes
- Fix: Cross-project entries (
project: "") now indexed and searchable - Fix: TTL refreshed on update via
expire/persist; was previously only set on create - Fix: Health check pings Valkey — returns 503 if disconnected instead of always 200
- Fix: Request body capped at 1MB (configurable via
MAX_BODY_BYTES) - Fix:
VALKEY_URLcredentials masked in log output - Fix:
entryToObjectnow returnstagsas a parsed array - Fix:
KEYS *replaced withSCANthroughout (non-blocking) - Harden: Dockerfile runs as
nodeuser, usesnpm ci --omit=dev - Harden:
package-lock.jsoncommitted for reproducible builds - Add: 14 integration tests (Vitest + MCP SDK client)
- Add: CI workflow — lint, tests, multi-arch Docker build on every PR and push to main
- Add: Branch protection on
main
Docker image
ghcr.io/joshdougall/memory-mcp:1.0.1
ghcr.io/joshdougall/memory-mcp:latest
v1.0.0
Initial release.
What's included
- Valkey-backed MCP memory server with 8 tools:
memory_search,memory_get,memory_set,memory_list,memory_delete,memory_history,memory_rollback,memory_prune_candidates - Tag-intersection search, type/project filtering, substring search
- Hit tracking and version history with rollback
- Optional bearer token auth (
AUTH_TOKEN) - Prometheus metrics at
/metrics - Docker Compose with multi-arch images (
linux/amd64,linux/arm64)
Quick start
cp .env.example .env
docker compose up -d
claude mcp add memory --transport http http://127.0.0.1:3106/mcpCopy AGENTS.md into your project for agent instructions.
Docker image
ghcr.io/joshdougall/memory-mcp:1.0.0
ghcr.io/joshdougall/memory-mcp:latest