Upgrade if you run OAuth with open registration (the Claude.ai connector setup). v12.10.0 closes GHSA-w8p9-5v6q-h9ch, reported by @hgrgr, whom I thank for the report. Please read the behaviour change and the cleanup step in the Security section below.
Security
- The OAuth authorize page did not say who was asking, and could hand out
admin(GHSA-w8p9-5v6q-h9ch, reported by @hgrgr).
WithMCP_DCR_REGISTRATION_KEYunset, anyone could register a client with their own
redirect URI and send the owner a link to the owner's own authorize page. The page asked
for the API key and showed nothing else, so a third party's request looked exactly like a
Claude.ai reconnect, and the resulting token could carryread write admin. The page now
shows the redirect host, the self-reported application name (unverified) and the scope
that will be granted, it cannot be framed, and redirect URIs containing backslashes,
whitespace or control characters are refused. Upgrade if you run OAuth with open
registration, which is the Claude.ai connector setup.
Behaviour change: theadminscope is granted only to a client that declaredadmin
in a registration gated byMCP_DCR_REGISTRATION_KEY. A client that obtainedadmin
through open registration, or through a gated registration that named noscope, now
getsread write; to get it back, register again with the registration key and
"scope": "read write admin". A refresh that asks only foradminon such a client is
refused withinvalid_scope.
Cleanup: a client registered with anadminceiling while registration was open keeps
that ceiling in the store and can holdadminagain if the key is set later. Review with
memory oauth list-clientsand remove what you do not recognise with
memory oauth delete-client <client_id>.
Added
- Semantic dedup can supersede an updated fact instead of dropping it (#1938).
A changed fact is often phrased almost exactly like the one it replaces, so the
default dedup check (cosine 0.85, 24 h) refused the write and left the old value
as the only one retrieval could return.MCP_SEMANTIC_DEDUP_MODE=supersede
stores that update and marks the matched memorysuperseded_bythe new hash,
the same lineagememory_updatewrites. A near-identical match (cosine at or
aboveMCP_SEMANTIC_DEDUP_NEAR_IDENTICAL_THRESHOLD, default 0.97, or the same
text once whitespace and case are collapsed) is still rejected. The default
mode staysreject, so nothing changes until the setting is opted in. Store
responses name the outcome:deduplicatedwith the existing hash, or
supersededwith the old hash, on the service, REST and MCP paths. - The dashboard's Manage tab shows one section at a time (#1973, shu-chen-dot-ca).
Bulk Operations, Tag Management and Ontology were stacked on one page, so on a store
with many tags the Ontology section sat below the whole tag table. The tab now has
sub-tabs (Bulk Operations, Tags, Ontology) that show only the chosen section, work
with the arrow, Home and End keys and a screen reader, and are translated in all
eleven catalogs. Opening the Manage tab starts on Bulk Operations. - The browser demo offers a prompt that lets a coding agent set up a trial (#1985).
The demo at mcpmemory.services/demo/ ended with an install command. It now also shows a
prompt for Claude Code, as visible text with a copy button: the agent asks before it
acts, installs the service into one new folder, proves a store and search round trip
withmemory status --deep, and registers the server asmemory-trialwith its own
MCP_MEMORY_BASE_DIRandMCP_MEMORY_SQLITE_PATH, so an existing installation and its database are not touched. - The browser demo reports what it is doing in one status capsule, and the agent prompt opens in Claude Code (#1999).
The capsule at the top of the demo card shows the real start-up steps, each store and
each search, and keeps an error with a retry on screen until it is handled. The model
download used to show "100%" from the first chunk when the server sent no
Content-Length; a percentage now needs a real total. A failed store or search no longer
leaves its button disabled, and Enter no longer stores a memory several times. The trial
prompt gains "Open in Claude Code" (claude://code/new?q=, built from the visible text)
and a copy-and-open entry for ChatGPT. - The dashboard shows its connection state in a status capsule in the header (#2000).
The "Connected" badge was a fixed overlay in the bottom right corner that covered content
on narrow screens, said "Connected" before the event stream was open, and showed
"Unknown" when the server refused the stream. It is replaced by a capsule in the logo
row (web/static/status-island.js) that reports connecting, connected, disconnected with
a reconnect action, and "Authentication required". The capsule keeps one entry per named
source and shows the most important one, an error before work in progress before a
confirmation, so further sources (sync, imports, consolidation, backup) can report to it
without covering each other's failures. It adds no catalog keys.
Fixed
- The dashboard's type-only search filter always failed with "Filter search failed" (#1971).
Choosing a content type with no query, tag or date askedGET /api/memoriesfor
page_size=1000, above the route's cap of 100, so the request was a 422. It now passes
the type to the server and pages through the result 100 at a time, so a type with more
than 100 memories is listed in full. - The Memory Types chart heading was cut short in German, Spanish, French, Japanese and Korean (#1993).
Those five catalogs definedanalytics.memoryTypestwice. The second entry, a short
"Memory Types" label left over from the Knowledge Graph translations, came later in the
file, andJSON.parsekeeps the last of two equal keys, so the dashboard showed it
instead of "Memory Types Distribution". The leftover entry is gone, and
tests/web/test_i18n_catalog_parity.pynow fails on any key a catalog defines twice.
It replaces five tests that asserted the duplicate was still there.
Internal
- A search summarization test passed only where anonymous access was switched on (#2014).
test_http_mcp_round_trip_with_read_scopeoverroderequire_read_access, butPOST /mcp
depends onget_current_userand checks the scope per tool call. The request was therefore
authenticated by the environment: as anonymous "read write" in CI, which sets
MCP_ALLOW_ANONYMOUS_ACCESS=true, and not at all elsewhere, where eight cases failed with
authorization_requiredand tookscripts/pr/pre_pr_check.shdown for any change under
src/. The test now overrides the dependency the route uses, so it runs with the read-only
user its name promises, in every environment. auto-assignalone opts an issue into/claim, and a claim written in prose is told the command (#2015).
Automatic assignment neededgood first issueas well, so a bug with a settled scope stayed
on the manual path. On #1998 a contributor waited three hours for a go-ahead and someone
else opened a pull request in that window.scripts/ci/mark_claim_pending.pynow takes the
auto-assignlabel as the maintainer's approval on any issue. A prose claim on such an
issue gets one reply per person naming the command; prose still does not assign. The claim
matcher also accepts "this one", "this issue", a trailing "too" or "as well" and "I would
like to take this", which is how the two claims on #1997 and #1998 were worded and why
neither was labelledclaim-pending.config/oauth.pyandconfig/ontology.pylog with lazy%-style arguments (#1146).
TheValueErrorthatvalidate_oauth_configuration()raises at import time
carriesMCP_OAUTH_ISSUERand a key path verbatim, and the base-type names
parsed fromMCP_CUSTOM_MEMORY_TYPESare logged as given: both are wrapped in
_sanitize_log_value. The JWT algorithm and the three counts go as%s/%d
arguments. Both modules are added toGUARDED_MODULES.storage/milvus_graph.pyandmodels/ontology.pylog without bare outside values (#1146).
Theexcof theexcept Exceptionblocks around the pymilvus calls, and the
caller-supplied relationship type logged when it fails validation, are wrapped
in_sanitize_log_value, as the entity-link blocks of the same module already
did; the hop list infind_shortest_pathis renamed so the guard no longer
reads it as a file path. The f-strings inload_custom_memory_typesbecome
lazy%s/%darguments with the same text. Both modules are added to
GUARDED_MODULES.web/oauth/discovery.pyandweb/oauth/storage/factory.pylog with sanitised lazy%-style arguments (#1146).
oauth_authorization_server_metadata()andcreate_oauth_storage()now pass
_sanitize_log_value()to%sinstead of f-strings, and both modules are added
toGUARDED_MODULES.consolidation/quarantine.pyandconsolidation/relationship_inference.pylog without bare outside values (#1146).
Thecontent_hash[:8]prefix in the quarantine, unquarantine and background
check messages goes through_sanitize_log_value, asstorage/hybrid.py
already does for the same slice; the belief-challenged message and the four
confidence/threshold messages of the relationship inferrer become lazy
%s/%.2farguments with the same text. Both modules are added to
GUARDED_MODULES.- Clear the remaining five
Check 6.5f-string logger modules and guardself.loggercalls (#1146).
config/documents.py,consolidation/associations.py,storage/migration_runner.py,
sync/litestream_config.py, andutils/directory_ingestion.pynow use lazy%-style
logger arguments with_sanitize_log_value()around migration names, error messages,
config paths, and ingested filenames. All five joinGUARDED_MODULES, and
test_log_injection_guard.pynow inspectsself.logger.<level>()calls alongside
module-levelloggerandlogging.
Full changelog: https://github.com/doobidoo/mcp-memory-service/blob/v12.10.0/CHANGELOG.md