@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
128 lines (127 loc) • 8.53 kB
TypeScript
/**
* Teaching surface — the strings and branch logic that teach a connected model
* how to USE this memory, kept in their own module so tests can import them
* without booting the stdio server (src/index.ts starts a transport on import).
*
* Product frame, first set 2026-08-01 and revised 2026-09-29: the guidance is
* never gated on the user explicitly asking, but it is also never a push. The
* Claude connector directory rejects descriptions that "tell Claude how to
* behave" or "direct Claude to pull behavioral instructions from external
* sources", and its submission form asks the publisher to confirm that "tool
* descriptions contain no instructions about model behavior, other tools, or
* external instruction sources". So this surface says what each tool does and
* when it applies, and proactivity lives in rules the user adds to their own
* agent: the setup_memory prompt (src/prompts.ts) renders them for CLAUDE.md,
* AGENTS.md, Cursor rules or a chat's preferences, and the host plugins carry
* the same skill. Honesty constraints stay: no over-claiming, and the
* never-store-secrets safety line is kept on write surfaces.
*/
import type { ReadScope } from "./scope.js";
/**
* Server-level instructions, passed to the McpServer constructor and landed
* verbatim in the connected model's system prompt by MCP clients.
*
* Constraints (tested in test/teaching-surface.test.ts):
* - 500–800 chars; some clients (ChatGPT) weight the FIRST ~512 chars, so the
* core habit (read before answering, write on learning) is front-loaded.
* - No brakes ("only when the user explicitly asks") and no pushes
* ("proactively", "always", "don't wait to be asked", "as a habit"):
* test/descriptions.test.ts bans both on every advertised surface.
* - Names the setup_memory prompt, the only pointer to guidance, and it points
* inside this server, never to a URL (2026-09-29).
* - Mentions every tool family: read/write/stats/feedback, rooms, vault.
* - No delete tool on this surface (2026-08-20): deletion was withdrawn to an
* administrative REST-only operation, so the instructions point a model at
* writing a corrected memory instead of asking to delete one.
*
* 2026-09-29 rewrite (793 chars): the ownership and habit framing and "don't
* wait to be asked" are gone (see the product frame above); the text says what
* each tool family is for, and ends on the setup_memory prompt. To fit it under
* the cap, the room-tool sentence ("Rooms: memory_create_room,
* memory_invite_to_room, memory_join_room") and "deletion is administrative"
* were dropped: the room rule itself
* and "write a fresh one" are kept verbatim, and the fact that deletion is not
* a tool stays stated in memory_read's description and in the README.
*/
export declare const SERVER_INSTRUCTIONS = "Long-term memory kept across sessions and AI tools on this account. memory_read searches earlier preferences, decisions and project context when an answer may depend on them. memory_write saves a durable fact, preference or decision. memory_list_recent is newest-first; memory_stats shows counts and domains; memory_feedback records which recalled memories helped an answer, which reorders later reads; memory_graph shows links. Shared rooms are SEPARATE stores: to read one, pass its address as domain; unscoped reads never cover rooms. To correct a memory, write a fresh one. vault_list names secrets by alias, never values. Never store passwords, API keys, payment data, MFA codes, government IDs, or health records. The setup_memory prompt explains memory rules for CLAUDE.md or AGENTS.md.";
/** The pre-existing zero-result message — kept as the fail-open fallback. */
export declare const NO_MATCH_MESSAGE = "No memories found for this query.";
/** Hint for an UNSCOPED no-match against a non-empty store: widen the query.
* (No drop-the-filter clause — none was set; advising to remove a filter that
* does not exist nudges the model into confabulating state.) */
export declare const NO_MATCH_HINT = " Try a broader query.";
/**
* Hint for a DOMAIN-SCOPED no-match: widen the query, or try another domain —
* and NEVER drop the scope.
*
* This doc used to read "here a filter genuinely exists, so suggesting to drop
* it is honest and actionable", which described a draft that no longer exists:
* the clause "…or drop the domain filter to search all domains" was deleted, and
* a test now forbids it. The advice was actively harmful when the domain is a
* room — an unscoped read does not cover rooms at all, so dropping the filter is
* the one move guaranteed to lose the content the reader is hunting, and it is
* the step that cost two agents a day on 2026-08-07.
*/
export declare const NO_MATCH_SCOPED_HINT = " Try a broader query, or a different domain.";
/**
* The honest line for an account whose OWN domains are empty but whose rooms
* are not — an invited teammate whose every memory lives in shared rooms.
*
* Without this, such a caller was greeted with EMPTY_STORE_WELCOME: "your
* memory is empty, nothing has been saved yet, which is why this search
* returned nothing" — three false clauses, immediately contradicted by the
* scope note appended underneath, in the same payload (review, 2026-08-08).
* That reader is precisely the person from the incident this release is about.
*
* ENDS WITH A COLON, so it is only ever emitted together with the disclosure
* that follows it. That pairing used to be two independent decisions and they
* disagreed: this line was chosen from a flag counting ALL rooms while the
* disclosure was built from the LIVE ones, so an account whose only room was
* archived received the colon and nothing after it. Both now come out of the
* same {@link RoomScope} arm, and the arm that has nothing to disclose has no
* `note` field to forget.
*/
export declare const EMPTY_PERSONAL_STORE_WITH_ROOMS = "Nothing in your own domains \u2014 they hold no memories yet. That is not the whole picture:";
/**
* The same situation with the room probe UNANSWERED. Says the one true thing —
* the personal store measured zero and the rest could not be checked — and
* deliberately does NOT say "nothing has been saved yet", which was reachable
* here and is a claim about a scope this client failed to reach.
*/
export declare const EMPTY_PERSONAL_STORE_ROOMS_UNCHECKED = "Nothing in your own domains \u2014 they hold no memories yet. Whether that is the whole picture could not be checked:";
/**
* First-contact greeting: shown ONLY when a read comes back empty, the personal
* store holds zero memories, AND the room probe answered that there are no rooms
* — i.e. the very first read of this account's life, established rather than
* assumed. Seeds the ANSWER, not the store: it says what this store is and
* how a memory gets into it. It states and does not instruct (2026-09-29):
* directory reviews treat instructions inside a tool result like instructions
* in a description.
*/
export declare const EMPTY_STORE_WELCOME: string;
/**
* Assemble the WHOLE answer for a zero-result read: the head sentence and the
* disclosure that belongs with it, in one place.
*
* They are assembled together on purpose. While the head came from here and the
* tail was concatenated by the caller, the two could be — and were — derived
* from different facts: a head promising "that is not the whole picture:" with
* an empty tail after it, and a first-contact greeting under a tail admitting
* the room list could not be fetched. Every arm below returns a complete answer,
* so a head cannot outlive the evidence for it.
*
* A NAMED scope never greets and never probes stats: that probe measures the
* PERSONAL store, so on a scoped read it could claim "the store is empty" about
* a domain whose memories the query merely missed. Its four arms are the four
* things we can know about the name.
*
* An UNSCOPED scope makes ONE stats call, on this path only:
* - stats unreachable / no number → the plain no-match message + the disclosure
* - total_atoms > 0 → no-match + broaden hint + the disclosure
* - total_atoms === 0 → one head per room state, each with its own
* disclosure; only `rooms.state === "none"` may claim the memory is empty,
* because only there has the claim been established.
*/
export declare function buildReadEmptyResponse(fetchStats: () => Promise<{
total_atoms?: number;
}>, scope: ReadScope): Promise<string>;