Advanced Topics

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.

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 resumability so clients can reconnect to an existing session

When to Use Sessions

Sessions are useful when your MCP tools need to:

Use CaseExample
Track conversation contextRemember user preferences, language, or prior answers within a session
Accumulate dataBuild up a shopping cart, a list of notes, or a set of selections across tool calls
Multi-step workflowsGuide users through a wizard (e.g. form builder, deployment pipeline) where each step depends on previous inputs
Per-session countersTrack API usage, rate limits, or progress within a session
Temporary cachesCache expensive computation results that are only relevant to the current session
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.

Setup

Enable sessions in your nuxt.config.ts:

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

You can also configure the session timeout:

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, so it works with any storage driver out of the box.

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

server/mcp/tools/counter.ts
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 textResult(`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:

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

API Reference

MethodDescription
get(key)Retrieve a value by key (returns null if missing)
set(key, value)Store a value for the given key
remove(key)Delete a key from the session
has(key)Check if a key exists
keys()List all keys in the session
clear()Remove all data from the session
storageAccess the underlying unstorage instance

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:

server/mcp/tools/add-note.ts
import { z } from 'zod'

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 textResult(`Note added (${notes.length} total).`)
  },
})
server/mcp/tools/get-notes.ts
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 textResult('No notes yet.')
    }
    return jsonResult(notes)
  },
})

Multi-Step Wizard

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

server/mcp/tools/wizard.ts
import { z } from 'zod'

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 textResult(`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 textResult(`Creating "${name}" with ${answer}. Setup complete!`)
    }

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

User Preferences

Remember user preferences for the duration of the session:

server/mcp/tools/set-preference.ts
import { z } from 'zod'

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 textResult('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:

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

Any unstorage driver 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.

Requirements

useMcpSession() requires:
  • mcp.sessions to be enabled in your config
  • nitro.experimental.asyncContext to be true (default since Nuxt 3.8+)
Copyright © 2026