@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
135 lines (134 loc) • 7.28 kB
TypeScript
/**
* The eleven tools (ten memory tools and vault_list), as one function any MCP server can register.
*
* ADR-025 (mnemoverse-core): this package defines the MCP surface, and every
* server that exposes Mnemoverse memory over MCP registers the SAME tools from
* here instead of keeping its own copy. The stdio server in src/index.ts is one
* such server; the hosted connector (mnemoverse-mcp-remote) is the other.
*
* What differs between servers is only how a request reaches the API: which
* credential it carries, where it goes, and how a transport failure is worded.
* That is `MemoryToolDeps.apiFetch`. Nothing in this file reads configuration
* or the environment.
*/
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { type WriteAuthor } from "./requests.js";
import { type Wording } from "./errors.js";
/**
* How the tools reach the Mnemoverse API: send `path` (relative to the API base,
* e.g. "/memory/read") and resolve to the parsed JSON body.
*
* Contract every implementation must keep, because the tools branch on it:
* a non-2xx rejects with `ApiError` (status, core's error code and an
* agent-facing message), a request that never got an HTTP answer rejects with
* `NetworkError`, and a 2xx whose body cannot be parsed rejects with
* `UnreadableBodyError`. All three are exported from ./shared and defined in
* src/errors.ts. `options.signal` must be honoured.
*/
export type ApiFetch = <T = unknown>(path: string, options?: RequestInit) => Promise<T>;
/**
* `deps.apiFetch` with `deps.wording` applied to what it throws (STEP4-2):
* every rejection passes through {@link rewordFailure}, so an `ApiError`,
* `NetworkError` or `UnreadableBodyError` the consumer built with no
* `wording` (or with a different one) reaches the model explained under the
* wording this registration was given. Results and every other rejection
* pass through untouched. Installed only when `wording` is supplied at all:
* the stdio server supplies none, and its `apiFetch` is used exactly as
* before.
*/
export declare function wordedApiFetch(inner: ApiFetch, wording: Wording): ApiFetch;
/**
* What a server supplies when it registers the memory tools.
*
* `wording` and `writeAuthor` (STEP4-2/3/5, owner decisions 2026-09-24) are
* both optional, and both default to exactly this server's own behaviour: a
* consumer that supplies neither gets byte-identical descriptions, error
* text and write bodies to every release before this one. They exist for a
* SECOND server registering these same tools (the hosted connector,
* ADR-025) that needs to say "this connector" instead of "this server",
* speak to an OAuth user who never sees an API key, and vouch for the
* end user it is writing on behalf of.
*/
export interface MemoryToolDeps {
apiFetch: ApiFetch;
/** How this registration wants its own tool descriptions and error
* explanations worded. See {@link Wording} for each field. Read at
* registration time for the three descriptions that name the server,
* and applied to every `ApiError`/`NetworkError`/`UnreadableBodyError`
* the consumer's `apiFetch` rejects with (see {@link wordedApiFetch}),
* so the consumer states it once, here. Passing the same value to the
* error constructors inside `apiFetch` is allowed and changes nothing. */
wording?: Wording;
/**
* Supplier-vouched authorship for `memory_write` (STEP4-5). Called once
* per write, with no arguments; a returned value is sent as the request
* body's `author` field EXACTLY as returned (this package applies no
* normalisation beyond a `typeof` guard; core re-normalises server-side,
* see {@link WriteAuthor}). Returning `undefined`, or omitting this
* dependency entirely, sends the body this package has always sent: the
* stdio server's own requests are exactly this case, and do not change.
*
* Core honours `author` ONLY for a SERVICE/supplier caller and IGNORES it
* for an OIDC end-user (mnemoverse-core src/mnemo/api/routes.py:211-276,
* `_get_provenance`, verified 2026-09-24), so this dependency is useful
* only to a server that authenticates to core as a supplier vouching for
* someone else, not to an end-user's own key.
*/
writeAuthor?: () => WriteAuthor | undefined;
}
export declare const MAX_RESULT_CHARS: number;
/**
* Truncate a result string to MAX_RESULT_CHARS, appending a notice if truncated.
* Required by Claude Connectors Directory submission policy.
*
* Defensive against splitting UTF-16 surrogate pairs: if the character right
* before the cut point is a high surrogate (U+D800–U+DBFF), drop it so the
* result stays well-formed. Otherwise an emoji or non-BMP character at the
* boundary can produce a lone surrogate and corrupt downstream JSON encoding.
*
* Exported (re-exported from ./shared) so a consumer applying MAX_RESULT_CHARS
* to its own tool results does not have to re-implement this code-point-safe
* truncation as a plain `slice`, which can cut a surrogate pair.
*/
export declare function capResult(text: string, moreHint?: string): string;
/**
* The sentence behind {@link unreadableAnswerReply}, exported so the memory
* resource (src/resources.ts) says the same thing about an unreadable answer
* rather than keeping its own copy.
*
* The tail attributes the unreadable 200 to "whatever answered this call", not
* to "the memory service" — this client cannot establish WHO answered: a
* gateway, a proxy, or the endpoint a mis-set MNEMOVERSE_API_URL points at
* produces the same 200 with an unrecognised body (truth re-verification,
* 2026-08-09). Pinned in test/handlers.test.ts.
*/
export declare function unreadableAnswerText(subject: string, notA: string, absence: string, extra?: string): string;
/**
* A success result carrying both a text block and `structuredContent`, which
* is what a tool with a declared `outputSchema` must return, or the SDK
* itself rejects the call. `validateToolOutput` in the installed SDK
* (node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:185-207)
* turns a non-error, text-only result into an `isError` "Output validation
* error: … has an output schema but no structured content was provided", the
* moment a tool declares that schema, even though the same text alone was a
* fine answer the day before the schema was added. Pinned against that
* installed SDK in test/structured-output.test.ts.
*
* Deliberately dumb: it does not call `capResult` or `withDomainEscapeLegend`
* itself. Every tool already measures and caps its own text with a per-tool
* hint and applies the legend after the cap (see memory_read below), so a
* helper that capped again would double-truncate; callers pass `text`
* already finished.
*/
export declare function structured(text: string, data: Record<string, unknown>): {
content: [{
type: "text";
text: string;
}];
structuredContent: Record<string, unknown>;
};
/**
* Register the eleven tools on `server`. Call once per server instance.
* `deps.apiFetch` is the only way the tools reach the API.
*/
export declare function registerMemoryTools(server: McpServer, deps: MemoryToolDeps): void;