进阶主题

通过 MCP 会话持久化状态

使用 useMcpSession() 在工具调用之间按会话持久化状态。

什么是会话 (Sessions)?

默认情况下,MCP 服务器是无状态的——每次请求都会创建一个全新的服务器实例。这对于简单的请求/响应工具来说没问题,但某些场景要求服务器在多次工具调用之间记住上下文。

在 Nuxt 中启用 MCP 会话

启用会话后,服务器会为每个客户端连接分配一个唯一的 MCP-Session-Id。该 ID 会包含在后续的每个请求中,从而使服务器能够:

  • 在同一会话的多次工具调用之间维护状态
  • 启用 SSE 流式传输,用于实时的服务端到客户端通信
  • 支持会话连续性,使客户端可以重新连接到现有会话

何时使用会话

当你的 MCP 工具需要以下功能时,会话将非常有用:

使用场景示例
跟踪对话上下文在会话中记住用户偏好、语言或之前的回答
累积数据在多次工具调用中构建购物车、笔记列表或一组选项
多步骤工作流引导用户完成向导(例如表单构建器、部署流水线),其中每个步骤都依赖于之前的输入
按会话计数器跟踪会话内的 API 使用量、速率限制或进度
临时缓存缓存仅与当前会话相关的昂贵计算结果
如果你的工具完全是无状态的(例如获取数据、执行计算、读取文件),则不需要会话。仅当跨工具调用的状态能带来实际价值时才启用它们。

设置

在你的 nuxt.config.ts 中启用会话:

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

你还可以配置会话超时时间:

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/mcp-toolkit'],
  mcp: {
    sessions: {
      enabled: true,
      maxDuration: 60 * 60 * 1000, // 1 小时(默认:30 分钟)
    },
  },
})

useMcpSession()

useMcpSession() 服务器工具函数提供了一个类型安全的、按会话划分的键值存储。它支持自动导入,并由 unstorage 提供支持,因此开箱即可与任何存储驱动配合使用。

类型化会话(推荐)

为你的会话数据定义一个接口并将其作为泛型传入。键和值将进行完整的类型检查:

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

interface CounterSession {
  counter: number
}

export default defineMcpTool({
  name: 'increment',
  description: '增加每个会话的计数器',
  handler: async () => {
    const session = useMcpSession<CounterSession>()
    const count = await session.get('counter') ?? 0
    await session.set('counter', count + 1)
    return `Counter: ${count + 1}`
  },
})

TypeScript 将强制执行以下规则:

  • session.get('counter') 返回 number | null
  • session.set('counter', 'wrong') 会导致编译错误
  • session.get('unknown_key') 会导致编译错误

非类型化会话

不使用泛型时,存储接受任何字符串键和未类型化的值:

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

API 参考

方法描述
get(key)按键检索值(如果不存在则返回 null
set(key, value)为指定键存储一个值
remove(key)从会话中删除一个键
has(key)检查键是否存在
keys()列出会话中的所有键
clear()移除会话中的所有数据
storage访问底层的 unstorage 实例

storage 外,所有方法均为异步方法并返回一个 Promise

示例

记事本

一对工具,允许 AI 在对话期间做笔记并在稍后检索它们:

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: '将笔记添加到会话记事本',
  inputSchema: {
    note: z.string().describe('笔记内容'),
  },
  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).`
  },
})
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: '从会话记事本中检索所有笔记',
  handler: async () => {
    const session = useMcpSession<NotesSession>()
    const notes = await session.get('notes') ?? []
    if (notes.length === 0) return '暂无笔记。'
    return notes
  },
})

多步骤向导

引导用户完成多步骤表单,其中每个步骤都依赖于上一步:

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: '推进到项目设置向导的下一步',
  inputSchema: {
    answer: z.string().describe('当前步骤的答案'),
  },
  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.'
  },
})

用户偏好

在会话持续期间记住用户偏好:

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: '为此会话设置用户偏好',
  inputSchema: {
    language: z.string().optional().describe('首选响应语言'),
    verbose: z.boolean().optional().describe('启用详细输出'),
  },
  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.'
  },
})

自定义存储驱动

默认情况下,会话数据存储在内存中。服务器重启时数据会丢失,这对于开发和大多数用例来说是可以接受的。

对于需要持久化或在多个服务器实例间共享状态的生产环境,可以通过标准的 Nitro storage 配置来配置不同的存储后端:

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

可以使用任何 unstorage 驱动:Redis、文件系统、Cloudflare KV、Vercel KV 等。

生命周期与清理

会话数据会在以下情况下自动清理

  • 客户端关闭会话(传输层 onclose
  • 会话在 maxDuration 的不活动期后过期(默认:30 分钟)

你无需手动管理清理工作。

Session Invalidation

使用 invalidateMcpSession()(自动导入)可通过程序终止当前会话。当认证状态发生变化时(例如令牌撤销),这很有用,你可以强制客户端重新初始化:

server/mcp/index.ts
export default defineMcpHandler({
  middleware: async (event) => {
    const isRevoked = await checkIfTokenRevoked(event)
    if (isRevoked) {
      invalidateMcpSession()
    }
  },
})
工具、资源和提示词上的 enabled 守卫会在会话创建时进行评估。如果认证状态在会话中途发生变化,请调用 invalidateMcpSession() 以强制重新评估。
**Node:**无效化与 HTTP 响应绑定——当前 MCP 请求仍可正常完成并返回 200,随后存储和传输层会被拆除;下一个使用相同 MCP-Session-Id 的请求会得到 404。**Cloudflare Workers:**在你的处理器运行之前,会话就会被标记为无效;同一个请求仍会完成,但下一个带有该 ID 的请求会得到 404。如果要在任何运行时下立即拒绝正在进行的 MCP 调用,请在中间件中返回 401/403(可在调用 invalidateMcpSession() 之后进行)。

会话连续性 vs 可恢复性

当前实现提供的是会话连续性——客户端可以使用相同的 MCP-Session-Id 发起多次请求,服务器会在这些请求之间维护状态。这不同于 SSE 事件重放(真正的可恢复性),后者需要一个 eventStore 来在重新连接后重放丢失的事件。SSE 事件重放可能会在未来版本中添加。

要求

useMcpSession() 需要:
  • 在配置中启用 mcp.sessions
  • nitro.experimental.asyncContexttrue(Nuxt 3.8+ 起默认开启)