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

169 lines (168 loc) • 8.24 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.) */ export interface ReadArgs { query: string; top_k?: number; domain?: string; order_by?: "relevance" | "recency"; since?: string; until?: string; exclude_author?: string; } 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;