diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/src/__tests__/tools/doc-tools.test.ts b/packages/igniteui-mcp/igniteui-doc-mcp/src/__tests__/tools/doc-tools.test.ts index ced383230..5927b3cff 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/src/__tests__/tools/doc-tools.test.ts +++ b/packages/igniteui-mcp/igniteui-doc-mcp/src/__tests__/tools/doc-tools.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { sanitizeSearchDocsQuery } from '../../tools/doc-tools.js'; +import { applyDocAlias, normalizeDocName, sanitizeSearchDocsQuery } from '../../tools/doc-tools.js'; describe('sanitizeSearchDocsQuery', () => { it('quotes plain terms with OR', () => { @@ -82,3 +82,105 @@ describe('sanitizeSearchDocsQuery', () => { expect(sanitizeSearchDocsQuery('grid" OR "1=1')).toBe('"grid" OR "OR" OR "1=1"'); }); }); + +describe('normalizeDocName', () => { + it('returns a plain kebab-case name unchanged', () => { + expect(normalizeDocName('grid-editing')).toBe('grid-editing'); + }); + + it('lowercases a plain name', () => { + expect(normalizeDocName('Carousel')).toBe('carousel'); + }); + + it('strips Angular Igx prefix', () => { + expect(normalizeDocName('IgxGrid')).toBe('grid'); + }); + + it('strips React Igr prefix', () => { + expect(normalizeDocName('IgrCombo')).toBe('combo'); + }); + + it('strips Web Components Igc prefix', () => { + expect(normalizeDocName('IgcAccordion')).toBe('accordion'); + }); + + it('strips Blazor Igb prefix', () => { + expect(normalizeDocName('IgbPivotGrid')).toBe('pivot-grid'); + }); + + it('strips trailing Component suffix', () => { + expect(normalizeDocName('IgxGridComponent')).toBe('grid'); + }); + + it('converts PascalCase to kebab-case', () => { + expect(normalizeDocName('HierarchicalGrid')).toBe('hierarchical-grid'); + }); + + it('converts PascalCase with prefix to kebab-case', () => { + expect(normalizeDocName('IgxHierarchicalGrid')).toBe('hierarchical-grid'); + }); + + it('handles camelCase input', () => { + expect(normalizeDocName('pivotGrid')).toBe('pivot-grid'); + }); + + it('falls back to lowercased input when normalization yields empty string', () => { + expect(normalizeDocName('Igx')).toBe('igx'); + }); +}); + +describe('applyDocAlias', () => { + it('returns the input unchanged when no alias exists', () => { + expect(applyDocAlias('angular', 'accordion')).toBe('accordion'); + }); + + it('resolves react combo to overview', () => { + expect(applyDocAlias('react', 'combo')).toBe('overview'); + }); + + it('resolves react combo-box to overview', () => { + expect(applyDocAlias('react', 'combo-box')).toBe('overview'); + }); + + it('resolves react grid to grid-grid', () => { + expect(applyDocAlias('react', 'grid')).toBe('grid-grid'); + }); + + it('resolves react hierarchical-grid to hierarchical-grid-overview', () => { + expect(applyDocAlias('react', 'hierarchical-grid')).toBe('hierarchical-grid-overview'); + }); + + it('resolves angular combo-box to combo', () => { + expect(applyDocAlias('angular', 'combo-box')).toBe('combo'); + }); + + it('resolves angular hierarchical-grid correctly', () => { + expect(applyDocAlias('angular', 'hierarchical-grid')).toBe('hierarchicalgrid-hierarchical-grid'); + }); + + it('resolves webcomponents combo to overview', () => { + expect(applyDocAlias('webcomponents', 'combo')).toBe('overview'); + }); + + it('resolves blazor radio-group to radio', () => { + expect(applyDocAlias('blazor', 'radio-group')).toBe('radio'); + }); + + it('resolves blazor range-slider to slider', () => { + expect(applyDocAlias('blazor', 'range-slider')).toBe('slider'); + }); + + it('returns input unchanged for unknown framework', () => { + expect(applyDocAlias('unknown-fw', 'combo')).toBe('combo'); + }); + + it('IgxGridComponent normalizes then aliases correctly for angular', () => { + const normalized = normalizeDocName('IgxGridComponent'); + expect(applyDocAlias('angular', normalized)).toBe('grid'); + }); + + it('IgrCombo normalizes then aliases correctly for react', () => { + const normalized = normalizeDocName('IgrCombo'); + expect(applyDocAlias('react', normalized)).toBe('overview'); + }); +}); diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/src/index.ts b/packages/igniteui-mcp/igniteui-doc-mcp/src/index.ts index 9e54a84e6..0ccf79e03 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/src/index.ts +++ b/packages/igniteui-mcp/igniteui-doc-mcp/src/index.ts @@ -12,7 +12,7 @@ import { RemoteDocsProvider } from "./providers/RemoteDocsProvider.js"; import { LocalDocsProvider } from "./providers/LocalDocsProvider.js"; import { getApiReferenceSchema, searchApiSchema } from "./tools/schemas.js"; import { createGetApiReferenceHandler, createSearchApiHandler } from "./tools/handlers.js"; -import { buildProjectSetupGuide, sanitizeSearchDocsQuery } from "./tools/doc-tools.js"; +import { applyDocAlias, buildProjectSetupGuide, normalizeDocName, sanitizeSearchDocsQuery } from "./tools/doc-tools.js"; import { ApiDocLoader } from "./lib/api-doc-loader.js"; import { getPlatforms } from "./config/platforms.js"; @@ -134,6 +134,7 @@ function registerDocTools(server: McpServer, docsProvider: DocsProvider) { framework: FRAMEWORK_ENUM, name: z .string() + .min(1, 'Doc name must not be empty.') .describe( 'Exact doc name in kebab-case without the .md extension. ' + 'Examples: "grid-editing", "combo-overview", "accordion". ' + @@ -143,8 +144,9 @@ function registerDocTools(server: McpServer, docsProvider: DocsProvider) { }, async ({ framework, name }) => { const start = performance.now(); - const { text, found } = await docsProvider.getDoc(framework, name); - log("get_doc", { framework, name }, text, Math.round(performance.now() - start)); + const resolvedName = applyDocAlias(framework, normalizeDocName(name.trim())); + const { text, found } = await docsProvider.getDoc(framework, resolvedName); + log("get_doc", { framework, name: resolvedName }, text, Math.round(performance.now() - start)); return { content: [{ type: "text" as const, text }], ...(found ? {} : { isError: true }) }; } ); diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/src/tools/doc-tools.ts b/packages/igniteui-mcp/igniteui-doc-mcp/src/tools/doc-tools.ts index 942debd9b..1bb403eaa 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/src/tools/doc-tools.ts +++ b/packages/igniteui-mcp/igniteui-doc-mcp/src/tools/doc-tools.ts @@ -35,6 +35,134 @@ export function sanitizeSearchDocsQuery(queryText: string): string | null { return sanitized || null; } +/** + * Normalise a doc name to kebab-case so callers can pass component class + * names (e.g. IgxCarousel, IgrCarousel, Carousel) in addition to the + * canonical kebab-case doc names (e.g. carousel). + * + * Steps: + * 1. Strip Ignite UI framework prefix: Igx (Angular), Igr (React), + * Igc (Web Components), Igb (Blazor) + * 2. Strip trailing "Component" suffix (e.g. IgxGridComponent → Grid) + * 3. Convert PascalCase / camelCase to kebab-case and lowercase + */ +export function normalizeDocName(name: string): string { + let normalized = name.replace(/^Ig[xrcb]/i, ''); + normalized = normalized.replace(/Component$/i, ''); + normalized = normalized.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); + return normalized || name.toLowerCase(); +} + +/** + * Per-framework alias maps: normalized kebab-case name → actual doc key. + * + * Covers cases where the doc key cannot be derived mechanically: + * - Combo Box overview is keyed as "overview" not "combo" / "combo-box" + * - Combo sub-docs use bare generic names: "features", "templates", "single-selection" + * - Grid overview is "data-grid", not "grid" + * - Several components append "-overview" or "-chart" suffix + * - "radio" covers both Radio and Radio Group + * - "slider" covers both Slider and Range Slider + */ +const DOC_ALIASES: Record> = { + react: { + // Combo Box + combo: 'overview', + 'combo-box': 'overview', + combobox: 'overview', + 'combo-overview': 'overview', + 'combo-features': 'features', + 'combobox-features': 'features', + 'combo-templates': 'templates', + 'combobox-templates': 'templates', + 'combo-single-selection': 'single-selection', + 'combobox-single-selection': 'single-selection', + // Grid + grid: 'grid-grid', + // Grid -overview suffix + 'hierarchical-grid': 'hierarchical-grid-overview', + 'tree-grid': 'tree-grid-overview', + 'pivot-grid': 'pivot-grid-overview', + 'grid-lite': 'grid-lite-overview', + spreadsheet: 'spreadsheet-overview', + 'zoom-slider': 'zoomslider-overview', + zoomslider: 'zoomslider-overview', + // Non-obvious renames + treemap: 'treemap-chart', + 'radio-group': 'radio', + 'radio-and-radio-group': 'radio', + 'range-slider': 'slider', + dashboard: 'dashboard-tile', + themes: 'themes-overview', + theme: 'themes-overview', + 'geographic-map': 'geo-map', + 'geo-map-overview': 'geo-map', + 'geographic-map-features': 'geo-map', + }, + angular: { + // Combo Box + 'combo-box': 'combo', + combobox: 'combo', + // Grid -overview suffix + 'hierarchical-grid': 'hierarchicalgrid-hierarchical-grid', + 'tree-grid': 'treegrid-tree-grid', + 'pivot-grid': 'pivotgrid-pivot-grid', + spreadsheet: 'spreadsheet-overview', + 'zoom-slider': 'zoomslider-overview', + zoomslider: 'zoomslider-overview', + // Non-obvious renames + treemap: 'types-treemap-chart', + 'radio-group': 'radio-button', + 'range-slider': 'slider', + 'geographic-map': 'geo-map', + 'geo-map-overview': 'geo-map', + }, + webcomponents: { + // Combo Box + combo: 'overview', + 'combo-box': 'overview', + combobox: 'overview', + // Grid -overview suffix + 'hierarchical-grid': 'hierarchical-grid-overview', + 'tree-grid': 'tree-grid-overview', + 'pivot-grid': 'pivot-grid-overview', + 'grid-lite': 'grid-lite-overview', + spreadsheet: 'spreadsheet-overview', + 'zoom-slider': 'zoomslider-overview', + zoomslider: 'zoomslider-overview', + // Non-obvious renames + treemap: 'treemap-chart', + 'radio-group': 'radio', + 'range-slider': 'slider', + 'geographic-map': 'geo-map', + 'geo-map-overview': 'geo-map', + }, + blazor: { + // Combo Box + combo: 'overview', + 'combo-box': 'overview', + combobox: 'overview', + // Grid -overview suffix + 'hierarchical-grid': 'hierarchical-grid-overview', + 'tree-grid': 'tree-grid-overview', + 'pivot-grid': 'pivot-grid-overview', + 'zoom-slider': 'zoomslider-overview', + zoomslider: 'zoomslider-overview', + // Non-obvious renames + treemap: 'treemap-chart', + 'radio-group': 'radio', + 'range-slider': 'slider', + 'geographic-map': 'geo-map', + 'geo-map-overview': 'geo-map', + }, +}; + + +/** Apply the alias map after normalizeDocName. Returns the alias if one exists, otherwise the input unchanged. */ +export function applyDocAlias(framework: string, normalizedName: string): string { + return DOC_ALIASES[framework]?.[normalizedName] ?? normalizedName; +} + // Build the setup-guide response for the requested framework. // For Blazor, combine the base .NET guide with any MCP-fetched docs // that are available for the configured setup document names.