Remembering Across Calls
A tool forgets everything when its call ends. A support agent tells the assistant to sign replies "Nour, Tier 2 support", and on the next call the signature is gone. A variable at the top of the file remembers it, but for every caller at once, and only until the server restarts. @frontmcp/plugin-remember gives every tool this.remember, a small key-value memory. Each value is stored in a scope, which decides who can read it back. Under MCP 2026-07-28, what a scope keeps depends on whether the caller signed in, so this lesson spends as much time on who as on what.
You will learn
- How to store and read values with
this.remember - Which scope to put a value in, and who can read it back
- What lasts for a signed-in caller, and what an anonymous caller keeps, under MCP 2026-07-28
- How to make a value expire, and how to forget it
- How to let the model remember things itself, and limit what it can store
A preference in a module variable
set_signature saves the signature an agent's replies end with, and draft_reply signs a reply with it. The signature lives in a variable at the top of the file.
To see whose signature ends up where, the tests need two callers who are signed in. The Playground's own client never signs in, so server.ts gives two support agents, Nour and Sam, a static key each, and callAs() in call-as.ts sends a tool call with a key, the way an MCP 2026-07-28 client would. Every call goes to the same server. Open the Tests tab:
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";
// 🚩 One signature for the whole server
let signature = "The Help Desk";
@Tool({
name: "set_signature",
description: "Set the signature your replies end with.",
inputSchema: { signature: z.string().max(80) },
})
export class SetSignature extends ToolContext {
async execute(input: { signature: string }) {
signature = input.signature;
return { signature };
}
}
@Tool({
name: "draft_reply",
description: "Draft a reply to a support ticket, signed with your signature.",
inputSchema: { ticketId: z.string(), text: z.string() },
})
export class DraftReply extends ToolContext {
async execute({ ticketId, text }: { ticketId: string; text: string }) {
return { ticketId, reply: `${text}\n\n${signature}` };
}
}
@App({ id: "help-desk", name: "Help Desk", tools: [SetSignature, DraftReply] })
export class HelpDeskApp {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
The variable remembers, and that's the trouble: it belongs to the file, so every caller shares it. When Nour sets her signature, Sam's replies are signed with it too. It's also lost when the server restarts, and each instance of the server has its own. Sharing State with Providers covers what else goes wrong with module variables. What this tool needs is a place that keeps a value for one agent, across calls.
Giving tools a memory
Install the plugin:
npm install @frontmcp/plugin-rememberRegister it on the app with RememberPlugin.init({ type: "memory" }). Every tool then has this.remember. set(key, value, { scope }) stores a value, and get(key, { scope, defaultValue }) reads it back. scope: "user" keeps a value for the caller:
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";
import { RememberPlugin } from "@frontmcp/plugin-remember";
@Tool({
name: "set_signature",
description: "Set the signature your replies end with.",
inputSchema: { signature: z.string().max(80) },
})
export class SetSignature extends ToolContext {
async execute({ signature }: { signature: string }) {
await this.remember.set("signature", signature, { scope: "user" }); // ✅ this caller's
return { signature };
}
}
@Tool({
name: "draft_reply",
description: "Draft a reply to a support ticket, signed with your signature.",
inputSchema: { ticketId: z.string(), text: z.string() },
})
export class DraftReply extends ToolContext {
async execute({ ticketId, text }: { ticketId: string; text: string }) {
const signature = await this.remember.get("signature", { scope: "user", defaultValue: "The Help Desk" });
return { ticketId, reply: `${text}\n\n${signature}` };
}
}
@App({
id: "help-desk",
name: "Help Desk",
tools: [SetSignature, DraftReply],
plugins: [RememberPlugin.init({ type: "memory" })],
})
export class HelpDeskApp {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Now Nour's signature is Nour's, Sam gets the default until he sets his own, and each is there on the agent's next request. The last test calls the Playground's own server, whose client doesn't sign in: user scope refuses it, and so does the call in the Call tab.
user scope keeps values for this.remember.userId, the caller the server worked out from the request:
- A signed-in user, with a token from an identity provider, is the token's
sub. Their values follow them across sessions, conversations and clients. - A static key is
static:and a hash of the key. Everyone who uses the key shares its values. - An anonymous caller has no identity to keep values under: under MCP 2026-07-28 it's a new
anon:id on every request, anduserscope refuses it.this.rememberthrows aRememberIdentityError, which reaches the client as its message, with the codeREMEMBER_IDENTITY_REQUIRED.
So a memory that belongs to a person needs the person to sign in. Authenticating Clients covers how.
Choosing a scope
Every this.remember method takes a scope, and without one it's "session". Under MCP 2026-07-28, where a client sends each request on its own and there are no sessions, the four scopes keep:
| Scope | A value belongs to | Signed-in caller | Anonymous caller |
|---|---|---|---|
user | The caller | Kept, across requests | Refused |
session (the default) | The caller, apart from their user values | Kept, across requests | Refused |
tool | The caller, and the tool that stored it | Kept, across requests, for that tool only | Refused |
global | Everyone | Kept, and shared | Kept, and shared |
A session-based client, on a protocol version before 2026-07-28, keeps session and tool values for its session instead (Scopes). This help desk uses global for a banner every caller sees during an outage, and session for the ticket an agent is working on:
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";
import { RememberPlugin } from "@frontmcp/plugin-remember";
@Tool({ name: "set_banner", description: "Show a banner to everyone, like an outage notice.", inputSchema: { text: z.string() } })
export class SetBanner extends ToolContext {
async execute({ text }: { text: string }) {
await this.remember.set("banner", text, { scope: "global" });
return { banner: text };
}
}
@Tool({ name: "get_banner", description: "The banner everyone sees, if there is one.", inputSchema: {} })
export class GetBanner extends ToolContext {
async execute() {
return { banner: await this.remember.get("banner", { scope: "global", defaultValue: null }) };
}
}
@Tool({ name: "work_on", description: "Mark a ticket as the one you're working on.", inputSchema: { ticketId: z.string() } })
export class WorkOn extends ToolContext {
async execute({ ticketId }: { ticketId: string }) {
await this.remember.set("ticket", ticketId); // session scope, the default
return { ticketId };
}
}
@Tool({ name: "current_ticket", description: "The ticket you're working on.", inputSchema: {} })
export class CurrentTicket extends ToolContext {
async execute() {
return { ticketId: await this.remember.get("ticket", { defaultValue: null }) };
}
}
@App({
id: "help-desk",
name: "Help Desk",
tools: [SetBanner, GetBanner, WorkOn, CurrentTicket],
plugins: [RememberPlugin.init({ type: "memory" })],
})
export class HelpDeskApp {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
The banner is in global scope, so Sam reads what Nour set, and a caller who isn't signed in can set and read a banner too. (The Playground's own server and the one callAs() starts are two servers, and each has its own memory: the anonymous caller's banner is a different one from Nour's.) The ticket is in session scope, the default: it lasts for Nour across her requests, Sam doesn't see it, and a caller who isn't signed in can't use session scope, or user, at all. this.remember throws a RememberIdentityError, and the call fails with the code REMEMBER_IDENTITY_REQUIRED. The error's message says what to do: authenticate the request, or use global if the value really is shared.
Which scope, then:
userfor what belongs to a person: a signature, a language, the columns they like in a report.globalfor what everyone may see: a banner, a shared counter, the last time a sync ran. Never for anything personal, because every caller reads it.sessionfor what a signed-in caller is in the middle of, with attl, knowing it lasts across their conversations.toolfor a tool's own bookkeeping that no other tool should read or change, like which ticketsnext_ticketalready suggested to this agent.
How long a value lives
A value lives until it's forgotten, unless you give it a ttl, in seconds, when you store it, or the plugin has a defaultTTL, which init() takes for values stored without one. A reply draft should be there tomorrow morning, but not next month. save_draft keeps a draft for a day, and send_reply forgets it once it's sent. The tests move the clock forward by replacing Date.now(), which is what the plugin and its memory store read:
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";
import { RememberPlugin } from "@frontmcp/plugin-remember";
const input = { ticketId: z.string().describe("Ticket id, like T-1") };
@Tool({ name: "save_draft", description: "Save a reply draft for a ticket. Drafts are kept for a day.", inputSchema: { ...input, text: z.string() } })
export class SaveDraft extends ToolContext {
async execute({ ticketId, text }: { ticketId: string; text: string }) {
await this.remember.set(`draft:${ticketId}`, text, { scope: "user", ttl: 24 * 60 * 60 });
return { saved: ticketId };
}
}
@Tool({ name: "get_draft", description: "Your reply draft for a ticket, if you have one.", inputSchema: input })
export class GetDraft extends ToolContext {
async execute({ ticketId }: { ticketId: string }) {
return { draft: await this.remember.get(`draft:${ticketId}`, { scope: "user", defaultValue: null }) };
}
}
@Tool({ name: "send_reply", description: "Send your reply draft for a ticket to the customer.", inputSchema: input })
export class SendReply extends ToolContext {
async execute({ ticketId }: { ticketId: string }) {
const key = `draft:${ticketId}`;
const draft = await this.remember.get<string>(key, { scope: "user" });
if (!draft) return { sent: false };
await this.remember.forget(key, { scope: "user" });
return { sent: true, text: draft };
}
}
@App({
id: "help-desk",
name: "Help Desk",
tools: [SaveDraft, GetDraft, SendReply],
plugins: [RememberPlugin.init({ type: "memory" })],
})
export class HelpDeskApp {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
forget(key, { scope }) deletes a value at once, and a value past its ttl reads as missing. this.remember has a few more methods, like knows() to check for a value and list() for the keys in a scope (this.remember).
Every value also lives only as long as its store:
type: "memory"keeps values in the server's process, one store per server. They're lost on restart and not shared between instances, or between two servers in one process. For a server that restarts or runs more than once, use Redis or Vercel KV (Stores).- Values are encrypted with a secret the server derives its keys from. In production, set
REMEMBER_SECRETto the same value on every instance. Without it, each process makes up its own, and values stored before a restart, or by another instance, can't be read (Encryption).
Letting the model remember things
So far your tools decide what to remember. The plugin can also give the model tools of its own. Set tools.enabled and it registers remember_this, recall, forget and list_memories, and your app lists none of them.
Each takes a scope, which defaults to session. Their descriptions say what each scope keeps under 2026-07-28, and end with "An anonymous caller cannot use user scope, nor session or tool scope without a session." The model still picks the scope, so limit it to the one you mean with tools.allowedScopes:
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 list_memories as its anonymous client, and is refused with REMEMBER_IDENTITY_REQUIRED: an anonymous caller has no user memory, so these tools are for signed-in users. For Nour, remember_this stores her time zone in user scope, and recall finds it for her and not for Sam. A call without a scope, or with another, is refused with a tool error that names the allowed scopes and has the code REMEMBER_SCOPE_NOT_ALLOWED, so the model can call again with scope: "user". tools.prefix puts a prefix on all four names, like memory_recall, and their descriptions name each other by the prefixed names.
The model decides what to remember, so treat what these tools store as the user's notes, not as settings your code relies on. For a value your code needs, like the signature, write a tool of your own that stores it in the scope you choose.
Recap
@frontmcp/plugin-remembergives every toolthis.remember. RegisterRememberPlugin.init({ type: "memory" }), thenset(key, value, { scope, ttl }),get(key, { scope, defaultValue })andforget(key, { scope }).userscope keeps a value for the caller: a signed-in user's tokensub, or a static key. An anonymous caller has no identity to keep a value under, anduserscope refuses it.session, the default, is kept for a signed-in caller across requests and conversations under 2026-07-28, and refused to an anonymous one.toolworks the same way, with values for one tool only.globalis shared by every caller, and the only scope an anonymous caller can use. A refusal has the codeREMEMBER_IDENTITY_REQUIRED.ttlis in seconds; without one, a value lasts until it's forgotten, or for the plugin'sdefaultTTLwhen you set one. The memory store is lost on restart; use Redis or Vercel KV, and oneREMEMBER_SECRET, for a server that runs more than once.tools: { enabled: true }makes the plugin registerremember_this,recall,forgetandlist_memories, so the model can remember things. Limit their scopes withtools.allowedScopes: a call with another scope is refused withREMEMBER_SCOPE_NOT_ALLOWED.- Every option, store and method is in the Remember plugin reference.
Try some challenges
Each challenge runs hidden checks against your code. Edit the code, then press Check.
Challenge 1 of 4
Give each agent their own signature
set_signature and draft_reply use this.remember, and still sign every agent's replies with whichever signature was set last. Make each agent's signature their own, with the default for agents who haven't set one.
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";
import { RememberPlugin } from "@frontmcp/plugin-remember";
@Tool({
name: "set_signature",
description: "Set the signature your replies end with.",
inputSchema: { signature: z.string().max(80) },
})
export class SetSignature extends ToolContext {
async execute({ signature }: { signature: string }) {
await this.remember.set("signature", signature, { scope: "global" });
return { signature };
}
}
@Tool({
name: "draft_reply",
description: "Draft a reply to a support ticket, signed with your signature.",
inputSchema: { ticketId: z.string(), text: z.string() },
})
export class DraftReply extends ToolContext {
async execute({ ticketId, text }: { ticketId: string; text: string }) {
const signature = await this.remember.get("signature", { scope: "global", defaultValue: "The Help Desk" });
return { ticketId, reply: `${text}\n\n${signature}` };
}
}
@App({
id: "help-desk",
name: "Help Desk",
tools: [SetSignature, DraftReply],
plugins: [RememberPlugin.init({ type: "memory" })],
})
export class HelpDeskApp {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.