@nuxtjs/mcp-toolkit 0.7.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.d.mts CHANGED
@@ -35,6 +35,23 @@ interface ModuleOptions {
35
35
  * @default 'mcp'
36
36
  */
37
37
  dir?: string;
38
+ /**
39
+ * Enable MCP session management (stateful transport).
40
+ * When enabled, the server assigns session IDs and maintains state across requests,
41
+ * enabling SSE streaming, server-to-client notifications, and resumability.
42
+ *
43
+ * Pass `true` for defaults or an object to configure session behavior.
44
+ * @default false
45
+ * @see https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
46
+ */
47
+ sessions?: boolean | {
48
+ enabled?: boolean;
49
+ /**
50
+ * Maximum session duration in milliseconds. Sessions inactive longer than this are cleaned up.
51
+ * @default 1800000 (30 minutes)
52
+ */
53
+ maxDuration?: number;
54
+ };
38
55
  }
39
56
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
40
57
 
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxtjs/mcp-toolkit",
3
- "version": "0.7.0",
3
+ "version": "0.10.0",
4
4
  "configKey": "mcp",
5
5
  "docs": "https://mcp-toolkit.nuxt.dev/getting-started/installation",
6
6
  "mcp": "https://mcp-toolkit.nuxt.dev/mcp",
package/dist/module.mjs CHANGED
@@ -2,7 +2,6 @@ import { logger, createResolver, defineNuxtModule, addComponent, addServerTempla
2
2
  import { loadAllDefinitions } from '../dist/runtime/server/mcp/loaders/index.js';
3
3
  import { defaultMcpConfig, getMcpConfig } from '../dist/runtime/server/mcp/config.js';
4
4
  import { ROUTES } from '../dist/runtime/server/mcp/constants.js';
5
- import { addDevToolsCustomTabs } from '../dist/runtime/server/mcp/devtools/index.js';
6
5
  import { execSync } from 'node:child_process';
7
6
  import { existsSync, readFileSync } from 'node:fs';
8
7
  import { homedir } from 'node:os';
@@ -72,7 +71,7 @@ function generateDeeplinkUrl(baseUrl, route, ide, serverName) {
72
71
  }
73
72
 
74
73
  const name = "@nuxtjs/mcp-toolkit";
75
- const version = "0.7.0";
74
+ const version = "0.10.0";
76
75
 
77
76
  const log = logger.withTag("@nuxtjs/mcp-toolkit");
78
77
  const { resolve } = createResolver(import.meta.url);
@@ -86,15 +85,23 @@ const module$1 = defineNuxtModule({
86
85
  },
87
86
  defaults: defaultMcpConfig,
88
87
  async setup(options, nuxt) {
89
- if (nuxt.options.nitro.static || nuxt.options._generate) {
88
+ const nitroOptions = nuxt.options.nitro;
89
+ if (nitroOptions?.static || nuxt.options._generate) {
90
90
  log.warn("@nuxtjs/mcp-toolkit is not compatible with `nuxt generate` as it needs a server to run.");
91
91
  return;
92
92
  }
93
93
  const resolver = createResolver(import.meta.url);
94
+ if (typeof options.sessions === "boolean") {
95
+ options.sessions = { enabled: options.sessions };
96
+ }
94
97
  const mcpConfig = getMcpConfig(options);
95
98
  if (!options.enabled) {
96
99
  return;
97
100
  }
101
+ if (mcpConfig.sessions.enabled && nitroOptions) {
102
+ nitroOptions.storage ??= {};
103
+ nitroOptions.storage["mcp:sessions"] ??= { driver: "memory" };
104
+ }
98
105
  addComponent({
99
106
  name: "InstallButton",
100
107
  filePath: resolver.resolve("runtime/components/InstallButton.vue")
@@ -163,10 +170,12 @@ const module$1 = defineNuxtModule({
163
170
  path: resolver.resolve("runtime/server/types.server.d.ts")
164
171
  });
165
172
  });
166
- nuxt.options.nitro.typescript ??= {};
167
- nuxt.options.nitro.typescript.tsConfig ??= {};
168
- nuxt.options.nitro.typescript.tsConfig.include ??= [];
169
- nuxt.options.nitro.typescript.tsConfig.include.push(resolver.resolve("runtime/server/types.server.d.ts"));
173
+ if (nitroOptions) {
174
+ nitroOptions.typescript ??= {};
175
+ nitroOptions.typescript.tsConfig ??= {};
176
+ nitroOptions.typescript.tsConfig.include ??= [];
177
+ nitroOptions.typescript.tsConfig.include.push(resolver.resolve("runtime/server/types.server.d.ts"));
178
+ }
170
179
  let isCloudflare = false;
171
180
  if (!nuxt.options.dev) {
172
181
  nuxt.hook("nitro:config", (nitroConfig) => {
@@ -182,6 +191,8 @@ const module$1 = defineNuxtModule({
182
191
  }
183
192
  });
184
193
  const mcpDefinitionsPath = resolver.resolve("runtime/server/mcp/definitions");
194
+ const mcpSessionPath = resolver.resolve("runtime/server/mcp/session");
195
+ const mcpServerPath = resolver.resolve("runtime/server/mcp/server");
185
196
  addServerImports([
186
197
  "defineMcpTool",
187
198
  "defineMcpResource",
@@ -192,6 +203,10 @@ const module$1 = defineNuxtModule({
192
203
  "errorResult",
193
204
  "imageResult"
194
205
  ].map((name2) => ({ name: name2, from: mcpDefinitionsPath })));
206
+ addServerImports([
207
+ { name: "useMcpSession", from: mcpSessionPath },
208
+ { name: "useMcpServer", from: mcpServerPath }
209
+ ]);
195
210
  addServerHandler({
196
211
  route: options.route,
197
212
  handler: resolver.resolve("runtime/server/mcp/handler")
@@ -204,7 +219,11 @@ const module$1 = defineNuxtModule({
204
219
  route: `${options.route}/badge.svg`,
205
220
  handler: resolver.resolve("runtime/server/mcp/badge-image")
206
221
  });
207
- addDevToolsCustomTabs(nuxt, options);
222
+ if (nuxt.options.dev) {
223
+ import('../dist/runtime/server/mcp/devtools/index.js').then(({ addDevToolsCustomTabs }) => {
224
+ addDevToolsCustomTabs(nuxt, options);
225
+ });
226
+ }
208
227
  }
209
228
  });
210
229
 
@@ -1,3 +1,3 @@
1
1
  export type SupportedIDE = 'cursor' | 'vscode';
2
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string>>;
2
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, string>;
3
3
  export default _default;
@@ -1,5 +1,4 @@
1
1
  import { defineEventHandler, getQuery, setHeader } from "h3";
2
- import satori from "satori";
3
2
  const IDE_CONFIG = {
4
3
  cursor: {
5
4
  defaultLabel: "Install MCP in Cursor"
@@ -8,125 +7,51 @@ const IDE_CONFIG = {
8
7
  defaultLabel: "Install MCP in VS Code"
9
8
  }
10
9
  };
11
- function CursorIcon() {
12
- return {
13
- type: "svg",
14
- props: {
15
- width: 18,
16
- height: 18,
17
- viewBox: "0 0 24 24",
18
- style: { filter: "invert(1)" },
19
- children: [
20
- {
21
- type: "path",
22
- props: {
23
- fill: "#666",
24
- d: "M11.925 24l10.425-6-10.425-6L1.5 18l10.425 6z"
25
- }
26
- },
27
- {
28
- type: "path",
29
- props: {
30
- fill: "#888",
31
- d: "M22.35 18V6L11.925 0v12l10.425 6z"
32
- }
33
- },
34
- {
35
- type: "path",
36
- props: {
37
- fill: "#777",
38
- d: "M11.925 0L1.5 6v12l10.425-6V0z"
39
- }
40
- },
41
- {
42
- type: "path",
43
- props: {
44
- fill: "#555",
45
- d: "M22.35 6L11.925 24V12L22.35 6z"
46
- }
47
- },
48
- {
49
- type: "path",
50
- props: {
51
- fill: "#333",
52
- d: "M22.35 6l-10.425 6L1.5 6h20.85z"
53
- }
54
- }
55
- ]
56
- }
57
- };
10
+ function cursorIconSvg() {
11
+ return `<g transform="translate(8,7) scale(0.75)">
12
+ <path fill="#999" d="M11.925 24l10.425-6-10.425-6L1.5 18l10.425 6z"/>
13
+ <path fill="#bbb" d="M22.35 18V6L11.925 0v12l10.425 6z"/>
14
+ <path fill="#aaa" d="M11.925 0L1.5 6v12l10.425-6V0z"/>
15
+ <path fill="#888" d="M22.35 6L11.925 24V12L22.35 6z"/>
16
+ <path fill="#fff" d="M22.35 6l-10.425 6L1.5 6h20.85z"/>
17
+ </g>`;
58
18
  }
59
- function VSCodeIconSimple() {
60
- return {
61
- type: "svg",
62
- props: {
63
- width: 18,
64
- height: 18,
65
- viewBox: "0 0 24 24",
66
- children: [
67
- {
68
- type: "path",
69
- props: {
70
- fill: "#007ACC",
71
- d: "M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63l-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12L.326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128l9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"
72
- }
73
- }
74
- ]
75
- }
76
- };
19
+ function vscodeIconSvg() {
20
+ return `<g transform="translate(8,7) scale(0.75)">
21
+ <path fill="#007ACC" d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63l-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12L.326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128l9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"/>
22
+ </g>`;
77
23
  }
78
- function getIcon(ide) {
79
- return ide === "vscode" ? VSCodeIconSimple() : CursorIcon();
24
+ function estimateTextWidth(text, fontSize) {
25
+ const ratio = fontSize / 13;
26
+ let width = 0;
27
+ for (const ch of text) {
28
+ if (ch === " ") width += 3.6;
29
+ else if ("iIl|1!:;.,".includes(ch)) width += 4.2;
30
+ else if (`fjrt()[]{}'"/`.includes(ch)) width += 5.2;
31
+ else if ("mwMW".includes(ch)) width += 10;
32
+ else if (ch >= "A" && ch <= "Z") width += 8.5;
33
+ else width += 7;
34
+ }
35
+ return width * ratio;
80
36
  }
81
- async function generateBadgeSVG(options) {
37
+ function generateBadgeSVG(options) {
82
38
  const { label, color, textColor, borderColor, showIcon, ide } = options;
83
- const icon = getIcon(ide);
84
- const element = {
85
- type: "div",
86
- props: {
87
- style: {
88
- display: "flex",
89
- alignItems: "center",
90
- gap: "8px",
91
- padding: "6px 8px",
92
- fontSize: "14px",
93
- fontWeight: 500,
94
- color: `#${textColor}`,
95
- backgroundColor: `#${color}`,
96
- border: `1px solid #${borderColor}`
97
- },
98
- children: showIcon ? [icon, { type: "span", props: { children: label } }] : [{ type: "span", props: { children: label } }]
99
- }
100
- };
101
39
  const iconWidth = showIcon ? 26 : 0;
102
- const textWidth = label.length * 8;
103
- const padding = 20;
104
- const width = Math.max(Math.ceil(iconWidth + textWidth + padding), 140);
40
+ const textWidth = estimateTextWidth(label, 13);
41
+ const padding = showIcon ? 24 : 22;
42
+ const width = Math.ceil(iconWidth + textWidth + padding);
105
43
  const height = 32;
106
- const svg = await satori(element, {
107
- width,
108
- height,
109
- fonts: [
110
- {
111
- name: "Inter",
112
- data: await loadFont(),
113
- weight: 500,
114
- style: "normal"
115
- }
116
- ]
117
- });
118
- return svg;
44
+ const textX = showIcon ? 34 : width / 2;
45
+ const textAnchor = showIcon ? "start" : "middle";
46
+ const icon = showIcon ? ide === "vscode" ? vscodeIconSvg() : cursorIconSvg() : "";
47
+ const escapedLabel = label.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
48
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
49
+ <rect x="0.5" y="0.5" width="${width - 1}" height="${height - 1}" rx="4" fill="#${color}" stroke="#${borderColor}"/>
50
+ ${icon}
51
+ <text x="${textX}" y="21" fill="#${textColor}" font-family="system-ui, -apple-system, sans-serif" font-size="13" font-weight="500" text-anchor="${textAnchor}">${escapedLabel}</text>
52
+ </svg>`;
119
53
  }
120
- async function loadFont() {
121
- const response = await fetch(
122
- "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuI6fAZ9hjp-Ek-_EeA.woff"
123
- );
124
- if (!response.ok) {
125
- throw new Error(`Failed to load font: ${response.status} ${response.statusText}`);
126
- }
127
- return response.arrayBuffer();
128
- }
129
- export default defineEventHandler(async (event) => {
54
+ export default defineEventHandler((event) => {
130
55
  const query = getQuery(event);
131
56
  const ide = query.ide || "cursor";
132
57
  const ideConfig = IDE_CONFIG[ide] || IDE_CONFIG.cursor;
@@ -138,17 +63,8 @@ export default defineEventHandler(async (event) => {
138
63
  borderColor: query.borderColor || "404040",
139
64
  showIcon: query.icon !== "false"
140
65
  };
141
- try {
142
- const svg = await generateBadgeSVG(options);
143
- setHeader(event, "Content-Type", "image/svg+xml");
144
- setHeader(event, "Cache-Control", "public, max-age=86400");
145
- return svg;
146
- } catch {
147
- setHeader(event, "Content-Type", "image/svg+xml");
148
- setHeader(event, "Cache-Control", "no-cache");
149
- return `<svg xmlns="http://www.w3.org/2000/svg" width="140" height="32">
150
- <rect width="140" height="32" fill="#171717" stroke="#404040"/>
151
- <text x="70" y="20" fill="#fff" font-size="12" text-anchor="middle">${options.label}</text>
152
- </svg>`;
153
- }
66
+ const svg = generateBadgeSVG(options);
67
+ setHeader(event, "Content-Type", "image/svg+xml");
68
+ setHeader(event, "Cache-Control", "public, max-age=86400");
69
+ return svg;
154
70
  });
@@ -1,3 +1,7 @@
1
+ export interface McpSessionsConfig {
2
+ enabled: boolean;
3
+ maxDuration: number;
4
+ }
1
5
  export interface McpConfig {
2
6
  enabled: boolean;
3
7
  route: string;
@@ -5,6 +9,7 @@ export interface McpConfig {
5
9
  name: string;
6
10
  version: string;
7
11
  dir: string;
12
+ sessions: McpSessionsConfig;
8
13
  }
9
14
  export declare const defaultMcpConfig: McpConfig;
10
15
  export declare function getMcpConfig(partial?: Partial<McpConfig>): McpConfig;
@@ -1,12 +1,26 @@
1
- import { defu } from "defu";
2
1
  export const defaultMcpConfig = {
3
2
  enabled: true,
4
3
  route: "/mcp",
5
4
  browserRedirect: "/",
6
5
  name: "",
7
6
  version: "1.0.0",
8
- dir: "mcp"
7
+ dir: "mcp",
8
+ sessions: {
9
+ enabled: false,
10
+ maxDuration: 30 * 60 * 1e3
11
+ // 30 minutes
12
+ }
9
13
  };
10
14
  export function getMcpConfig(partial) {
11
- return defu(partial, defaultMcpConfig);
15
+ if (!partial) return { ...defaultMcpConfig };
16
+ const sessions = partial.sessions ? { ...defaultMcpConfig.sessions, ...partial.sessions } : defaultMcpConfig.sessions;
17
+ return {
18
+ enabled: partial.enabled ?? defaultMcpConfig.enabled,
19
+ route: partial.route ?? defaultMcpConfig.route,
20
+ browserRedirect: partial.browserRedirect ?? defaultMcpConfig.browserRedirect,
21
+ name: partial.name ?? defaultMcpConfig.name,
22
+ version: partial.version ?? defaultMcpConfig.version,
23
+ dir: partial.dir ?? defaultMcpConfig.dir,
24
+ sessions
25
+ };
12
26
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Cache duration strings supported by the `ms` package
2
+ * Cache duration strings (e.g. '1h', '30m', '2 days')
3
3
  */
4
4
  export type MsCacheDuration = '1s' | '5s' | '10s' | '15s' | '30s' | '45s' | '1m' | '2m' | '5m' | '10m' | '15m' | '30m' | '45m' | '1h' | '2h' | '3h' | '4h' | '6h' | '8h' | '12h' | '24h' | '1d' | '2d' | '3d' | '7d' | '14d' | '30d' | '1w' | '2w' | '4w' | '1 second' | '1 minute' | '1 hour' | '1 day' | '1 week' | '2 seconds' | '5 seconds' | '10 seconds' | '30 seconds' | '2 minutes' | '5 minutes' | '10 minutes' | '15 minutes' | '30 minutes' | '2 hours' | '3 hours' | '6 hours' | '12 hours' | '24 hours' | '2 days' | '3 days' | '7 days' | '14 days' | '30 days' | '2 weeks' | '4 weeks' | (string & Record<never, never>);
5
5
  /**
@@ -1,10 +1,41 @@
1
1
  import { defineCachedFunction } from "nitropack/runtime";
2
- import ms from "ms";
2
+ const DURATION_UNITS = {
3
+ ms: 1,
4
+ millisecond: 1,
5
+ milliseconds: 1,
6
+ s: 1e3,
7
+ sec: 1e3,
8
+ second: 1e3,
9
+ seconds: 1e3,
10
+ m: 6e4,
11
+ min: 6e4,
12
+ minute: 6e4,
13
+ minutes: 6e4,
14
+ h: 36e5,
15
+ hr: 36e5,
16
+ hour: 36e5,
17
+ hours: 36e5,
18
+ d: 864e5,
19
+ day: 864e5,
20
+ days: 864e5,
21
+ w: 6048e5,
22
+ week: 6048e5,
23
+ weeks: 6048e5
24
+ };
25
+ function parseDurationToMs(str) {
26
+ const match = str.trim().match(/^(\d+)\s*([a-z]+)$/i);
27
+ if (!match) return void 0;
28
+ const value = Number(match[1]);
29
+ const unit = match[2].toLowerCase();
30
+ const multiplier = DURATION_UNITS[unit];
31
+ if (multiplier === void 0) return void 0;
32
+ return value * multiplier;
33
+ }
3
34
  export function parseCacheDuration(duration) {
4
35
  if (typeof duration === "number") {
5
36
  return duration;
6
37
  }
7
- const parsed = ms(duration);
38
+ const parsed = parseDurationToMs(duration);
8
39
  if (parsed === void 0) {
9
40
  throw new Error(`Invalid cache duration: ${duration}`);
10
41
  }
@@ -29,7 +29,7 @@ import type { McpPromptDefinition } from './prompts.js';
29
29
  * }
30
30
  * ```
31
31
  */
32
- export type McpMiddleware = (event: H3Event, next: () => Promise<Response | undefined>) => Promise<Response | undefined | void> | Response | undefined | void;
32
+ export type McpMiddleware = (event: H3Event, next: () => Promise<Response>) => Promise<Response | void> | Response | void;
33
33
  /**
34
34
  * Options for defining a custom MCP handler
35
35
  * @see https://mcp-toolkit.nuxt.dev/core-concepts/handlers
@@ -70,9 +70,9 @@ export interface McpHandlerOptions {
70
70
  * ```
71
71
  */
72
72
  middleware?: McpMiddleware;
73
- tools?: Array<McpToolDefinition<any, any>>;
74
- resources?: McpResourceDefinition[];
75
- prompts?: McpPromptDefinition[];
73
+ tools?: Array<McpToolDefinition<any, any>> | ((event: H3Event) => Array<McpToolDefinition<any, any>> | Promise<Array<McpToolDefinition<any, any>>>);
74
+ resources?: McpResourceDefinition[] | ((event: H3Event) => McpResourceDefinition[] | Promise<McpResourceDefinition[]>);
75
+ prompts?: McpPromptDefinition[] | ((event: H3Event) => McpPromptDefinition[] | Promise<McpPromptDefinition[]>);
76
76
  }
77
77
  export interface McpHandlerDefinition extends Required<Omit<McpHandlerOptions, 'tools' | 'resources' | 'prompts' | 'middleware'>> {
78
78
  tools: Array<McpToolDefinition<any, any>>;
@@ -1,3 +1,4 @@
1
+ import type { H3Event } from 'h3';
1
2
  import type { ZodRawShape } from 'zod';
2
3
  import type { GetPromptResult, ServerRequest, ServerNotification } from '@modelcontextprotocol/sdk/types.js';
3
4
  import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
@@ -18,6 +19,12 @@ export interface McpPromptDefinition<Args extends ZodRawShape | undefined = unde
18
19
  inputSchema?: Args;
19
20
  _meta?: Record<string, unknown>;
20
21
  handler: McpPromptCallback<Args>;
22
+ /**
23
+ * Guard that controls whether this prompt is registered for a given request.
24
+ * Receives the H3 event (with `event.context` populated by middleware) and
25
+ * returns `true` to include the prompt or `false` to hide it.
26
+ */
27
+ enabled?: (event: H3Event) => boolean | Promise<boolean>;
21
28
  }
22
29
  /**
23
30
  * Register a prompt from a McpPromptDefinition
@@ -1,3 +1,4 @@
1
+ import type { H3Event } from 'h3';
1
2
  import type { McpServer, ResourceTemplate, ReadResourceCallback, ReadResourceTemplateCallback, ResourceMetadata } from '@modelcontextprotocol/sdk/server/mcp.js';
2
3
  import { type McpCacheOptions, type McpCache } from './cache.js';
3
4
  export type McpResourceCacheOptions = McpCacheOptions<URL>;
@@ -34,6 +35,12 @@ export interface StandardMcpResourceDefinition {
34
35
  * @see https://nitro.build/guide/cache#options
35
36
  */
36
37
  cache?: McpResourceCache;
38
+ /**
39
+ * Guard that controls whether this resource is registered for a given request.
40
+ * Receives the H3 event (with `event.context` populated by middleware) and
41
+ * returns `true` to include the resource or `false` to hide it.
42
+ */
43
+ enabled?: (event: H3Event) => boolean | Promise<boolean>;
37
44
  }
38
45
  /**
39
46
  * Definition of a file-based MCP resource
@@ -61,6 +68,12 @@ export interface FileMcpResourceDefinition {
61
68
  * @see https://nitro.build/guide/cache#options
62
69
  */
63
70
  cache?: McpResourceCache;
71
+ /**
72
+ * Guard that controls whether this resource is registered for a given request.
73
+ * Receives the H3 event (with `event.context` populated by middleware) and
74
+ * returns `true` to include the resource or `false` to hide it.
75
+ */
76
+ enabled?: (event: H3Event) => boolean | Promise<boolean>;
64
77
  }
65
78
  /**
66
79
  * Definition of an MCP resource matching the SDK's registerResource signature
@@ -1,3 +1,4 @@
1
+ import type { H3Event } from 'h3';
1
2
  import type { ZodRawShape } from 'zod';
2
3
  import type { CallToolResult, ServerRequest, ServerNotification } from '@modelcontextprotocol/sdk/types.js';
3
4
  import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
@@ -51,6 +52,14 @@ export interface McpToolDefinition<InputSchema extends ZodRawShape | undefined =
51
52
  * @see https://nitro.build/guide/cache#options
52
53
  */
53
54
  cache?: McpToolCache<InputSchema extends ZodRawShape ? ShapeOutput<InputSchema> : undefined>;
55
+ /**
56
+ * Guard that controls whether this tool is registered for a given request.
57
+ * Receives the H3 event (with `event.context` populated by middleware) and
58
+ * returns `true` to include the tool or `false` to hide it.
59
+ *
60
+ * Evaluated after middleware runs, so authentication context is available.
61
+ */
62
+ enabled?: (event: H3Event) => boolean | Promise<boolean>;
54
63
  }
55
64
  /**
56
65
  * Register a tool from a McpToolDefinition
@@ -1,4 +1,9 @@
1
- import { kebabCase, titleCase } from "scule";
1
+ function kebabCase(str) {
2
+ return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
3
+ }
4
+ function titleCase(str) {
5
+ return str.replace(/[-_]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/\b\w/g, (c) => c.toUpperCase());
6
+ }
2
7
  export function enrichNameTitle(options) {
3
8
  const { name, title, _meta, type } = options;
4
9
  const filename = _meta?.filename;
@@ -27,14 +27,16 @@ export default createMcpHandler((event) => {
27
27
  }
28
28
  const defaultHandlerDef = defaultHandler;
29
29
  if (defaultHandlerDef) {
30
+ const globalTools = tools;
31
+ const globalResources = resources;
32
+ const globalPrompts = prompts;
30
33
  return {
31
34
  name: defaultHandlerDef.name ?? config.name ?? "MCP Server",
32
35
  version: defaultHandlerDef.version ?? config.version,
33
36
  browserRedirect: defaultHandlerDef.browserRedirect ?? config.browserRedirect,
34
- // Use handler's definitions if specified, otherwise use global definitions
35
- tools: defaultHandlerDef.tools ?? tools,
36
- resources: defaultHandlerDef.resources ?? resources,
37
- prompts: defaultHandlerDef.prompts ?? prompts,
37
+ tools: defaultHandlerDef.tools ?? globalTools,
38
+ resources: defaultHandlerDef.resources ?? globalResources,
39
+ prompts: defaultHandlerDef.prompts ?? globalPrompts,
38
40
  middleware: defaultHandlerDef.middleware
39
41
  };
40
42
  }
@@ -1,5 +1,5 @@
1
+ import { resolve as resolvePath } from "node:path";
1
2
  import { getLayerDirectories } from "@nuxt/kit";
2
- import { resolve as resolvePath } from "pathe";
3
3
  import { glob } from "tinyglobby";
4
4
  const RESERVED_KEYWORDS = /* @__PURE__ */ new Set([
5
5
  "break",
@@ -6,7 +6,9 @@ const fallbackCtx = {
6
6
  passThroughOnException: () => {
7
7
  }
8
8
  };
9
- export default createMcpTransportHandler(async (server, event) => {
9
+ export default createMcpTransportHandler(async (createServer, event) => {
10
+ const server = createServer();
11
+ event.context._mcpServer = server;
10
12
  const { createMcpHandler } = await import("agents/mcp");
11
13
  const handler = createMcpHandler(server, {
12
14
  route: ""
@@ -1,13 +1,87 @@
1
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2
- import { readBody } from "h3";
1
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
2
+ import { toWebRequest, getHeader } from "h3";
3
+ import { useStorage } from "nitropack/runtime";
4
+ import config from "#nuxt-mcp-toolkit/config.mjs";
3
5
  import { createMcpTransportHandler } from "./types.js";
4
- export default createMcpTransportHandler(async (server, event) => {
5
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
6
- event.node.res.on("close", () => {
7
- transport.close();
8
- server.close();
6
+ const sessions = /* @__PURE__ */ new Map();
7
+ let cleanupInterval = null;
8
+ function ensureCleanup(maxDuration) {
9
+ if (cleanupInterval) return;
10
+ cleanupInterval = setInterval(() => {
11
+ const now = Date.now();
12
+ for (const [id, session] of sessions) {
13
+ if (now - session.lastAccessed > maxDuration) {
14
+ session.transport.close();
15
+ session.server.close();
16
+ sessions.delete(id);
17
+ useStorage(`mcp:sessions:${id}`).clear();
18
+ }
19
+ }
20
+ if (sessions.size === 0 && cleanupInterval) {
21
+ clearInterval(cleanupInterval);
22
+ cleanupInterval = null;
23
+ }
24
+ }, 6e4);
25
+ }
26
+ export default createMcpTransportHandler(async (createServer, event) => {
27
+ const sessionsConfig = config.sessions;
28
+ const sessionsEnabled = sessionsConfig?.enabled ?? false;
29
+ const request = toWebRequest(event);
30
+ if (!sessionsEnabled) {
31
+ const server2 = createServer();
32
+ event.context._mcpServer = server2;
33
+ const transport2 = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
34
+ event.node.res.on("close", () => {
35
+ transport2.close();
36
+ server2.close();
37
+ });
38
+ await server2.connect(transport2);
39
+ return transport2.handleRequest(request);
40
+ }
41
+ const maxDuration = sessionsConfig?.maxDuration ?? 30 * 60 * 1e3;
42
+ const sessionId = getHeader(event, "mcp-session-id");
43
+ if (sessionId) {
44
+ const session = sessions.get(sessionId);
45
+ if (!session) {
46
+ return new Response(JSON.stringify({
47
+ jsonrpc: "2.0",
48
+ error: { code: -32001, message: "Session not found" },
49
+ id: null
50
+ }), {
51
+ status: 404,
52
+ headers: { "Content-Type": "application/json" }
53
+ });
54
+ }
55
+ session.lastAccessed = Date.now();
56
+ event.context._mcpServer = session.server;
57
+ return session.transport.handleRequest(request);
58
+ }
59
+ const server = createServer();
60
+ event.context._mcpServer = server;
61
+ let sessionStored = false;
62
+ const transport = new WebStandardStreamableHTTPServerTransport({
63
+ sessionIdGenerator: () => globalThis.crypto.randomUUID(),
64
+ onsessioninitialized: (id) => {
65
+ sessionStored = true;
66
+ sessions.set(id, { server, transport, lastAccessed: Date.now() });
67
+ ensureCleanup(maxDuration);
68
+ }
9
69
  });
70
+ transport.onclose = () => {
71
+ const sid = transport.sessionId;
72
+ if (sid && sessions.has(sid)) {
73
+ sessions.delete(sid);
74
+ useStorage(`mcp:sessions:${sid}`).clear();
75
+ }
76
+ server.close();
77
+ };
10
78
  await server.connect(transport);
11
- const body = await readBody(event);
12
- await transport.handleRequest(event.node.req, event.node.res, body);
79
+ const response = await transport.handleRequest(request);
80
+ if (!sessionStored) {
81
+ event.node.res.on("close", () => {
82
+ transport.close();
83
+ server.close();
84
+ });
85
+ }
86
+ return response;
13
87
  });
@@ -1,4 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { H3Event } from 'h3';
3
- export type McpTransportHandler = (server: McpServer, event: H3Event) => Promise<Response | void> | Response | void;
3
+ export type McpTransportHandler = (createServer: () => McpServer, event: H3Event) => Promise<Response>;
4
4
  export declare const createMcpTransportHandler: (handler: McpTransportHandler) => McpTransportHandler;
@@ -0,0 +1,27 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export interface McpServerHelper {
3
+ /** Register a new tool mid-session. The client is notified automatically. */
4
+ registerTool: McpServer['registerTool'];
5
+ /** Register a new prompt mid-session. The client is notified automatically. */
6
+ registerPrompt: McpServer['registerPrompt'];
7
+ /** Register a new resource mid-session. The client is notified automatically. */
8
+ registerResource: McpServer['registerResource'];
9
+ /** Remove a dynamically registered tool by name. Returns `true` if found. */
10
+ removeTool(name: string): boolean;
11
+ /** Remove a dynamically registered prompt by name. Returns `true` if found. */
12
+ removePrompt(name: string): boolean;
13
+ /** Remove a dynamically registered resource by name. Returns `true` if found. */
14
+ removeResource(name: string): boolean;
15
+ /** The underlying `McpServer` instance for advanced SDK operations. */
16
+ server: McpServer;
17
+ }
18
+ /**
19
+ * Returns a helper to mutate the MCP server mid-session.
20
+ *
21
+ * Use inside tool, resource, or prompt handlers to register, remove,
22
+ * or update definitions while a session is active. The SDK automatically
23
+ * sends `list_changed` notifications to the client.
24
+ *
25
+ * Requires `nitro.experimental.asyncContext: true` in your Nuxt config.
26
+ */
27
+ export declare function useMcpServer(): McpServerHelper;
@@ -0,0 +1,44 @@
1
+ import { useEvent } from "nitropack/runtime";
2
+ const registrations = /* @__PURE__ */ new WeakMap();
3
+ function getRegistrations(server) {
4
+ let reg = registrations.get(server);
5
+ if (!reg) {
6
+ reg = { tools: /* @__PURE__ */ new Map(), prompts: /* @__PURE__ */ new Map(), resources: /* @__PURE__ */ new Map() };
7
+ registrations.set(server, reg);
8
+ }
9
+ return reg;
10
+ }
11
+ function removeByName(map, name) {
12
+ const handle = map.get(name);
13
+ if (!handle) return false;
14
+ handle.remove();
15
+ map.delete(name);
16
+ return true;
17
+ }
18
+ function wrapRegister(server, method, map) {
19
+ const fn = server[method].bind(server);
20
+ return (...args) => {
21
+ const handle = fn(...args);
22
+ map.set(args[0], handle);
23
+ return handle;
24
+ };
25
+ }
26
+ export function useMcpServer() {
27
+ const event = useEvent();
28
+ const server = event.context._mcpServer;
29
+ if (!server) {
30
+ throw new Error(
31
+ "No MCP server instance available. Ensure this is called within an MCP tool/resource/prompt handler and `nitro.experimental.asyncContext` is true."
32
+ );
33
+ }
34
+ const reg = getRegistrations(server);
35
+ return {
36
+ registerTool: wrapRegister(server, "registerTool", reg.tools),
37
+ registerPrompt: wrapRegister(server, "registerPrompt", reg.prompts),
38
+ registerResource: wrapRegister(server, "registerResource", reg.resources),
39
+ removeTool: (name) => removeByName(reg.tools, name),
40
+ removePrompt: (name) => removeByName(reg.prompts, name),
41
+ removeResource: (name) => removeByName(reg.resources, name),
42
+ server
43
+ };
44
+ }
@@ -0,0 +1,12 @@
1
+ import type { Storage } from 'unstorage';
2
+ export interface McpSessionStore<T = Record<string, unknown>> {
3
+ get<K extends keyof T & string>(key: K): Promise<T[K] | null>;
4
+ set<K extends keyof T & string>(key: K, value: T[K]): Promise<void>;
5
+ remove<K extends keyof T & string>(key: K): Promise<void>;
6
+ has<K extends keyof T & string>(key: K): Promise<boolean>;
7
+ keys(): Promise<string[]>;
8
+ clear(): Promise<void>;
9
+ /** Access the underlying unstorage instance */
10
+ storage: Storage;
11
+ }
12
+ export declare function useMcpSession<T = Record<string, unknown>>(): McpSessionStore<T>;
@@ -0,0 +1,21 @@
1
+ import { useStorage, useEvent } from "nitropack/runtime";
2
+ import { getHeader } from "h3";
3
+ export function useMcpSession() {
4
+ const event = useEvent();
5
+ const sessionId = getHeader(event, "mcp-session-id");
6
+ if (!sessionId) {
7
+ throw new Error(
8
+ "No active MCP session. Ensure `mcp.sessions` is enabled and `nitro.experimental.asyncContext` is true."
9
+ );
10
+ }
11
+ const storage = useStorage(`mcp:sessions:${sessionId}`);
12
+ return {
13
+ get: (key) => storage.getItem(key),
14
+ set: (key, value) => storage.setItem(key, value),
15
+ remove: (key) => storage.removeItem(key),
16
+ has: (key) => storage.hasItem(key),
17
+ keys: () => storage.getKeys(),
18
+ clear: () => storage.clear(),
19
+ storage
20
+ };
21
+ }
@@ -1,17 +1,29 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { H3Event } from 'h3';
3
- import type { McpToolDefinition, McpResourceDefinition, McpPromptDefinition, McpMiddleware } from './definitions/index.js';
3
+ import type { McpMiddleware } from './definitions/handlers.js';
4
+ import type { McpPromptDefinition } from './definitions/prompts.js';
5
+ import type { McpResourceDefinition } from './definitions/resources.js';
6
+ import type { McpToolDefinition } from './definitions/tools.js';
4
7
  export type { McpTransportHandler } from './providers/types.js';
5
8
  export { createMcpTransportHandler } from './providers/types.js';
9
+ type MaybeDynamic<T> = T | ((event: H3Event) => T | Promise<T>);
10
+ type MaybeDynamicTools = MaybeDynamic<Array<McpToolDefinition<any, any>>>;
6
11
  export interface ResolvedMcpConfig {
7
12
  name: string;
8
13
  version: string;
9
14
  browserRedirect: string;
10
- tools?: McpToolDefinition[];
11
- resources?: McpResourceDefinition[];
12
- prompts?: McpPromptDefinition[];
15
+ tools?: MaybeDynamicTools;
16
+ resources?: MaybeDynamic<McpResourceDefinition[]>;
17
+ prompts?: MaybeDynamic<McpPromptDefinition[]>;
13
18
  middleware?: McpMiddleware;
14
19
  }
20
+ interface StaticMcpConfig {
21
+ name: string;
22
+ version: string;
23
+ tools: McpToolDefinition[];
24
+ resources: McpResourceDefinition[];
25
+ prompts: McpPromptDefinition[];
26
+ }
15
27
  export type CreateMcpHandlerConfig = ResolvedMcpConfig | ((event: H3Event) => ResolvedMcpConfig);
16
- export declare function createMcpServer(config: ResolvedMcpConfig): McpServer;
28
+ export declare function createMcpServer(config: StaticMcpConfig): McpServer;
17
29
  export declare function createMcpHandler(config: CreateMcpHandlerConfig): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
@@ -1,25 +1,60 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { sendRedirect, getHeader, defineEventHandler } from "h3";
3
- import { registerToolFromDefinition, registerResourceFromDefinition, registerPromptFromDefinition } from "./definitions/index.js";
3
+ import { registerPromptFromDefinition } from "./definitions/prompts.js";
4
+ import { registerResourceFromDefinition } from "./definitions/resources.js";
5
+ import { registerToolFromDefinition } from "./definitions/tools.js";
4
6
  import handleMcpRequest from "#nuxt-mcp-toolkit/transport.mjs";
5
7
  export { createMcpTransportHandler } from "./providers/types.js";
6
8
  function resolveConfig(config, event) {
7
9
  return typeof config === "function" ? config(event) : config;
8
10
  }
11
+ async function filterByEnabled(definitions, event) {
12
+ const results = await Promise.all(
13
+ definitions.map(async (def) => {
14
+ if (!def.enabled) return true;
15
+ return def.enabled(event);
16
+ })
17
+ );
18
+ return definitions.filter((_, i) => results[i]);
19
+ }
20
+ async function resolveDynamicDefinitions(config, event) {
21
+ const tools = typeof config.tools === "function" ? await config.tools(event) : config.tools || [];
22
+ const resources = typeof config.resources === "function" ? await config.resources(event) : config.resources || [];
23
+ const prompts = typeof config.prompts === "function" ? await config.prompts(event) : config.prompts || [];
24
+ return {
25
+ name: config.name,
26
+ version: config.version,
27
+ tools: await filterByEnabled(tools, event),
28
+ resources: await filterByEnabled(resources, event),
29
+ prompts: await filterByEnabled(prompts, event)
30
+ };
31
+ }
32
+ function registerEmptyDefinitionFallbacks(server, config) {
33
+ if (!config.tools.length) {
34
+ server.registerTool("__init__", {}, async () => ({ content: [] })).remove();
35
+ }
36
+ if (!config.resources.length) {
37
+ server.registerResource("__init__", "noop://init", {}, async () => ({ contents: [] })).remove();
38
+ }
39
+ if (!config.prompts.length) {
40
+ server.registerPrompt("__init__", {}, async () => ({ messages: [] })).remove();
41
+ }
42
+ }
9
43
  export function createMcpServer(config) {
10
44
  const server = new McpServer({
11
45
  name: config.name,
12
46
  version: config.version
13
47
  });
14
- for (const tool of config.tools || []) {
48
+ for (const tool of config.tools) {
15
49
  registerToolFromDefinition(server, tool);
16
50
  }
17
- for (const resource of config.resources || []) {
51
+ for (const resource of config.resources) {
18
52
  registerResourceFromDefinition(server, resource);
19
53
  }
20
- for (const prompt of config.prompts || []) {
54
+ for (const prompt of config.prompts) {
21
55
  registerPromptFromDefinition(server, prompt);
22
56
  }
57
+ registerEmptyDefinitionFallbacks(server, config);
23
58
  return server;
24
59
  }
25
60
  export function createMcpHandler(config) {
@@ -29,8 +64,8 @@ export function createMcpHandler(config) {
29
64
  return sendRedirect(event, resolvedConfig.browserRedirect);
30
65
  }
31
66
  const handler = async () => {
32
- const server = createMcpServer(resolvedConfig);
33
- return handleMcpRequest(server, event);
67
+ const staticConfig = await resolveDynamicDefinitions(resolvedConfig, event);
68
+ return handleMcpRequest(() => createMcpServer(staticConfig), event);
34
69
  };
35
70
  if (resolvedConfig.middleware) {
36
71
  let nextCalled = false;
@@ -3,6 +3,10 @@ declare module '@nuxt/schema' {
3
3
  /**
4
4
  * Add additional directories to scan for MCP definition files (tools, resources, prompts, handlers).
5
5
  * @param paths - Object containing arrays of directory paths for each definition type.
6
+ * @param paths.tools - Array of directory paths to scan for tool definitions.
7
+ * @param paths.resources - Array of directory paths to scan for resource definitions.
8
+ * @param paths.prompts - Array of directory paths to scan for prompt definitions.
9
+ * @param paths.handlers - Array of directory paths to scan for handler definitions.
6
10
  * @returns void | Promise<void>
7
11
  */
8
12
  'mcp:definitions:paths': (paths: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxtjs/mcp-toolkit",
3
- "version": "0.7.0",
3
+ "version": "0.10.0",
4
4
  "description": "Create MCP servers directly in your Nuxt application. Define tools, resources, and prompts with a simple and intuitive API.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,20 +39,19 @@
39
39
  "dist"
40
40
  ],
41
41
  "dependencies": {
42
- "@modelcontextprotocol/sdk": "^1.26.0",
43
- "@nuxt/kit": "^4.3.1",
44
- "defu": "^6.1.4",
45
- "ms": "^2.1.3",
46
- "pathe": "^2.0.3",
47
- "satori": "^0.19.2",
48
- "scule": "^1.3.0",
42
+ "@modelcontextprotocol/sdk": "^1.27.1",
43
+ "@nuxt/kit": "^4.4.2",
49
44
  "tinyglobby": "^0.2.15"
50
45
  },
51
46
  "peerDependencies": {
47
+ "h3": "^1.15.6",
52
48
  "zod": "^4.1.13",
53
- "agents": ">=0.4.1"
49
+ "agents": ">=0.7.6"
54
50
  },
55
51
  "peerDependenciesMeta": {
52
+ "h3": {
53
+ "optional": false
54
+ },
56
55
  "zod": {
57
56
  "optional": false
58
57
  },
@@ -61,18 +60,18 @@
61
60
  }
62
61
  },
63
62
  "devDependencies": {
64
- "@nuxt/devtools": "^3.2.1",
65
- "@nuxt/eslint-config": "^1.15.1",
63
+ "@nuxt/devtools": "^3.2.3",
64
+ "@nuxt/eslint-config": "^1.15.2",
66
65
  "@nuxt/module-builder": "^1.0.2",
67
- "@nuxt/schema": "^4.3.1",
66
+ "@nuxt/schema": "^4.4.2",
68
67
  "@nuxt/test-utils": "^4.0.0",
69
68
  "@types/node": "latest",
70
69
  "changelogen": "^0.6.2",
71
- "eslint": "^9.39.2",
72
- "nuxt": "^4.3.1",
70
+ "eslint": "^9.39.4",
71
+ "nuxt": "^4.4.2",
73
72
  "typescript": "~5.9.3",
74
- "vitest": "^4.0.18",
75
- "vue-tsc": "^3.2.4"
73
+ "vitest": "^4.1.0",
74
+ "vue-tsc": "^3.2.5"
76
75
  },
77
76
  "publishConfig": {
78
77
  "access": "public"