@nuxtjs/mcp-toolkit 0.8.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.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxtjs/mcp-toolkit",
3
- "version": "0.8.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.8.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);
@@ -99,6 +98,10 @@ const module$1 = defineNuxtModule({
99
98
  if (!options.enabled) {
100
99
  return;
101
100
  }
101
+ if (mcpConfig.sessions.enabled && nitroOptions) {
102
+ nitroOptions.storage ??= {};
103
+ nitroOptions.storage["mcp:sessions"] ??= { driver: "memory" };
104
+ }
102
105
  addComponent({
103
106
  name: "InstallButton",
104
107
  filePath: resolver.resolve("runtime/components/InstallButton.vue")
@@ -188,6 +191,8 @@ const module$1 = defineNuxtModule({
188
191
  }
189
192
  });
190
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");
191
196
  addServerImports([
192
197
  "defineMcpTool",
193
198
  "defineMcpResource",
@@ -198,6 +203,10 @@ const module$1 = defineNuxtModule({
198
203
  "errorResult",
199
204
  "imageResult"
200
205
  ].map((name2) => ({ name: name2, from: mcpDefinitionsPath })));
206
+ addServerImports([
207
+ { name: "useMcpSession", from: mcpSessionPath },
208
+ { name: "useMcpServer", from: mcpServerPath }
209
+ ]);
201
210
  addServerHandler({
202
211
  route: options.route,
203
212
  handler: resolver.resolve("runtime/server/mcp/handler")
@@ -210,7 +219,11 @@ const module$1 = defineNuxtModule({
210
219
  route: `${options.route}/badge.svg`,
211
220
  handler: resolver.resolve("runtime/server/mcp/badge-image")
212
221
  });
213
- 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
+ }
214
227
  }
215
228
  });
216
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,4 +1,3 @@
1
- import { defu } from "defu";
2
1
  export const defaultMcpConfig = {
3
2
  enabled: true,
4
3
  route: "/mcp",
@@ -13,5 +12,15 @@ export const defaultMcpConfig = {
13
12
  }
14
13
  };
15
14
  export function getMcpConfig(partial) {
16
- 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
+ };
17
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",
@@ -8,6 +8,7 @@ const fallbackCtx = {
8
8
  };
9
9
  export default createMcpTransportHandler(async (createServer, event) => {
10
10
  const server = createServer();
11
+ event.context._mcpServer = server;
11
12
  const { createMcpHandler } = await import("agents/mcp");
12
13
  const handler = createMcpHandler(server, {
13
14
  route: ""
@@ -1,6 +1,6 @@
1
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2
- import { randomUUID } from "uncrypto";
3
- import { readBody, getHeader, getMethod } from "h3";
1
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
2
+ import { toWebRequest, getHeader } from "h3";
3
+ import { useStorage } from "nitropack/runtime";
4
4
  import config from "#nuxt-mcp-toolkit/config.mjs";
5
5
  import { createMcpTransportHandler } from "./types.js";
6
6
  const sessions = /* @__PURE__ */ new Map();
@@ -14,6 +14,7 @@ function ensureCleanup(maxDuration) {
14
14
  session.transport.close();
15
15
  session.server.close();
16
16
  sessions.delete(id);
17
+ useStorage(`mcp:sessions:${id}`).clear();
17
18
  }
18
19
  }
19
20
  if (sessions.size === 0 && cleanupInterval) {
@@ -25,41 +26,41 @@ function ensureCleanup(maxDuration) {
25
26
  export default createMcpTransportHandler(async (createServer, event) => {
26
27
  const sessionsConfig = config.sessions;
27
28
  const sessionsEnabled = sessionsConfig?.enabled ?? false;
29
+ const request = toWebRequest(event);
28
30
  if (!sessionsEnabled) {
29
31
  const server2 = createServer();
30
- const transport2 = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
32
+ event.context._mcpServer = server2;
33
+ const transport2 = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
31
34
  event.node.res.on("close", () => {
32
35
  transport2.close();
33
36
  server2.close();
34
37
  });
35
38
  await server2.connect(transport2);
36
- const body2 = await readBody(event);
37
- await transport2.handleRequest(event.node.req, event.node.res, body2);
38
- return;
39
+ return transport2.handleRequest(request);
39
40
  }
40
41
  const maxDuration = sessionsConfig?.maxDuration ?? 30 * 60 * 1e3;
41
- const method = getMethod(event);
42
42
  const sessionId = getHeader(event, "mcp-session-id");
43
43
  if (sessionId) {
44
44
  const session = sessions.get(sessionId);
45
45
  if (!session) {
46
- event.node.res.writeHead(404, { "Content-Type": "application/json" });
47
- event.node.res.end(JSON.stringify({
46
+ return new Response(JSON.stringify({
48
47
  jsonrpc: "2.0",
49
48
  error: { code: -32001, message: "Session not found" },
50
49
  id: null
51
- }));
52
- return;
50
+ }), {
51
+ status: 404,
52
+ headers: { "Content-Type": "application/json" }
53
+ });
53
54
  }
54
55
  session.lastAccessed = Date.now();
55
- const body2 = method === "POST" ? await readBody(event) : void 0;
56
- await session.transport.handleRequest(event.node.req, event.node.res, body2);
57
- return;
56
+ event.context._mcpServer = session.server;
57
+ return session.transport.handleRequest(request);
58
58
  }
59
59
  const server = createServer();
60
+ event.context._mcpServer = server;
60
61
  let sessionStored = false;
61
- const transport = new StreamableHTTPServerTransport({
62
- sessionIdGenerator: () => randomUUID(),
62
+ const transport = new WebStandardStreamableHTTPServerTransport({
63
+ sessionIdGenerator: () => globalThis.crypto.randomUUID(),
63
64
  onsessioninitialized: (id) => {
64
65
  sessionStored = true;
65
66
  sessions.set(id, { server, transport, lastAccessed: Date.now() });
@@ -70,14 +71,17 @@ export default createMcpTransportHandler(async (createServer, event) => {
70
71
  const sid = transport.sessionId;
71
72
  if (sid && sessions.has(sid)) {
72
73
  sessions.delete(sid);
74
+ useStorage(`mcp:sessions:${sid}`).clear();
73
75
  }
74
76
  server.close();
75
77
  };
76
78
  await server.connect(transport);
77
- const body = await readBody(event);
78
- await transport.handleRequest(event.node.req, event.node.res, body);
79
+ const response = await transport.handleRequest(request);
79
80
  if (!sessionStored) {
80
- transport.close();
81
- server.close();
81
+ event.node.res.on("close", () => {
82
+ transport.close();
83
+ server.close();
84
+ });
82
85
  }
86
+ return response;
83
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 = (createServer: () => 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
+ }
@@ -6,15 +6,24 @@ import type { McpResourceDefinition } from './definitions/resources.js';
6
6
  import type { McpToolDefinition } from './definitions/tools.js';
7
7
  export type { McpTransportHandler } from './providers/types.js';
8
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>>>;
9
11
  export interface ResolvedMcpConfig {
10
12
  name: string;
11
13
  version: string;
12
14
  browserRedirect: string;
13
- tools?: McpToolDefinition[];
14
- resources?: McpResourceDefinition[];
15
- prompts?: McpPromptDefinition[];
15
+ tools?: MaybeDynamicTools;
16
+ resources?: MaybeDynamic<McpResourceDefinition[]>;
17
+ prompts?: MaybeDynamic<McpPromptDefinition[]>;
16
18
  middleware?: McpMiddleware;
17
19
  }
20
+ interface StaticMcpConfig {
21
+ name: string;
22
+ version: string;
23
+ tools: McpToolDefinition[];
24
+ resources: McpResourceDefinition[];
25
+ prompts: McpPromptDefinition[];
26
+ }
18
27
  export type CreateMcpHandlerConfig = ResolvedMcpConfig | ((event: H3Event) => ResolvedMcpConfig);
19
- export declare function createMcpServer(config: ResolvedMcpConfig): McpServer;
28
+ export declare function createMcpServer(config: StaticMcpConfig): McpServer;
20
29
  export declare function createMcpHandler(config: CreateMcpHandlerConfig): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
@@ -8,14 +8,35 @@ export { createMcpTransportHandler } from "./providers/types.js";
8
8
  function resolveConfig(config, event) {
9
9
  return typeof config === "function" ? config(event) : config;
10
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
+ }
11
32
  function registerEmptyDefinitionFallbacks(server, config) {
12
- if (!config.tools?.length) {
33
+ if (!config.tools.length) {
13
34
  server.registerTool("__init__", {}, async () => ({ content: [] })).remove();
14
35
  }
15
- if (!config.resources?.length) {
36
+ if (!config.resources.length) {
16
37
  server.registerResource("__init__", "noop://init", {}, async () => ({ contents: [] })).remove();
17
38
  }
18
- if (!config.prompts?.length) {
39
+ if (!config.prompts.length) {
19
40
  server.registerPrompt("__init__", {}, async () => ({ messages: [] })).remove();
20
41
  }
21
42
  }
@@ -24,13 +45,13 @@ export function createMcpServer(config) {
24
45
  name: config.name,
25
46
  version: config.version
26
47
  });
27
- for (const tool of config.tools || []) {
48
+ for (const tool of config.tools) {
28
49
  registerToolFromDefinition(server, tool);
29
50
  }
30
- for (const resource of config.resources || []) {
51
+ for (const resource of config.resources) {
31
52
  registerResourceFromDefinition(server, resource);
32
53
  }
33
- for (const prompt of config.prompts || []) {
54
+ for (const prompt of config.prompts) {
34
55
  registerPromptFromDefinition(server, prompt);
35
56
  }
36
57
  registerEmptyDefinitionFallbacks(server, config);
@@ -43,7 +64,8 @@ export function createMcpHandler(config) {
43
64
  return sendRedirect(event, resolvedConfig.browserRedirect);
44
65
  }
45
66
  const handler = async () => {
46
- return handleMcpRequest(() => createMcpServer(resolvedConfig), event);
67
+ const staticConfig = await resolveDynamicDefinitions(resolvedConfig, event);
68
+ return handleMcpRequest(() => createMcpServer(staticConfig), event);
47
69
  };
48
70
  if (resolvedConfig.middleware) {
49
71
  let nextCalled = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxtjs/mcp-toolkit",
3
- "version": "0.8.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",
@@ -41,11 +41,6 @@
41
41
  "dependencies": {
42
42
  "@modelcontextprotocol/sdk": "^1.27.1",
43
43
  "@nuxt/kit": "^4.4.2",
44
- "defu": "^6.1.4",
45
- "ms": "^2.1.3",
46
- "pathe": "^2.0.3",
47
- "satori": "^0.25.0",
48
- "scule": "^1.3.0",
49
44
  "tinyglobby": "^0.2.15"
50
45
  },
51
46
  "peerDependencies": {