v11.7.0
Special thanks to timkjr for five PRs this cycle — the biggest single contribution to this release — and to @tecnobrat for the reports behind the whole quality/docker cluster (#170 and its follow-ups) plus production measurements that corrected two false claims in the Docker docs, and to @ghulands for the precise Milvus report.
MINOR release, though in substance a security release: three unconditional TLS certificate-verification bypasses — claude-hooks, the opencode plugin, and examples/http-mcp-bridge.js — are now gated behind an explicit allowSelfSignedCerts opt-in instead of running by default, and a real credential committed in the bundled hooks config is gone, with a CI gate that keeps it from coming back. Default endpoints for all three are http://127.0.0.1:8000, so most installs are unaffected, but if you point any of them at https:// with a self-signed certificate you now need to set the opt-in (MCP_MEMORY_ALLOW_SELF_SIGNED_CERTS, OPENCODE_MEMORY_ALLOW_SELF_SIGNED_CERTS, or allowSelfSignedCerts in the hooks config) — the old silent bypass is gone. Also in this release: a configurable ONNX model directory for containers that don't run as root, the Milvus backend's store() signature brought back in line with every other backend, and the consolidation merge action finally returning the hash it always should have. The Claude Code plugin manifest goes to 1.0.3 to ship the eight hook files that changed since 1.0.2.
Security
-
fix(hooks): gate claude-hooks' TLS verification bypass behind explicit opt-in (#198, timkjr).
rejectUnauthorized: falsewas hardcoded and unconditional across seven call sites in five files —session-end.js,memory-retrieval.js,topic-change.js,dynamic-context-updater.js, and three inmemory-client.js— so every HTTPS request these hooks made skipped certificate verification regardless of whether the endpoint needed it.session-end-harvest.jswas the one file already doing this right. The bypass is nowallowSelfSignedCerts, off by default inconfig.template.json, threaded through a sharedutilities/tls-options.jshelper instead of five copies of the same check, and it logs a warning on every use. Anyone pointed at a properly issued certificate (or an internal CA viaNODE_EXTRA_CA_CERTS) now gets real verification; self-signed setups opt in on purpose. -
fix(opencode): gate the opencode plugin's TLS bypass behind explicit opt-in (#210, timkjr). Same shape as #198, in a different plugin:
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"disabled certificate verification for every TLS connection the opencode process made, not only plugin requests — worse than a per-request bypass, since it reached anything else running in that process — and a pairedhttps.Agent({ rejectUnauthorized: false })applied unconditionally to every HTTPS request with no way back. Both are gone.allowSelfSignedCerts(config field, orOPENCODE_MEMORY_ALLOW_SELF_SIGNED_CERTS) is off by default and threaded throughhttpsFetch->requestJson, matching the claude-hooks pattern and the existing correctly-gated opt-in insession-end-harvest.js. -
fix(examples): gate the HTTP bridge's TLS bypass behind an explicit opt-in.
examples/http-mcp-bridge.jsbuilt an unconditional insecurehttps.Agentfor every HTTPS request, and went further than the cases #198 and #210 fixed: alongsiderejectUnauthorized: falseit setrequestCert: false(a server-side option that did nothing here) andcheckServerIdentity: () => undefined, disabling hostname checking as well — redundant whilerejectUnauthorizedis false, but it would have survived someone re-enabling verification. Both extras are gone and the bypass now requiresMCP_MEMORY_ALLOW_SELF_SIGNED_CERTS=true|1, warning on every request that uses it. The agent is still constructed either way, so connection behaviour (keepAlive: false) is unchanged; only verification is. This is example code rather than a shipped path, but examples get copied, and this one was missed by the repo-wide TLS audit in #210. -
fix(hooks): remove the credential and local path committed in the bundled hooks config (#197, reported by timkjr).
claude-hooks/config.jsonis a shipped package asset, not a personal file:utilities/config-loader.jsreads it when~/.claude/hooks/config.jsonis absent, andinstall_hooks.pycopies it into the user's home as the base config when no existing MCP install is detected. A real 40-charactermemoryService.http.apiKeyand a maintainer-localserverWorkingDirhad been in it since 24af496. The installer's key generation (#531) only mints a fresh key when the value is empty orauto-detect, so every install that took that path adopted the committed key verbatim rather than getting its own. The two values are nowauto-detectandnull. The committed key was already out of use on the maintainer's side and only ever authenticated against a loopback-bound server, so nothing needed revoking — but that is not the point: the shipped default meant an unknown number of installs share one publicly readable token as their memory service credential. A new CI gate,scripts/ci/check_hooks_config_secrets.sh, fails the build when the bundled config carries anything that looks like a credential or an absolute local path, so this cannot come back. If you installed the hooks before this release, check~/.claude/hooks/config.json: anapiKeyyou did not choose yourself should be replaced with one you generate, matchingMCP_API_KEYon your server. That matters most if your server binds anything wider than127.0.0.1.
Added
-
ci: the JS test job now covers
opencode/as well asclaude-hooks/.scripts/ci/run_hooks_tests.sh(#202) globbedclaude-hooks/tests/*.test.jsonly, so #210's TLS gate underopencode/shipped untested — asking for a test there would have produced a file that never executed. The runner takes a list of directories, checks each one separately so emptying a suite cannot hide behind another still having tests, and prints paths rather than basenames now that two directories both hold atls-verification-opt-in.test.js.opencode/memory-plugin.jsgains an_internaltest export mirroring theclaude-hooksconvention, and thirteen tests pin the gate — including that only a realtrueopts in (not"true","1", or any other truthy value) and that it never touches the process-wideNODE_TLS_REJECT_UNAUTHORIZEDswitch #210 removed. -
ci(hooks): gate
config.template.jsondrift fromconfig.json(#201, #204, timkjr). The template lists every config key a fresh install should have; nothing enforced that it stayed in sync with the realconfig.json, andallowSelfSignedCerts(added in #198) was one of several keys the template hadn't caught up on. #201's original PR landed against a feature branch instead of main, so the sync sat unmerged until #204 re-targeted it at main and added the test coverage.scripts/ci/check_hooks_config_drift.shnow diffs the two files' key sets in CI and inpre_pr_check.sh, so a new hook config key without a matching template entry fails the build instead of surfacing later as a support question. -
feat(quality):
MCP_QUALITY_ONNX_MODEL_DIRmakes the ONNX model directory explicit (#171, from #170 reported by @tecnobrat).ONNXRankerModelhardcoded its cache toPath.home() / ".cache" / "mcp_memory" / "onnx_models". In a container that resolved to/root/.cache/...only because the images run as root and set neitherUSERnorHOME, so mounting pre-exported models depended on an implementation accident — a non-root or read-only-rootfs deployment had to redirectHOMEat a writable volume just to place them. The new variable replaces that parent directory (the loader still appends/<model_name>, so one setting covers every model) and defaults to the previous path, so nothing moves for existing installs.
Changed
-
docs(docker): retire the
:quality-cpupull instructions, document the golden path (#171).README.mdandtools/docker/README.mdboth told users todocker pull doobidoo/mcp-memory-service:quality-cpu; that tag was last published as10-quality-cpuon 2026-05-29 and its build job lived in the GitHub workflow removed in9c7f8b89. It is not returning as a per-release build — the ONNX models are version-independent, so rebuilding them every patch is waste.Dockerfile.quality-cpustays as a build-it-yourself option, and the docs now cover the three real paths: export once and mount withMCP_QUALITY_ONNX_MODEL_DIR, build the image yourself, or pointMCP_QUALITY_AI_PROVIDER=openai-compatibleat an endpoint you already run. Includes @tecnobrat's verified non-root read-only-rootfs Kubernetes recipe, in both the new form and theHOME-redirect form that works on current releases. Two things the old docs got wrong, both caught by @tecnobrat's production measurements: the quantization note no longer implies a size win — int8 measured 0.28-0.39 correlation against fp32 fornvidia-quality-classifier-deberta, so the 0.98 gate rejects it and the build keeps fp32 at roughly 736 MB — and the tags table's "~600 MB larger than:slim" was wrong; the last published10-quality-cpuwas 1.26 GB compressed against 164 MB for10-slim. -
chore(harvest): hoist
rewriter.py's inline stdlib imports (#207).pre_pr_check.shcheck 5 flagged three indented imports on every PR that merged main in, because a merge commit puts every merged file back in the index.asyncioandconcurrent.futuresmove to module level — both are already resident insys.modulesoncemcp_memory_service.harvesthas imported, so hoisting costs nothing measured.httpxstays lazy: it costs about 36ms to import, nothing else on that path needs it, andclassifier.pyimports this module at top level whileharvest/__init__.pyimports the classifier eagerly, so hoistinghttpxwould add that cost to every harvest import, including ones that never call an LLM.
Fixed
-
fix(harvest): pin the rewriter's provider chain the same way #194 pinned the classifier's (#199, timkjr).
HarvestRewriter.__init__storedload_llm_providers()unfiltered, so a credential-less legacy Groq entry — synthesized wheneverHARVEST_LLM_PROVIDERSis unset — sat in the list, and_call_llm'sif self._providers:check treated it as usable. Every rewrite on an unconfigured chain made a doomed call to Groq's real endpoint instead of falling through correctly. The list is now filtered throughis_usable_providerat construction, mirroringclassifier.pyexactly. The sixlogger.warningcalls in the file also get the same log-injection sanitizing #194 gave the classifier — provider names and exception text (which can carry a remote response body) now go through lazy%-formatting with_sanitize_log_valueinstead of landing in an f-string. -
fix(consolidate): return the merged memory's real content hash from the merge action, not
store()'s status message (#208, closes #112).BaseStorage.store()returns(success, message)— the second element is prose, not a hash. The merge handler unpacked it into a variable namedresult_hashand put that string in the response, so every caller'scontent_hashfield read"Memory stored successfully"and had to re-search by content to find the memory it had just created. The correct value,new_hashfromgenerate_content_hash(), was already computed two lines above; it's what the response now carries, and the misleading variable is renamed tostore_messageso the mistake can't recur by copy-paste. The regression test's mock had matched the bug — it returned a hash-shaped string where real backends return prose — and now returns what sqlite_vec, Cloudflare, and hybrid actually return. -
fix(milvus): restore the multi-store
storekeyword on the Milvus backend'sstore(),count_all_memories(), andsearch_memories()(#148, closes #133). The #57 Phase 1 partition-key change addedstoreto the concrete backends and to every call site but missed these three Milvus overrides, somemory status/list_memoriesand store/search paths crashed withcount_all_memories() got an unexpected keyword argument 'store'on the Milvus backend. The Milvus backend is single-collection and does not partition by store, so the argument is accepted for interface parity and ignored (matching the existingget_all_memories).The abstract declaration had drifted the other way:
MemoryStorage.store()never got the parameter either, while all four concrete backends accepted it and four call sites passed it (services/memory_service.py,server/handlers/documents.py,utils/document_processing.py), so a backend written against the interface would have reproduced this bug on day one. The interface now declares it. The new backend signature-conformance test covers the abstract class as well as the implementations — checking only the implementations is why the drift survived — so the multi-store contract cannot silently diverge per backend again. Also addsmilvusto the bug-report storage-backend dropdown. Thanks to @ghulands for the precise report.
Upgrading
pip install --upgrade mcp-memory-service
Docker images are published for this tag: 11.7.0, 11.7, 11, latest and the matching -slim variants.
If you point the hooks, the opencode plugin, or examples/http-mcp-bridge.js at an https:// endpoint with a self-signed certificate, they will now refuse to connect until you opt in. That is the intended change — the old behaviour skipped certificate verification silently. Set allowSelfSignedCerts in the hooks config, or OPENCODE_MEMORY_ALLOW_SELF_SIGNED_CERTS / MCP_MEMORY_ALLOW_SELF_SIGNED_CERTS for the other two. Default endpoints are http://127.0.0.1:8000, so most installs are unaffected.
Plugin users should update the plugin so the Marketplace picks up 1.0.3 — that bump is what delivers the eight changed hook files, including the TLS gate.
If you installed the hooks before this release, check ~/.claude/hooks/config.json: an apiKey you did not choose yourself should be replaced with one you generate, matching MCP_API_KEY on your server.
Full changelog: https://codeberg.org/doobidoo/mcp-memory-service/src/branch/main/CHANGELOG.md