> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentfront.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Remember Plugin

> Encrypted session memory with scoped storage for AI agent interactions.

The Remember Plugin provides encrypted session memory for FrontMCP servers, enabling AI agents to remember context across conversations.

## Why Use Remember?

<CardGroup cols={2}>
  <Card title="Session Memory" icon="brain">
    Store user preferences, conversation context, and state across tool invocations
  </Card>

  <Card title="Encrypted Storage" icon="lock">
    AES-256-GCM encryption protects sensitive data at rest
  </Card>

  <Card title="Multiple Backends" icon="database">
    Redis, Vercel KV, or in-memory storage for different deployment needs
  </Card>

  <Card title="Scoped Storage" icon="folder-tree">
    Organize data by session, user, tool, or global scope
  </Card>
</CardGroup>

<Info>
  For tool approval workflows (Claude Code-style permissions), see the [Approval Plugin](/frontmcp/plugins/approval-plugin).
</Info>

## Installation

```bash theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
npm install @frontmcp/plugin-remember
```

## How It Works

<Steps>
  <Step title="Context Extension">
    The plugin adds `this.remember` to all execution contexts (ToolContext, AgentContext, etc.)
  </Step>

  <Step title="Scoped Storage">
    Data is organized by scope (session, user, tool, global) with automatic key prefixing
  </Step>

  <Step title="Encryption">
    Values are encrypted before storage using keys derived from session/user identifiers
  </Step>

  <Step title="TTL Management">
    Entries expire automatically based on configured TTL or scope lifetime
  </Step>
</Steps>

<Info>
  All stored values are **encrypted by default** using AES-256-GCM. Keys are derived using HKDF-SHA256 from session and user identifiers.
</Info>

***

## Quick Start

### Basic Setup

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
import { FrontMcp, App } from '@frontmcp/sdk';
import { RememberPlugin } from '@frontmcp/plugin-remember';

@App({
  id: 'my-app',
  name: 'My App',
  plugins: [RememberPlugin], // Default: memory store, encryption enabled
  tools: [
    /* your tools */
  ],
})
class MyApp {}

@FrontMcp({
  info: { name: 'My Server', version: '1.0.0' },
  apps: [MyApp],
})
export default class Server {}
```

### Using Memory in Tools

The plugin extends all execution contexts with `this.remember`:

<CodeGroup>
  ```ts Class-based tool theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  import { Tool, ToolContext } from '@frontmcp/sdk';
  import { z } from '@frontmcp/sdk';

  @Tool({
    name: 'personalize-greeting',
    description: 'Greet user with their preferences',
    inputSchema: { name: z.string() },
  })
  export default class PersonalizeGreetingTool extends ToolContext {
    async execute(input: { name: string }) {
      // Get remembered preference (or default)
      const theme = await this.remember.get('theme', { defaultValue: 'light' });

      // Store for future use
      await this.remember.set('last_greeted', input.name);

      return `Hello ${input.name}! Your theme is ${theme}.`;
    }
  }
  ```

  ```ts Function-based tool theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  import { tool } from '@frontmcp/sdk';
  import { z } from '@frontmcp/sdk';

  export const PersonalizeGreeting = tool({
    name: 'personalize-greeting',
    description: 'Greet user with their preferences',
    inputSchema: { name: z.string() },
  })(async (input, ctx) => {
    // Access remember through context
    const theme = await ctx.remember.get('theme', { defaultValue: 'light' });
    await ctx.remember.set('last_greeted', input.name);

    return `Hello ${input.name}! Your theme is ${theme}.`;
  });
  ```
</CodeGroup>

***

## Memory Scopes

Memory is organized into four scopes with different visibility and lifetime:

| Scope | Description | Use Case |
| - | - | - |
| `session` | The verified session (default); without one, the signed-in principal, across its requests | Temporary state, conversation context |
| `user` | The signed-in user, across all of its sessions (refused for an anonymous caller) | User preferences, settings |
| `tool` | A specific tool plus the `session` identity | Tool-specific cache |
| `global` | Shared by every caller, across all sessions and users | Application-wide configuration |

<Warning>
  **`session`, `tool`, and `user` scopes require a per-client identity.**

  `session` and `tool` memory belong to the session the server verified for the request, never to
  an `mcp-session-id` the client merely sends. A request without a verified session has no session
  identity: a stateless HTTP transport injects the same session id (`__stateless__`) into every
  request, MCP 2026-07-28 has no sessions, and an `mcp-session-id` the server did not verify could be
  anyone's. `session` and `tool` scope then fall back to the authenticated principal (the memory
  lasts across that principal's requests), and an **unauthenticated request without a verified
  session is refused** with a `RememberIdentityError` rather than being given a namespace it would
  share with other clients. `user` scope is likewise refused when there is no authenticated user, and an anonymous
  subject counts as none: the `anon:<id>` client id the SDK gives an anonymous caller is made up for one session (or for
  each request without one), so `user` memory kept under it would never be found again. The refusal reaches the client as
  its message (`RememberIdentityError` is a public MCP error, code `REMEMBER_IDENTITY_REQUIRED`), in production too.

  If the data really is shared across clients, say so with `scope: 'global'`. Otherwise
  authenticate the request or use a stateful transport.
</Warning>

### Using Scopes

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
// Session scope (default)
await this.remember.set('temp_token', 'xyz');
await this.remember.get('temp_token');

// User scope - persists across sessions
await this.remember.set('language', 'en', { scope: 'user' });
const lang = await this.remember.get('language', { scope: 'user' });

// Tool scope - isolated per tool
await this.remember.set('last_query', query, { scope: 'tool' });

// Global scope - shared everywhere
await this.remember.set('maintenance_mode', true, { scope: 'global' });
```

***

## Data Structure

### Entry Format

Each stored value is wrapped in an entry with metadata:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
interface RememberEntry<T> {
  value: T;                           // The stored value
  brand?: PayloadBrandType;           // Semantic type hint
  createdAt: number;                  // Unix timestamp (ms)
  updatedAt: number;                  // Unix timestamp (ms)
  expiresAt?: number;                 // Expiration timestamp (ms)
  metadata?: Record<string, unknown>; // Custom metadata
}
```

### Branded Payloads

Use brands to categorize stored data semantically:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
// Store with brand
await this.remember.set('theme', 'dark', {
  scope: 'user',
  brand: 'preference',
});

// Available brands
type PayloadBrandType =
  | 'approval'      // Approval state (use with ApprovalPlugin)
  | 'preference'    // User preferences
  | 'cache'         // Cached data
  | 'state'         // Application state
  | 'conversation'  // Conversation context
  | 'custom';       // Custom data
```

Brands enable filtering and batch operations by category:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
// List all preferences
const prefKeys = await this.remember.list({
  scope: 'user',
  pattern: '*',
});
```

***

## Storage Options

### In-Memory (Default)

Best for: Single-instance deployments, development, non-persistent data

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
RememberPlugin.init({
  type: 'memory',
  defaultTTL: 3600, // 1 hour
});
```

<Warning>
  Memory storage resets when the process restarts. Not shared across instances, and not shared between servers in one
  process either: every server gets its own store, even when several servers are built from the same app class or
  `RememberPlugin.init()` result, so a `global` value one server stores is not visible to another.
</Warning>

### Redis (Recommended for Production)

Best for: Multi-instance deployments, persistent memory, production

<CodeGroup>
  ```ts Redis connection config theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  RememberPlugin.init({
    type: 'redis',
    defaultTTL: 86400, // 1 day
    config: {
      host: '127.0.0.1',
      port: 6379,
      password: process.env.REDIS_PASSWORD,
      db: 0,
    },
  });
  ```

  ```ts Reuse existing Redis client theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  import { Redis } from 'ioredis';

  const redis = new Redis({
    host: 'redis.example.com',
    port: 6379,
  });

  RememberPlugin.init({
    type: 'redis-client',
    client: redis,
  });
  ```
</CodeGroup>

### Vercel KV

Best for: Vercel deployments, serverless environments

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
RememberPlugin.init({
  type: 'vercel-kv',
  // Uses KV_REST_API_URL and KV_REST_API_TOKEN env vars by default
});
```

### Global Store

Use the store configuration from `@FrontMcp` decorator:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
RememberPlugin.init({
  type: 'global-store', // Uses redis/vercel-kv from FrontMcp config
});
```

***

## Encryption

All stored values are encrypted by default using AES-256-GCM.

### How Keys Are Derived

1. A master secret is derived from the `REMEMBER_SECRET` environment variable (or auto-generated and persisted)
2. Per-entry keys are derived using HKDF-SHA256 over that secret plus the scope's identity
3. Each entry gets a unique key based on its scope and identity

Every scope mixes in the master secret, `session` and `tool` included. A session id is not a
secret -- the client knows it and it travels in the `mcp-session-id` header -- so a key derived
from it alone could be recomputed by anyone who had seen it.

<Warning>
  Because the key now depends on the master secret, **every instance sharing a store must use
  the same `REMEMBER_SECRET`**. Without it each instance auto-generates its own and cannot
  decrypt entries written by the others.
</Warning>

### Upgrading past the key-derivation change

Two changes moved data written by earlier versions:

* **Key derivation.** `session` and `tool` keys now mix in the master secret, so ciphertext
  written before the change no longer decrypts.
* **Namespace encoding.** `session`, `tool` and `user` prefixes now percent-encode every
  variable component, so an identity containing a character `encodeURIComponent` escapes --
  a `:` in a user id, for instance -- now lives under a different key.

Both failures used to be **silent**: decryption returns `null` and a moved key simply misses,
so the value reads as absent rather than raising.

These three scopes are therefore stored under a `v2:` segment
(`remember:v2:session:<identity>:<key>`), and the plugin **purges the pre-`v2` entries
automatically**, logging a warning with the number removed. The version segment is what makes
that safe: a purge pattern of `remember:session:*` cannot match a live `remember:v2:session:*`
key, not even one written concurrently by another instance.

`global` is not versioned and is not purged -- neither its keys nor its key derivation changed.

### When the purge runs

Twenty-four hours after the **fleet** first reached the `v2:` layout -- not after this process
started -- and never on the request path.

The first instance to reach the store stamps a marker at `<keyPrefix>__layout__`:

```json theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
{ "version": 2, "firstSeenAt": 1758457200000 }
```

Every instance reads that marker, and sweeps only once it is older than the window; until then
it re-arms for the remainder. Keeping the clock in the store rather than in the process is what
makes the window mean something. A process-local timer restarts on every deploy and every
crash, so it never converges on "the fleet has been on `v2:` for a while", and an instance that
booted early would fire on its own schedule no matter when the last old instance drained. The
marker is written once and never overwritten, so a later instance cannot reset it, and whichever
instance is alive when the window passes completes the sweep.

The window has to outlast two things: the rollout, and the period in which a bad deploy gets
rolled back. The second is the one that matters -- a rollback after the sweep makes the old
fleet permanent again with its memory already deleted. Waiting costs nothing, because the
entries are unreadable the whole time. Tune it with `legacyPurgeDelayMs`:

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
RememberPlugin.init({
  type: 'global-store',
  legacyPurgeDelayMs: 259_200_000, // 3 days, for a long canary
});
```

<Note>
  **Serverless and edge runtimes.** An invocation that ends before the timer fires never
  purges, and the timer is unreferenced so it will not hold a process open. That is the safe
  outcome -- the entries are inert either way. Clear them with the commands below if you want
  the storage back.
</Note>

<Note>
  If the marker cannot be read or written -- a permission error, or a value that is not the
  expected JSON -- the purge stands down and deletes nothing. It never treats a missing clock
  as licence to delete.
</Note>

If you would rather migrate the data yourself, opt out entirely:

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
RememberPlugin.init({ type: 'global-store', skipLegacyPurge: true });
```

```bash theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
# Redis example -- adjust for your keyPrefix
redis-cli --scan --pattern 'remember:session:*' | xargs -r redis-cli del
redis-cli --scan --pattern 'remember:tool:*'    | xargs -r redis-cli del
redis-cli --scan --pattern 'remember:user:*'    | xargs -r redis-cli del
```

### Secret Persistence

In **development**, the plugin automatically generates and persists an encryption secret to `.frontmcp/remember-secret.json`:

```json theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
{
  "secret": "base64url-encoded-32-byte-secret",
  "createdAt": 1704067200000,
  "version": 1
}
```

This enables consistent encryption across process restarts during development without manual configuration.

<Warning>
  In **production**, set the `REMEMBER_SECRET` environment variable instead:

  ```bash theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  # Generate a secure secret
  openssl rand -base64 32

  # Set in environment
  export REMEMBER_SECRET="your-generated-secret"
  ```

  File-based persistence is disabled in production by default for security. If none of
  `REMEMBER_SECRET`, `MCP_MEMORY_SECRET` or `MCP_SESSION_SECRET` is set there, the plugin falls back
  to a random in-memory secret and logs a warning once: encrypted memory is then unreadable after a
  restart and by any other instance that shares the store.
</Warning>

### Configuration

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
RememberPlugin.init({
  type: 'redis',
  encryption: {
    enabled: true, // default
    customKey: process.env.CUSTOM_ENCRYPTION_KEY, // optional override
  },
  config: { host: 'localhost', port: 6379 },
});
```

***

## API Reference

### RememberAccessor Methods

<ParamField path="set(key, value, options?)" type="Promise<void>">
  Store a value with optional scope, TTL, and metadata

  ```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  await this.remember.set('key', { data: 'value' }, {
    scope: 'user',
    ttl: 3600, // seconds
    brand: 'preference',
    metadata: { source: 'api' },
  });
  ```
</ParamField>

<ParamField path="get<T>(key, options?)" type="Promise<T | undefined>">
  Retrieve a value with optional default

  ```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  const value = await this.remember.get('key', {
    scope: 'user',
    defaultValue: 'fallback',
  });
  ```
</ParamField>

<ParamField path="getEntry<T>(key, options?)" type="Promise<RememberEntry<T> | undefined>">
  Retrieve the full entry including metadata, timestamps, and brand

  ```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  const entry = await this.remember.getEntry('key', { scope: 'user' });
  if (entry) {
    console.log('Created:', entry.createdAt);
    console.log('Updated:', entry.updatedAt);
    console.log('Brand:', entry.brand);
    console.log('Value:', entry.value);
  }
  ```
</ParamField>

<ParamField path="update<T>(key, value, options?)" type="Promise<boolean>">
  Update an existing value while preserving metadata. Returns `false` if key doesn't exist.

  ```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  const updated = await this.remember.update('counter', 42, {
    scope: 'session',
    ttl: 3600,
  });
  if (!updated) {
    // Key didn't exist, create it instead
    await this.remember.set('counter', 42);
  }
  ```
</ParamField>

<ParamField path="knows(key, options?)" type="Promise<boolean>">
  Check if a key exists

  ```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  if (await this.remember.knows('onboarded', { scope: 'user' })) {
    // Skip onboarding
  }
  ```
</ParamField>

<ParamField path="forget(key, options?)" type="Promise<void>">
  Delete a key

  ```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  await this.remember.forget('temp_token', { scope: 'session' });
  ```
</ParamField>

<ParamField path="list(options?)" type="Promise<string[]>">
  List keys with optional pattern matching

  ```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  const keys = await this.remember.list({
    scope: 'session',
    pattern: 'user:*',
  });
  ```
</ParamField>

***

## LLM-Accessible Tools

Enable built-in tools that let the LLM manage memory directly:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
RememberPlugin.init({
  type: 'redis',
  tools: {
    enabled: true,
    allowedScopes: ['session', 'user'], // Restrict which scopes LLM can access
    prefix: 'memory_', // Tool name prefix
  },
  config: { host: 'localhost', port: 6379 },
});
```

This exposes `memory_remember_this`, `memory_recall`, `memory_forget`, and `memory_list_memories`. Without `prefix` they are `remember_this`, `recall`, `forget` and `list_memories`; with `enabled` unset or `false` no memory tool is registered. Each tool's description names its prefixed siblings, and a scope outside `allowedScopes` is rejected with a public `REMEMBER_SCOPE_NOT_ALLOWED` error (HTTP 400) whose message lists the allowed scopes, so the model can retry. That includes the default `session` scope when a call omits `scope` and `allowedScopes` does not list it; the refusal used to surface as `Internal FrontMCP error` in production.

Each takes an optional `scope` (default `session`), described to the model the same way in all four tools:

| `scope` | What the model is told |
| - | - |
| `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 `recall` does not see what `remember_this` stored in `tool` scope |
| `global` | Shared by every caller |

An anonymous caller cannot use `user`, nor `session` or `tool` without a session. None of these lifetimes is "until
disconnect" or "forever": entries last until they are forgotten or their `ttl` runs out, and `session` memory without a
session belongs to the signed-in caller rather than ending with a connection.

***

## Best Practices

<AccordionGroup>
  <Accordion title="1. Use Appropriate Scopes">
    * **Session**: Temporary data, current conversation context
    * **User**: Preferences, settings that should persist
    * **Tool**: Tool-specific cache to avoid scope pollution
    * **Global**: Only for true application-wide settings
  </Accordion>

  <Accordion title="2. Set TTLs for Sensitive Data">
    Don't store sensitive data forever:

    ```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    await this.remember.set('auth_token', token, {
      ttl: 300, // 5 minutes
      scope: 'session',
    });
    ```
  </Accordion>

  <Accordion title="3. Use Redis for Production">
    Redis provides:

    * Persistence across restarts
    * Sharing across multiple server instances
    * Better memory management with eviction policies
  </Accordion>

  <Accordion title="4. Set REMEMBER_SECRET in Production">
    ```bash theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    # Generate a secure secret
    openssl rand -base64 32

    # Set in environment
    export REMEMBER_SECRET="your-generated-secret"
    ```

    <Info>
      In development, the secret is auto-generated and stored in `.frontmcp/remember-secret.json`. Add this file to `.gitignore`.
    </Info>
  </Accordion>
</AccordionGroup>

***

## Complete Example

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk';
import { RememberPlugin } from '@frontmcp/plugin-remember';
import { z } from '@frontmcp/sdk';

// Configure with Redis
const rememberPlugin = RememberPlugin.init({
  type: 'redis',
  defaultTTL: 86400, // 1 day
  encryption: { enabled: true },
  config: {
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT || '6379'),
    password: process.env.REDIS_PASSWORD,
  },
});

// Tool using memory
@Tool({
  name: 'set-preferences',
  description: 'Set user preferences',
  inputSchema: {
    theme: z.enum(['light', 'dark']),
    language: z.string(),
  },
})
class SetPreferencesTool extends ToolContext {
  async execute(input: { theme: string; language: string }) {
    await this.remember.set('theme', input.theme, { scope: 'user' });
    await this.remember.set('language', input.language, { scope: 'user' });

    return { success: true, message: 'Preferences saved' };
  }
}

@App({
  id: 'user-management',
  name: 'User Management',
  plugins: [rememberPlugin],
  tools: [SetPreferencesTool],
})
class UserManagementApp {}

@FrontMcp({
  info: { name: 'User Server', version: '1.0.0' },
  apps: [UserManagementApp],
  http: { port: 3000 },
})
export default class Server {}
```

***

## Links & Resources

<CardGroup cols={2}>
  <Card title="Source Code" icon="github" href="https://github.com/agentfront/frontmcp/tree/main/plugins/plugin-remember">
    View the remember plugin source code
  </Card>

  <Card title="Approval Plugin" icon="shield-check" href="/frontmcp/plugins/approval-plugin">
    For tool authorization workflows
  </Card>

  <Card title="Plugin Guide" icon="puzzle-piece" href="/frontmcp/extensibility/plugins">
    Learn more about FrontMCP plugins
  </Card>

  <Card title="Cache Plugin" icon="database" href="/frontmcp/plugins/cache-plugin">
    For tool response caching
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.