@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
99 lines • 4.97 kB
JavaScript
/**
* MCP resource: one saved memory by id, as `memory://item/{memory_id}`.
*
* Clients that support resources (Claude.ai connectors attach them through an
* @-mention) can pull one specific memory into context. The ids are the `id:`
* lines of memory_read and memory_list_recent results, so this is the "open
* this memory" companion to the recall tools. There is no listing: the
* template is advertised, and reading needs a known id.
*
* Moved here from the hosted connector (mnemoverse-mcp-remote,
* src/resources/index.ts) in step 3c of the ADR-025 plan, with the same URI
* template, name and output, so the connector can register this instead of
* its copy. It returns only `memory_id`, `content` and `domain`: the engine's
* point read also carries importance, valence, access counts and metadata,
* which a model opening a memory does not need.
*
* Two limits, both the engine's. The point read looks only in the caller's
* own store (GET /memory/atoms/{id} takes no domain), so a memory read from a
* shared room cannot be opened here; the description says so. And a Vault
* secret cannot be opened at all: the point read answers a secret's id with
* 404, exactly as it answers an id that names nothing, so this resource
* returns resource-not-found (-32002) for it, as for any missing memory.
*/
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
import { ApiError } from "./errors.js";
import { unreadableAnswerText, wordedApiFetch } from "./tools.js";
/** MCP's code for a resource that does not exist (spec, "Resources: Error Handling"). */
const RESOURCE_NOT_FOUND = -32002;
/**
* The URI segment as the template hands it over is still percent-encoded.
* Decode it once, so the request path encodes the id itself rather than its
* encoding; a malformed escape is kept as sent (and then encoded, so it can
* never become a path separator).
*/
function decodeOnce(segment) {
try {
return decodeURIComponent(segment);
}
catch {
return segment;
}
}
/**
* Register the `memory://item/{memory_id}` resource on `server`. It reaches the
* API only through `deps.apiFetch`, like the tools.
*/
export function registerMemoryResources(server, deps) {
// The same `wording` treatment as registerMemoryTools (STEP4-2): a
// resource read that fails is explained in this registration's own voice.
const { wording } = deps;
const apiFetch = wording === undefined ? deps.apiFetch : wordedApiFetch(deps.apiFetch, wording);
server.registerResource("memory-item", new ResourceTemplate("memory://item/{memory_id}", { list: undefined }), {
title: "Saved memory",
description: "Read one saved memory by its memory ID. IDs come from memory_read results. Opens memories in your own store; a memory read from a shared room cannot be opened by ID.",
mimeType: "application/json",
}, async (uri, variables) => {
const id = decodeOnce(String(variables.memory_id));
let answer;
try {
answer = await apiFetch(`/memory/atoms/${encodeURIComponent(id)}`);
}
catch (err) {
// The message is the package's own explanation of the failure
// (src/errors.ts), so a 429 keeps the engine's quota sentence and a
// 404 says what was not found. A 404 also gets MCP's resource-not-found
// code, so a client can tell "no such memory" from a failure.
const message = err instanceof Error ? err.message : String(err);
const code = err instanceof ApiError && err.status === 404 ? RESOURCE_NOT_FOUND : ErrorCode.InternalError;
throw new McpError(code, message);
}
// A success whose body is not a memory (an empty 204, which apiFetch
// returns as {}, or any other shape) is not passed off as one: without
// this check it became {"memory_id": "<the id asked for>"}, a resource
// made up from the request (Copilot on #149). Content and domain must be
// strings, as the engine's AtomDetailSchema requires.
const atom = answer;
if (typeof atom !== "object" ||
atom === null ||
typeof atom.content !== "string" ||
typeof atom.domain !== "string") {
throw new McpError(ErrorCode.InternalError, unreadableAnswerText("The memory", "the memory", "it is empty or gone"));
}
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
memory_id: typeof atom.id === "string" ? atom.id : id,
content: atom.content,
domain: atom.domain,
}),
},
],
};
});
}
//# sourceMappingURL=resources.js.map