Remember plugin

@frontmcp/plugin-remember gives every tool a small key-value memory, this.remember, so a tool can store something on one call and read it on a later one: a user's language, a draft, the last ticket they looked at. Values are encrypted before they're stored, in memory, Redis or Vercel KV. Each value lives in a scope, session, user, tool or global, which decides who can read it back. Set tools.enabled and the plugin also registers four tools (remember_this, recall, forget and list_memories) that let the model store and recall things itself. What a scope keeps depends on who's calling and how, so read Scopes before choosing one. Remembering Across Calls teaches it step by step.

plugins: [RememberPlugin.init({ type: "memory", keyPrefix?, encryption?, ... })]

await this.remember.set(key, value, { scope?, ttl?, brand?, metadata? })
await this.remember.get(key, { scope?, defaultValue? })

Reference

RememberPlugin.init(options)

Install the package and register the plugin with init(). Tools then have this.remember:

npm install @frontmcp/plugin-remember
preferences.app.ts
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";
import { RememberPlugin } from "@frontmcp/plugin-remember";

@Tool({ name: "set_language", description: "Set the language the user wants answers in", inputSchema: { language: z.string() } })
export class SetLanguage extends ToolContext {
  async execute({ language }: { language: string }) {
    await this.remember.set("language", language, { scope: "user" });
    return { language };
  }
}

@App({ id: "prefs", name: "Preferences", tools: [SetLanguage], plugins: [RememberPlugin.init({ type: "memory" })] })
export class PreferencesApp {}

Importing @frontmcp/plugin-remember also tells TypeScript about this.remember. The plugin is exported by @frontmcp/plugins too; see the plugins overview. See more examples below.

Options

OptionTypeDefaultDescription
type"memory", "redis", "redis-client", "vercel-kv" or "global-store""memory"Where values are kept. See Stores.
keyPrefixstring"remember:"Starts every key in the store. Give each server its own when several share a Redis.
encryption.enabledbooleantrueEncrypt values before they're stored. See Encryption.
config{ host, port, password?, db? }With type: "redis": where to connect.
clientRedis (ioredis)With type: "redis-client": a client you created.
url, tokenstringKV_REST_API_URL, KV_REST_API_TOKENWith type: "vercel-kv": the store's REST URL and token. Give both or neither.
defaultTTLnumber (seconds)How long a value set without ttl lives, in every store. A whole number; 0 means no default. Anything else makes init() throw a RememberConfigurationError.
tools.enabledbooleanfalseRegister the memory tools: remember_this, recall, forget and list_memories.
tools.prefixstringnoneStarts the memory tools' names: "memory_" gives memory_remember_this, and so on. Their descriptions name the prefixed tools.
tools.allowedScopesRememberScope[]All fourWhich scopes the memory tools accept. A call with another scope, or none when session isn't allowed, is refused with RememberScopeNotAllowedError.
skipLegacyPurgebooleanfalseDon't delete entries left by older versions of the plugin, whose keys can no longer be read.
legacyPurgeDelayMsnumber86400000 (24 hours)How long after the current key layout first reached the store to wait before that purge.
encryption.customKeystringThe secret keys are derived from, in place of REMEMBER_SECRET: give every instance that shares a store the same one. Each scope still gets its own key. Values stored under the previous secret read as missing once it's set. See Encryption.

Stores

typeWhere values live
"memory"A Map in the server's process, one for each server: lost on restart, and not shared between instances, or between two servers started in one process.
"redis"A Redis server the plugin connects to with config.
"redis-client"Your own ioredis client, in client. The plugin doesn't close it.
"vercel-kv"Vercel KV, through the @vercel/kv package, which you install.
"global-store"The store in @FrontMcp({ redis }): Redis, or Vercel KV with provider: "vercel-kv". Without redis, the server doesn't start.

Only "memory" runs in the Playground, which has no network. On Redis, checked outside the Playground with FrontMCP 1.9.2 and Redis 7, values read back as stored, the store expires each one at its ttl, or defaultTTL, and keys start with keyPrefix once: remember:v2:user:nour:language. Before 1.9.2, the Redis and Vercel KV stores added keyPrefix a second time, as in remember:remember:v2:user:nour:language. An entry stored that way is still read, in place and with its expiry, and stays under that key until it expires.

this.remember

this.remember is a RememberAccessor for the current call, on tools, resources, prompts and agents. Every method takes a scope, which defaults to "session":

MethodReturnsDescription
set(key, value, options?)Promise<RememberEntry>Stores value, which must survive JSON.stringify, replacing any value under key. options: scope, ttl (seconds), brand (a label: "preference", "state", "conversation", "cache", "approval" or "custom") and metadata (any object).
get(key, options?)Promise<T | undefined>The value, or options.defaultValue when there's none, or it expired.
getEntry(key, options?)Promise<RememberEntry | undefined>The value with what was stored around it: { value, brand?, metadata?, createdAt, updatedAt, expiresAt? }, times in milliseconds.
update(key, value, options?)Promise<boolean>Replaces the value, keeping brand, metadata and createdAt, and the expiry unless you pass ttl. false when there's no value to update, or it has expired.
knows(key, options?)Promise<boolean>Whether get() would find a value under key: there is one, it hasn't expired, and it can be decrypted.
forget(key, options?)Promise<void>Deletes the value.
list(options?)Promise<string[]>The keys in a scope, without their prefix. options.pattern filters them, with * and ?.
sessionIdstringThe session id the request carries. The session and tool scopes don't use it: they use the session the server verified, or the signed-in caller (see Scopes).
userIdstring | undefinedThe caller the user scope uses: the token's sub, static:… for a static key, or anon:… for an anonymous caller.

The same accessor is available as getRemember(this), which throws when it isn't there, tryGetRemember(this), which returns undefined instead, and this.get(RememberAccessorToken).

this.remember needs the plugin on the tool's app or the server. Its options are optional: RememberPlugin.init() and plugins: [RememberPlugin], the class without init(), both keep values in memory (type: "memory"), with the same scopes and the same refusals for an anonymous caller. Changed in 1.9.4: the class registered no accessor, and this.remember failed with RememberPlugin is not installed. Add RememberPlugin.init() to your plugins array.

Scopes

A scope decides which callers share a value. The table shows what each keeps for a client using MCP 2026-07-28, which sends every request on its own, and for a client using an earlier version, which opens a session:

ScopeKeyed by2026-07-28 clientSession-based client
session (default)The session the server verified for the request, else the signed-in callerKept for a signed-in user or a static key, across requests, apart from their user values. An anonymous caller is refused with RememberIdentityErrorKept for the session
useruserIdKept for a signed-in user or a static key. An anonymous caller, a new anon: user on every request, is refused with RememberIdentityErrorKept across sessions for a signed-in user or a static key. An anonymous caller is refused, whatever its session
toolThe same as session, and the name of the tool that's runningKept for a signed-in caller, and only for that tool. An anonymous caller is refusedKept for the session, and only for that tool
globalNothingKept, and shared by every callerKept, and shared by every caller

The 2026-07-28 column is what this page's Playgrounds show. The session-based column was checked on FrontMCP 1.9.2's Node HTTP server; the Playground can only send 2026-07-28 requests. create() and createDirect() give their caller one session for the life of the server, so session values last as long as it does.

A session counts only when the request presents it and the server verified it: the mcp-session-id a session-based client got from initialize, or the sessionId a legacy SSE client posts with. A 2026-07-28 request never has one, and an mcp-session-id a 2026-07-28 client sends doesn't count, so a caller can't reach another caller's session or tool values by sending their id. A request without a verified session uses the signed-in caller instead, as stateless-user:<userId>, and an anonymous one is refused with RememberIdentityError.

tool scope keeps one tool's values from another's: each value is keyed by the tool that stored it, as <app id>:<tool name>, so another tool reading the same key in tool scope gets nothing. See What each scope keeps.

Encryption

With encryption.enabled (the default), each value is stored as {"alg":"A256GCM","iv":…,"tag":…,"data":…}: the value and its metadata, encrypted with AES-256-GCM and a new IV every time. The key is derived with HKDF-SHA256 from a server secret and the scope's identity: the verified session, or stateless-user:<userId> without one, for session and tool, userId for user, and the secret alone for global. Anyone who can read the store sees keys and expiry times, not values. See What's stored.

The server secret is encryption.customKey when you set it, else the first of these that's set:

  1. REMEMBER_SECRET, MCP_MEMORY_SECRET or MCP_SESSION_SECRET in the environment.
  2. Outside production (NODE_ENV isn't production): a secret the plugin generates and saves to .frontmcp/remember-secret.json in the working directory, readable only by its owner. Keep .frontmcp/ out of version control.
  3. In production: a random secret for this process, with a warning in the log. Values can't be read after a restart, or by another instance.

Set REMEMBER_SECRET, or encryption.customKey, to the same value on every instance that shares a store. A value encrypted with another secret reads as missing: get() returns the default, and knows() says false. See What's stored. Changed in 1.9: knows() didn't decrypt, so it said true. Changed in 1.9.2: customKey was ignored.

With encryption: { enabled: false }, values are stored as plain JSON.

The memory tools

With tools.enabled, the plugin registers four tools, under tools.prefix if you set one, whether it's in an app's plugins or in @FrontMcp({ plugins }). You don't list them in tools:

ToolArgumentsResult
remember_thiskey, value (any JSON), scope?, ttl? (whole seconds), brand?{ success, key, scope, expiresAt? }, with the expiry from ttl or defaultTTL
recallkey, scope?{ found, key, scope, value?, createdAt?, expiresAt? }
forgetkey, scope?{ success, key, scope, existed }
list_memoriesscope?, pattern?, limit? (up to 100, default 50){ keys, scope, count, truncated }
  • Without tools.enabled there are no memory tools. The package still exports their classes, RememberThisTool, RecallTool, ForgetTool and ListMemoriesTool, to list in an app's tools yourself. Don't do both: the app would have every tool twice, and FrontMCP renames each pair to <owner>:<name>, such as assistant:recall and remember:recall. A class you list is never prefixed.
  • tools.prefix changes the registered names, and the descriptions follow: with prefix: "memory_", memory_recall says it reads what memory_remember_this saved, so the model isn't pointed at a tool that doesn't exist.
  • scope defaults to session in every one of them, which under 2026-07-28 lasts for the signed-in caller and is refused to an anonymous one (see Scopes).
  • A scope outside tools.allowedScopes, the default session included, fails the call with Scope 'session' is not allowed. Allowed scopes: user and the code REMEMBER_SCOPE_NOT_ALLOWED. See RememberScopeNotAllowedError.
  • recall and list_memories are marked readOnlyHint: true, remember_this and forget readOnlyHint: false.
  • In tool scope, each memory tool keeps its own values, so recall doesn't find what remember_this stored there.
  • An anonymous caller is refused in user scope, and in session and tool scope: list_memories answers with an error, not an empty list.

All four share one description for scope, which says the same:

Whose memory to use (default: session). session: this session; without one (stateless HTTP, MCP 2026-07-28), the signed-in caller across its requests. user: the signed-in caller, across all of its sessions. tool: the tool running the call, for the same caller as session; each memory tool has its own, so the other memory tools do not see what one of them stores in tool scope. global: shared by every caller. An anonymous caller cannot use user scope, nor session or tool scope without a session.

Each tool's own description points at its siblings by their registered names. With prefix: "memory_", memory_recall says:

Recall something that was previously remembered. Use this to retrieve stored preferences, settings, or any information that was saved with memory_remember_this.

RememberScopeNotAllowedError

Thrown by a memory tool when the call's scope isn't in tools.allowedScopes, including the default scope when the caller left scope out. It's a PublicMcpError with the code REMEMBER_SCOPE_NOT_ALLOWED, so the model reads its message in production too, and can call again with an allowed scope:

Scope 'session' is not allowed. Allowed scopes: user

The class is exported from @frontmcp/plugin-remember.

RememberIdentityError

Thrown when a scope has no caller to keep values apart with. It's a PublicMcpError with the code REMEMBER_IDENTITY_REQUIRED, so a client reads its message, in production too:

  • user scope for an anonymous caller, an anon: subject, on any transport: Remember cannot use user scope without an authenticated user: all unauthenticated callers would share one namespace. Authenticate the request or use the 'global' scope.
  • session or tool scope on a request with no session the server verified and no signed-in caller: Remember cannot use session or tool scope for an unauthenticated request without a verified session: …, which ends with what to do: authenticate the request, use a stateful transport, or choose global scope if the data really is shared.

Under 2026-07-28, that's every scope but global for a caller without credentials.

Caveats

  • Registered on one app, it serves that app. Another app's tools have no this.remember: it fails with RememberPlugin is not installed. To give every app the same memory, register it on @FrontMcp. Changed in 1.9: the accessor reached every app's tools, which read and wrote the same values.
  • The plugin has no hooks: it only adds this.remember. Registering it on @App or @FrontMcp makes no other difference.
  • set() with a ttl, or with the plugin's defaultTTL, expires the value in the store too; without either, a value lives until it's forgotten or the store is cleared. update() without ttl keeps the expiry, in the store as well, so knows() and list() stop reporting the key when get() stops returning it. Changed in 1.9: it removed the expiry from the store, so they kept reporting the key until a get() deleted it.
  • list() reads every key in the store that matches, so it gets slower as the store grows, and on Redis it runs SCAN.

Usage

Remembering a user's preference

A preference belongs to a user, so it goes in user scope, and the server needs to know who's calling. Here server.ts uses two static keys; the Playground's own server is public, so the tests start server.ts with createFetchHandler() and call it with each key:

Open
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";
import { RememberPlugin } from "@frontmcp/plugin-remember";

@Tool({ name: "set_language", description: "Set the language the user wants answers in", inputSchema: { language: z.string() } })
export class SetLanguage extends ToolContext {
  async execute({ language }: { language: string }) {
    await this.remember.set("language", language, { scope: "user" });
    return { language };
  }
}

@Tool({ name: "get_language", description: "The language the user wants answers in", inputSchema: {} })
export class GetLanguage extends ToolContext {
  async execute() {
    return { language: await this.remember.get("language", { scope: "user", defaultValue: "en" }) };
  }
}

@App({
  id: "prefs",
  name: "Preferences",
  tools: [SetLanguage, GetLanguage],
  plugins: [RememberPlugin.init({ type: "memory" })],
})
export class PreferencesApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The Playground calls set_language as an anonymous client, which user scope refuses with REMEMBER_IDENTITY_REQUIRED (RememberIdentityError). Users who sign in with local, remote or transparent auth work the same way: userId is their token's sub. Keeping users apart with transparent auth runs it with two signed-in users.

What each scope keeps

save and load store and read a value in the scope you pass. The tests store a value, then read it on a later request, as a signed-in caller, as another, and as an anonymous one, all over MCP 2026-07-28:

Open
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";
import { RememberPlugin } from "@frontmcp/plugin-remember";

const scope = z.enum(["session", "user", "tool", "global"]);

@Tool({ name: "save", description: "Save a value", inputSchema: { key: z.string(), value: z.string(), scope } })
export class Save extends ToolContext {
  async execute({ key, value, scope }: { key: string; value: string; scope: z.infer<typeof scope> }) {
    await this.remember.set(key, value, { scope });
    return { saved: key };
  }
}

@Tool({ name: "load", description: "Load a value", inputSchema: { key: z.string(), scope } })
export class Load extends ToolContext {
  async execute({ key, scope }: { key: string; scope: z.infer<typeof scope> }) {
    return { value: (await this.remember.get<string>(key, { scope })) ?? null };
  }
}

@App({ id: "notes", name: "Notes", tools: [Save, Load], plugins: [RememberPlugin.init({ type: "memory" })] })
export class NotesApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

count_calls reads back its own tool value, for each caller, while load doesn't find the one save stored, although it finds save's session value. The anonymous Playground client can use global and nothing else, and a second server started in the same process has a store of its own. The last two tests use createDirect(), whose caller has one session for the life of the server, and a tool in an app without the plugin has no this.remember. On a session-based client (before MCP 2026-07-28), session and tool values last for the session.

Working with entries

Beyond set and get, an entry can expire, carry a brand and metadata, and be updated in place. This tool runs through the accessor's methods:

Open
import { App, Tool, ToolContext } from "@frontmcp/sdk";
import { RememberPlugin, tryGetRemember } from "@frontmcp/plugin-remember";

@Tool({ name: "draft_tour", description: "Store and read back a reply draft", inputSchema: {} })
export class DraftTour extends ToolContext {
  async execute() {
    const memory = this.remember;
    const scope = "global" as const; // the Playground's callers are anonymous; use "user" with sign-in

    await memory.set("draft:T-1", "Thanks for the report.", { scope, ttl: 3600, brand: "state", metadata: { ticket: "T-1" } });
    await memory.set("draft:T-2", "We're on it.", { scope });
    await memory.set("signature", "The Help Desk", { scope });

    const updated = await memory.update("draft:T-1", "Thanks, fixed in 2.1.", { scope });
    const entry = await memory.getEntry<string>("draft:T-1", { scope });
    return {
      updated,
      updatedMissing: await memory.update("draft:T-9", "?", { scope }),
      value: entry?.value,
      brand: entry?.brand,
      metadata: entry?.metadata,
      expiresInMinutes: entry?.expiresAt ? Math.round((entry.expiresAt - Date.now()) / 60_000) : null,
      drafts: (await memory.list({ scope, pattern: "draft:*" })).sort(),
      knowsT2: await memory.knows("draft:T-2", { scope }),
      missing: await memory.get("draft:T-9", { scope, defaultValue: "(none)" }),
      hasAccessor: tryGetRemember(this) !== undefined,
    };
  }
}

@Tool({ name: "forget_draft", description: "Forget a reply draft", inputSchema: {} })
export class ForgetDraft extends ToolContext {
  async execute() {
    await this.remember.forget("draft:T-2", { scope: "global" });
    return { knowsT2: await this.remember.knows("draft:T-2", { scope: "global" }) };
  }
}

@App({ id: "drafts", name: "Drafts", tools: [DraftTour, ForgetDraft], plugins: [RememberPlugin.init({ type: "memory" })] })
export class DraftsApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

A value with a ttl is gone once it expires: get() gives the default, knows() is false, and list() leaves it out. An update() without ttl keeps the expiry, and fails once it has passed. A value set without ttl lives for the plugin's defaultTTL, when it has one. The tests move the clock forward by replacing Date.now(), which is what the plugin and the memory store read:

Open
import { App, Tool, ToolContext } from "@frontmcp/sdk";
import { RememberPlugin } from "@frontmcp/plugin-remember";

@Tool({ name: "start_otp", description: "Send a one-time code, valid for 5 minutes", inputSchema: {} })
export class StartOtp extends ToolContext {
  async execute() {
    await this.remember.set("otp", "482913", { scope: "global", ttl: 300 });
    return { sent: true };
  }
}

@Tool({ name: "resend_otp", description: "Send a new one-time code, keeping the expiry", inputSchema: {} })
export class ResendOtp extends ToolContext {
  async execute() {
    return { resent: await this.remember.update("otp", "551204", { scope: "global" }) };
  }
}

@Tool({ name: "check_otp", description: "What's left of the one-time code", inputSchema: {} })
export class CheckOtp extends ToolContext {
  async execute() {
    const scope = "global" as const;
    const knows = await this.remember.knows("otp", { scope });
    const listed = (await this.remember.list({ scope })).includes("otp");
    return { knows, listed, code: await this.remember.get("otp", { scope, defaultValue: null }) };
  }
}

@App({
  id: "otp",
  name: "OTP",
  tools: [StartOtp, ResendOtp, CheckOtp],
  plugins: [RememberPlugin.init({ type: "memory" })],
})
export class OtpApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Letting the model remember things

Set tools.enabled and the plugin registers the memory tools. Restrict them to the scopes you mean with tools.allowedScopes. The app lists no tools of its own here:

Open
import { App } from "@frontmcp/sdk";
import { RememberPlugin } from "@frontmcp/plugin-remember";

@App({
  id: "assistant",
  name: "Assistant",
  plugins: [RememberPlugin.init({ type: "memory", tools: { enabled: true, allowedScopes: ["user"] } })],
})
export class AssistantApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The Playground calls as an anonymous client, so user scope refuses it, with REMEMBER_IDENTITY_REQUIRED, and list_memories is an error too. A call with a scope outside allowedScopes is a tool error that names the allowed scopes, REMEMBER_SCOPE_NOT_ALLOWED, so the model can call again with scope: "user", in production too. Without allowedScopes, the same call succeeds and stores the value in session scope, which lasts for the caller under 2026-07-28 but only for the session on a session-based client, apart from their user values, and is refused to an anonymous caller. Leave tool out of allowedScopes as well: each memory tool has its own tool values, so recall never finds what remember_this put there.

tools works the same whether you pass it to init() or return it from init({ inject, useFactory }), whose factory may be async, as the useFactory tests show. Changed in 1.9: a useFactory that returned tools.enabled registered no tools, and said nothing.

Keeping users apart with transparent auth

With transparent auth, your identity provider signs the tokens, and userId is the token's sub. this.remember is built for each request, so every caller reads and writes their own user values, on one server:

Open
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";
import { RememberPlugin } from "@frontmcp/plugin-remember";

@Tool({ name: "set_language", description: "Set the language the user wants answers in", inputSchema: { language: z.string() } })
export class SetLanguage extends ToolContext {
  async execute({ language }: { language: string }) {
    await this.remember.set("language", language, { scope: "user" });
    return { language };
  }
}

@Tool({ name: "get_language", description: "The language the user wants answers in", inputSchema: {} })
export class GetLanguage extends ToolContext {
  async execute() {
    const memory = this.remember;
    return { user: memory.userId, language: await memory.get("language", { scope: "user", defaultValue: "en" }) };
  }
}

@App({
  id: "prefs",
  name: "Preferences",
  tools: [SetLanguage, GetLanguage],
  plugins: [RememberPlugin.init({ type: "memory" })],
})
export class PreferencesApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The test uses one server for both users, as a deployed server would.

What's stored

raw_store reads the plugin's store directly, through RememberStoreToken, to show what someone with access to your Redis would see:

Open
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";
import { RememberPlugin, RememberStoreToken } from "@frontmcp/plugin-remember";

const scope = z.enum(["session", "user", "global"]);

@Tool({ name: "save_note", description: "Save a note", inputSchema: { note: z.string(), scope } })
export class SaveNote extends ToolContext {
  async execute({ note, scope }: { note: string; scope: z.infer<typeof scope> }) {
    await this.remember.set("note", note, { scope });
    return { saved: true };
  }
}

@Tool({ name: "read_note", description: "Read the shared note", inputSchema: {} })
export class ReadNote extends ToolContext {
  async execute() {
    return {
      note: await this.remember.get("note", { scope: "global", defaultValue: null }),
      knows: await this.remember.knows("note", { scope: "global" }),
    };
  }
}

@Tool({ name: "raw_store", description: "The store's raw contents", inputSchema: {} })
export class RawStore extends ToolContext {
  async execute() {
    const store = this.get(RememberStoreToken);
    const raw: Record<string, unknown> = {};
    for (const key of await store.keys("*")) raw[key] = JSON.parse(String(await store.getValue(key)));
    return raw;
  }
}

@App({
  id: "vault",
  name: "Vault",
  tools: [SaveNote, ReadNote, RawStore],
  plugins: [RememberPlugin.init({ type: "memory" })],
})
export class VaultApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The store also holds <keyPrefix>__layout__, which records when the current key layout first reached it. Keys for user scope hold the caller's userId, here a static key's static: and a hash, and keys for session scope hold the verified session the same way, or stateless-user: and the caller's userId without one, as the last test shows; tool keys hold the tool's name and the same identity. Encryption hides values, not keys: anyone who can read the store sees who stored something, in which scope, and under which key.


Troubleshooting

A value stored on one call is gone on the next

The value is in tool scope and another tool reads it; or it's in session or tool scope (the default is session) and a session-based client's session ended; or the store is "memory", and the server restarted, or another instance or server answered. Under 2026-07-28, session and tool values last for a signed-in caller. Use user scope with authentication, or global for values every caller may share. See Scopes.

One user sees another user's memory

The value is in global scope, which every caller shares, and every app's tools too. Put per-user values in user scope, with sign-in. Before 1.8.3, this.remember could also belong to the first caller with a transparent token, and a 2026-07-28 client could reach another caller's session values by sending their mcp-session-id; they're kept apart now, as Keeping users apart with transparent auth and What each scope keeps show.

RememberPlugin is not installed. Add RememberPlugin.init() to your plugins array.

this.remember was read on a server without the plugin, or in an app other than the one the plugin is registered on. Register RememberPlugin.init({ type: "memory" }), or another store; RememberPlugin.init(), as the message suggests, works too and keeps values in memory. Before 1.9.4, plugins: [RememberPlugin], the class without init(), registered no accessor and gave this error too; update, or use init(). (Before 1.8.6 it threw TypeError: Cannot read properties of undefined (reading 'providers').) Use tryGetRemember(this) in code that should work without the plugin.

The memory tools don't show up in tools/list

tools.enabled isn't true in the plugin's options, from init() or its useFactory. The plugin registers the tools only then. Before 1.9, a useFactory that returned it registered none: update, or list RememberThisTool, RecallTool, ForgetTool and ListMemoriesTool in your app's tools. See Letting the model remember things.

Every memory tool appears twice, as <app>:recall and remember:recall

The app lists the four tool classes in tools, and tools.enabled is true, so the plugin registers them too. Keep one: drop the classes from tools, or drop tools.enabled.

Scope 'session' is not allowed. Allowed scopes: …

A memory tool was called with a scope outside tools.allowedScopes, or without scope, which means session. It's a RememberScopeNotAllowedError, code REMEMBER_SCOPE_NOT_ALLOWED, and the model reads the message in production too, so it can call again with an allowed scope. Add the scope to allowedScopes, or have the model pass one it may use.

Remember cannot use session or tool scope for an unauthenticated request without a verified session…

A RememberIdentityError, code REMEMBER_IDENTITY_REQUIRED: the request had no session the server verified and no signed-in user. Under 2026-07-28 that's every anonymous caller of session or tool scope, the memory tools' default included. Sign the caller in, or use global scope for data every caller may share. See RememberIdentityError.

Remember cannot use user scope without an authenticated user…

The same error, code REMEMBER_IDENTITY_REQUIRED, for user scope: the caller is anonymous, an anon: user that's new on every request, so nothing could be found again. Sign the caller in, or use global scope. Before 1.8.6 the value was stored and lost without an error, and list_memories answered an empty list. See RememberIdentityError.

Values can't be read after a restart, or on another instance

The server secret changed. In production without REMEMBER_SECRET or encryption.customKey, each process makes up its own. Set one of them to the same value everywhere; setting customKey for the first time changes the secret too. See Encryption. With the memory store, values are also simply lost on restart.

Plugin "RememberPlugin" requires global "redis" configuration. Add "redis" to your @FrontMcp decorator options.

type: "global-store" uses the server's store, and @FrontMcp has no redis. Add one, or use type: "redis" with config.