@mnemoverse/mcp-memory-server
Version:
Hosted AI agent memory over MCP that learns from outcomes — feedback re-ranks recall — with shared rooms for multi-agent work; one key or OAuth across Claude Code, Cursor, VS Code & ChatGPT, no infra to run
180 lines • 9.23 kB
JavaScript
/**
* Request bodies, built by pure functions so the wire contract can be TESTED.
*
* Why this module exists. 0.8.1 is a patch, and its rule is that wording may
* change but where data is written or read from may not. The branch broke that
* rule twice and neither break was caught:
*
* 1. A `trim()` on the way out normalised past a deliberate 400-guard in
* core, so a padded room address would have written into a shared room
* visible to other accounts.
* 2. The revert of that trim dropped `|| undefined` on the read path, so
* `domain: ""` became `WHERE domain = ''` — a store that cannot exist —
* turning a search of every domain into a guaranteed miss.
*
* The guard test at the time grepped src/index.ts for the ABSENCE of a trim.
* That cannot catch a DELETED coercion, which is exactly what shipped: 60 tests
* green with the divergence in place (reviews, 2026-08-08). A denylist over
* source text is not a contract; a function whose output you can compare is.
*
* Rule for changing anything here: if a body changes for ANY input, that is a
* MINOR release, however obviously correct the change looks. The tests pin the
* whole matrix — whitespace, non-breaking and zero-width spaces, padded room
* addresses, "0", ":" — because every one of those was a real finding.
*
* Deliberately NOT normalising: core matches domains byte-for-byte and rejects
* non-canonical room addresses on purpose. Cleaning input here silently moves
* data and defeats a security guard. Normalisation belongs in a future
* release, with room addresses exempt, and with zero-width characters
* handled — `trim()` does not strip those. (memory_delete_domain, once named
* here as a second surface needing the same treatment, was withdrawn
* 2026-08-20 — deletion is administrative-only now.)
*/
// KEYS_URL is imported for refusePlaceholderKey at the bottom of this file:
// a config-only guard that otherwise has nothing to do with request bodies,
// but needs the same console URL a 401 already points at (src/errors.ts) and
// is documented at its own definition rather than here.
import { KEYS_URL } from "./errors.js";
/**
* The scope actually searched: what core receives, and therefore the only value
* any message about the result may describe.
*
* `|| undefined` rather than a null check, because core filters on
* `domain is not None` — an empty string would become a real, impossible
* filter. Everything else passes through untouched.
*/
export function searchedScope(domain) {
return domain || undefined;
}
/** memory_read. The temporal keys are omitted entirely when unused so the body
* stays byte-identical for callers that never pass them (#404). */
export function readRequestBody(a) {
return {
query: a.query,
top_k: a.top_k || 5,
domain: searchedScope(a.domain),
include_associations: true,
...(a.order_by ? { order_by: a.order_by } : {}),
...(a.since ? { since: a.since } : {}),
...(a.until ? { until: a.until } : {}),
...(a.exclude_author ? { exclude_author: a.exclude_author } : {}),
// Sent only when it can change anything: 0 is the engine's own default
// (core#656), and leaving the field out keeps the body byte-identical to
// what a deployment that predates it accepts.
...(a.diversity ? { diversity: a.diversity } : {}),
};
}
/** memory_list_recent. */
export function recentRequestBody(a) {
return {
domain: searchedScope(a.domain),
since: a.since || undefined,
until: a.until || undefined,
exclude_author: a.exclude_author || undefined,
limit: a.limit || 20,
cursor: a.cursor || undefined,
};
}
/**
* memory_graph. `depth` and `limit` are sent with an explicit default (1 and
* 100), the same convention `readRequestBody` uses for `top_k` — this
* server's own default rather than a field left out for the engine's to
* apply, so it stays stated here even though, unlike `top_k`, both currently
* match the engine's own (GraphRequestSchema `depth.default`/`limit.default`).
*
* `min_weight` is `??`, never `||`: `0.0` is a meaningful, contract-honoured
* value (an explicit floor of zero, honoured at every hop with no server
* override — GraphRequestSchema), not a falsy placeholder for "unset". It is
* omitted from the body entirely when the caller passed none, so core applies
* its OWN default (no floor at depth 1; its own 0.05 floor from depth 2) —
* this package does not invent one.
*/
export function graphRequestBody(a) {
return {
seeds: a.seeds,
depth: a.depth ?? 1,
domain: searchedScope(a.domain),
...(a.min_weight === undefined ? {} : { min_weight: a.min_weight }),
limit: a.limit ?? 100,
};
}
/**
* memory_write. `"general"` is the server-side default made explicit, and is
* what 0.8.0 sent — NOT a normalisation of the caller's value.
*
* `author` (STEP4-5): sent verbatim, exactly as the caller's `writeAuthor()`
* dependency returned it, when present: this function does no field-level
* normalisation of it (core re-normalises server-side, see {@link WriteAuthor}).
* Omitted entirely, not sent as `undefined`, when there is none, so every
* existing write body this function ever produced stays byte-identical.
*/
export function writeRequestBody(a, author) {
return {
content: a.content,
concepts: a.concepts || [],
domain: a.domain || "general",
...(author === undefined ? {} : { author }),
};
}
/**
* Refuse a docs placeholder key from CONFIGURATION ALONE, before any tool
* call sends it anywhere.
*
* WHY THIS EXISTS. The engine sees this server calling it again and again
* with the example key from the install snippets. The agent got a 401, got
* this server's generic replace-the-key sentence, and called again. A value this client can recognise as a placeholder WITHOUT any
* request is still worth refusing before the network, exactly as
* `refuseInsecureBaseUrl` (src/index.ts) refuses an insecure
* MNEMOVERSE_API_URL before the network, for the same reason: a config-only
* guard is checked once and costs nothing, while a wasted round trip costs a
* rate-limited request every time an agent cannot help but retry.
*
* FIRES ONLY FOR VALUES THAT ARE CERTAINLY PLACEHOLDERS: two shapes, and
* nothing else, however odd:
*
* (a) mk_live_ followed by an upper-case label (mk_live_YOUR_KEY,
* mk_live_USER_KEY, mk_live_CODING_AGENT_KEY), the same shape the
* engine itself classifies as placeholder_key.
* (b) mk_live_ followed only by the letter x, at least four of them,
* either case (mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx), the
* template value shipped in src/configs/source.json.
*
* A self-hosted static-auth deployment can use an arbitrary key that does
* not start with mk_live_ at all, and a TRUNCATED real key (mk_live_deadbeef,
* say) must still reach the engine, whose answer is more precise than a
* local guess would be. So both a mixed-case label (mk_live_Your_Key) and
* anything shaped like a real key are left alone here on purpose, same as
* every other value this function has never seen.
*
* PURE AND EXPORTED, colocated with the request builders rather than with
* `refuseInsecureBaseUrl` itself, because that one needs a `URL` parse this
* module has no other reason to import (the shape of the guard is the same,
* the machinery differs). `apiFetch` in src/index.ts evaluates this once into
* a module constant next to `BASE_URL_REFUSAL`, and checks both at the same
* two call sites: inside `apiFetch`, and inside the startup probe.
*
* `KEYS_URL` is imported from src/errors.ts rather than repeated here as a
* second literal, the same console page a 401 already points at.
*/
export function refusePlaceholderKey(key) {
const isLabelledPlaceholder = /^mk_live_[A-Z][A-Z_]*$/.test(key);
const isTemplatePlaceholder = /^mk_live_[xX]{4,}$/.test(key);
if (!isLabelledPlaceholder && !isTemplatePlaceholder)
return undefined;
return {
toolCall: "Mnemoverse: this tool did not run, because MNEMOVERSE_API_KEY is " +
"still the example value from the documentation, not a real key. " +
"Nothing was sent: this client refuses the call instead. This is the " +
"user's configuration, not a fault of the key, the network or the " +
`service. Tell them to create a key at ${KEYS_URL}, put it in the ` +
"MCP client config in place of the placeholder, and restart the MCP " +
"server. Do not retry until then; every memory tool will fail the " +
"same way until it is replaced.",
startupLog: "Mnemoverse: startup key check SKIPPED, and nothing was sent. " +
"MNEMOVERSE_API_KEY is still the example value from the " +
`documentation, not a real key. Create one at ${KEYS_URL}, put it in ` +
"the MCP client config in place of the placeholder, and restart. " +
"Every memory tool will fail until it is replaced.",
};
}
//# sourceMappingURL=requests.js.map