UNPKG

@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
/** * 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