---
title: "Persist state with MCP sessions"
description: "Persist per-session state across tool calls with useMcpSession()."
canonical_url: "https://mcp-toolkit.nuxt.dev/advanced/sessions"
---
# Persist state with MCP sessions

> Persist per-session state across tool calls with useMcpSession().

## What are Sessions?

By default, the MCP server is **stateless** — a fresh server instance is created for every request. This works fine for simple request/response tools, but some scenarios require the server to remember context across multiple tool calls.

<prompt :actions="["copy","cursor","windsurf"]" description="Enable MCP sessions in Nuxt" icon="i-lucide-database">

Add session support to my Nuxt MCP server (@nuxtjs/mcp-toolkit).

- Enable sessions in nuxt.config.ts: mcp: { sessions: true }
- Use useMcpSession<t>

() (auto-imported) in tool handlers to get/set per-session state

</t>
- await session.get(key) and await session.set(key, value) persist data across tool calls in the same session
- Define a TypeScript interface for your session data and pass it as the generic parameter to useMcpSession<my-session>

()

</my-session>
- Sessions are identified by the MCP-Session-Id header, assigned automatically
- For custom timeout: mcp: { sessions: { enabled: true, maxDuration: 3600000 } }
- Sessions also enable SSE streaming and session continuity

Docs: [https://mcp-toolkit.nuxt.dev/advanced/sessions](https://mcp-toolkit.nuxt.dev/advanced/sessions)

</prompt>

When sessions are enabled, the server assigns a unique `MCP-Session-Id` to each client connection. This ID is included in every subsequent request, allowing the server to:

- **Maintain state** across multiple tool calls within the same conversation
- **Enable SSE streaming** for real-time server-to-client communication
- **Support session continuity** so clients can reconnect to an existing session

## When to Use Sessions

Sessions are useful when your MCP tools need to:

<table>
<thead>
  <tr>
    <th>
      Use Case
    </th>
    
    <th>
      Example
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Track conversation context
      </strong>
    </td>
    
    <td>
      Remember user preferences, language, or prior answers within a session
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Accumulate data
      </strong>
    </td>
    
    <td>
      Build up a shopping cart, a list of notes, or a set of selections across tool calls
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Multi-step workflows
      </strong>
    </td>
    
    <td>
      Guide users through a wizard (e.g. form builder, deployment pipeline) where each step depends on previous inputs
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Per-session counters
      </strong>
    </td>
    
    <td>
      Track API usage, rate limits, or progress within a session
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Temporary caches
      </strong>
    </td>
    
    <td>
      Cache expensive computation results that are only relevant to the current session
    </td>
  </tr>
</tbody>
</table>

<callout color="info" icon="i-lucide-info">

If your tools are purely stateless (e.g. fetching data, performing calculations, reading files), you don't need sessions. Only enable them when state across tool calls adds real value.

</callout>

## Setup

Enable sessions in your `nuxt.config.ts`:

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
  modules: ['@nuxtjs/mcp-toolkit'],
  mcp: {
    sessions: true,
  },
})
```

You can also configure the session timeout:

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
  modules: ['@nuxtjs/mcp-toolkit'],
  mcp: {
    sessions: {
      enabled: true,
      maxDuration: 60 * 60 * 1000, // 1 hour (default: 30 minutes)
    },
  },
})
```

## `useMcpSession()`

The `useMcpSession()` server utility provides a typed, per-session key-value store. It is auto-imported and backed by [unstorage](https://unstorage.unjs.io), so it works with any storage driver out of the box.

### Typed Session (Recommended)

Define an interface for your session data and pass it as a generic. Keys and values are fully type-checked:

```typescript [server/mcp/tools/counter.ts]
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server'

interface CounterSession {
  counter: number
}

export default defineMcpTool({
  name: 'increment',
  description: 'Increment a per-session counter',
  handler: async () => {
    const session = useMcpSession<CounterSession>()
    const count = await session.get('counter') ?? 0
    await session.set('counter', count + 1)
    return `Counter: ${count + 1}`
  },
})
```

TypeScript will enforce that:

- `session.get('counter')` returns `number | null`
- `session.set('counter', 'wrong')` is a compile error
- `session.get('unknown_key')` is a compile error

### Untyped Session

Without generics, the store accepts any string key and untyped values:

```typescript
const session = useMcpSession()
await session.set('key', { any: 'value' })
const data = await session.get('key')
```

### API Reference

<table>
<thead>
  <tr>
    <th>
      Method
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        get(key)
      </code>
    </td>
    
    <td>
      Retrieve a value by key (returns <code>
        null
      </code>
      
       if missing)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        set(key, value)
      </code>
    </td>
    
    <td>
      Store a value for the given key
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        remove(key)
      </code>
    </td>
    
    <td>
      Delete a key from the session
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        has(key)
      </code>
    </td>
    
    <td>
      Check if a key exists
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        keys()
      </code>
    </td>
    
    <td>
      List all keys in the session
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        clear()
      </code>
    </td>
    
    <td>
      Remove all data from the session
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        storage
      </code>
    </td>
    
    <td>
      Access the underlying unstorage instance
    </td>
  </tr>
</tbody>
</table>

All methods except `storage` are asynchronous and return a `Promise`.

## Examples

### Notepad

A pair of tools that let the AI take notes during a conversation and retrieve them later:

```typescript [server/mcp/tools/add-note.ts]
import { z } from 'zod'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server'

interface NotesSession {
  notes: { text: string, createdAt: string }[]
}

export default defineMcpTool({
  name: 'add_note',
  description: 'Add a note to the session notepad',
  inputSchema: {
    note: z.string().describe('The note content'),
  },
  handler: async ({ note }) => {
    const session = useMcpSession<NotesSession>()
    const notes = await session.get('notes') ?? []
    notes.push({ text: note, createdAt: new Date().toISOString() })
    await session.set('notes', notes)
    return `Note added (${notes.length} total).`
  },
})
```

```typescript [server/mcp/tools/get-notes.ts]
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server'

interface NotesSession {
  notes: { text: string, createdAt: string }[]
}

export default defineMcpTool({
  name: 'get_notes',
  description: 'Retrieve all notes from the session notepad',
  handler: async () => {
    const session = useMcpSession<NotesSession>()
    const notes = await session.get('notes') ?? []
    if (notes.length === 0) return 'No notes yet.'
    return notes
  },
})
```

### Multi-Step Wizard

Guide the user through a multi-step form where each step depends on the previous one:

```typescript [server/mcp/tools/wizard.ts]
import { z } from 'zod'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server'

interface WizardSession {
  step: number
  projectName: string
  framework: string
}

export default defineMcpTool({
  name: 'wizard_next',
  description: 'Advance to the next step of the project setup wizard',
  inputSchema: {
    answer: z.string().describe('Answer for the current step'),
  },
  handler: async ({ answer }) => {
    const session = useMcpSession<WizardSession>()
    const step = await session.get('step') ?? 1

    if (step === 1) {
      await session.set('projectName', answer)
      await session.set('step', 2)
      return `Project name set to "${answer}". Step 2: Choose a framework (nuxt, next, svelte).`
    }

    if (step === 2) {
      await session.set('framework', answer)
      await session.set('step', 3)
      const name = await session.get('projectName')
      return `Creating "${name}" with ${answer}. Setup complete!`
    }

    return 'Wizard already completed. Use session.clear() to restart.'
  },
})
```

### User Preferences

Remember user preferences for the duration of the session:

```typescript [server/mcp/tools/set-preference.ts]
import { z } from 'zod'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server'

interface PreferencesSession {
  language: string
  verbose: boolean
}

export default defineMcpTool({
  name: 'set_preference',
  description: 'Set a user preference for this session',
  inputSchema: {
    language: z.string().optional().describe('Preferred response language'),
    verbose: z.boolean().optional().describe('Enable verbose output'),
  },
  handler: async ({ language, verbose }) => {
    const session = useMcpSession<PreferencesSession>()
    if (language) await session.set('language', language)
    if (verbose !== undefined) await session.set('verbose', verbose)
    return 'Preferences updated.'
  },
})
```

## Custom Storage Driver

By default, session data is stored **in memory**. Data is lost when the server restarts, which is fine for development and most use cases.

For production environments where you need persistence or shared state across multiple server instances, configure a different storage backend via the standard Nitro `storage` config:

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
  mcp: { sessions: true },
  nitro: {
    storage: {
      'mcp:sessions': {
        driver: 'redis',
        url: 'redis://localhost:6379',
      },
    },
  },
})
```

Any [unstorage driver](https://unstorage.unjs.io/drivers) can be used: Redis, filesystem, Cloudflare KV, Vercel KV, etc.

## Lifecycle and Cleanup

Session data is **automatically cleaned up** when:

- A session is closed by the client (transport `onclose`)
- A session expires after `maxDuration` of inactivity (default: 30 minutes)

You don't need to manage cleanup manually.

## Session Invalidation

Use `invalidateMcpSession()` (auto-imported) to terminate the current session programmatically. This is useful when auth state changes (e.g., token revocation) and you want to force the client to re-initialize:

```typescript [server/mcp/index.ts]
export default defineMcpHandler({
  middleware: async (event) => {
    const isRevoked = await checkIfTokenRevoked(event)
    if (isRevoked) {
      invalidateMcpSession()
    }
  },
})
```

<callout color="info" icon="i-lucide-info">

`enabled` guards on tools, resources, and prompts are evaluated when the session is created. If auth state changes mid-session, call `invalidateMcpSession()` to force re-evaluation.

</callout>

<callout color="info" icon="i-lucide-info">

Invalidation is tied to the HTTP response — the current MCP request can still complete with **200**, then storage and transport are torn down; the next request with the same `MCP-Session-Id` gets **404**. On Cloudflare Workers there is no session to invalidate (see [Requirements](#requirements)). To reject the in-flight MCP call immediately, return **401**/**403** from middleware (optionally after calling `invalidateMcpSession()`).

</callout>

## Session Continuity vs Resumability

The current implementation provides **session continuity** — a client can make multiple requests using the same `MCP-Session-Id` and the server maintains state across them. This is not the same as **SSE event replay** (true resumability), which would require an `eventStore` to replay missed events after a reconnection. SSE event replay may be added in a future release.

## Requirements

<callout color="warning" icon="i-lucide-triangle-alert">

`useMcpSession()` requires:

- `mcp.sessions` to be enabled in your config
- `nitro.experimental.asyncContext` to be `true` (default since Nuxt 3.8+)
- the Node server runtime — on Cloudflare Workers the transport is stateless and never assigns an `MCP-Session-Id`

It is available in tool, resource and prompt handlers, and only for a session the server assigned: an `MCP-Session-Id` the client made up does not open a session store. Calling it from `middleware` throws, because the session is checked after middleware runs.

</callout>


## Sitemap

See the full [sitemap](https://mcp-toolkit.nuxt.dev/sitemap.md) for all pages.
