diff --git a/docs/content/docs/1.getting-started/7.ai/1.mcp.md b/docs/content/docs/1.getting-started/7.ai/1.mcp.md index c6c8438433..51db62723c 100644 --- a/docs/content/docs/1.getting-started/7.ai/1.mcp.md +++ b/docs/content/docs/1.getting-started/7.ai/1.mcp.md @@ -30,10 +30,16 @@ The Nuxt UI MCP server provides the following tools organized by category: - **`list_components`**: Lists all available Nuxt UI components with their categories and basic information - **`list_composables`**: Lists all available Nuxt UI composables with their categories and basic information -- **`get_component`**: Retrieves component documentation and details +- **`list_component_sections`**: Lists all available documentation sections for a specific component. Use this to discover what sections you can fetch (e.g., props, slots, theme, usage, examples) +- **`get_component`**: Retrieves component documentation and details. Supports optional `sections` parameter to fetch only specific parts (e.g., "props,slots") instead of full documentation to save tokens +- **`get_component_sections`**: Token-efficient tool to retrieve only specific sections of component documentation (e.g., props, slots, theme). Recommended when you only need certain information - **`get_component_metadata`**: Retrieves detailed metadata for a component including props, slots, and events - **`search_components_by_category`**: Searches components by category or text filter +::note{icon="i-lucide-lightbulb"} +**Token Optimization Tip**: Use `get_component_sections` or pass the `sections` parameter to `get_component` when you only need specific information like props or slots. This can reduce token usage by 70-90% compared to fetching full documentation. +:: + ### Template Tools - **`list_templates`**: Lists all available Nuxt UI templates with optional category filtering @@ -236,6 +242,8 @@ Once configured, you can ask your AI assistant questions like: - "List all available Nuxt UI components" - "Get Button component documentation" - "What props does Input accept?" +- "What slots are available for the Chip component?" +- "Show me only the theme configuration for Button" - "Find form-related components" - "List dashboard templates" - "Get template setup instructions" @@ -245,3 +253,25 @@ Once configured, you can ask your AI assistant questions like: - "Get ContactForm example code" The AI assistant will use the MCP server to fetch structured JSON data and provide guided assistance for Nuxt UI during development. + +### Token-Efficient Queries + +When working with component documentation, you can request specific sections to save tokens: + +- "What sections are available for the Button component?" (uses `list_component_sections`) +- "Get only the props section for the Button component" (uses `get_component_sections`) +- "Show me just the slots and emits for Card" (uses `get_component_sections`) +- "What's in the theme section for Input?" (uses `get_component_sections`) + +The AI will automatically use the appropriate tool to fetch only the requested information, significantly reducing token usage while still providing the exact information you need. + +#### Discovery Workflow + +For best results, the AI can follow this pattern: +1. First, use `list_component_sections` to see what sections are available +2. Then, use `get_component_sections` to fetch only the sections you need + +Example interaction: +- User: "I need to customize the Button component" +- AI uses `list_component_sections` → discovers: props, slots, theme, usage, examples +- AI uses `get_component_sections` with `sections=theme,props` → fetches only relevant parts diff --git a/docs/server/api/mcp/get-component-sections.ts b/docs/server/api/mcp/get-component-sections.ts new file mode 100644 index 0000000000..bbe151d253 --- /dev/null +++ b/docs/server/api/mcp/get-component-sections.ts @@ -0,0 +1,63 @@ +import { z } from 'zod' +import { kebabCase } from 'scule' +import { normalizeComponentName } from '~~/server/utils/normalizeComponentName' +import { parseMarkdownSections, getAvailableSections } from '~~/server/utils/parseMarkdownSections' +import { queryCollection } from '@nuxt/content/server' + +const querySchema = z.object({ + componentName: z.string(), + sections: z.string().optional().transform((val) => { + // Parse comma-separated sections or return all common sections + if (!val) return ['props', 'slots', 'emits', 'theme'] + return val.split(',').map(s => s.trim().toLowerCase()) + }) +}) + +export default defineCachedEventHandler(async (event) => { + const { componentName, sections } = await getValidatedQuery(event, querySchema.parse) + + // Normalize component name by removing "U" or "u-" prefix if present + const normalizedName = normalizeComponentName(componentName) + + // Convert to kebab-case for path lookup + const kebabName = kebabCase(normalizedName) + + // Get component documentation using queryCollection + const page = await queryCollection(event, 'docs') + .where('path', 'LIKE', `%/components/${kebabName}`) + .where('extension', '=', 'md') + .select('id', 'title', 'description', 'path', 'category', 'links') + .first() + + if (!page) { + throw createError({ + statusCode: 404, + statusMessage: `Component '${componentName}' not found in documentation` + }) + } + + // Fetch the raw markdown documentation + const documentation = await $fetch(`/raw${page.path}.md`) + + // Parse and extract only the requested sections + const extractedSections = parseMarkdownSections(documentation, sections) + + // Get list of available sections for reference + const availableSections = getAvailableSections(documentation) + + return { + name: normalizedName, + title: page.title, + description: page.description, + category: page.category, + documentation_url: `https://ui.nuxt.com${page.path}`, + requested_sections: sections, + available_sections: availableSections, + sections: extractedSections, + // Provide a hint if requested sections weren't found + missing_sections: sections.filter(s => !Object.keys(extractedSections).some(k => k.includes(s))) + } +}, { + name: 'mcp-get-component-sections', + maxAge: 1800 // 30 minutes +}) diff --git a/docs/server/api/mcp/get-component.ts b/docs/server/api/mcp/get-component.ts index aaab60f9d1..6146ac5f42 100644 --- a/docs/server/api/mcp/get-component.ts +++ b/docs/server/api/mcp/get-component.ts @@ -1,14 +1,20 @@ import { z } from 'zod' import { kebabCase } from 'scule' import { normalizeComponentName } from '~~/server/utils/normalizeComponentName' +import { parseMarkdownSections } from '~~/server/utils/parseMarkdownSections' import { queryCollection } from '@nuxt/content/server' const querySchema = z.object({ - componentName: z.string() + componentName: z.string(), + sections: z.string().optional().transform((val) => { + // Parse comma-separated sections if provided + if (!val) return null + return val.split(',').map(s => s.trim().toLowerCase()) + }) }) export default defineCachedEventHandler(async (event) => { - const { componentName } = await getValidatedQuery(event, querySchema.parse) + const { componentName, sections } = await getValidatedQuery(event, querySchema.parse) // Normalize component name by removing "U" or "u-" prefix if present const normalizedName = normalizeComponentName(componentName) @@ -32,6 +38,21 @@ export default defineCachedEventHandler(async (event) => { const documentation = await $fetch(`/raw${page.path}.md`) + // If sections are requested, parse and filter the documentation + if (sections) { + const extractedSections = parseMarkdownSections(documentation, sections) + + return { + name: normalizedName, + title: page.title, + description: page.description, + category: page.category, + sections: extractedSections, + documentation_url: `https://ui.nuxt.com${page.path}` + } + } + + // Otherwise return full documentation (backward compatible) return { name: normalizedName, title: page.title, diff --git a/docs/server/api/mcp/list-component-sections.ts b/docs/server/api/mcp/list-component-sections.ts new file mode 100644 index 0000000000..85e5ce7283 --- /dev/null +++ b/docs/server/api/mcp/list-component-sections.ts @@ -0,0 +1,78 @@ +import { z } from 'zod' +import { kebabCase } from 'scule' +import { normalizeComponentName } from '~~/server/utils/normalizeComponentName' +import { getAvailableSections } from '~~/server/utils/parseMarkdownSections' +import { queryCollection } from '@nuxt/content/server' + +const querySchema = z.object({ + componentName: z.string() +}) + +export default defineCachedEventHandler(async (event) => { + const { componentName } = await getValidatedQuery(event, querySchema.parse) + + // Normalize component name by removing "U" or "u-" prefix if present + const normalizedName = normalizeComponentName(componentName) + + // Convert to kebab-case for path lookup + const kebabName = kebabCase(normalizedName) + + // Get component documentation using queryCollection + const page = await queryCollection(event, 'docs') + .where('path', 'LIKE', `%/components/${kebabName}`) + .where('extension', '=', 'md') + .select('id', 'title', 'description', 'path', 'category', 'links') + .first() + + if (!page) { + throw createError({ + statusCode: 404, + statusMessage: `Component '${componentName}' not found in documentation` + }) + } + + // Fetch the raw markdown documentation + const documentation = await $fetch(`/raw${page.path}.md`) + + // Get list of available sections + const availableSections = getAvailableSections(documentation) + + // Group sections by common categories for easier understanding + const categorizedSections = { + api: availableSections.filter(s => ['props', 'slots', 'emits'].some(term => s.includes(term))), + configuration: availableSections.filter(s => s.includes('theme')), + documentation: availableSections.filter(s => ['usage', 'examples'].some(term => s.includes(term))), + meta: availableSections.filter(s => ['changelog', 'intellisense', 'api'].includes(s)), + other: [] as string[] + } + + // Collect sections that don't fit in any category + const categorized = new Set([ + ...categorizedSections.api, + ...categorizedSections.configuration, + ...categorizedSections.documentation, + ...categorizedSections.meta + ]) + categorizedSections.other = availableSections.filter(s => !categorized.has(s)) + + // Common sections that are typically useful + const commonSections = ['props', 'slots', 'emits', 'theme', 'usage', 'examples'] + const recommendedSections = availableSections.filter(s => + commonSections.some(common => s.includes(common)) + ) + + return { + name: normalizedName, + title: page.title, + description: page.description, + category: page.category, + documentation_url: `https://ui.nuxt.com${page.path}`, + available_sections: availableSections, + recommended_sections: recommendedSections, + categorized_sections: categorizedSections, + total_sections: availableSections.length + } +}, { + name: 'mcp-list-component-sections', + maxAge: 1800 // 30 minutes +}) diff --git a/docs/server/routes/mcp.ts b/docs/server/routes/mcp.ts index 6b3aec0bde..03132f64df 100644 --- a/docs/server/routes/mcp.ts +++ b/docs/server/routes/mcp.ts @@ -217,9 +217,10 @@ function createServer() { server.tool( 'get_component', - 'Retrieves Nuxt UI component documentation and details. Parameters: componentName (string, required) - the component name in PascalCase. Returns: A JSON object containing name, title, description, category, documentation, and documentation_url.', + 'Retrieves Nuxt UI component documentation and details. Parameters: componentName (string, required) - the component name in PascalCase; sections (string, optional) - comma-separated list of sections to retrieve (e.g., "props,slots,theme") to save tokens by fetching only specific parts. Returns: A JSON object containing name, title, description, category, and either full documentation or filtered sections.', { - componentName: z.string().describe('The name of the component (PascalCase)') + componentName: z.string().describe('The name of the component (PascalCase)'), + sections: z.string().optional().describe('Comma-separated sections to retrieve (e.g., "props,slots,emits,theme"). Use this to save tokens by fetching only needed sections instead of full documentation.') }, async (params) => { const result = await $fetch('/api/mcp/get-component', { query: params }) @@ -239,6 +240,31 @@ function createServer() { } ) + server.tool( + 'get_component_sections', + 'Retrieves specific sections of Nuxt UI component documentation. This is a token-efficient alternative to get_component when you only need certain parts like props, slots, or theme. Parameters: componentName (string, required) - the component name in PascalCase; sections (string, optional) - comma-separated sections to retrieve. Common sections: "props", "slots", "emits", "theme", "usage", "examples". Defaults to "props,slots,emits,theme" if not specified. Returns: A JSON object with filtered sections and metadata about available sections.', + { + componentName: z.string().describe('The name of the component (PascalCase)'), + sections: z.string().optional().describe('Comma-separated sections to retrieve (e.g., "props,slots"). Common sections: props, slots, emits, theme, usage, examples. Defaults to props,slots,emits,theme') + }, + async (params) => { + const result = await $fetch('/api/mcp/get-component-sections', { query: params }) + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } + } + ) + + server.tool( + 'list_component_sections', + 'Lists all available documentation sections for a specific Nuxt UI component. Use this tool first to discover what sections are available before fetching specific content. Parameters: componentName (string, required) - the component name in PascalCase. Returns: A JSON object with available_sections array, recommended_sections array, and categorized sections grouped by type (api, configuration, documentation, meta).', + { + componentName: z.string().describe('The name of the component (PascalCase)') + }, + async (params) => { + const result = await $fetch('/api/mcp/list-component-sections', { query: params }) + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } + } + ) + server.tool( 'list_templates', 'Lists all available Nuxt UI templates with optional category filtering. Parameters: category (string, optional) - filter by template category. Returns: A JSON object containing templates array, categories array, and total count.', diff --git a/docs/server/utils/parseMarkdownSections.ts b/docs/server/utils/parseMarkdownSections.ts new file mode 100644 index 0000000000..60b0f533bb --- /dev/null +++ b/docs/server/utils/parseMarkdownSections.ts @@ -0,0 +1,115 @@ +/** + * Parses markdown content and extracts specific sections based on headings + * @param markdown - The full markdown content + * @param sections - Array of section names to extract (e.g., ['props', 'slots', 'emits', 'theme']) + * @returns Object with extracted sections where keys are the normalized section names + * + * @example + * ```ts + * const markdown = ` + * ## API + * ### Props + * prop1: string + * ### Slots + * default slot + * ## Theme + * theme config + * ` + * const result = parseMarkdownSections(markdown, ['props', 'theme']) + * // result = { 'props': '### Props\nprop1: string', 'theme': '## Theme\ntheme config' } + * ``` + */ +export function parseMarkdownSections(markdown: string, sections: string[]): Record { + const result: Record = {} + + // Normalize section names to lowercase for case-insensitive matching + const normalizedSections = sections.map(s => s.toLowerCase()) + + // Split markdown into lines for processing + const lines = markdown.split('\n') + + let currentSectionKey: string | null = null + let currentContent: string[] = [] + let currentDepth = 0 + let inTargetSection = false + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + if (!line) continue + + // Check if this is a heading line + const headingMatch = line.match(/^(#{1,6}) +(\S.*)$/) + + if (headingMatch && headingMatch[1] && headingMatch[2]) { + const depth = headingMatch[1].length + const title = headingMatch[2].trim() + const normalizedTitle = title.toLowerCase() + + // Check if this heading matches one of our target sections + const matchesSection = normalizedSections.some((section) => { + return normalizedTitle === section + || normalizedTitle.includes(section) + || section.includes(normalizedTitle) + }) + + if (matchesSection) { + // Save previous section if we were capturing one + if (currentSectionKey && inTargetSection && currentContent.length > 0) { + result[currentSectionKey] = currentContent.join('\n').trim() + } + + // Start capturing this new section + currentSectionKey = normalizedTitle + currentDepth = depth + inTargetSection = true + currentContent = [line] // Include the heading itself + } else if (inTargetSection) { + // We're currently in a target section + // Check if this new heading ends our section + if (depth <= currentDepth) { + // This is a heading at the same or higher level, so our section ends + if (currentSectionKey && currentContent.length > 0) { + result[currentSectionKey] = currentContent.join('\n').trim() + } + currentSectionKey = null + currentContent = [] + inTargetSection = false + } else { + // This is a subsection within our target section, keep capturing + currentContent.push(line) + } + } + } else if (inTargetSection && line) { + // We're in a target section, capture the line + currentContent.push(line) + } + } + + // Don't forget the last section + if (currentSectionKey && inTargetSection && currentContent.length > 0) { + result[currentSectionKey] = currentContent.join('\n').trim() + } + + return result +} + +/** + * Gets available sections from markdown content + * @param markdown - The full markdown content + * @returns Array of section names found in the document + */ +export function getAvailableSections(markdown: string): string[] { + const sections: string[] = [] + const lines = markdown.split('\n') + + for (const line of lines) { + if (!line) continue + const headingMatch = line.match(/^(#{2,3}) +(\S.*)$/) + if (headingMatch && headingMatch[2]) { + const title = headingMatch[2].trim().toLowerCase() + sections.push(title) + } + } + + return sections +}