Skip to content

Commit c2b7176

Browse files
authored
refactor(tools): combine components, docs for search (#129)
* pf.getResources, fix aliases, combine components, docs for improved search * pf.search, annotations * tools, apply default pf versions, markdown formatting
1 parent d7df9c2 commit c2b7176

9 files changed

Lines changed: 219 additions & 53 deletions

src/__tests__/__snapshots__/patternFly.getResources.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ exports[`getPatternFlyComponentNames should return multiple organized facets: pr
55
"componentNamesIndex",
66
"componentNamesIndexMap",
77
"byVersion",
8+
"byDocs",
89
]
910
`;
1011

src/__tests__/__snapshots__/tool.patternFlyDocs.test.ts.snap

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,22 @@ exports[`usePatternFlyDocsTool, callback should attempt to parse parameters, sin
1515
exports[`usePatternFlyDocsTool, callback should attempt to parse parameters, with invalid urlList 1`] = `"# Content for invalid-path"`;
1616

1717
exports[`usePatternFlyDocsTool, callback should attempt to parse parameters, with name and actual path 1`] = `"# Content for documentation:chatbot/README.md"`;
18+
19+
exports[`usePatternFlyDocsTool, callback should have a specific markdown format: Button 1`] = `
20+
[
21+
{
22+
"text": "# Content for components/ipsumButton.md
23+
Source: components/ipsumButton.md
24+
25+
ipsum documentation content
26+
27+
---
28+
29+
# Content for components/loremButton.md
30+
Source: components/loremButton.md
31+
32+
lorem documentation content",
33+
"type": "text",
34+
},
35+
]
36+
`;

src/__tests__/__snapshots__/tool.searchPatternFlyDocs.test.ts.snap

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,65 @@ exports[`searchPatternFlyDocsTool should have a consistent return structure: str
88
}
99
`;
1010

11-
exports[`searchPatternFlyDocsTool, callback should parse parameters, default: search 1`] = `"# Search results for "Button". Showing 2 exact matches."`;
11+
exports[`searchPatternFlyDocsTool, callback should have a specific markdown format: tooltip 1`] = `
12+
[
13+
{
14+
"text": "# Search results for PatternFly version "v6" and "button". Showing 2 exact matches.
15+
1. **button**:
16+
"usePatternFlyDocs" resource parameter "name" and "URLs"
17+
- **Name**: button
18+
- **URLs**:
19+
- [Button - (v6) - Design Guidelines for the button component.](https://raw.githubusercontent.com/patternfly/patternfly-org/2d5fec39ddb8aa32ce78c9a63cdfc1653692b193/packages/documentation-site/patternfly-docs/content/components/button/button.md)
20+
- [Button - (v6) - Accessibility for the button component.](https://raw.githubusercontent.com/patternfly/patternfly-org/2d5fec39ddb8aa32ce78c9a63cdfc1653692b193/packages/documentation-site/patternfly-docs/content/components/accessibility/button/button.md)
21+
- [Button - (v6) - Examples for the button component.](https://raw.githubusercontent.com/patternfly/patternfly-react/476782a298df81cb78de7f3cd804f3ff8f33993c/packages/react-core/src/components/Button/examples/Button.md)
22+
- **Resources**:
23+
- **URI**: patternfly://docs/button?version=v6
24+
- **JSON Schemas**: patternfly://schemas/button?version=v6
1225
13-
exports[`searchPatternFlyDocsTool, callback should parse parameters, with "*" searchQuery all: search 1`] = `"# Search results for "all" resources. Only showing the first 10 results. There are 798 potential match variations. Try searching with a more specific query."`;
26+
2. **patterns**:
27+
"usePatternFlyDocs" resource parameter "name" and "URLs"
28+
- **Name**: patterns
29+
- **URLs**:
30+
- [Actions - (v6) - The best practices for designing processes that a user can trigger by clicking or selecting a UI element, such as a button or link.](https://raw.githubusercontent.com/patternfly/patternfly-org/2d5fec39ddb8aa32ce78c9a63cdfc1653692b193/packages/documentation-site/patternfly-docs/content/patterns/actions/actions.md)
31+
- [Bulk selection - (v6) - A defined method for users to select or deselect multiple items within complex content views or data tables.](https://raw.githubusercontent.com/patternfly/patternfly-org/2d5fec39ddb8aa32ce78c9a63cdfc1653692b193/packages/documentation-site/patternfly-docs/content/patterns/bulk-selection/bulk-selection.md)
32+
- [Card view - (v6) - A structured layout designed to display a grid of cards in a gallery, optimizing for browsing and interaction.](https://raw.githubusercontent.com/patternfly/patternfly-org/2d5fec39ddb8aa32ce78c9a63cdfc1653692b193/packages/documentation-site/patternfly-docs/content/patterns/card-view/card-view.md)
33+
- [Component usage and behavior - (v6) - Guidance on how to choose between similar components and use them appropriately based on specific user contexts and use cases.](https://raw.githubusercontent.com/patternfly/patternfly-org/2d5fec39ddb8aa32ce78c9a63cdfc1653692b193/packages/documentation-site/patternfly-docs/content/patterns/usage-and-behavior.md)
34+
- [Dashboard - (v6) - A highly customizable layout that serves as a high-level overview of key metrics or performance indicators.](https://raw.githubusercontent.com/patternfly/patternfly-org/2d5fec39ddb8aa32ce78c9a63cdfc1653692b193/packages/documentation-site/patternfly-docs/content/patterns/dashboard/dashboard.md)
35+
- [Filters - (v6) - The design rules for implementing filtering mechanisms that allow users to narrow down content from large datasets or complex views.](https://raw.githubusercontent.com/patternfly/patternfly-org/2d5fec39ddb8aa32ce78c9a63cdfc1653692b193/packages/documentation-site/patternfly-docs/content/patterns/filters/filters.md)
36+
- [Primary-detail - (v6) - A two-pane layout that shows a list of items and corresponding details for the currently selected item.](https://raw.githubusercontent.com/patternfly/patternfly-org/2d5fec39ddb8aa32ce78c9a63cdfc1653692b193/packages/documentation-site/patternfly-docs/content/patterns/primary-detail/primary-detail.md)
37+
- [Status and severity - (v6) - Guidance on the consistent and accessible use of color and iconography to communicate status and severity across the UI.](https://raw.githubusercontent.com/patternfly/patternfly-org/2d5fec39ddb8aa32ce78c9a63cdfc1653692b193/packages/documentation-site/patternfly-docs/content/patterns/status-and-severity/status-and-severity.md)
38+
- **Resources**:
39+
- **URI**: patternfly://docs/patterns?version=v6
1440
15-
exports[`searchPatternFlyDocsTool, callback should parse parameters, with "all" searchQuery all: search 1`] = `"# Search results for "all" resources. Only showing the first 10 results. There are 798 potential match variations. Try searching with a more specific query."`;
1641
17-
exports[`searchPatternFlyDocsTool, callback should parse parameters, with explicit valid version: search 1`] = `"# Search results for "Button". Showing 2 exact matches."`;
1842
19-
exports[`searchPatternFlyDocsTool, callback should parse parameters, with lower case componentName: search 1`] = `"# Search results for "button". Showing 2 exact matches."`;
43+
---
44+
45+
46+
**Important**:
47+
- Use the "usePatternFlyDocs" tool with the above names and URLs to fetch resource content.
48+
- Use a search all ("*") to find all available resources.",
49+
"type": "text",
50+
},
51+
]
52+
`;
53+
54+
exports[`searchPatternFlyDocsTool, callback should parse parameters, default: search 1`] = `"# Search results for PatternFly version "v6" and "Button". Showing 2 exact matches."`;
55+
56+
exports[`searchPatternFlyDocsTool, callback should parse parameters, with "*" searchQuery all: search 1`] = `"# Search results for PatternFly version "v6" and "all" resources. Only showing the first 10 results. There are 806 potential match variations. Try searching with a more specific query."`;
57+
58+
exports[`searchPatternFlyDocsTool, callback should parse parameters, with "all" searchQuery all: search 1`] = `"# Search results for PatternFly version "v6" and "all" resources. Only showing the first 10 results. There are 806 potential match variations. Try searching with a more specific query."`;
59+
60+
exports[`searchPatternFlyDocsTool, callback should parse parameters, with explicit valid version: search 1`] = `"# Search results for PatternFly version "v6" and "Button". Showing 2 exact matches."`;
61+
62+
exports[`searchPatternFlyDocsTool, callback should parse parameters, with lower case componentName: search 1`] = `"# Search results for PatternFly version "v6" and "button". Showing 2 exact matches."`;
2063
2164
exports[`searchPatternFlyDocsTool, callback should parse parameters, with made up componentName: search 1`] = `"No PatternFly resources found matching "lorem ipsum dolor sit amet""`;
2265
23-
exports[`searchPatternFlyDocsTool, callback should parse parameters, with multiple words: search 1`] = `"# Search results for "Button Card Table". Showing 4 related matches."`;
66+
exports[`searchPatternFlyDocsTool, callback should parse parameters, with multiple words: search 1`] = `"# Search results for PatternFly version "v6" and "Button Card Table". Showing 4 related matches."`;
2467
25-
exports[`searchPatternFlyDocsTool, callback should parse parameters, with partial componentName: search 1`] = `"# Search results for "ton". Showing 5 related matches."`;
68+
exports[`searchPatternFlyDocsTool, callback should parse parameters, with partial componentName: search 1`] = `"# Search results for PatternFly version "v6" and "ton". Showing 5 related matches."`;
2669
27-
exports[`searchPatternFlyDocsTool, callback should parse parameters, with trimmed componentName: search 1`] = `"# Search results for " Button ". Showing 2 exact matches."`;
70+
exports[`searchPatternFlyDocsTool, callback should parse parameters, with trimmed componentName: search 1`] = `"# Search results for PatternFly version "v6" and " Button ". Showing 2 exact matches."`;
2871
29-
exports[`searchPatternFlyDocsTool, callback should parse parameters, with upper case componentName: search 1`] = `"# Search results for "BUTTON". Showing 2 exact matches."`;
72+
exports[`searchPatternFlyDocsTool, callback should parse parameters, with upper case componentName: search 1`] = `"# Search results for PatternFly version "v6" and "BUTTON". Showing 2 exact matches."`;

src/__tests__/tool.patternFlyDocs.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,22 @@ describe('usePatternFlyDocsTool, callback', () => {
126126
await expect(callback({ urlList: ['https://www.patternfly.org//missing.md'] })).rejects.toThrow(McpError);
127127
await expect(callback({ urlList: ['https://www.patternfly.org/missing.md'] })).rejects.toThrow('Failed to fetch documentation');
128128
});
129+
130+
it('should have a specific markdown format', async () => {
131+
mockProcessDocs.mockResolvedValue([
132+
{
133+
path: 'components/loremButton.md',
134+
content: 'lorem documentation content'
135+
},
136+
{
137+
path: 'components/ipsumButton.md',
138+
content: 'ipsum documentation content'
139+
}
140+
] as any);
141+
142+
const [_name, _schema, callback] = usePatternFlyDocsTool();
143+
const result = await callback({ name: 'button' });
144+
145+
expect(result.content).toMatchSnapshot('Button');
146+
});
129147
});

src/__tests__/tool.searchPatternFlyDocs.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,11 @@ describe('searchPatternFlyDocsTool, callback', () => {
104104
await expect(callback({ searchQuery })).rejects.toThrow(McpError);
105105
await expect(callback({ searchQuery })).rejects.toThrow(error);
106106
});
107+
108+
it('should have a specific markdown format', async () => {
109+
const [_name, _schema, callback] = searchPatternFlyDocsTool();
110+
const result = await callback({ searchQuery: 'button' });
111+
112+
expect(result.content).toMatchSnapshot('tooltip');
113+
});
107114
});

src/patternFly.getResources.ts

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,45 @@ import { INDEX_BLOCKLIST_WORDS, INDEX_NOISE_WORDS } from './docs.filterWords';
2222
*/
2323
type PatternFlyComponentSchema = Awaited<ReturnType<typeof getPatternFlyComponentSchema>>;
2424

25+
/**
26+
* Map of PatternFly component names to their versioned metadata.
27+
*
28+
* Properties for each component include:
29+
* - `isSchemasAvailable`: Boolean indicating whether schemas are available for the component.
30+
* - `displayName`: String that defines the human-readable name of the component.
31+
*/
2532
type PatternFlyMcpComponentNamesByVersion = {
2633
[name: string]: {
2734
isSchemasAvailable: boolean;
2835
displayName: string;
2936
}
3037
};
3138

39+
/**
40+
* Documentation structure for PatternFly MCP component names. Intended to help
41+
* merge with existing documents catalog.
42+
*
43+
* Unique Properties:
44+
* - `path`: Non-existent, omitted from the base catalog documentation type.
45+
* - `isSchemasAvailable`: A boolean flag that indicates whether schemas are available for the component.
46+
*/
47+
type PatternFlyMcpComponentNamesDoc = Omit<PatternFlyMcpDocsCatalogDoc, 'path'> & { path?: never; isSchemasAvailable: boolean };
48+
49+
/**
50+
* Component names metadata.
51+
*
52+
* @interface PatternFlyMcpComponentNames
53+
*
54+
* @property componentNamesIndex - Component names index.
55+
* @property componentNamesIndexMap - Component names index map.
56+
* @property byVersion - Component names by version map.
57+
* @property byDocs - Component names by docs map.
58+
*/
3259
interface PatternFlyMcpComponentNames {
3360
componentNamesIndex: string[];
3461
componentNamesIndexMap: Map<string, string>;
3562
byVersion: Map<string, PatternFlyMcpComponentNamesByVersion>;
63+
byDocs: Map<string, PatternFlyMcpComponentNamesDoc[]>;
3664
}
3765

3866
/**
@@ -230,14 +258,27 @@ const getPatternFlyComponentNames = async (contextPathOverride?: string): Promis
230258
const latestNamesIndex = [...Array.from(new Set([...pfComponentNames, 'Table'])).map(name => name.toLowerCase()).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))];
231259
const latestNamesIndexMap = new Map(Array.from(new Set([...pfComponentNames, 'Table'])).map(name => [name.toLowerCase(), name]));
232260
const latestByNameMap = new Map<string, { isSchemasAvailable: boolean, displayName: string }>();
261+
const latestByDocsFormat = new Map<string, PatternFlyMcpComponentNamesDoc[]>();
262+
const createDocEntry = (name: string, isSchemasAvailable: boolean) => ({
263+
displayName: name,
264+
description: `PatternFly React component: ${name}`,
265+
pathSlug: `schemas-${name}`.toLowerCase(),
266+
category: 'react',
267+
section: 'components',
268+
source: 'schemas',
269+
version: latestSchemasVersion,
270+
isSchemasAvailable
271+
});
233272

234273
pfComponentNames.forEach(name => {
235274
latestByNameMap.set(name.toLowerCase(), { isSchemasAvailable: true, displayName: name });
275+
latestByDocsFormat.set(name.toLowerCase(), [createDocEntry(name, true)]);
236276
});
237277

238278
const byVersion: PatternFlyMcpComponentNames['byVersion'] = new Map();
239279

240280
latestByNameMap.set('table', { isSchemasAvailable: false, displayName: 'Table' });
281+
latestByDocsFormat.set('table', [createDocEntry('Table', false)]);
241282
const convert = Object.fromEntries(latestByNameMap);
242283

243284
byVersion.set(
@@ -248,7 +289,8 @@ const getPatternFlyComponentNames = async (contextPathOverride?: string): Promis
248289
return {
249290
componentNamesIndex: latestNamesIndex,
250291
componentNamesIndexMap: latestNamesIndexMap,
251-
byVersion
292+
byVersion,
293+
byDocs: latestByDocsFormat
252294
};
253295
};
254296

@@ -357,7 +399,7 @@ const mutateKeyWordsMap = (
357399
const getPatternFlyMcpResources = async (contextPathOverride?: string): Promise<PatternFlyMcpAvailableResources> => {
358400
const versionContext = await getPatternFlyVersionContext.memo(contextPathOverride);
359401
const componentNames = await getPatternFlyComponentNames.memo(contextPathOverride);
360-
const { componentNamesIndex, byVersion: componentNamesByVersion, byVersion: byVersionSchemaNames } = componentNames;
402+
const { componentNamesIndex, byVersion: componentNamesByVersion, byDocs: componentNamesByDocs } = componentNames;
361403

362404
const originalDocs = await getPatternFlyDocsCatalog.memo();
363405
const resources = new Map<string, PatternFlyMcpResourceMetadata>();
@@ -367,24 +409,35 @@ const getPatternFlyMcpResources = async (contextPathOverride?: string): Promise<
367409
const pathIndex = new Set<string>();
368410
const rawKeywordsMap: PatternFlyMcpKeywordsMap = new Map();
369411

370-
Object.entries(originalDocs.docs).forEach(([docsName, entries]) => {
371-
const name = docsName.toLowerCase();
372-
const resource: PatternFlyMcpResourceMetadata = {
373-
name,
374-
isSchemasAvailable: undefined,
375-
uri: undefined,
376-
uriSchemas: undefined,
377-
entries: [],
378-
versions: {}
379-
};
412+
const catalog = [...Object.entries(originalDocs.docs), ...Array.from(componentNamesByDocs)];
413+
414+
catalog.forEach(([unifiedName, entries]) => {
415+
const name = unifiedName.toLowerCase();
416+
417+
if (!resources.has(name)) {
418+
// isSchemasAvailable, uri, and uriSchemas are contextually populated from the patternFly.search
419+
// functions, search and filter. They are intended to be undefined here.
420+
resources.set(name, {
421+
name,
422+
isSchemasAvailable: undefined,
423+
uri: undefined,
424+
uriSchemas: undefined,
425+
entries: [],
426+
versions: {}
427+
});
428+
}
429+
430+
const resource = resources.get(name) as PatternFlyMcpResourceMetadata;
380431

381432
entries.forEach(entry => {
382433
const version = (entry.version || 'unknown').toLowerCase();
383-
const isSchemasAvailable = versionContext.latestSchemasVersion === version && byVersionSchemaNames.get(version)?.[name]?.isSchemasAvailable;
434+
const isSchemasAvailable = versionContext.latestSchemasVersion === version && componentNamesByVersion.get(version)?.[name]?.isSchemasAvailable;
384435
const path = entry.path;
385-
const uri = `patternfly://docs/${version}/${name}`;
436+
const uri = `patternfly://docs/${name}?version=${version}`;
386437

387-
pathIndex.add(path);
438+
if (path) {
439+
pathIndex.add(path);
440+
}
388441

389442
resource.versions[version] ??= {
390443
isSchemasAvailable,
@@ -393,18 +446,28 @@ const getPatternFlyMcpResources = async (contextPathOverride?: string): Promise<
393446
entries: []
394447
};
395448

396-
const displayCategory = setCategoryDisplayLabel(entry);
449+
const displayName = entry.displayName || name;
450+
const displayCategory = setCategoryDisplayLabel(entry as PatternFlyMcpDocsCatalogDoc);
397451
let uriSchemas;
398452

399453
if (isSchemasAvailable) {
400-
uriSchemas = `patternfly://schemas/${version}/${name}`;
454+
uriSchemas = `patternfly://schemas/${name}?version=${version}`;
401455

402456
resource.versions[version].uriSchemas = uriSchemas;
403457
}
404458

405-
const extendedEntry = { ...entry, name: docsName, displayCategory, uri, uriSchemas };
459+
const extendedEntry = {
460+
...entry,
461+
name,
462+
displayName,
463+
displayCategory,
464+
uri,
465+
uriSchemas
466+
} as (PatternFlyMcpDocsCatalogDoc & PatternFlyMcpDocsMeta);
406467

407-
byPath[path] = extendedEntry;
468+
if (path) {
469+
byPath[path] = extendedEntry;
470+
}
408471

409472
byUri[uri] ??= [];
410473
byUri[uri]?.push(extendedEntry);
@@ -434,8 +497,6 @@ const getPatternFlyMcpResources = async (contextPathOverride?: string): Promise<
434497
resource.entries.push(extendedEntry);
435498
resource.versions[version].entries.push(extendedEntry);
436499
});
437-
438-
resources.set(name, resource);
439500
});
440501

441502
Object.entries(byVersion).forEach(([_version, entries]) => {
@@ -504,6 +565,7 @@ export {
504565
setCategoryDisplayLabel,
505566
type PatternFlyMcpComponentNames,
506567
type PatternFlyMcpComponentNamesByVersion,
568+
type PatternFlyMcpComponentNamesDoc,
507569
type PatternFlyComponentSchema,
508570
type PatternFlyMcpAvailableResources,
509571
type PatternFlyMcpResourceMetadata,

src/patternFly.search.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ interface SearchPatternFlyOptions {
106106
/**
107107
* Apply sequenced priority filters for predictable filtering, filter PatternFly data.
108108
*
109+
* @note It is tempting to apply a default version to this function. Do not. Architecture
110+
* dictates that this function remains purely data-driven, apply default versions in the caller.
111+
* See both MCP resources and tools for examples.
112+
*
109113
* @note This is a predictable filter, not a search. Use searchPatternFly for fuzzy search.
110114
* - Has case-insensitive filtering for all fields
111115
* - Exact "version" filtering only
@@ -200,6 +204,10 @@ filterPatternFly.memo = memo(filterPatternFly, DEFAULT_OPTIONS.resourceMemoOptio
200204
/**
201205
* Search for PatternFly component documentation URLs using fuzzy search.
202206
*
207+
* @note It is tempting to apply a default version to this function. Do not. Architecture
208+
* dictates that this function remains purely data-driven, apply default versions in the caller.
209+
* See both MCP resources and tools for examples.
210+
*
203211
* @note Uses `filterPatternFly` for additional filtering.
204212
*
205213
* @param searchQuery - Search query. Values are coerced to string for fuzzy search.

0 commit comments

Comments
 (0)