From 0fc29dcb232c7017bf0cbaed9be204f91c7214d5 Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Tue, 5 May 2026 13:44:14 +0100 Subject: [PATCH] fix(module): disable cache `swr` by default to preserve observability --- .changeset/cache-swr-default-false.md | 5 +++++ apps/docs/content/2.tools/3.errors-caching.md | 6 +++++- .../skills/manage-mcp/references/tools.md | 2 ++ .../runtime/server/mcp/definitions/cache.ts | 12 +++++++++-- .../test/cache-options.test.ts | 20 +++++++++++++++++++ 5 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 .changeset/cache-swr-default-false.md create mode 100644 packages/nuxt-mcp-toolkit/test/cache-options.test.ts diff --git a/.changeset/cache-swr-default-false.md b/.changeset/cache-swr-default-false.md new file mode 100644 index 00000000..f121666a --- /dev/null +++ b/.changeset/cache-swr-default-false.md @@ -0,0 +1,5 @@ +--- +"@nuxtjs/mcp-toolkit": patch +--- + +Default `swr` to `false` for cached tools and resources. Nitro's underlying default (`swr: true`) returned stale entries immediately and refreshed the handler in a background task that ran after the MCP request had been answered, silently dropping any request-scoped writes (e.g. `useLogger(event).set()`). Pass `cache: { maxAge: '1h', swr: true }` to opt back in. diff --git a/apps/docs/content/2.tools/3.errors-caching.md b/apps/docs/content/2.tools/3.errors-caching.md index 08c979d9..69cd638f 100644 --- a/apps/docs/content/2.tools/3.errors-caching.md +++ b/apps/docs/content/2.tools/3.errors-caching.md @@ -125,10 +125,14 @@ export default defineMcpTool({ | `maxAge` | `string \| number` | Yes | Cache duration (e.g., `'1h'`, `3600000`) | | `getKey` | `(args) => string` | No | Custom cache key generator | | `staleMaxAge` | `number` | No | Duration for stale-while-revalidate | -| `swr` | `boolean` | No | Enable stale-while-revalidate | +| `swr` | `boolean` | No | Enable stale-while-revalidate (defaults to `false`, see warning below) | | `name` | `string` | No | Cache name (auto-generated from tool name) | | `group` | `string` | No | Cache group (default: `'mcp'`) | ::callout{icon="i-lucide-info" color="info"} See the [Nitro Cache documentation](https://nitro.build/guide/cache#options) for all available options. :: + +::callout{icon="i-lucide-triangle-alert" color="warning"} +`swr` defaults to `false` (Nitro itself defaults to `true`). With `swr: true`, stale hits return immediately and the handler refreshes in the background after the request is answered, so request-scoped writes (structured logs, traces) may be dropped. Opt in only when you accept that trade-off. +:: diff --git a/apps/docs/skills/manage-mcp/references/tools.md b/apps/docs/skills/manage-mcp/references/tools.md index 9bdda555..969da4cb 100644 --- a/apps/docs/skills/manage-mcp/references/tools.md +++ b/apps/docs/skills/manage-mcp/references/tools.md @@ -60,6 +60,8 @@ export default defineMcpTool({ `cache: '10m'` keys on the input arguments. Pass a full Nitro cache options object for `swr`, custom `getKey`, etc. ([Nitro caching →](https://nitro.build/guide/cache#options)) +`swr` defaults to `false` here (Nitro defaults to `true`). With `swr: true`, the handler refreshes after the response is sent, so request-scoped logs/traces may be dropped — opt in only when you accept that. + ## Database Mutation (DB + soft auth + observability) ```typescript [server/mcp/tools/create-todo.ts] diff --git a/packages/nuxt-mcp-toolkit/src/runtime/server/mcp/definitions/cache.ts b/packages/nuxt-mcp-toolkit/src/runtime/server/mcp/definitions/cache.ts index 914fe16c..40dbce9e 100644 --- a/packages/nuxt-mcp-toolkit/src/runtime/server/mcp/definitions/cache.ts +++ b/packages/nuxt-mcp-toolkit/src/runtime/server/mcp/definitions/cache.ts @@ -66,7 +66,12 @@ export interface McpCacheOptions { getKey?: (args: Args) => string /** Cache group (default: 'mcp') */ group?: string - /** Enable stale-while-revalidate behavior */ + /** + * Enable stale-while-revalidate (default: `false`). + * When `true`, the handler refreshes in the background after the request + * has been answered, so request-scoped writes (loggers, traces) may be + * dropped. + */ swr?: boolean } @@ -91,7 +96,8 @@ export function parseCacheDuration(duration: MsCacheDuration | number): number { } /** - * Create cache options from McpCache config + * Create cache options from McpCache config. + * Forces `swr: false` by default — see {@link McpCacheOptions.swr}. */ export function createCacheOptions( cache: McpCache, @@ -101,6 +107,7 @@ export function createCacheOptions( if (typeof cache === 'object') { return { getKey: defaultGetKey, + swr: false, ...cache, maxAge: parseCacheDuration(cache.maxAge), name: cache.name ?? name, @@ -113,6 +120,7 @@ export function createCacheOptions( name, group: 'mcp', getKey: defaultGetKey, + swr: false, } } diff --git a/packages/nuxt-mcp-toolkit/test/cache-options.test.ts b/packages/nuxt-mcp-toolkit/test/cache-options.test.ts new file mode 100644 index 00000000..b0fdacb4 --- /dev/null +++ b/packages/nuxt-mcp-toolkit/test/cache-options.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect, vi } from 'vitest' + +vi.mock('nitropack/runtime', () => ({ + defineCachedFunction: (fn: T) => fn, +})) + +const { createCacheOptions } = await import( + '../src/runtime/server/mcp/definitions/cache' +) + +describe('createCacheOptions', () => { + it('defaults swr to false to keep handlers inside the request lifetime', () => { + expect(createCacheOptions('1h', 'mcp-tool:get-page').swr).toBe(false) + expect(createCacheOptions({ maxAge: '1h' }, 'mcp-tool:get-page').swr).toBe(false) + }) + + it('respects explicit swr opt-in', () => { + expect(createCacheOptions({ maxAge: '1h', swr: true }, 'mcp-tool:get-page').swr).toBe(true) + }) +})