# Remembering Across Calls

> How FrontMCP tools keep values from one call to the next with @frontmcp/plugin-remember and this.remember, which scope to store a value in, what lasts for signed-in and anonymous callers under MCP 2026-07-28, how values expire, and the memory tools the model can use.

Source: https://frontmcp.dev/learn/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](https://frontmcp.dev/learn/authenticating-clients#requiring-a-shared-key-static-mode) 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:

```ts signature.app.ts active
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 {}
```

```ts server.ts
import { HelpDeskApp } from "./signature.app";

// Two support agents, each with a key of their own.
// In your project: @FrontMcp(config) export default class Server {}
export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDeskApp],
  auth: { mode: "static" as const, tokens: ["agent-nour-key", "agent-sam-key"] },
};
```

```ts call-as.ts
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./server";

let server: ReturnType<typeof FrontMcpInstance.createFetchHandler> | undefined;

/** Sends one tools/call with this key, as an MCP 2026-07-28 client would. Every call goes to the same server. */
export async function callAs(key: string, tool: string, args: Record<string, unknown> = {}) {
  server ??= FrontMcpInstance.createFetchHandler(config);
  const response = await (await server)(
    new Request("https://desk.example.com/", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "mcp-protocol-version": "2026-07-28",
        "mcp-method": "tools/call",
        "mcp-name": tool,
        authorization: `Bearer ${key}`,
      },
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: tool, arguments: args, _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } },
      }),
    }),
  );
  return (await response.json()).result;
}
```

```ts signature.test.ts
import { test, expect } from "@frontmcp/testing";
import { callAs } from "./call-as";

const NOUR = "agent-nour-key";
const SAM = "agent-sam-key";

test("the signature is there on the next call", async () => {
  await callAs(NOUR, "set_signature", { signature: "Nour, Tier 2 support" });
  const draft = await callAs(NOUR, "draft_reply", { ticketId: "T-1", text: "You can log in again." });
  expect(draft.structuredContent.reply).toBe("You can log in again.\n\nNour, Tier 2 support");
});

test("🚩 Sam's replies are signed with Nour's signature", async () => {
  await callAs(NOUR, "set_signature", { signature: "Nour, Tier 2 support" });
  const draft = await callAs(SAM, "draft_reply", { ticketId: "T-2", text: "Your invoice is fixed." });
  expect(draft.structuredContent.reply).toContain("Nour, Tier 2 support");
});
```

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](https://frontmcp.dev/learn/sharing-state-with-providers#state-in-a-module-variable) 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:

```bash
npm install @frontmcp/plugin-remember
```

Register 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:

```ts signature.app.ts active
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 {}
```

```ts server.ts
import { HelpDeskApp } from "./signature.app";

// Two support agents, each with a key of their own.
// In your project: @FrontMcp(config) export default class Server {}
export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDeskApp],
  auth: { mode: "static" as const, tokens: ["agent-nour-key", "agent-sam-key"] },
};
```

```ts call-as.ts
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./server";

let server: ReturnType<typeof FrontMcpInstance.createFetchHandler> | undefined;

/** Sends one tools/call with this key, as an MCP 2026-07-28 client would. Every call goes to the same server. */
export async function callAs(key: string, tool: string, args: Record<string, unknown> = {}) {
  server ??= FrontMcpInstance.createFetchHandler(config);
  const response = await (await server)(
    new Request("https://desk.example.com/", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "mcp-protocol-version": "2026-07-28",
        "mcp-method": "tools/call",
        "mcp-name": tool,
        authorization: `Bearer ${key}`,
      },
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: tool, arguments: args, _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } },
      }),
    }),
  );
  return (await response.json()).result;
}
```

```ts signature.test.ts
import { test, expect } from "@frontmcp/testing";
import { callAs } from "./call-as";

const NOUR = "agent-nour-key";
const SAM = "agent-sam-key";

test("each agent's signature is their own", async () => {
  await callAs(NOUR, "set_signature", { signature: "Nour, Tier 2 support" });
  const nours = await callAs(NOUR, "draft_reply", { ticketId: "T-1", text: "You can log in again." });
  const sams = await callAs(SAM, "draft_reply", { ticketId: "T-2", text: "Your invoice is fixed." });
  expect(nours.structuredContent.reply).toBe("You can log in again.\n\nNour, Tier 2 support");
  expect(sams.structuredContent.reply).toBe("Your invoice is fixed.\n\nThe Help Desk");
});

test("setting Sam's doesn't change Nour's", async () => {
  await callAs(SAM, "set_signature", { signature: "Sam, Billing" });
  const nours = await callAs(NOUR, "draft_reply", { ticketId: "T-1", text: "Done." });
  expect(nours.structuredContent.reply).toBe("Done.\n\nNour, Tier 2 support");
});

test("an anonymous caller can't keep a signature", async ({ mcp }) => {
  // The Playground's own server, whose callers don't sign in
  const result = await mcp.tools.call("set_signature", { signature: "Someone" });
  expect(result).toBeError("REMEMBER_IDENTITY_REQUIRED");
  expect(result.text()).toContain("Remember cannot use user scope without an authenticated user");
});
```

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, and `user` scope refuses it. `this.remember` throws a `RememberIdentityError`, which reaches the client as its message, with the code `REMEMBER_IDENTITY_REQUIRED`.

So a memory that belongs to a person needs the person to sign in. [Authenticating Clients](https://frontmcp.dev/learn/authenticating-clients) covers how.

> **Note**
Changed in 1.8.6: `user` scope used to accept an anonymous caller. Its `anon:` id is new on every request, so `set()` stored the value where nothing looks again, and `get()` found nothing, without an error. Now it's refused, like `session` scope.

> **Note**
`RememberPlugin.init()` with no argument, or `plugins: [RememberPlugin]` without `init()`, keeps values in memory. (Before FrontMCP 1.9.4, the class without `init()` left tools without `this.remember`.) Registered on one app, the plugin gives `this.remember` to that app's tools only: register it on `@FrontMcp` for every app. (Before FrontMCP 1.9, it reached every app's tools, which read and wrote the same values: [Caveats](https://frontmcp.dev/reference/plugins/remember#caveats).)

## 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](https://frontmcp.dev/reference/plugins/remember#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:

```ts desk.app.ts active
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 {}
```

```ts server.ts
import { HelpDeskApp } from "./desk.app";

// Two support agents, each with a key of their own.
// In your project: @FrontMcp(config) export default class Server {}
export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDeskApp],
  auth: { mode: "static" as const, tokens: ["agent-nour-key", "agent-sam-key"] },
};
```

```ts call-as.ts
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./server";

let server: ReturnType<typeof FrontMcpInstance.createFetchHandler> | undefined;

/** Sends one tools/call with this key, as an MCP 2026-07-28 client would. Every call goes to the same server. */
export async function callAs(key: string, tool: string, args: Record<string, unknown> = {}) {
  server ??= FrontMcpInstance.createFetchHandler(config);
  const response = await (await server)(
    new Request("https://desk.example.com/", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "mcp-protocol-version": "2026-07-28",
        "mcp-method": "tools/call",
        "mcp-name": tool,
        authorization: `Bearer ${key}`,
      },
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: tool, arguments: args, _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } },
      }),
    }),
  );
  return (await response.json()).result;
}
```

```ts scopes.test.ts
import { test, expect } from "@frontmcp/testing";
import { callAs } from "./call-as";

const NOUR = "agent-nour-key";
const SAM = "agent-sam-key";

test("global: every signed-in agent reads the banner", async () => {
  await callAs(NOUR, "set_banner", { text: "Login is down. We're on it." });
  expect((await callAs(SAM, "get_banner")).structuredContent).toEqual({ banner: "Login is down. We're on it." });
});

test("global: an anonymous caller can set and read one too", async ({ mcp }) => {
  await mcp.tools.call("set_banner", { text: "Exports are slow today." });
  expect((await mcp.tools.call("get_banner", {})).json()).toEqual({ banner: "Exports are slow today." });
});

test("session: kept for a signed-in agent across requests, and only for them", async () => {
  await callAs(NOUR, "work_on", { ticketId: "T-3" });
  expect((await callAs(NOUR, "current_ticket")).structuredContent).toEqual({ ticketId: "T-3" });
  expect((await callAs(SAM, "current_ticket")).structuredContent).toEqual({ ticketId: null });
});

test("session: an anonymous caller is refused", async ({ mcp }) => {
  const result = await mcp.tools.call("work_on", { ticketId: "T-1" });
  expect(result).toBeError("REMEMBER_IDENTITY_REQUIRED");
  expect(result.text()).toContain("Remember cannot use session or tool scope for an unauthenticated request");
});
```

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.

> **Pitfall: Under 2026-07-28, session doesn't mean a conversation**
The name suggests a value that ends with the conversation. MCP 2026-07-28 has no sessions, so FrontMCP keeps `session` and `tool` values for the signed-in caller instead: Nour's current ticket is still there tomorrow, in every conversation and every client she uses, until it's forgotten or expires. Give conversation-sized values a `ttl` (next section), put what belongs to a person in `user` scope, and remember that an anonymous caller can use only `global`.

Which scope, then:

- **`user`** for what belongs to a person: a signature, a language, the columns they like in a report.
- **`global`** for what everyone may see: a banner, a shared counter, the last time a sync ran. Never for anything personal, because every caller reads it.
- **`session`** for what a signed-in caller is in the middle of, with a `ttl`, knowing it lasts across their conversations.
- **`tool`** for a tool's own bookkeeping that no other tool should read or change, like which tickets `next_ticket` already 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:

```ts drafts.app.ts active
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 {}
```

```ts server.ts
import { HelpDeskApp } from "./drafts.app";

// Two support agents, each with a key of their own.
// In your project: @FrontMcp(config) export default class Server {}
export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDeskApp],
  auth: { mode: "static" as const, tokens: ["agent-nour-key", "agent-sam-key"] },
};
```

```ts call-as.ts
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./server";

let server: ReturnType<typeof FrontMcpInstance.createFetchHandler> | undefined;

/** Sends one tools/call with this key, as an MCP 2026-07-28 client would. Every call goes to the same server. */
export async function callAs(key: string, tool: string, args: Record<string, unknown> = {}) {
  server ??= FrontMcpInstance.createFetchHandler(config);
  const response = await (await server)(
    new Request("https://desk.example.com/", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "mcp-protocol-version": "2026-07-28",
        "mcp-method": "tools/call",
        "mcp-name": tool,
        authorization: `Bearer ${key}`,
      },
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: tool, arguments: args, _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } },
      }),
    }),
  );
  return (await response.json()).result;
}
```

```ts drafts.test.ts
import { test, expect } from "@frontmcp/testing";
import { callAs } from "./call-as";

const NOUR = "agent-nour-key";

/** Runs `fn` with the clock `hours` ahead. */
async function inHours<T>(hours: number, fn: () => Promise<T>) {
  const realNow = Date.now;
  const now = realNow();
  Date.now = () => now + hours * 60 * 60 * 1000;
  try {
    return await fn();
  } finally {
    Date.now = realNow;
  }
}

test("a draft is there the next morning", async () => {
  await callAs(NOUR, "save_draft", { ticketId: "T-1", text: "You can log in again." });
  const later = await inHours(20, () => callAs(NOUR, "get_draft", { ticketId: "T-1" }));
  expect(later.structuredContent).toEqual({ draft: "You can log in again." });
});

test("and gone after a day", async () => {
  await callAs(NOUR, "save_draft", { ticketId: "T-2", text: "Your invoice is fixed." });
  const later = await inHours(25, () => callAs(NOUR, "get_draft", { ticketId: "T-2" }));
  expect(later.structuredContent).toEqual({ draft: null });
});

test("sending a reply forgets its draft", async () => {
  await callAs(NOUR, "save_draft", { ticketId: "T-3", text: "Here's a new link." });
  expect((await callAs(NOUR, "send_reply", { ticketId: "T-3" })).structuredContent).toEqual({ sent: true, text: "Here's a new link." });
  expect((await callAs(NOUR, "get_draft", { ticketId: "T-3" })).structuredContent).toEqual({ draft: null });
});
```

`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`](https://frontmcp.dev/reference/plugins/remember#thisremember)).

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](https://frontmcp.dev/reference/plugins/remember#stores)).
- **Values are encrypted** with a secret the server derives its keys from. In production, set `REMEMBER_SECRET` to 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](https://frontmcp.dev/reference/plugins/remember#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`:

```ts assistant.app.ts active
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 {}
```

```ts server.ts
import { AssistantApp } from "./assistant.app";

// Two support agents, each with a key of their own.
// In your project: @FrontMcp(config) export default class Server {}
export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [AssistantApp],
  auth: { mode: "static" as const, tokens: ["agent-nour-key", "agent-sam-key"] },
};
```

```ts call-as.ts
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./server";

let server: ReturnType<typeof FrontMcpInstance.createFetchHandler> | undefined;

/** Sends one tools/call with this key, as an MCP 2026-07-28 client would. Every call goes to the same server. */
export async function callAs(key: string, tool: string, args: Record<string, unknown> = {}) {
  server ??= FrontMcpInstance.createFetchHandler(config);
  const response = await (await server)(
    new Request("https://desk.example.com/", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "mcp-protocol-version": "2026-07-28",
        "mcp-method": "tools/call",
        "mcp-name": tool,
        authorization: `Bearer ${key}`,
      },
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: tool, arguments: args, _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } },
      }),
    }),
  );
  return (await response.json()).result;
}
```

```ts memory-tools.test.ts
import { test, expect } from "@frontmcp/testing";
import { App, FrontMcpInstance } from "@frontmcp/sdk";
import { ForgetTool, ListMemoriesTool, RecallTool, RememberPlugin, RememberThisTool } from "@frontmcp/plugin-remember";
import { callAs } from "./call-as";

const NOUR = "agent-nour-key";
const SAM = "agent-sam-key";

test("the plugin registers the four memory tools, and the app lists none", async ({ mcp }) => {
  const names = (await mcp.tools.list()).map((t: { name: string }) => t.name).sort();
  expect(names).toEqual(["forget", "list_memories", "recall", "remember_this"]);
});

test("the tools' scope description says what an anonymous caller can't use", async ({ mcp }) => {
  const tools = await mcp.tools.list();
  const rememberThis = tools.find((t: { name: string }) => t.name === "remember_this");
  expect(rememberThis.inputSchema.properties.scope.description).toContain("user: the signed-in caller, across all of its sessions.");
  expect(rememberThis.inputSchema.properties.scope.description).toContain("An anonymous caller cannot use user scope, nor session or tool scope without a session.");
});

test("an anonymous caller can't list memories in user scope", async ({ mcp }) => {
  const result = await mcp.tools.call("list_memories", { scope: "user" });
  expect(result).toBeError("REMEMBER_IDENTITY_REQUIRED");
  expect(result.text()).toContain("Remember cannot use user scope without an authenticated user");
});

test("the model remembers and recalls things for the agent", async () => {
  const saved = await callAs(NOUR, "remember_this", { key: "timezone", value: "Europe/Paris", scope: "user" });
  expect(saved.structuredContent).toEqual({ success: true, key: "timezone", scope: "user" });
  expect((await callAs(NOUR, "recall", { key: "timezone", scope: "user" })).structuredContent).toMatchObject({ found: true, value: "Europe/Paris" });
  expect((await callAs(SAM, "recall", { key: "timezone", scope: "user" })).structuredContent).toMatchObject({ found: false });
  expect((await callAs(NOUR, "list_memories", { scope: "user" })).structuredContent).toMatchObject({ keys: ["timezone"], count: 1 });
});

test("another scope is refused, the default included, and the message names the allowed scope", async () => {
  const refused = await callAs(NOUR, "remember_this", { key: "timezone", value: "Europe/Paris" });
  expect(refused.isError).toBe(true);
  expect(refused._meta.code).toBe("REMEMBER_SCOPE_NOT_ALLOWED");
  expect(refused.content[0].text).toBe("Scope 'session' is not allowed. Allowed scopes: user");
  const recalled = await callAs(NOUR, "recall", { key: "timezone", scope: "global" });
  expect(recalled._meta.code).toBe("REMEMBER_SCOPE_NOT_ALLOWED");
});

test("tools.prefix puts a prefix on the names", async () => {
  @App({ id: "assistant", name: "Assistant", plugins: [RememberPlugin.init({ type: "memory", tools: { enabled: true, prefix: "memory_" } })] })
  class Prefixed {}

  const server = await FrontMcpInstance.createDirect({ info: { name: "help-desk", version: "1.0.0" }, apps: [Prefixed] });
  const { tools } = await server.listTools();
  expect(tools.map((t) => t.name).sort()).toEqual(["memory_forget", "memory_list_memories", "memory_recall", "memory_remember_this"]);
  expect(tools.find((t) => t.name === "memory_recall")?.description).toContain("saved with memory_remember_this");
  await server.dispose();
});

test("an app that lists the tool classes too has every tool twice", async () => {
  @App({
    id: "assistant",
    name: "Assistant",
    tools: [RememberThisTool, RecallTool, ForgetTool, ListMemoriesTool],
    plugins: [RememberPlugin.init({ type: "memory", tools: { enabled: true } })],
  })
  class Twice {}

  const server = await FrontMcpInstance.createDirect({ info: { name: "help-desk", version: "1.0.0" }, apps: [Twice] });
  const names = (await server.listTools()).tools.map((t) => t.name);
  expect(names).toHaveLength(8);
  expect(names).toContain("assistant:recall");
  expect(names).toContain("remember:recall");
  await server.dispose();
});
```

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.

> **Note**
Changed in 1.8.6: `list_memories` for an anonymous caller in `user` scope answered `{ keys: [], count: 0 }`, as if there were nothing to list. It's an error now.

> **Note**
Changed in 1.8.7: the plugin registers these tools itself, and a refused scope is a public error, so in production the model reads the message instead of `Internal FrontMCP error…`. Before, the plugin added no tools: the app listed `RememberThisTool` and the other classes in its `tools`, and `tools` options passed to `init()` stopped the server with `TypeError: list is not iterable`, so they went through `useFactory`. An app that still lists the classes and sets `tools.enabled` has every tool twice: drop one of the two.

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-remember` gives every tool `this.remember`. Register `RememberPlugin.init({ type: "memory" })`, then `set(key, value, { scope, ttl })`, `get(key, { scope, defaultValue })` and `forget(key, { scope })`.
- `user` scope keeps a value for the caller: a signed-in user's token `sub`, or a static key. An anonymous caller has no identity to keep a value under, and `user` scope 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. `tool` works the same way, with values for one tool only. `global` is shared by every caller, and the only scope an anonymous caller can use. A refusal has the code `REMEMBER_IDENTITY_REQUIRED`.
- `ttl` is in seconds; without one, a value lasts until it's forgotten, or for the plugin's `defaultTTL` when you set one. The memory store is lost on restart; use Redis or Vercel KV, and one `REMEMBER_SECRET`, for a server that runs more than once.
- `tools: { enabled: true }` makes the plugin register `remember_this`, `recall`, `forget` and `list_memories`, so the model can remember things. Limit their scopes with `tools.allowedScopes`: a call with another scope is refused with `REMEMBER_SCOPE_NOT_ALLOWED`.
- Every option, store and method is in the [Remember plugin reference](https://frontmcp.dev/reference/plugins/remember).

## Try some challenges

Each challenge runs hidden checks against your code. Edit the code, then press **Check**.

### Challenge: 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.

```ts signature.app.ts active
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 {}
```

```ts signature.app.ts solution
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" });
    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 {}
```

```ts server.ts
import { HelpDeskApp } from "./signature.app";

// Two support agents, each with a key of their own.
export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDeskApp],
  auth: { mode: "static" as const, tokens: ["agent-nour-key", "agent-sam-key"] },
};
```

```ts call-as.ts
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./server";

let server: ReturnType<typeof FrontMcpInstance.createFetchHandler> | undefined;

/** Sends one tools/call with this key, as an MCP 2026-07-28 client would. Every call goes to the same server. */
export async function callAs(key: string, tool: string, args: Record<string, unknown> = {}) {
  server ??= FrontMcpInstance.createFetchHandler(config);
  const response = await (await server)(
    new Request("https://desk.example.com/", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "mcp-protocol-version": "2026-07-28",
        "mcp-method": "tools/call",
        "mcp-name": tool,
        authorization: `Bearer ${key}`,
      },
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: tool, arguments: args, _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } },
      }),
    }),
  );
  return (await response.json()).result;
}
```

```ts signature.test.ts hidden
import { test, expect } from "@frontmcp/testing";
import { callAs } from "./call-as";

const NOUR = "agent-nour-key";
const SAM = "agent-sam-key";

test("an agent's replies are signed with their own signature", async () => {
  await callAs(NOUR, "set_signature", { signature: "Nour, Tier 2 support" });
  const draft = await callAs(NOUR, "draft_reply", { ticketId: "T-1", text: "Fixed." });
  expect(draft.structuredContent.reply).toBe("Fixed.\n\nNour, Tier 2 support");
});

test("an agent without a signature gets the default", async () => {
  const draft = await callAs(SAM, "draft_reply", { ticketId: "T-2", text: "Fixed." });
  expect(draft.structuredContent.reply).toBe("Fixed.\n\nThe Help Desk");
});

test("one agent's change doesn't reach another's replies", async () => {
  await callAs(SAM, "set_signature", { signature: "Sam, Billing" });
  const draft = await callAs(NOUR, "draft_reply", { ticketId: "T-1", text: "Fixed." });
  expect(draft.structuredContent.reply).toBe("Fixed.\n\nNour, Tier 2 support");
});
```

**Hint:**
The code already stores the signature, and every agent reads it back. The question is who shares it.

**Solution:**
`global` scope is one value for every caller, so the last signature set was on everyone's replies. `scope: "user"` in both `set()` and `get()` keeps a value for each caller: the checks sign in with Nour's and Sam's keys, so each has their own, and Sam gets the default until he sets his. Both calls must use the same scope, or `draft_reply` looks in a different place from where `set_signature` wrote.

### Challenge: Show the outage banner to everyone
During an outage, an agent sets a banner that every caller should see, including customers who haven't signed in. Right now the banner is only there for the agent who set it, and an anonymous caller's `get_banner` fails. Make every caller read the same banner.

```ts banner.app.ts active
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);
    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", { defaultValue: null }) };
  }
}

@App({
  id: "help-desk",
  name: "Help Desk",
  tools: [SetBanner, GetBanner],
  plugins: [RememberPlugin.init({ type: "memory" })],
})
export class HelpDeskApp {}
```

```ts banner.app.ts solution
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 }) };
  }
}

@App({
  id: "help-desk",
  name: "Help Desk",
  tools: [SetBanner, GetBanner],
  plugins: [RememberPlugin.init({ type: "memory" })],
})
export class HelpDeskApp {}
```

```ts server.ts
import { HelpDeskApp } from "./banner.app";

// Two support agents, each with a key of their own.
export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDeskApp],
  auth: { mode: "static" as const, tokens: ["agent-nour-key", "agent-sam-key"] },
};
```

```ts call-as.ts
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./server";

let server: ReturnType<typeof FrontMcpInstance.createFetchHandler> | undefined;

/** Sends one tools/call with this key, as an MCP 2026-07-28 client would. Every call goes to the same server. */
export async function callAs(key: string, tool: string, args: Record<string, unknown> = {}) {
  server ??= FrontMcpInstance.createFetchHandler(config);
  const response = await (await server)(
    new Request("https://desk.example.com/", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "mcp-protocol-version": "2026-07-28",
        "mcp-method": "tools/call",
        "mcp-name": tool,
        authorization: `Bearer ${key}`,
      },
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: tool, arguments: args, _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } },
      }),
    }),
  );
  return (await response.json()).result;
}
```

```ts banner.test.ts hidden
import { test, expect } from "@frontmcp/testing";
import { callAs } from "./call-as";

test("another agent sees the banner an agent set", async () => {
  await callAs("agent-nour-key", "set_banner", { text: "Login is down. We're on it." });
  expect((await callAs("agent-sam-key", "get_banner")).structuredContent).toEqual({ banner: "Login is down. We're on it." });
});

test("a caller who isn't signed in can post and read a banner too", async ({ mcp }) => {
  expect(await mcp.tools.call("set_banner", { text: "Exports are slow today." })).toBeSuccessful();
  const result = await mcp.tools.call("get_banner", {});
  expect(result).toBeSuccessful();
  expect(result.json()).toEqual({ banner: "Exports are slow today." });
});
```

**Hint:**
Neither call names a scope, so both use the default. What does the default keep under MCP 2026-07-28, and for whom?

**Solution:**
Without a `scope`, `this.remember` uses `session`, which under 2026-07-28 belongs to the signed-in caller: Nour's banner was Nour's alone, and an anonymous caller was refused with `RememberIdentityError`. `scope: "global"` in both calls keeps one banner for everyone. That's right for a banner, because it's meant for every caller, and wrong for anything personal.

### Challenge: Let drafts expire after a day
Reply drafts are kept forever, and agents keep finding week-old drafts for tickets that were solved long ago. Keep a draft for one day after it's saved, and no longer.

```ts drafts.app.ts active
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.", 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" });
    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 }) };
  }
}

@App({
  id: "help-desk",
  name: "Help Desk",
  tools: [SaveDraft, GetDraft],
  plugins: [RememberPlugin.init({ type: "memory" })],
})
export class HelpDeskApp {}
```

```ts drafts.app.ts solution
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 }) };
  }
}

@App({
  id: "help-desk",
  name: "Help Desk",
  tools: [SaveDraft, GetDraft],
  plugins: [RememberPlugin.init({ type: "memory" })],
})
export class HelpDeskApp {}
```

```ts server.ts
import { HelpDeskApp } from "./drafts.app";

// Two support agents, each with a key of their own.
export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDeskApp],
  auth: { mode: "static" as const, tokens: ["agent-nour-key", "agent-sam-key"] },
};
```

```ts call-as.ts
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./server";

let server: ReturnType<typeof FrontMcpInstance.createFetchHandler> | undefined;

/** Sends one tools/call with this key, as an MCP 2026-07-28 client would. Every call goes to the same server. */
export async function callAs(key: string, tool: string, args: Record<string, unknown> = {}) {
  server ??= FrontMcpInstance.createFetchHandler(config);
  const response = await (await server)(
    new Request("https://desk.example.com/", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "mcp-protocol-version": "2026-07-28",
        "mcp-method": "tools/call",
        "mcp-name": tool,
        authorization: `Bearer ${key}`,
      },
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: tool, arguments: args, _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } },
      }),
    }),
  );
  return (await response.json()).result;
}
```

```ts drafts.test.ts hidden
import { test, expect } from "@frontmcp/testing";
import { callAs } from "./call-as";

const NOUR = "agent-nour-key";

async function inHours<T>(hours: number, fn: () => Promise<T>) {
  const realNow = Date.now;
  const now = realNow();
  Date.now = () => now + hours * 60 * 60 * 1000;
  try {
    return await fn();
  } finally {
    Date.now = realNow;
  }
}

test("a draft is still there after 23 hours", async () => {
  await callAs(NOUR, "save_draft", { ticketId: "T-1", text: "You can log in again." });
  const later = await inHours(23, () => callAs(NOUR, "get_draft", { ticketId: "T-1" }));
  expect(later.structuredContent).toEqual({ draft: "You can log in again." });
});

test("a draft is gone after 25 hours", async () => {
  await callAs(NOUR, "save_draft", { ticketId: "T-2", text: "Your invoice is fixed." });
  const later = await inHours(25, () => callAs(NOUR, "get_draft", { ticketId: "T-2" }));
  expect(later.structuredContent).toEqual({ draft: null });
});
```

**Hint:**
`set()` takes one more option. It's in seconds.

**Solution:**
`ttl: 24 * 60 * 60` stores the draft for 86,400 seconds, a day. After that `get()` treats it as missing and returns the `defaultValue`, `null`, and the store drops it. The description tells the model drafts are kept for a day, so it can tell the agent.

### Challenge: Let the model remember, for users only
Give the assistant the four memory tools, so the model can remember things for a signed-in agent and recall them later. Allow only `user` scope: a call with any other scope, or none, must be refused.

```ts assistant.app.ts active
import { App, Tool, ToolContext } from "@frontmcp/sdk";
import { RememberPlugin } from "@frontmcp/plugin-remember";

@Tool({ name: "whats_new", description: "What's new in the help desk this week.", inputSchema: {} })
export class WhatsNew extends ToolContext {
  async execute() {
    return { news: ["Tickets can now be merged."] };
  }
}

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

```ts assistant.app.ts solution
import { App, Tool, ToolContext } from "@frontmcp/sdk";
import { RememberPlugin } from "@frontmcp/plugin-remember";

@Tool({ name: "whats_new", description: "What's new in the help desk this week.", inputSchema: {} })
export class WhatsNew extends ToolContext {
  async execute() {
    return { news: ["Tickets can now be merged."] };
  }
}

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

```ts server.ts
import { AssistantApp } from "./assistant.app";

// Two support agents, each with a key of their own.
export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [AssistantApp],
  auth: { mode: "static" as const, tokens: ["agent-nour-key", "agent-sam-key"] },
};
```

```ts call-as.ts
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./server";

let server: ReturnType<typeof FrontMcpInstance.createFetchHandler> | undefined;

/** Sends one tools/call with this key, as an MCP 2026-07-28 client would. Every call goes to the same server. */
export async function callAs(key: string, tool: string, args: Record<string, unknown> = {}) {
  server ??= FrontMcpInstance.createFetchHandler(config);
  const response = await (await server)(
    new Request("https://desk.example.com/", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "mcp-protocol-version": "2026-07-28",
        "mcp-method": "tools/call",
        "mcp-name": tool,
        authorization: `Bearer ${key}`,
      },
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: tool, arguments: args, _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } },
      }),
    }),
  );
  return (await response.json()).result;
}
```

```ts memory.test.ts hidden
import { test, expect } from "@frontmcp/testing";
import { callAs } from "./call-as";

const NOUR = "agent-nour-key";

test("`remember_this`, `recall`, `forget` and `list_memories` are listed", async ({ mcp }) => {
  const names = (await mcp.tools.list()).map((t: { name: string }) => t.name).sort();
  expect(names).toEqual(["forget", "list_memories", "recall", "remember_this", "whats_new"]);
});

test("the model can remember something for an agent and recall it", async () => {
  await callAs(NOUR, "remember_this", { key: "team", value: "Tier 2", scope: "user" });
  expect((await callAs(NOUR, "recall", { key: "team", scope: "user" })).structuredContent).toMatchObject({ found: true, value: "Tier 2" });
});

test("a call without `scope: \"user\"` is refused", async () => {
  const withoutScope = await callAs(NOUR, "remember_this", { key: "team", value: "Tier 2" });
  const inGlobal = await callAs(NOUR, "remember_this", { key: "team", value: "Tier 2", scope: "global" });
  expect(withoutScope.isError).toBe(true);
  expect(inGlobal.isError).toBe(true);
});
```

**Hint:**
The plugin registers the memory tools itself, when you ask it to. Two of its `tools` options do the job: one turns them on, the other limits their scopes.

**Solution:**
`tools: { enabled: true }` makes the plugin register `remember_this`, `recall`, `forget` and `list_memories` next to `whats_new`, and `allowedScopes: ["user"]` limits them to `user` scope. A call without a scope asks for `session`, which isn't allowed, so the model is told to use `user`.
