Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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');
});
Comment on lines +145 to +147

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');
});

Comment on lines +157 to +172
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');
});
Comment on lines +177 to +180

it('IgrCombo normalizes then aliases correctly for react', () => {
const normalized = normalizeDocName('IgrCombo');
expect(applyDocAlias('react', normalized)).toBe('overview');
});
});
8 changes: 5 additions & 3 deletions packages/igniteui-mcp/igniteui-doc-mcp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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". ' +
Expand All @@ -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 }) };
Comment thread
onlyexeption marked this conversation as resolved.
}
);
Expand Down
128 changes: 128 additions & 0 deletions packages/igniteui-mcp/igniteui-doc-mcp/src/tools/doc-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Comment thread
onlyexeption marked this conversation as resolved.
}

/**
* 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<string, Record<string, string>> = {
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
Comment on lines +102 to +106
'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',
Comment on lines +120 to +124
// 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',
Comment on lines +140 to +144
// 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.
Expand Down