Skip to content

Commit edff1ae

Browse files
onlyexeptionkdinevCopilot
authored
Improve get_doc name resolution with normalization and alias map (#1727)
* feat(mcp): improve get_doc name resolution with normalization and alias map * fix: correct grid doc name * fix(mcp): address pr comments, add tests * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * chore: address pr comments * fix: rename blazorgrid doc --------- Co-authored-by: Konstantin Dinev <kdinev@bellumgens.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 695ab54 commit edff1ae

3 files changed

Lines changed: 249 additions & 4 deletions

File tree

packages/igniteui-mcp/igniteui-doc-mcp/src/__tests__/tools/doc-tools.test.ts

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from 'vitest';
2-
import { sanitizeSearchDocsQuery } from '../../tools/doc-tools.js';
2+
import { applyDocAlias, normalizeDocName, sanitizeSearchDocsQuery } from '../../tools/doc-tools.js';
33

44
describe('sanitizeSearchDocsQuery', () => {
55
it('quotes plain terms with OR', () => {
@@ -82,3 +82,116 @@ describe('sanitizeSearchDocsQuery', () => {
8282
expect(sanitizeSearchDocsQuery('grid" OR "1=1')).toBe('"grid" OR "OR" OR "1=1"');
8383
});
8484
});
85+
86+
describe('normalizeDocName', () => {
87+
it('returns a plain kebab-case name unchanged', () => {
88+
expect(normalizeDocName('grid-editing')).toBe('grid-editing');
89+
});
90+
91+
it('lowercases a plain name', () => {
92+
expect(normalizeDocName('Carousel')).toBe('carousel');
93+
});
94+
95+
it('strips Angular Igx prefix', () => {
96+
expect(normalizeDocName('IgxGrid')).toBe('grid');
97+
});
98+
99+
it('strips React Igr prefix', () => {
100+
expect(normalizeDocName('IgrCombo')).toBe('combo');
101+
});
102+
103+
it('strips Web Components Igc prefix', () => {
104+
expect(normalizeDocName('IgcAccordion')).toBe('accordion');
105+
});
106+
107+
it('strips Blazor Igb prefix', () => {
108+
expect(normalizeDocName('IgbPivotGrid')).toBe('pivot-grid');
109+
});
110+
111+
it('strips trailing Component suffix', () => {
112+
expect(normalizeDocName('IgxGridComponent')).toBe('grid');
113+
});
114+
115+
it('converts PascalCase to kebab-case', () => {
116+
expect(normalizeDocName('HierarchicalGrid')).toBe('hierarchical-grid');
117+
});
118+
119+
it('converts PascalCase with prefix to kebab-case', () => {
120+
expect(normalizeDocName('IgxHierarchicalGrid')).toBe('hierarchical-grid');
121+
});
122+
123+
it('handles camelCase input', () => {
124+
expect(normalizeDocName('pivotGrid')).toBe('pivot-grid');
125+
});
126+
127+
it('falls back to lowercased input when normalization yields empty string', () => {
128+
expect(normalizeDocName('Igx')).toBe('igx');
129+
});
130+
});
131+
132+
describe('applyDocAlias', () => {
133+
it('returns the input unchanged when no alias exists', () => {
134+
expect(applyDocAlias('angular', 'accordion')).toBe('accordion');
135+
});
136+
137+
it('resolves react combo to overview', () => {
138+
expect(applyDocAlias('react', 'combo')).toBe('overview');
139+
});
140+
141+
it('resolves react combo-box to overview', () => {
142+
expect(applyDocAlias('react', 'combo-box')).toBe('overview');
143+
});
144+
145+
it('resolves react grid to data-grid', () => {
146+
expect(applyDocAlias('react', 'grid')).toBe('data-grid');
147+
});
148+
149+
it('resolves react hierarchical-grid to hierarchical-grid-overview', () => {
150+
expect(applyDocAlias('react', 'hierarchical-grid')).toBe('hierarchical-grid-overview');
151+
});
152+
153+
it('resolves angular combo-box to combo', () => {
154+
expect(applyDocAlias('angular', 'combo-box')).toBe('combo');
155+
});
156+
157+
it('resolves angular hierarchical-grid correctly', () => {
158+
expect(applyDocAlias('angular', 'hierarchical-grid')).toBe('hierarchicalgrid-hierarchical-grid');
159+
});
160+
161+
it('resolves angular grid to grid-grid', () => {
162+
expect(applyDocAlias('angular', 'grid')).toBe('grid-grid');
163+
});
164+
165+
it('resolves webcomponents combo to overview', () => {
166+
expect(applyDocAlias('webcomponents', 'combo')).toBe('overview');
167+
});
168+
169+
it('resolves webcomponents grid to data-grid', () => {
170+
expect(applyDocAlias('webcomponents', 'grid')).toBe('data-grid');
171+
});
172+
173+
it('resolves blazor radio-group to radio', () => {
174+
expect(applyDocAlias('blazor', 'radio-group')).toBe('radio');
175+
});
176+
177+
it('resolves blazor range-slider to slider', () => {
178+
expect(applyDocAlias('blazor', 'range-slider')).toBe('slider');
179+
});
180+
181+
it('resolves blazor grid to data-grid', () => {
182+
expect(applyDocAlias('blazor', 'grid')).toBe('data-grid');
183+
});
184+
it('returns input unchanged for unknown framework', () => {
185+
expect(applyDocAlias('unknown-fw', 'combo')).toBe('combo');
186+
});
187+
188+
it('IgxGridComponent normalizes then aliases correctly for angular', () => {
189+
const normalized = normalizeDocName('IgxGridComponent');
190+
expect(applyDocAlias('angular', normalized)).toBe('grid-grid');
191+
});
192+
193+
it('IgrCombo normalizes then aliases correctly for react', () => {
194+
const normalized = normalizeDocName('IgrCombo');
195+
expect(applyDocAlias('react', normalized)).toBe('overview');
196+
});
197+
});

packages/igniteui-mcp/igniteui-doc-mcp/src/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { RemoteDocsProvider } from "./providers/RemoteDocsProvider.js";
1212
import { LocalDocsProvider } from "./providers/LocalDocsProvider.js";
1313
import { getApiReferenceSchema, searchApiSchema } from "./tools/schemas.js";
1414
import { createGetApiReferenceHandler, createSearchApiHandler } from "./tools/handlers.js";
15-
import { buildProjectSetupGuide, sanitizeSearchDocsQuery } from "./tools/doc-tools.js";
15+
import { applyDocAlias, buildProjectSetupGuide, normalizeDocName, sanitizeSearchDocsQuery } from "./tools/doc-tools.js";
1616
import { ApiDocLoader } from "./lib/api-doc-loader.js";
1717
import { getPlatforms } from "./config/platforms.js";
1818

@@ -134,6 +134,7 @@ function registerDocTools(server: McpServer, docsProvider: DocsProvider) {
134134
framework: FRAMEWORK_ENUM,
135135
name: z
136136
.string()
137+
.min(1, 'Doc name must not be empty.')
137138
.describe(
138139
'Exact doc name in kebab-case without the .md extension. ' +
139140
'Examples: "grid-editing", "combo-overview", "accordion". ' +
@@ -143,8 +144,9 @@ function registerDocTools(server: McpServer, docsProvider: DocsProvider) {
143144
},
144145
async ({ framework, name }) => {
145146
const start = performance.now();
146-
const { text, found } = await docsProvider.getDoc(framework, name);
147-
log("get_doc", { framework, name }, text, Math.round(performance.now() - start));
147+
const resolvedName = applyDocAlias(framework, normalizeDocName(name.trim()));
148+
const { text, found } = await docsProvider.getDoc(framework, resolvedName);
149+
log("get_doc", { framework, name: resolvedName }, text, Math.round(performance.now() - start));
148150
return { content: [{ type: "text" as const, text }], ...(found ? {} : { isError: true }) };
149151
}
150152
);

packages/igniteui-mcp/igniteui-doc-mcp/src/tools/doc-tools.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,136 @@ export function sanitizeSearchDocsQuery(queryText: string): string | null {
3535
return sanitized || null;
3636
}
3737

38+
/**
39+
* Normalise a doc name to kebab-case so callers can pass component class
40+
* names (e.g. IgxCarousel, IgrCarousel, Carousel) in addition to the
41+
* canonical kebab-case doc names (e.g. carousel).
42+
*
43+
* Steps:
44+
* 1. Strip Ignite UI framework prefix: Igx (Angular), Igr (React),
45+
* Igc (Web Components), Igb (Blazor)
46+
* 2. Strip trailing "Component" suffix (e.g. IgxGridComponent → Grid)
47+
* 3. Convert PascalCase / camelCase to kebab-case and lowercase
48+
*/
49+
export function normalizeDocName(name: string): string {
50+
let normalized = name.replace(/^Ig[xrcb]/i, '');
51+
normalized = normalized.replace(/Component$/i, '');
52+
normalized = normalized.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
53+
return normalized || name.toLowerCase();
54+
}
55+
56+
/**
57+
* Per-framework alias maps: normalized kebab-case name → actual doc key.
58+
*
59+
* Covers cases where the doc key cannot be derived mechanically:
60+
* - Combo Box overview is keyed as "overview" not "combo" / "combo-box"
61+
* - Combo sub-docs use bare generic names: "features", "templates", "single-selection"
62+
* - Grid overview is "grid-grid" or "data-grid", not "grid"
63+
* - Several components append "-overview" or "-chart" suffix
64+
* - "radio" covers both Radio and Radio Group
65+
* - "slider" covers both Slider and Range Slider
66+
*/
67+
const DOC_ALIASES: Record<string, Record<string, string>> = {
68+
react: {
69+
// Combo Box
70+
combo: 'overview',
71+
'combo-box': 'overview',
72+
combobox: 'overview',
73+
'combo-overview': 'overview',
74+
'combo-features': 'features',
75+
'combobox-features': 'features',
76+
'combo-templates': 'templates',
77+
'combobox-templates': 'templates',
78+
'combo-single-selection': 'single-selection',
79+
'combobox-single-selection': 'single-selection',
80+
// Grid types
81+
grid: 'data-grid',
82+
'hierarchical-grid': 'hierarchical-grid-overview',
83+
'tree-grid': 'tree-grid-overview',
84+
'pivot-grid': 'pivot-grid-overview',
85+
'grid-lite': 'grid-lite-overview',
86+
spreadsheet: 'spreadsheet-overview',
87+
'zoom-slider': 'zoomslider-overview',
88+
zoomslider: 'zoomslider-overview',
89+
// Non-obvious renames
90+
treemap: 'treemap-chart',
91+
'radio-group': 'radio',
92+
'radio-and-radio-group': 'radio',
93+
'range-slider': 'slider',
94+
dashboard: 'dashboard-tile',
95+
themes: 'themes-overview',
96+
theme: 'themes-overview',
97+
'geographic-map': 'geo-map',
98+
'geo-map-overview': 'geo-map',
99+
'geographic-map-features': 'geo-map',
100+
},
101+
angular: {
102+
// Combo Box
103+
'combo-box': 'combo',
104+
combobox: 'combo',
105+
// Grid types
106+
grid: 'grid-grid',
107+
'hierarchical-grid': 'hierarchicalgrid-hierarchical-grid',
108+
'tree-grid': 'treegrid-tree-grid',
109+
'pivot-grid': 'pivotgrid-pivot-grid',
110+
spreadsheet: 'spreadsheet-overview',
111+
'zoom-slider': 'zoomslider-overview',
112+
zoomslider: 'zoomslider-overview',
113+
// Non-obvious renames
114+
treemap: 'types-treemap-chart',
115+
'radio-group': 'radio-button',
116+
'range-slider': 'slider',
117+
'geographic-map': 'geo-map',
118+
'geo-map-overview': 'geo-map',
119+
},
120+
webcomponents: {
121+
// Combo Box
122+
combo: 'overview',
123+
'combo-box': 'overview',
124+
combobox: 'overview',
125+
// Grid types
126+
grid: 'data-grid',
127+
'hierarchical-grid': 'hierarchical-grid-overview',
128+
'tree-grid': 'tree-grid-overview',
129+
'pivot-grid': 'pivot-grid-overview',
130+
'grid-lite': 'grid-lite-overview',
131+
spreadsheet: 'spreadsheet-overview',
132+
'zoom-slider': 'zoomslider-overview',
133+
zoomslider: 'zoomslider-overview',
134+
// Non-obvious renames
135+
treemap: 'treemap-chart',
136+
'radio-group': 'radio',
137+
'range-slider': 'slider',
138+
'geographic-map': 'geo-map',
139+
'geo-map-overview': 'geo-map',
140+
},
141+
blazor: {
142+
// Combo Box
143+
combo: 'overview',
144+
'combo-box': 'overview',
145+
combobox: 'overview',
146+
// Grid types
147+
grid: 'data-grid',
148+
'hierarchical-grid': 'hierarchical-grid-overview',
149+
'tree-grid': 'tree-grid-overview',
150+
'pivot-grid': 'pivot-grid-overview',
151+
'zoom-slider': 'zoomslider-overview',
152+
zoomslider: 'zoomslider-overview',
153+
// Non-obvious renames
154+
treemap: 'treemap-chart',
155+
'radio-group': 'radio',
156+
'range-slider': 'slider',
157+
'geographic-map': 'geo-map',
158+
'geo-map-overview': 'geo-map',
159+
},
160+
};
161+
162+
163+
/** Apply the alias map after normalizeDocName. Returns the alias if one exists, otherwise the input unchanged. */
164+
export function applyDocAlias(framework: string, normalizedName: string): string {
165+
return DOC_ALIASES[framework]?.[normalizedName] ?? normalizedName;
166+
}
167+
38168
// Build the setup-guide response for the requested framework.
39169
// For Blazor, combine the base .NET guide with any MCP-fetched docs
40170
// that are available for the configured setup document names.

0 commit comments

Comments
 (0)