import { streamText, convertToModelMessages, smoothStream, jsonSchema, isStepCount, toUIMessageStream, createUIMessageStreamResponse } from 'ai' import type { AnthropicLanguageModelOptions } from '@ai-sdk/anthropic' import { gateway } from '@ai-sdk/gateway' import { z } from 'zod' import { tools as mcpToolDefinitions } from '#nuxt-mcp-toolkit/tools.mjs' import { themeIcons, cssVariableDefaults } from '../../app/utils/theme' function mcpToolsToAiTools() { const aiTools: Record, execute: (args: any) => Promise }> = {} for (const def of mcpToolDefinitions as any[]) { const filename = def._meta?.filename as string | undefined const name = def.name || (filename ? filename.replace(/\.(ts|js|mts|mjs)$/, '').replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/[_\s]+/g, '-').toLowerCase() : null) if (!name) continue const schema = def.inputSchema ? z.toJSONSchema(z.object(def.inputSchema)) as Record : { type: 'object' as const, properties: {} } aiTools[name] = { description: def.description || '', inputSchema: jsonSchema(schema), execute: async (args: any) => { try { return await def.handler(args, {}) } catch (error: any) { return { error: error.statusCode ? `[${error.statusCode}] ${error.message}` : error.message || String(error) } } } } } return aiTools } const applyTheme = { description: 'Apply theme settings live on the docs site. Call this when users ask to change colors, radius, font, or other theme properties. Only include properties that changed.', inputSchema: jsonSchema>({ type: 'object' as const, properties: { primary: { type: 'string', description: 'Primary color name (e.g., green, blue, red, indigo)' }, neutral: { type: 'string', description: 'Neutral color name (slate, gray, zinc, neutral, stone, taupe, mauve, mist, olive)' }, secondary: { type: 'string', description: 'Secondary color name' }, success: { type: 'string', description: 'Success color name' }, info: { type: 'string', description: 'Info color name' }, warning: { type: 'string', description: 'Warning color name' }, error: { type: 'string', description: 'Error color name' }, radius: { type: 'number', description: 'Border radius in rem: 0, 0.125, 0.25, 0.375, 0.5' }, font: { type: 'string', description: 'Font family name — any Google Font works (e.g. Public Sans, DM Sans, Geist, Inter, Poppins, Outfit, Raleway, Playfair Display, Nunito, JetBrains Mono, etc.)' }, blackAsPrimary: { type: 'boolean', description: 'Use solid black/white as primary color for a monochrome look' }, icons: { type: 'string', description: 'Icon set for live preview: lucide (default), phosphor, or tabler. For exported code, any Iconify icon set can be suggested.' }, customColors: { type: 'object', description: 'Custom color palettes with shades 50-950 as hex values', additionalProperties: { type: 'object', additionalProperties: { type: 'string' } } }, cssVariables: { type: 'object', description: 'Fine-tuning CSS variable overrides (last resort). Use only for subtle shade adjustments that can\'t be achieved with color names or customColors. Always provide both light and dark.', properties: { light: { type: 'object', description: 'CSS variables for light mode (.light). Keys: --ui-text, --ui-bg, --ui-border, --ui-primary, etc. Values: var(--ui-color--), hex, white, black.', additionalProperties: { type: 'string' } }, dark: { type: 'object', description: 'CSS variables for dark mode (.dark). Same variable names as light.', additionalProperties: { type: 'string' } } } }, ui: { type: 'object', description: 'Component-level theme overrides. MUST include ALL component customizations here so they are applied live. Keys are camelCase component names (e.g. button, badge, popover). Values have slots, defaultVariants, variants, compoundVariants.', additionalProperties: true } } }), execute: async (settings: Record) => ({ applied: true, ...settings }) } const resetTheme = { description: 'Reset the theme back to defaults (primary: green, neutral: slate, radius: 0.25rem, font: Public Sans). Call this when users ask to reset, revert, or restore the default theme.', inputSchema: jsonSchema>({ type: 'object' as const, properties: {} }), execute: async () => ({ reset: true }) } const getThemeGuide = { description: 'Get detailed instructions for applying live theme changes. Call this ONLY when you are about to use applyTheme (e.g. user says "make it blue", "create a dark theme"). Do NOT call for documentation questions about theming — search docs instead.', inputSchema: jsonSchema>({ type: 'object' as const, properties: {} }), execute: async () => ({ guide: `When users ask to change the theme, customize colors, or modify the appearance, use the \`applyTheme\` tool to apply changes live on this docs site. Only include properties that changed. When users ask for a complete theme, to change "all colors", or describe a broad aesthetic (e.g. "sakura-inspired theme"), you MUST set ALL of: primary, neutral, secondary, success, info, warning, error, radius, and font. You can change the icon set (lucide, phosphor, or tabler) if it really enhances the theme, but prefer keeping lucide as the default — it works well with most themes. You can optionally include component-level \`ui\` overrides for a more polished result — if you do, look up the component theme first with \`getComponentTheme\` and prefer \`defaultVariants\` (e.g. button size or variant) over slot class overrides. Create a cohesive design system, not just random colors: - Pick a **primary** that embodies the theme's identity. If no standard Tailwind color fits, use \`customColors\` to define a bespoke palette with all shades 50-950 as hex values — this is encouraged for creative/unique themes. - Pick a **secondary** that complements the primary (analogous or contrasting on the color wheel). Can also be a custom palette. - Pick **success/info/warning/error** that feel harmonious with the palette while staying semantically meaningful (success = green-ish, error = red-ish, warning = amber/yellow-ish, info = blue/cyan-ish). You can shift hues — e.g. \`lime\` for success in a nature theme, \`rose\` for error in a warm theme — but keep them recognizable. - For monochrome/black-and-white themes, keep semantic colors meaningful. Only primary, secondary, and neutral should go monochrome. Use \`blackAsPrimary: true\` for monochrome primary. When users ask to reset, revert, or restore the default theme, use the \`resetTheme\` tool. This resets primary to green, neutral to slate, radius to 0.25rem, font to Public Sans, and removes any custom colors. There are two types of customization: **1. CSS Variables (main.css)** The main.css file uses Tailwind CSS directives to configure design tokens: *Fonts* — use the \`@theme\` directive. Any Google Font works, \`@nuxt/fonts\` will automatically load and optimize it: \`\`\`css @theme { --font-sans: 'Inter', sans-serif; } \`\`\` *Custom Colors* — define palettes with ALL shades (50-950) using \`@theme static\`: \`\`\`css @theme static { --color-brand-50: #fef2f2; --color-brand-100: #fee2e2; /* ... all shades 200-900 ... */ --color-brand-950: #450a0a; } \`\`\` *Radius:* \`\`\`css :root { --ui-radius: 0.375rem; } \`\`\` *Monochrome primary:* \`\`\`css :root { --ui-primary: black; } .dark { --ui-primary: white; } \`\`\` *True black & white theme* — for a monochrome theme, also set \`--ui-bg\` to pure black/white: \`\`\`css .dark { --ui-bg: black; } \`\`\` *Semantic shade overrides* — override which shade a semantic color uses: \`\`\`css :root, .light { --ui-primary: var(--ui-color-primary-700); } .dark { --ui-primary: var(--ui-color-primary-200); } \`\`\` **CSS Variable fine-tuning (last resort)** — use the \`cssVariables\` property in \`applyTheme\` ONLY for subtle one-shade adjustments. Example: shifting \`--ui-bg\` from neutral-900 to neutral-950 in dark mode, or \`--ui-border\` from neutral-200 to neutral-300 in light mode. CRITICAL RULES for \`cssVariables\`: - ONLY shift by 1-2 shade levels from the default (e.g. neutral-900 → neutral-950). NEVER replace the neutral palette with a completely different color (e.g. setting \`--ui-bg\` to a custom color like cream). If you want warm/cool backgrounds, choose the right \`neutral\` color instead (see color options below). Exception: for monochrome/black-and-white themes, you MAY use \`black\` or \`white\` as values (e.g. \`--ui-bg: black\` in dark mode). - ALWAYS provide BOTH \`light\` and \`dark\` objects, but only include variables you are CHANGING from their defaults. Do NOT include variables that keep their default value. - Values MUST use \`var(--ui-color--)\` references (e.g. \`var(--ui-color-neutral-950)\`), \`white\`, or \`black\`. NEVER use raw hex values. - The \`\` in the variable reference MUST match the current neutral color (which the user may have changed). Use \`neutral\` as the name since it maps to whatever neutral palette is active. - In the exported main.css code, ONLY show overridden CSS variables (not defaults). Use \`:root, .light { }\` for light-mode overrides and \`.dark { }\` for dark-mode overrides. NEVER put CSS variable overrides in a plain \`:root { }\` block (that's only for \`--ui-radius\` and monochrome \`--ui-primary\`). Default values — only override the ones you want to change: Light defaults (\`:root, .light\`): ${Object.entries(cssVariableDefaults.light).map(([k, v]) => `- \`${k}\`: \`${v}\``).join('\n')} Dark defaults (\`.dark\`): ${Object.entries(cssVariableDefaults.dark).map(([k, v]) => `- \`${k}\`: \`${v}\``).join('\n')} Semantic colors (\`--ui-primary\`, \`--ui-secondary\`, \`--ui-success\`, \`--ui-info\`, \`--ui-warning\`, \`--ui-error\`) default to shade 500 in light, 400 in dark. Do NOT use \`cssVariables\` for things achievable with \`primary\`, \`neutral\`, \`customColors\`, or component \`ui\` overrides. **2. Config (app.config.ts for Nuxt / vite.config.ts for Vue)** For semantic color assignment and component-level theming. The \`ui\` object is the same for both frameworks: \`\`\` ui: { colors: { primary: 'blue', neutral: 'zinc' }, button: { slots: { base: 'font-bold' }, defaultVariants: { size: 'lg' } } } \`\`\` For Nuxt, wrap in \`defineAppConfig({ ui: { ... } })\`. For Vue, pass as \`ui({ ui: { ... } })\` in the Vite plugin. **Color options:** - Standard Tailwind: red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose - Neutral palettes (pick one that matches the aesthetic): - **slate** — cool blue-gray, professional, default - **gray** — true neutral, clean, no color tint - **zinc** — slightly cool, modern, techy - **neutral** — perfectly balanced, no warmth or coolness - **stone** — warm gray, earthy, organic feel - **taupe** — warm brown-gray, sophisticated, vintage - **mauve** — purple-tinted gray, elegant, creative - **mist** — soft blue-gray, airy, light - **olive** — green-tinted gray, natural, earthy For example: warm/cozy themes → stone or taupe, elegant/creative → mauve, tech/minimal → zinc, nature → olive, etc. ALWAYS change the neutral when creating a complete theme — don't leave it as the default slate unless it genuinely fits. - Custom: define all shades 50-950 in \`customColors\` of the \`applyTheme\` tool, then reference the name **Other options:** - Radius (pick one that matches the aesthetic): - **0** — sharp, brutalist, no rounding - **0.125** — subtle, minimal softness - **0.25** — balanced, default - **0.375** — rounded, friendly - **0.5** — pill-like, playful, soft - Font: any Google Font works, \`@nuxt/fonts\` auto-loads it. Pick a font that matches the theme's personality: - Sans-serif (clean/modern): Inter, DM Sans, Geist, Public Sans, Outfit, Plus Jakarta Sans, Space Grotesk - Serif (elegant/editorial): Playfair Display, Lora, Merriweather, Fraunces, Newsreader - Rounded (friendly/playful): Nunito, Quicksand, Varela Round - Monospace (techy/dev): JetBrains Mono, Fira Code, IBM Plex Mono ALWAYS change the font when creating a complete theme — don't leave the default unless it genuinely fits. - Icons: lucide (default), phosphor, or tabler for live preview. Any Iconify icon set works in the exported config. When suggesting a non-default icon set, include the FULL \`ui.icons\` mapping in the exported config and tell the user to install \`@iconify-json/{collection}\` (e.g. \`@iconify-json/ph\` for Phosphor). Required keys: ${Object.keys(themeIcons.phosphor).join(', ')}. Values use \`i--\` format. **Component Theme Lookup:** When users ask about component-specific customization, use the \`getComponentTheme\` tool to get the exact slots, variants, and defaults for that component. This lets you suggest precise config overrides. When you want to suggest component \`ui\` overrides (e.g. customizing button styles), call \`getComponentTheme\` for that component first — never guess slot names. Only look up component themes when you actually plan to include \`ui\` overrides in the \`applyTheme\` call. Prefer \`defaultVariants\` as your first tool for component customization — changing a component's default size, variant, or color is impactful and low-risk. Only include values that actually differ from the component's defaults (use \`getComponentTheme\` to check) — never include unchanged defaults like \`size: 'md'\` if that's already the default. Only add slot class overrides when \`defaultVariants\` alone can't achieve the desired effect. When creating a complete theme, if you want to customize one component to give it personality, prioritize **button** — e.g. \`button: { defaultVariants: { variant: 'subtle' } }\`. Button is the most visible and impactful component to customize. CRITICAL rules for component \`ui\` overrides: - NEVER use \`rounded-*\` classes in component slot overrides. Border radius is controlled globally by \`--ui-radius\` — hardcoding rounded classes would override the CSS variable and break consistency. - Only ADD new classes that aren't already in the component's default theme. Do NOT repeat or duplicate default classes (e.g. \`inline-flex\`, \`items-center\`, \`disabled:cursor-not-allowed\`, \`transition-colors\` on button are already defaults). Use \`getComponentTheme\` to check what's already there. - Keep overrides minimal and intentional — only include classes that actually change the look from the default. - Tailwind v4 only generates classes that are already used in source files. Arbitrary Tailwind utility classes (e.g. \`tracking-wide\`, \`shadow-2xl\`) may NOT exist in the user's CSS output. Prefer overrides that use classes already present in the component's default theme (e.g. changing \`font-medium\` to \`font-semibold\`) or CSS variables. For the exported code, mention that users may need to safelist any new utility classes. **When suggesting theme changes, you MUST:** 1. Call the \`applyTheme\` tool with the settings so changes apply live 2. Show the full **main.css** code block so users can copy it. Use this structure: \`\`\`css @import "tailwindcss"; @import "@nuxt/ui"; @theme { --font-sans: 'FontName', sans-serif; /* only if font changed */ } @theme static { /* custom color palettes here */ } :root { --ui-radius: 0.375rem; /* only radius and monochrome --ui-primary go here */ } :root, .light { /* light-mode CSS variable overrides here (if any) */ } .dark { /* dark-mode CSS variable overrides AND monochrome --ui-primary: white here */ } \`\`\` 3. Show the config code block if colors, icons, or component overrides changed. Use **app.config.ts** for Nuxt or **vite.config.ts** for Vue (based on the user's framework). IMPORTANT: this must include ALL settings from the entire conversation — not just the current \`applyTheme\` call but also all previous calls (colors, icons with full \`ui.icons\` mapping, component \`ui\` overrides like button, popover, etc.). If a non-default icon set was chosen, the exported config MUST include the complete \`ui.icons\` object with every key mapped. Review earlier \`applyTheme\` calls in the conversation and merge everything into one complete config. For **Nuxt** — \`app.config.ts\`: \`\`\`typescript export default defineAppConfig({ ui: { // ... ALL accumulated config from this conversation } }) \`\`\` For **Vue** — \`vite.config.ts\`: \`\`\`typescript import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import ui from '@nuxt/ui/vite' export default defineConfig({ plugins: [ vue(), ui({ ui: { // ... ALL accumulated config from this conversation } }) ] }) \`\`\` NEVER recommend \`appConfig.theme.*\` properties (like \`blackAsPrimary\`, \`radius\`, \`font\`) — those are internal to the docs site. Users should use CSS variables in main.css for radius, fonts, and monochrome primary.` }) } export default defineEventHandler(async (event) => { const { messages, theme, framework, currentPage } = await readBody(event) if (!messages || !Array.isArray(messages)) { throw createError({ statusCode: 400, message: 'Invalid or missing messages array.' }) } // `currentPage` is interpolated verbatim into the system prompt below, so it is an // indirect prompt-injection surface (a crafted /docs/... link can smuggle newlines and // instructions via Vue Router's path decoding). Accept it only when it is a plain docs // path with no control characters; otherwise drop it. const safeCurrentPage = typeof currentPage === 'string' && currentPage.length <= 128 && !/[\r\n]/.test(currentPage) && /^\/docs\/[\w/-]*$/.test(currentPage) ? currentPage : null const componentNames = theme ? Object.keys(theme) : [] const getComponentTheme = { description: 'Get the theme definition (slots, variants, compoundVariants, defaultVariants) for a specific Nuxt UI component. Call this when you need to know the available slots and customization options to suggest component-level theming.', inputSchema: jsonSchema<{ componentName: string }>({ type: 'object' as const, properties: { componentName: { type: 'string', description: `Component name in camelCase. Available: ${componentNames.join(', ')}` } }, required: ['componentName'] }), execute: async ({ componentName }: { componentName: string }) => { if (!theme?.[componentName]) { return { error: `Component "${componentName}" not found`, availableComponents: componentNames } } return { componentName, theme: theme[componentName] } } } const mcpTools = mcpToolsToAiTools() const abortController = new AbortController() event.node.req.on('close', () => abortController.abort()) const instructions = `You are a helpful assistant for Nuxt UI, a UI library for Nuxt and Vue. Nuxt UI includes \`@nuxt/fonts\` and \`@nuxt/icon\` as built-in dependencies — never tell users to install them separately. Use your knowledge base tools to search for relevant information before answering questions. The user is using **${framework === 'vue' ? 'Vue' : 'Nuxt'}**. Tailor your answers accordingly — ${framework === 'vue' ? 'use the Vite plugin setup, Vue Router, and vite.config.ts instead of Nuxt-specific features like modules or app.config.ts. IMPORTANT: The Vite plugin auto-imports components and Nuxt UI composables, but Vue core APIs and VueUse must be explicitly imported — always include these in code examples (e.g. `import { ref, computed } from \'vue\'`, `import { useColorMode } from \'@vueuse/core\'`).' : 'use Nuxt modules, auto-imports, app.config.ts, and other Nuxt-specific features. Nuxt auto-imports Vue APIs (ref, computed, etc.), composables, and components — do not include these imports in code examples.'} ${safeCurrentPage ? `\nThe user is currently viewing the documentation page at \`${safeCurrentPage}\`. Use this context to provide more relevant answers (e.g. read that page first if the question seems related), but don't limit yourself to that page if the question is broader or unrelated.\n` : ''} Guidelines: - For documentation questions, ALWAYS use tools to search for information. Never rely on pre-trained knowledge for Nuxt UI APIs, props, or usage. - For questions about how to customize themes (e.g. "how do I customize colors?", "how does theming work?"), search the documentation like any other docs question. - When users ask you to APPLY a theme change live (e.g. "make it blue", "create a sakura theme", "change the font"), call \`getThemeGuide\` first for detailed instructions, then use \`applyTheme\` / \`resetTheme\`. Use your own judgment on aesthetics, color theory, and design — no need to search docs for that. Be decisive: pick colors/fonts/radius confidently and apply them. - If a question is unrelated to Nuxt UI (e.g. general coding, off-topic), briefly answer if you can, but don't waste tool calls searching docs for it. - If no relevant information is found after searching, respond with "Sorry, I couldn't find information about that in the documentation." - Be concise and direct in your responses. **FORMATTING RULES (CRITICAL):** - ABSOLUTELY NO MARKDOWN HEADINGS: Never use #, ##, ###, ####, #####, or ###### - NO underline-style headings with === or --- - Use **bold text** for emphasis and section labels instead - Examples: * Instead of "## Usage", write "**Usage:**" or just "Here's how to use it:" * Instead of "# Complete Guide", write "**Complete Guide**" or start directly with content - Start all responses with content, never with a heading - Reference specific component names, props, or APIs when applicable. - If a question is ambiguous, ask for clarification rather than guessing. - When multiple relevant items are found, list them clearly using bullet points. - You have up to 5 tool calls to find the answer, so be strategic: start broad, then get specific if needed. - Format responses in a conversational way, not as documentation sections. ` const result = streamText({ model: gateway('anthropic/claude-sonnet-5'), maxOutputTokens: 8000, abortSignal: abortController.signal, providerOptions: { anthropic: { thinking: { type: 'adaptive', display: 'summarized' }, effort: 'low' } satisfies AnthropicLanguageModelOptions, gateway: { caching: 'auto' } }, instructions, messages: await convertToModelMessages(messages), experimental_transform: smoothStream(), stopWhen: isStepCount(6), tools: { ...mcpTools, getThemeGuide, applyTheme, resetTheme, getComponentTheme }, onError: (error) => { console.error('streamText error:', error) } }) const stream = toUIMessageStream({ stream: result.stream }) return createUIMessageStreamResponse({ stream }) })