@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
170 lines (169 loc) • 8.27 kB
TypeScript
/**
* 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.)
*/
export interface ReadArgs {
query: string;
top_k?: number;
domain?: string;
order_by?: "relevance" | "recency";
since?: string;
until?: string;
exclude_author?: string;
diversity?: number;
}
export interface RecentArgs {
domain?: string;
since?: string;
until?: string;
exclude_author?: string;
limit?: number;
cursor?: string;
}
export interface WriteArgs {
content: string;
concepts?: string[];
domain?: string;
}
export interface GraphArgs {
seeds: string[];
depth?: number;
domain?: string;
min_weight?: number;
limit?: number;
}
/**
* Supplier-vouched authorship for a write (STEP4-5, owner 2026-09-24),
* mirroring core's `ProvenanceWriteSchema` field for field
* (mnemoverse-core src/mnemo/api/schemas.py:123-150): `principal`, `agent`,
* `agent_name`, `client_env` and `is_external`, all optional.
*
* Core honours this ONLY for a SERVICE/supplier caller (`author` in the
* write body) and IGNORES it entirely for an OIDC end-user, who is stamped
* solely from their own verified token (`routes._get_provenance`,
* mnemoverse-core src/mnemo/api/routes.py:211-276), so a caller supplying
* this for a plain end-user key gets no error, just a silently dropped
* value. Core also RE-NORMALISES every field server-side (an allow-list on
* `client_env`, length caps, `is_external` coerced to a real boolean), so
* this package does not re-validate the shape beyond the `typeof` guard at
* the one call site that reads it: that is core's job, not this one's.
*/
export interface WriteAuthor {
principal?: string;
agent?: string;
agent_name?: string;
client_env?: string;
is_external?: boolean;
}
/**
* 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 declare function searchedScope(domain?: string): string | 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 declare function readRequestBody(a: ReadArgs): Record<string, unknown>;
/** memory_list_recent. */
export declare function recentRequestBody(a: RecentArgs): Record<string, unknown>;
/**
* 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 declare function graphRequestBody(a: GraphArgs): Record<string, unknown>;
/**
* 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 declare function writeRequestBody(a: WriteArgs, author?: WriteAuthor): Record<string, unknown>;
/**
* 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 declare function refusePlaceholderKey(key: string): {
toolCall: string;
startupLog: string;
} | undefined;