Skip to content

v11.7.0

Choose a tag to compare

@doobidoo doobidoo released this 05 Sep 07:07
· 193 commits to main since this release

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: false was 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 in memory-client.js — so every HTTPS request these hooks made skipped certificate verification regardless of whether the endpoint needed it. session-end-harvest.js was the one file already doing this right. The bypass is now allowSelfSignedCerts, off by default in config.template.json, threaded through a shared utilities/tls-options.js helper 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 via NODE_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 paired https.Agent({ rejectUnauthorized: false }) applied unconditionally to every HTTPS request with no way back. Both are gone. allowSelfSignedCerts (config field, or OPENCODE_MEMORY_ALLOW_SELF_SIGNED_CERTS) is off by default and threaded through httpsFetch -> requestJson, matching the claude-hooks pattern and the existing correctly-gated opt-in in session-end-harvest.js.

  • fix(examples): gate the HTTP bridge's TLS bypass behind an explicit opt-in. examples/http-mcp-bridge.js built an unconditional insecure https.Agent for every HTTPS request, and went further than the cases #198 and #210 fixed: alongside rejectUnauthorized: false it set requestCert: false (a server-side option that did nothing here) and checkServerIdentity: () => undefined, disabling hostname checking as well — redundant while rejectUnauthorized is false, but it would have survived someone re-enabling verification. Both extras are gone and the bypass now requires MCP_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.json is a shipped package asset, not a personal file: utilities/config-loader.js reads it when ~/.claude/hooks/config.json is absent, and install_hooks.py copies it into the user's home as the base config when no existing MCP install is detected. A real 40-character memoryService.http.apiKey and a maintainer-local serverWorkingDir had been in it since 24af496. The installer's key generation (#531) only mints a fresh key when the value is empty or auto-detect, so every install that took that path adopted the committed key verbatim rather than getting its own. The two values are now auto-detect and null. 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: an apiKey you did not choose yourself should be replaced with one you generate, matching MCP_API_KEY on your server. That matters most if your server binds anything wider than 127.0.0.1.

Added

  • ci: the JS test job now covers opencode/ as well as claude-hooks/. scripts/ci/run_hooks_tests.sh (#202) globbed claude-hooks/tests/*.test.js only, so #210's TLS gate under opencode/ 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 a tls-verification-opt-in.test.js. opencode/memory-plugin.js gains an _internal test export mirroring the claude-hooks convention, and thirteen tests pin the gate — including that only a real true opts in (not "true", "1", or any other truthy value) and that it never touches the process-wide NODE_TLS_REJECT_UNAUTHORIZED switch #210 removed.

  • ci(hooks): gate config.template.json drift from config.json (#201, #204, timkjr). The template lists every config key a fresh install should have; nothing enforced that it stayed in sync with the real config.json, and allowSelfSignedCerts (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.sh now diffs the two files' key sets in CI and in pre_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_DIR makes the ONNX model directory explicit (#171, from #170 reported by @tecnobrat). ONNXRankerModel hardcoded its cache to Path.home() / ".cache" / "mcp_memory" / "onnx_models". In a container that resolved to /root/.cache/... only because the images run as root and set neither USER nor HOME, so mounting pre-exported models depended on an implementation accident — a non-root or read-only-rootfs deployment had to redirect HOME at 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-cpu pull instructions, document the golden path (#171). README.md and tools/docker/README.md both told users to docker pull doobidoo/mcp-memory-service:quality-cpu; that tag was last published as 10-quality-cpu on 2026-05-29 and its build job lived in the GitHub workflow removed in 9c7f8b89. It is not returning as a per-release build — the ONNX models are version-independent, so rebuilding them every patch is waste. Dockerfile.quality-cpu stays as a build-it-yourself option, and the docs now cover the three real paths: export once and mount with MCP_QUALITY_ONNX_MODEL_DIR, build the image yourself, or point MCP_QUALITY_AI_PROVIDER=openai-compatible at an endpoint you already run. Includes @tecnobrat's verified non-root read-only-rootfs Kubernetes recipe, in both the new form and the HOME-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 for nvidia-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 published 10-quality-cpu was 1.26 GB compressed against 164 MB for 10-slim.

  • chore(harvest): hoist rewriter.py's inline stdlib imports (#207). pre_pr_check.sh check 5 flagged three indented imports on every PR that merged main in, because a merge commit puts every merged file back in the index. asyncio and concurrent.futures move to module level — both are already resident in sys.modules once mcp_memory_service.harvest has imported, so hoisting costs nothing measured. httpx stays lazy: it costs about 36ms to import, nothing else on that path needs it, and classifier.py imports this module at top level while harvest/__init__.py imports the classifier eagerly, so hoisting httpx would 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__ stored load_llm_providers() unfiltered, so a credential-less legacy Groq entry — synthesized whenever HARVEST_LLM_PROVIDERS is unset — sat in the list, and _call_llm's if 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 through is_usable_provider at construction, mirroring classifier.py exactly. The six logger.warning calls 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_value instead 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 named result_hash and put that string in the response, so every caller's content_hash field read "Memory stored successfully" and had to re-search by content to find the memory it had just created. The correct value, new_hash from generate_content_hash(), was already computed two lines above; it's what the response now carries, and the misleading variable is renamed to store_message so 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 store keyword on the Milvus backend's store(), count_all_memories(), and search_memories() (#148, closes #133). The #57 Phase 1 partition-key change added store to the concrete backends and to every call site but missed these three Milvus overrides, so memory status/list_memories and store/search paths crashed with count_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 existing get_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 adds milvus to 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