From 6382f6225ddb75c4e72444affbc2e760da2f6ea6 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 19 Feb 2026 22:49:44 -0500 Subject: [PATCH 1/7] refactor(search): centralize pf search, get resources * options.defaults, refactor for initial pf versions * pf.getResources, api prep, centralize all pf related data * pf.helpers, pf versions from options, disable findClosestPFVersion * pf.search, refactor, centralize pf search --- jest.config.ts | 5 +- .../options.defaults.test.ts.snap | 12 +- .../patternFly.getResources.test.ts.snap | 49 +++ .../patternFly.helpers.test.ts.snap | 100 +++++ .../patternFly.search.test.ts.snap | 25 ++ src/__tests__/patternFly.getResources.test.ts | 109 +++++ src/__tests__/patternFly.helpers.test.ts | 169 +++++++- src/__tests__/patternFly.search.test.ts | 55 +++ src/options.defaults.ts | 31 +- src/patternFly.getResources.ts | 389 ++++++++++++++++++ src/patternFly.helpers.ts | 181 +++++++- src/patternFly.search.ts | 94 +++++ 12 files changed, 1189 insertions(+), 30 deletions(-) create mode 100644 src/__tests__/__snapshots__/patternFly.getResources.test.ts.snap create mode 100644 src/__tests__/__snapshots__/patternFly.helpers.test.ts.snap create mode 100644 src/__tests__/__snapshots__/patternFly.search.test.ts.snap create mode 100644 src/__tests__/patternFly.getResources.test.ts create mode 100644 src/__tests__/patternFly.search.test.ts create mode 100644 src/patternFly.getResources.ts create mode 100644 src/patternFly.search.ts diff --git a/jest.config.ts b/jest.config.ts index 09a2001..56ac0f0 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -32,7 +32,10 @@ export default { { ...tsConfig, diagnostics: { - ignoreCodes: [1343] + // See codes https://github.com/Microsoft/TypeScript/blob/main/src/compiler/diagnosticMessages.json + // 1324 - Dynamic imports only support a second argument when the '--module' option is... + // 1343 - The 'import.meta' meta-property is only allowed when the '--module' option is... + ignoreCodes: [1324, 1343] }, astTransformers: { before: [ diff --git a/src/__tests__/__snapshots__/options.defaults.test.ts.snap b/src/__tests__/__snapshots__/options.defaults.test.ts.snap index eefe88c..2163e1b 100644 --- a/src/__tests__/__snapshots__/options.defaults.test.ts.snap +++ b/src/__tests__/__snapshots__/options.defaults.test.ts.snap @@ -34,8 +34,18 @@ exports[`options defaults should return specific properties: defaults 1`] = ` "availableResourceVersions": [ "6.0.0", ], + "availableSchemasVersions": [ + "v6", + ], + "availableSearchVersions": [ + "current", + "latest", + "v6", + ], "default": { - "defaultVersion": "6.0.0", + "latestSchemasVersion": "v6", + "latestSemVer": "6.0.0", + "latestVersion": "v6", "versionStrategy": "highest", "versionWhitelist": [ "@patternfly/react-core", diff --git a/src/__tests__/__snapshots__/patternFly.getResources.test.ts.snap b/src/__tests__/__snapshots__/patternFly.getResources.test.ts.snap new file mode 100644 index 0000000..5b88bd0 --- /dev/null +++ b/src/__tests__/__snapshots__/patternFly.getResources.test.ts.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`getPatternFlyMcpResources should return multiple organized facets: properties 1`] = ` +[ + "availableSemVer", + "availableVersions", + "availableSchemasVersions", + "enumeratedVersions", + "envSemVer", + "envVersion", + "latestVersion", + "latestSchemasVersion", + "isEnvTheLatestVersion", + "isEnvTheLatestSchemasVersion", + "resources", + "docsIndex", + "componentsIndex", + "keywordsIndex", + "isFallbackDocumentation", + "pathIndex", + "byPath", + "byUri", + "byVersion", + "byVersionComponentNames", +] +`; + +exports[`getPatternFlyReactComponentNames should return multiple organized facets: properties 1`] = ` +[ + "byVersion", + "componentNamesIndex", + "componentNamesWithSchemasIndex", + "componentNamesWithSchemasMap", +] +`; + +exports[`setCategoryDisplayLabel should normalize categories and apply linking markdown, accessibility 1`] = `"Accessibility"`; + +exports[`setCategoryDisplayLabel should normalize categories and apply linking markdown, design 1`] = `"Design Guidelines"`; + +exports[`setCategoryDisplayLabel should normalize categories and apply linking markdown, empty string 1`] = `"Documentation"`; + +exports[`setCategoryDisplayLabel should normalize categories and apply linking markdown, example 1`] = `"Examples"`; + +exports[`setCategoryDisplayLabel should normalize categories and apply linking markdown, guidelines 1`] = `"AI Guidance"`; + +exports[`setCategoryDisplayLabel should normalize categories and apply linking markdown, null 1`] = `"Documentation"`; + +exports[`setCategoryDisplayLabel should normalize categories and apply linking markdown, undefined 1`] = `"Documentation"`; diff --git a/src/__tests__/__snapshots__/patternFly.helpers.test.ts.snap b/src/__tests__/__snapshots__/patternFly.helpers.test.ts.snap new file mode 100644 index 0000000..34957fd --- /dev/null +++ b/src/__tests__/__snapshots__/patternFly.helpers.test.ts.snap @@ -0,0 +1,100 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`filterEnumeratedPatternFlyVersions should attempt to refine a PatternFly versions based on available enumerations, current 1`] = ` +[ + "v6", +] +`; + +exports[`filterEnumeratedPatternFlyVersions should attempt to refine a PatternFly versions based on available enumerations, detected 1`] = ` +[ + "v6", +] +`; + +exports[`filterEnumeratedPatternFlyVersions should attempt to refine a PatternFly versions based on available enumerations, empty 1`] = ` +[ + "current", + "latest", + "v6", +] +`; + +exports[`filterEnumeratedPatternFlyVersions should attempt to refine a PatternFly versions based on available enumerations, exact semver 1`] = ` +[ + "v6", +] +`; + +exports[`filterEnumeratedPatternFlyVersions should attempt to refine a PatternFly versions based on available enumerations, latest 1`] = ` +[ + "v6", +] +`; + +exports[`filterEnumeratedPatternFlyVersions should attempt to refine a PatternFly versions based on available enumerations, null 1`] = ` +[ + "current", + "latest", + "v6", +] +`; + +exports[`filterEnumeratedPatternFlyVersions should attempt to refine a PatternFly versions based on available enumerations, semver 1`] = ` +[ + "v6", +] +`; + +exports[`filterEnumeratedPatternFlyVersions should attempt to refine a PatternFly versions based on available enumerations, tag 1`] = ` +[ + "v6", +] +`; + +exports[`filterEnumeratedPatternFlyVersions should attempt to refine a PatternFly versions based on available enumerations, unavailable exact semver 1`] = ` +[ + "current", + "latest", + "v6", +] +`; + +exports[`filterEnumeratedPatternFlyVersions should attempt to refine a PatternFly versions based on available enumerations, unavailable semver 1`] = ` +[ + "current", + "latest", + "v6", +] +`; + +exports[`filterEnumeratedPatternFlyVersions should attempt to refine a PatternFly versions based on available enumerations, unavailable tag 1`] = ` +[ + "current", + "latest", + "v6", +] +`; + +exports[`filterEnumeratedPatternFlyVersions should attempt to refine a PatternFly versions based on available enumerations, undefined 1`] = ` +[ + "current", + "latest", + "v6", +] +`; + +exports[`getPatternFlyVersionContext should temporarily return option.defaults and latest versions with specific properties: keys 1`] = ` +[ + "availableSemVer", + "availableVersions", + "availableSchemasVersions", + "enumeratedVersions", + "envSemVer", + "envVersion", + "latestVersion", + "latestSchemasVersion", + "isEnvTheLatestVersion", + "isEnvTheLatestSchemasVersion", +] +`; diff --git a/src/__tests__/__snapshots__/patternFly.search.test.ts.snap b/src/__tests__/__snapshots__/patternFly.search.test.ts.snap new file mode 100644 index 0000000..bf89a3e --- /dev/null +++ b/src/__tests__/__snapshots__/patternFly.search.test.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`searchPatternFly should attempt to return an array of all available results, all search: keys 1`] = ` +[ + "isSearchWildCardAll", + "firstExactMatch", + "exactMatches", +] +`; + +exports[`searchPatternFly should attempt to return an array of all available results, empty all search: keys 1`] = ` +[ + "isSearchWildCardAll", + "firstExactMatch", + "exactMatches", +] +`; + +exports[`searchPatternFly should attempt to return an array of all available results, wildcard search: keys 1`] = ` +[ + "isSearchWildCardAll", + "firstExactMatch", + "exactMatches", +] +`; diff --git a/src/__tests__/patternFly.getResources.test.ts b/src/__tests__/patternFly.getResources.test.ts new file mode 100644 index 0000000..2f6bd98 --- /dev/null +++ b/src/__tests__/patternFly.getResources.test.ts @@ -0,0 +1,109 @@ +import { + setCategoryDisplayLabel, + getPatternFlyComponentSchema, + getPatternFlyReactComponentNames, + getPatternFlyMcpResources +} from '../patternFly.getResources'; + +describe('setCategoryDisplayLabel', () => { + it.each([ + { + description: 'empty string', + entry: '' + }, + { + description: 'undefined', + entry: undefined + }, + { + description: 'null', + entry: null + }, + { + description: 'design', + entry: { + displayName: 'Lorem Ipsum', + section: 'components', + category: 'design-guidelines', + path: 'https://www.patternfly.org/v6/components/lorem-ipsum/design-guidelines' + } + }, + { + description: 'accessibility', + entry: { + displayName: 'Dolor Sit', + section: 'components', + category: 'accessibility', + path: 'https://www.patternfly.org/v6/components/dolor-sit/accessibility' + } + }, + { + description: 'example', + entry: { + displayName: 'Lorem Sit', + section: 'components', + category: 'react', + path: 'https://www.patternfly.org/v6/components/lorem-sit/components' + } + }, + { + description: 'guidelines', + entry: + { + displayName: 'Sit Sit', + section: 'guidelines', + category: 'react', + path: 'documentation:components/sit-sit/guidelines.md' + } + } + ])('should normalize categories and apply linking markdown, $description', ({ entry }) => { + expect(setCategoryDisplayLabel(entry as any)).toMatchSnapshot(); + }); +}); + +describe('getPatternFlyComponentSchema', () => { + it.each([ + { + description: 'default', + componentName: 'Button', + expected: true + }, + { + description: 'unknown component', + componentName: 'Lorem', + expected: false + } + ])('should attempt to return a schema', async ({ componentName, expected }) => { + const output = await getPatternFlyComponentSchema(componentName); + + expect(Boolean(output)).toBe(expected); + }); + + it('should have a memoized property', () => { + expect(getPatternFlyComponentSchema).toHaveProperty('memo'); + }); +}); + +describe('getPatternFlyReactComponentNames', () => { + it('should return multiple organized facets', async () => { + const result = await getPatternFlyReactComponentNames(); + + expect(Object.keys(result)).toMatchSnapshot('properties'); + }); + + it('should have a memoized property', () => { + expect(getPatternFlyReactComponentNames).toHaveProperty('memo'); + }); +}); + +describe('getPatternFlyMcpResources', () => { + it('should return multiple organized facets', async () => { + const result = await getPatternFlyMcpResources(); + + expect(Object.keys(result)).toMatchSnapshot('properties'); + }); + + it('should have a memoized property', async () => { + expect(getPatternFlyMcpResources).toHaveProperty('memo'); + }); +}); diff --git a/src/__tests__/patternFly.helpers.test.ts b/src/__tests__/patternFly.helpers.test.ts index 67dcfd2..39b9e6b 100644 --- a/src/__tests__/patternFly.helpers.test.ts +++ b/src/__tests__/patternFly.helpers.test.ts @@ -1,4 +1,10 @@ -import { findClosestPatternFlyVersion } from '../patternFly.helpers'; +import { + findClosestPatternFlyVersion, + getPatternFlyVersionContext, + normalizeEnumeratedPatternFlyVersion, + filterEnumeratedPatternFlyVersions, + disabled_findClosestPatternFlyVersion +} from '../patternFly.helpers'; import { readLocalFileFunction } from '../server.getResources'; import { DEFAULT_OPTIONS } from '../options.defaults'; @@ -12,6 +18,161 @@ jest.mock('../server.getResources', () => ({ const mockReadLocalFile = readLocalFileFunction.memo as jest.Mock; describe('findClosestPatternFlyVersion', () => { + it('should provide a temporary closest version that always returns the latest version', async () => { + await expect(findClosestPatternFlyVersion()).resolves.toBe('6.0.0'); + }); + + it('should have a memoized property', () => { + expect(findClosestPatternFlyVersion).toHaveProperty('memo'); + }); +}); + +describe('getPatternFlyVersionContext', () => { + it('should temporarily return option.defaults and latest versions with specific properties', async () => { + const result = await getPatternFlyVersionContext(); + + expect(Object.keys(result)).toMatchSnapshot('keys'); + expect(result.envSemVer).toBe('6.0.0'); + expect(result.envVersion).toBe('v6'); + }); + + it('should have a memoized property', () => { + expect(getPatternFlyVersionContext).toHaveProperty('memo'); + }); +}); + +describe('normalizeEnumeratedPatternFlyVersion', () => { + it.each([ + { + description: 'exact semver', + version: '6.0.0', + expected: 'v6' + }, + { + description: 'semver', + version: '6.4.10', + expected: 'v6' + }, + { + description: 'tag', + version: 'v6', + expected: 'v6' + }, + { + description: 'current', + version: 'current', + expected: 'v6' + }, + { + description: 'latest', + version: 'latest', + expected: 'v6' + }, + { + description: 'detected', + version: 'detected', + expected: 'v6' + }, + { + description: 'unknown', + version: 'unknown', + expected: undefined + }, + { + description: 'unavailable exact semver', + version: '5.0.0', + expected: undefined + }, + { + description: 'unavailable semver', + version: '5.2.10', + expected: undefined + }, + { + description: 'unavailable tag', + version: 'v5', + expected: undefined + }, + { + description: 'undefined', + version: undefined, + expected: undefined + }, + { + description: 'null', + version: null, + expected: undefined + }, + { + description: 'empty', + version: '', + expected: undefined + } + ])('should attempt to normalize a PatternFly version to a valid tag display version, $description', async ({ version, expected }) => { + const result = await normalizeEnumeratedPatternFlyVersion(version as any); + + expect(result).toBe(expected); + }); +}); + +describe('filterEnumeratedPatternFlyVersions', () => { + it.each([ + { + description: 'exact semver', + version: '6.0.0' + }, + { + description: 'semver', + version: '6.4.10' + }, + { + description: 'tag', + version: 'v6' + }, + { + description: 'current', + version: 'current' + }, + { + description: 'latest', + version: 'latest' + }, + { + description: 'detected', + version: 'detected' + }, + { + description: 'unavailable exact semver', + version: '5.0.0' + }, + { + description: 'unavailable semver', + version: '5.2.10' + }, + { + description: 'unavailable tag', + version: 'v5' + }, + { + description: 'undefined', + version: undefined + }, + { + description: 'null', + version: null + }, + { + description: 'empty', + version: '' + } + ])('should attempt to refine a PatternFly versions based on available enumerations, $description', async ({ version }) => { + const result = await filterEnumeratedPatternFlyVersions(version as any); + + expect(result).toMatchSnapshot(); + }); +}); + +describe('disabled_findClosestPatternFlyVersion', () => { it.each([ { description: 'non-existent path', @@ -24,7 +185,7 @@ describe('findClosestPatternFlyVersion', () => { expected: '6.0.0' } ])('should return default version if no package.json is found, $description', async ({ path, expected }) => { - const version = await findClosestPatternFlyVersion(path as any); + const version = await disabled_findClosestPatternFlyVersion(path as any); expect(version).toBe(expected); }); @@ -143,11 +304,11 @@ describe('findClosestPatternFlyVersion', () => { })); // Use the PF MCP package.json so we can override with "mockReadLocalFile". Override available resource versions. - const version = await findClosestPatternFlyVersion(process.cwd(), { + const version = await disabled_findClosestPatternFlyVersion(process.cwd(), { ...DEFAULT_OPTIONS, patternflyOptions: { ...DEFAULT_OPTIONS.patternflyOptions, - availableResourceVersions: ['4.0.0', '5.0.0', '6.0.0'] + availableResourceVersions: ['4.0.0', '5.0.0', '6.0.0'] as any } }); diff --git a/src/__tests__/patternFly.search.test.ts b/src/__tests__/patternFly.search.test.ts new file mode 100644 index 0000000..c014989 --- /dev/null +++ b/src/__tests__/patternFly.search.test.ts @@ -0,0 +1,55 @@ +import { searchPatternFly } from '../patternFly.search'; + +describe('searchPatternFly', () => { + it.each([ + { + description: 'wildcard search', + search: '*' + }, + { + description: 'all search', + search: 'all' + }, + { + description: 'empty all search', + search: '' + } + ])('should attempt to return an array of all available results, $description', async ({ search }) => { + const { searchResults, ...rest } = await searchPatternFly(search, { allowWildCardAll: true }); + + expect(searchResults.length).toBeGreaterThan(0); + expect(Object.keys(rest)).toMatchSnapshot('keys'); + }); + + it.each([ + { + description: 'exact match', + search: 'button', + matchType: 'exact' + }, + { + description: 'partial prefix match', + search: 'bu', + matchType: 'prefix' + }, + { + description: 'partial suffix match', + search: 'tton', + matchType: 'suffix' + }, + { + description: 'partial contains match', + search: 'utt', + matchType: 'contains' + } + ])('should attempt to match components and keywords, $description', async ({ search, matchType }) => { + const { searchResults } = await searchPatternFly(search); + + expect(searchResults.filter(({ matchType: returnMatchType }) => returnMatchType === matchType)).toEqual([ + expect.objectContaining({ + item: expect.stringContaining(search), + query: expect.stringMatching(search) + }) + ]); + }); +}); diff --git a/src/options.defaults.ts b/src/options.defaults.ts index 0b676f5..2dabbd4 100644 --- a/src/options.defaults.ts +++ b/src/options.defaults.ts @@ -20,6 +20,9 @@ import { type ToolModule } from './server.toolsUser'; * @property maxSearchLength - Maximum length for search strings. * @property recommendedMaxDocsToLoad - Recommended maximum number of docs to load. * @property {typeof MODE_LEVELS} mode - Specifies the mode of operation. + * - `cli`: Command-line interface mode. + * - `programmatic`: Programmatic interaction mode where the application is used as a library or API. + * - `test`: Testing or debugging mode. * @property {ModeOptions} modeOptions - Mode-specific options. * @property name - Name of the package. * @property nodeVersion - Node.js major version. @@ -159,18 +162,27 @@ interface ModeOptions { /** * PatternFly-specific options. * - * @property availableResourceVersions List of intended available PatternFly resource versions to the MCP server. + * @property availableResourceVersions List of available PatternFly resource versions to the MCP server. + * @property availableSearchVersions List of available PatternFly search versions to the MCP server. + * @property availableSchemasVersions List of available PatternFly schema versions to the MCP server. * @property default Default specific options. - * @property default.defaultVersion Default PatternFly version. + * @property default.latestSemVer Default PatternFly `SemVer` major version (e.g., '6.0.0'). + * @property default.latestVersion Default PatternFly `tag` major version, used for display and file paths (e.g., 'v6'). + * @property default.latestSchemasVersion Default PatternFly `tag` major version, used for schemas. * @property default.versionWhitelist List of mostly reliable dependencies to scan for when detecting the PatternFly version. * @property default.versionStrategy Strategy to use when multiple PatternFly versions are detected. * - 'highest': Use the highest major version found. * - 'lowest': Use the lowest major version found. */ interface PatternFlyOptions { - availableResourceVersions: string[]; + availableResourceVersions: ('6.0.0')[]; + // availableSearchVersions: ('current' | 'detected' | 'latest' | 'v3' | 'v4' | 'v5' | 'v6')[]; + availableSearchVersions: ('current' | 'detected' | 'latest' | 'v6')[]; + availableSchemasVersions: ('v6')[]; default: { - defaultVersion: string; + latestSemVer: '6.0.0'; + latestVersion: 'v6'; + latestSchemasVersion: 'v6'; versionWhitelist: string[]; versionStrategy: 'highest' | 'lowest'; } @@ -349,8 +361,12 @@ const LOG_BASENAME = 'pf-mcp:log'; */ const PATTERNFLY_OPTIONS: PatternFlyOptions = { availableResourceVersions: ['6.0.0'], + availableSearchVersions: ['current', 'latest', 'v6'], + availableSchemasVersions: ['v6'], default: { - defaultVersion: '6.0.0', + latestSemVer: '6.0.0', + latestVersion: 'v6', + latestSchemasVersion: 'v6', versionWhitelist: [ '@patternfly/react-core', '@patternfly/patternfly' @@ -366,11 +382,6 @@ const URL_REGEX = /^(https?:)\/\//i; /** * Available operational modes for the MCP server. - * - * Each mode represents an operational domain: - * - `cli`: Command-line interface mode. - * - `programmatic`: Programmatic interaction mode where the application is used as a library or API. - * - `test`: Testing or debugging mode. */ const MODE_LEVELS: DefaultOptions['mode'][] = ['cli', 'programmatic', 'test']; diff --git a/src/patternFly.getResources.ts b/src/patternFly.getResources.ts new file mode 100644 index 0000000..145b980 --- /dev/null +++ b/src/patternFly.getResources.ts @@ -0,0 +1,389 @@ +import { + componentNames as pfComponentNames, + getComponentSchema +} from '@patternfly/patternfly-component-schemas/json'; +import { memo } from './server.caching'; +import { DEFAULT_OPTIONS } from './options.defaults'; +import { + getPatternFlyVersionContext, + type PatternFlyVersionContext +} from './patternFly.helpers'; +import { log, formatUnknownError } from './logger'; +import { + EMBEDDED_DOCS, + type PatternFlyMcpDocsCatalog, + type PatternFlyMcpDocsCatalogEntry, + type PatternFlyMcpDocsCatalogDoc +} from './docs.embedded'; + +/** + * Derive the component schema type from @patternfly/patternfly-component-schemas + */ +type PatternFlyComponentSchema = Awaited>; + +/** + * PatternFly JSON extended documentation metadata + */ +type PatternFlyMcpDocsMeta = { + name: string; + displayCategory: string; + uri: string; + uriSchemas?: string | undefined +}; + +/** + * PatternFly resource, the original JSON. + * + * @interface PatternFlyMcpDocs + */ +type PatternFlyMcpResources = PatternFlyMcpDocsCatalogEntry; + +/** + * PatternFly resources by Path with an entry. + */ +type PatternFlyMcpResourcesByPath = { + [path: string]: PatternFlyMcpDocsCatalogDoc & PatternFlyMcpDocsMeta; +}; +// type PatternFlyMcpResourcesByPath = Map; + +/** + * PatternFly resources by URI with a list of entries. + */ +type PatternFlyMcpResourcesByUri = { + [uri: string]: (PatternFlyMcpDocsCatalogDoc & PatternFlyMcpDocsMeta)[]; +}; + +/** + * PatternFly resources by version with a list of entries. + */ +type PatternFlyMcpResourcesByVersion = { + [version: string]: (PatternFlyMcpDocsCatalogDoc & PatternFlyMcpDocsMeta)[]; +}; + +/** + * PatternFly resource metadata. + * + * @note This might need to be called resource metadata. `docs.json` doesn't just contain component metadata. + * + * @property name - The name of component entry. + * @property urls - All entry URLs for component documentation. + * @property urlsNoGuidance - All entry URLs for component documentation without AI guidance. + * @property urlsGuidance - All entry URLs for component documentation with AI guidance. + * @property entriesGuidance - All entry PatternFly documentation entries with AI guidance. + * @property entriesNoGuidance - All entry PatternFly documentation entries without AI guidance. + * @property versions - Entry segmented by versions. + */ +type PatternFlyMcpResourceMetadata = { + name: string; + // isSchemasAvailable: boolean; + urls: string[]; + urlsNoGuidance: string[]; + urlsGuidance: string[]; + entriesGuidance: (PatternFlyMcpDocsCatalogDoc & PatternFlyMcpDocsMeta)[]; + entriesNoGuidance: (PatternFlyMcpDocsCatalogDoc & PatternFlyMcpDocsMeta)[]; + versions: Record; +}; + +/** + * Patternfly available documentation. + * + * @note To avoid lookup issues we normalize most keys and indexes to lowercase, except docs.json `paths`. + * GitHub has case-sensitive links. + * + * @interface PatternFlyMcpAvailableDocs + * @extends PatternFlyVersionContext + * + * @property resources - Patternfly available documentation and metadata by resource name. + * @property docsIndex - Patternfly available documentation index. + * @property componentsIndex - Patternfly available components index. + * @property keywordsIndex - Patternfly available keywords index. + * @property isFallbackDocumentation - Whether the fallback documentation is used. + * @property pathIndex - Patternfly documentation path index. + * @property byPath - Patternfly documentation by path with entries + * @property byUri - Patternfly documentation by uri with entries + * @property byVersion - Patternfly documentation by version with entries + * @property byVersionComponentNames - Patternfly documentation by version with component names + */ +interface PatternFlyMcpAvailableResources extends PatternFlyVersionContext { + resources: Map; + docsIndex: string[]; + componentsIndex: string[]; + keywordsIndex: string[]; + isFallbackDocumentation: boolean; + pathIndex: string[]; + byPath: PatternFlyMcpResourcesByPath; + byUri: PatternFlyMcpResourcesByUri; + byVersion: PatternFlyMcpResourcesByVersion; + byVersionComponentNames: { + [version: string]: string[]; + }; +} + +/** + * Lazy load the PatternFly documentation catalog. + * + * @returns PatternFly documentation catalog JSON, or fallback catalog if import fails. + */ +const getPatternFlyDocsCatalog = async (): Promise => { + let docsCatalog = EMBEDDED_DOCS; + let isFallback = false; + + try { + docsCatalog = (await import('#docsCatalog', { with: { type: 'json' } })).default; + } catch (error) { + isFallback = true; + log.debug(`Failed to import docs catalog '#docsCatalog': ${formatUnknownError(error)}`, 'Using fallback docs catalog.'); + } + + return { ...docsCatalog, isFallback }; +}; + +/** + * Memoized version of getPatternFlyDocsCatalog. + */ +getPatternFlyDocsCatalog.memo = memo(getPatternFlyDocsCatalog); + +/** + * Set the category display label based on the entry's section and category. + * + * @note Review integrating locale strings with some level of display logic. + * + * @param entry - PatternFly documentation entry + * @returns The category display label + */ +const setCategoryDisplayLabel = (entry?: PatternFlyMcpDocsCatalogDoc) => { + let categoryLabel = typeof entry?.category === 'string' ? entry.category.trim().toLowerCase() : undefined; + + if (categoryLabel === undefined) { + return 'Documentation'; + } + + switch (categoryLabel) { + case 'grammar': + categoryLabel = 'Grammar'; + break; + case 'writing-guides': + categoryLabel = 'Writing Guidelines'; + break; + case 'design-guidelines': + categoryLabel = 'Design Guidelines'; + break; + case 'accessibility': + categoryLabel = 'Accessibility'; + break; + case 'react': + categoryLabel = 'Examples'; + break; + default: + categoryLabel = categoryLabel.charAt(0).toUpperCase() + categoryLabel.slice(1); + break; + } + + return entry?.section?.trim()?.toLowerCase() === 'guidelines' ? 'AI Guidance' : categoryLabel; +}; + +/** + * A multifaceted list of all PatternFly React component names. + * + * @note The "table" component is manually added to the `componentNamesIndex` list because it's not currently included + * in the component schemas package. + * + * @note To avoid lookup issues we normalize all keys and indexes to lowercase. Component names are lowercased. + * + * @param contextPathOverride - Context path for updating the returned PatternFly versions. + * @returns A multifaceted React component breakdown. Use the "memoized" property for performance. + * - `byVersion`: Map of lowercase PatternFly versions to lowercase component names. + * - `componentNamesIndex`: Latest PF version, lowercase component names sorted alphabetically. + * - `componentNamesWithSchemasIndex`: Latest PF version, lowercase component names sorted alphabetically. + * - `componentNamesWithSchemasMap`: Latest PF version, Map of lowercase component names to original case component names. + */ +const getPatternFlyReactComponentNames = async (contextPathOverride?: string) => { + const { latestSchemasVersion, isEnvTheLatestSchemasVersion } = await getPatternFlyVersionContext.memo(contextPathOverride); + const byVersion = new Map(); + + const latestNamesIndex = [...Array.from(new Set([...pfComponentNames, 'Table'])).map(name => name.toLowerCase()).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))]; + const latestNamesWithSchemaIndex = [...pfComponentNames.map(name => name.toLowerCase()).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))]; + const latestNamesWithSchemasMap = new Map(pfComponentNames.map(name => [name.toLowerCase(), name])); + + byVersion.set(latestSchemasVersion, latestNamesIndex); + + return { + byVersion: Object.fromEntries(byVersion), + componentNamesIndex: isEnvTheLatestSchemasVersion ? latestNamesIndex : [], + componentNamesWithSchemasIndex: isEnvTheLatestSchemasVersion ? latestNamesWithSchemaIndex : [], + componentNamesWithSchemasMap: isEnvTheLatestSchemasVersion ? Object.fromEntries(latestNamesWithSchemasMap) : {} + }; +}; + +/** + * Memoized version of getPatternFlyReactComponentNames. + */ +getPatternFlyReactComponentNames.memo = memo(getPatternFlyReactComponentNames); + +/** + * Get a multifaceted resources breakdown from PatternFly. + * + * @param contextPathOverride - Context path for updating the returned PatternFly versions. + * @returns A multifaceted documentation breakdown. Use the "memoized" property for performance. + */ +const getPatternFlyMcpResources = async (contextPathOverride?: string): Promise => { + const versionContext = await getPatternFlyVersionContext.memo(contextPathOverride); + const componentNames = await getPatternFlyReactComponentNames.memo(contextPathOverride); + const { byVersion: componentNamesByVersion, componentNamesIndex, componentNamesWithSchemasIndex: schemaNames } = componentNames; + + const originalDocs = await getPatternFlyDocsCatalog.memo(); + const resources = new Map(); + const byPath: PatternFlyMcpResourcesByPath = {}; + const byUri: PatternFlyMcpResourcesByUri = {}; + const byVersion: PatternFlyMcpResourcesByVersion = {}; + const pathIndex = new Set(); + + Object.entries(originalDocs.docs).forEach(([docsName, entries]) => { + const name = docsName.toLowerCase(); + const resource: PatternFlyMcpResourceMetadata = { + name, + urls: [], + urlsNoGuidance: [], + urlsGuidance: [], + entriesGuidance: [], + entriesNoGuidance: [], + versions: {} + }; + + entries.forEach(entry => { + const version = (entry.version || 'unknown').toLowerCase(); + const isSchemasAvailable = versionContext.latestSchemasVersion === version && schemaNames.includes(name); + const path = entry.path; + const uri = `patternfly://docs/${version}/${name}`; + + pathIndex.add(path); + + resource.versions[version] ??= { + isSchemasAvailable, + uri, + uriSchemas: undefined, + urls: [], + urlsGuidance: [], + urlsNoGuidance: [], + entriesGuidance: [], + entriesNoGuidance: [] + }; + + const displayCategory = setCategoryDisplayLabel(entry); + let uriSchemas; + + if (isSchemasAvailable) { + uriSchemas = `patternfly://schemas/${version}/${name}`; + + resource.versions[version].uriSchemas = uriSchemas; + } + + const extendedEntry = { ...entry, name: docsName, displayCategory, uri, uriSchemas }; + + byPath[path] = extendedEntry; + + byUri[uri] ??= []; + byUri[uri]?.push(extendedEntry); + + if (uriSchemas) { + byUri[uriSchemas] ??= []; + byUri[uriSchemas]?.push(extendedEntry); + } + + byVersion[entry.version] ??= []; + byVersion[entry.version]?.push(extendedEntry); + + resource.urls.push(path); + resource.versions[version].urls.push(path); + + if (extendedEntry.section === 'guidelines') { + resource.urlsGuidance.push(path); + resource.entriesGuidance.push(extendedEntry); + resource.versions[version].urlsGuidance.push(path); + resource.versions[version].entriesGuidance.push(extendedEntry); + } else { + resource.urlsNoGuidance.push(path); + resource.entriesNoGuidance.push(extendedEntry); + resource.versions[version].urlsNoGuidance.push(path); + resource.versions[version].entriesNoGuidance.push(extendedEntry); + } + }); + + resources.set(name, resource); + }); + + Object.entries(byVersion).forEach(([_version, entries]) => { + entries.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })); + }); + + return { + ...versionContext, + resources, + docsIndex: Array.from(resources.keys()).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })), + componentsIndex: componentNamesIndex, + keywordsIndex: Array.from(new Set([...Array.from(resources.keys()), ...componentNamesIndex])) + .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })), + isFallbackDocumentation: originalDocs.isFallback, + pathIndex: Array.from(pathIndex).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })), + byPath, + byUri, + byVersion, + byVersionComponentNames: componentNamesByVersion + }; +}; + +/** + * Memoized version of getPatternFlyLocalDocs. + */ +getPatternFlyMcpResources.memo = memo(getPatternFlyMcpResources); + +/** + * Get the component schema from @patternfly/patternfly-component-schemas. + * + * @param componentName - Name of the component to retrieve the schema for. + * @returns The component schema, or `undefined` if the component name is not found. + */ +const getPatternFlyComponentSchema = async (componentName: string) => { + const { componentNamesWithSchemasMap } = await getPatternFlyReactComponentNames.memo(); + + try { + const updatedComponentName = componentNamesWithSchemasMap[componentName.toLowerCase()]; + + if (!updatedComponentName) { + return undefined; + } + + return await getComponentSchema(updatedComponentName); + } catch {} + + return undefined; +}; + +/** + * Memoized version of getComponentSchema. + */ +getPatternFlyComponentSchema.memo = memo(getPatternFlyComponentSchema, DEFAULT_OPTIONS.toolMemoOptions.usePatternFlyDocs); + +export { + getPatternFlyComponentSchema, + getPatternFlyMcpResources, + getPatternFlyReactComponentNames, + setCategoryDisplayLabel, + type PatternFlyComponentSchema, + type PatternFlyMcpAvailableResources, + type PatternFlyMcpResourceMetadata, + type PatternFlyMcpDocsMeta, + type PatternFlyMcpResources, + type PatternFlyMcpResourcesByPath, + type PatternFlyMcpResourcesByUri, + type PatternFlyMcpResourcesByVersion +}; diff --git a/src/patternFly.helpers.ts b/src/patternFly.helpers.ts index 9be2bdb..5daa257 100644 --- a/src/patternFly.helpers.ts +++ b/src/patternFly.helpers.ts @@ -1,13 +1,161 @@ import semver, { type SemVer } from 'semver'; import { getOptions } from './options.context'; -import { - findNearestPackageJson, - matchPackageVersion, - readLocalFileFunction -} from './server.getResources'; +import { type PatternFlyOptions } from './options.defaults'; +import { findNearestPackageJson, matchPackageVersion, readLocalFileFunction } from './server.getResources'; import { fuzzySearch } from './server.search'; import { memo } from './server.caching'; +interface PatternFlyVersionContext { + availableSemVer: string[]; + availableVersions: string[]; + availableSchemasVersions: string[]; + enumeratedVersions: string[]; + envSemVer: string; + envVersion: string; + latestVersion: PatternFlyOptions['default']['latestVersion']; + latestSchemasVersion: PatternFlyOptions['default']['latestSchemasVersion']; + isEnvTheLatestVersion: boolean; + isEnvTheLatestSchemasVersion: boolean; +} + +/** + * Find the closest PatternFly version used within the project context. + * + * @note Temporary closest version until environment audit tooling is available, + * see `disabled_findClosestPatternFlyVersion` for the actual implementation. + * + * @param _contextPathOverride - Temporary placeholder for future context path override + * @param options - Global options + * @returns Temporary latest PF semVer (e.g., '6.0.0') + */ +const findClosestPatternFlyVersion = async ( + _contextPathOverride: string | undefined = undefined, + options = getOptions() +): Promise => + options.patternflyOptions?.default?.latestSemVer; + +/** + * Memoized version of findClosestPatternFlyVersion. + */ +findClosestPatternFlyVersion.memo = memo(findClosestPatternFlyVersion); + +/** + * Get the PatternFly version context. + * + * @note We may need to keep the latest version outside of context and add an environment latest version. + * + * @param contextPathOverride - Optional override for the context path + * @param options - Global options + * @returns The PatternFly version context, including the closest version, the latest version, and the available versions. + * - `availableSemVer`: The list of available PatternFly SemVer versions, (e.g. "4.0.0", "5.0.0", "6.0.0") + * - `availableVersions`: The list of available PatternFly `tag` versions, (e.g. "v4", "v5", "v6") + * - `availableSchemaVersions`: The list of available PatternFly `tag` schema versions, (e.g. "v6") + * - `enumeratedVersions`: The list of available PatternFly `tag` and `display` versions, (e.g. "v4", "v5", "v6", "current", "latest") + * - `envSemVer`: The "closest" or "detected" environment SemVer version detected in the project context. + * - `envVersion`: The "closest" or "detected" environment PatternFly `tag` version detected in the project context, (e.g. "v4", "v5", "v6") + * - `latestVersion`: The latest PatternFly `tag` version, (e.g. "v4", "v5", "v6") + * - `latestSchemasVersion`: The latest PatternFly `tag` schemas version + * - `isEnvTheLatestVersion`: Whether the environment version is the actual latest `default` version provided by MCP options. + * - `isEnvTheLatestSchemasVersion`: Whether the environment version is the actual latest schemas `default` version provided by MCP options. + */ +const getPatternFlyVersionContext = async ( + contextPathOverride: string | undefined = undefined, + options = getOptions() +): Promise => { + const availableSemVer = options.patternflyOptions?.availableResourceVersions; + const availableVersions = availableSemVer?.map?.(version => { + const majorVersion = semver.coerce(version)?.major; + + return majorVersion ? `v${majorVersion}` : undefined; + }).filter(Boolean) as string[] || []; + + const availableSchemasVersions = options.patternflyOptions?.availableSchemasVersions || []; + const enumeratedVersions = Array.from(new Set([...options.patternflyOptions?.availableSearchVersions || [], ...availableVersions])); + + const latestVersion = options.patternflyOptions?.default?.latestVersion; + const latestSchemasVersion = options.patternflyOptions?.default?.latestSchemasVersion; + + const envSemVer = await findClosestPatternFlyVersion.memo(contextPathOverride); + const majorVersion = semver.coerce(envSemVer)?.major; + const envVersion = majorVersion ? `v${majorVersion}` : latestVersion; + + return { + availableSemVer, + availableVersions, + availableSchemasVersions, + enumeratedVersions, + envSemVer, + envVersion, + latestVersion, + latestSchemasVersion, + isEnvTheLatestVersion: envVersion === latestVersion, + isEnvTheLatestSchemasVersion: envVersion === latestSchemasVersion + }; +}; + +/** + * Memoized version of getPatternFlyVersionContext. + */ +getPatternFlyVersionContext.memo = memo(getPatternFlyVersionContext); + +/** + * Normalize the version string to a valid PatternFly `tag` display version, (e.g. "v4", "v5", "v6") + * + * @param version - The version string to normalize. + * @returns The normalized version string, or `undefined` if the version is not recognized. + */ +const normalizeEnumeratedPatternFlyVersion = async (version?: string) => { + const { envVersion, latestVersion, availableVersions } = await getPatternFlyVersionContext.memo(); + const updatedVersion = typeof version === 'string' ? version.toLowerCase().trim() : undefined; + let refineVersion = updatedVersion; + + switch (updatedVersion) { + case 'current': + case 'latest': + refineVersion = latestVersion; + break; + case 'detected': + refineVersion = envVersion; + break; + } + + if (refineVersion && refineVersion.includes('.')) { + const majorVersion = semver.coerce(refineVersion)?.major; + const tagVersion = majorVersion ? `v${majorVersion}` : undefined; + + if (tagVersion && availableVersions.includes(tagVersion)) { + return tagVersion; + } + } + + if (refineVersion && availableVersions.includes(refineVersion)) { + return refineVersion; + } + + return undefined; +}; + +/** + * Memoized version of normalizeEnumeratedPatternFlyVersion. + */ +normalizeEnumeratedPatternFlyVersion.memo = memo(normalizeEnumeratedPatternFlyVersion); + +/** + * Get all available PatternFly enumerations OR filter a version string to a valid PatternFly `tag` OR `display` version, + * (e.g. "current", "v6", etc.) + * + * @param version - The version string to filter. + * @returns If version is provided returns the filtered version string array, or an empty array if the version is not recognized, + * otherwise returns all available versions. + */ +const filterEnumeratedPatternFlyVersions = async (version?: string) => { + const { enumeratedVersions } = await getPatternFlyVersionContext.memo(); + const normalizedVersion = await normalizeEnumeratedPatternFlyVersion.memo(version); + + return enumeratedVersions + .filter(version => version.toLowerCase().startsWith(normalizedVersion || '')); +}; + /** * Find the closest PatternFly version used within the project context. * @@ -17,6 +165,9 @@ import { memo } from './server.caching'; * @note In the future consider adding a log.debug to the try/catch block if/when the find closest version * is integrated into tooling and resources. * + * @note Getting the user's directory context requires additional MCP tooling. Aspects of this function will + * be re-enabled and triggered via the new MCP tooling around auditing the PatternFly environment. + * * Logic: * 1. Locates the nearest package.json. * 2. Scans whitelisted dependencies using fuzzy matching. @@ -27,14 +178,14 @@ import { memo } from './server.caching'; * @param options - Global options * @returns Matched PatternFly semver version (e.g., '6.0.0', '5.0.0', '4.0.0') */ -const findClosestPatternFlyVersion = async ( +const disabled_findClosestPatternFlyVersion = async ( contextPathOverride: string | undefined = undefined, options = getOptions() ): Promise => { const availableVersions = options.patternflyOptions.availableResourceVersions; - const { defaultVersion, versionWhitelist, versionStrategy } = options.patternflyOptions.default; + const { latestSemVer, versionWhitelist, versionStrategy } = options.patternflyOptions.default; const pkgPath = findNearestPackageJson(contextPathOverride || options.contextPath); - const updatedDefaultVersion = semver.coerce(defaultVersion)?.version || defaultVersion; + const updatedDefaultVersion = latestSemVer; if (!pkgPath) { return updatedDefaultVersion; @@ -83,9 +234,11 @@ const findClosestPatternFlyVersion = async ( } }; -/** - * Memoized version of findClosestPatternFlyVersion. - */ -findClosestPatternFlyVersion.memo = memo(findClosestPatternFlyVersion); - -export { findClosestPatternFlyVersion }; +export { + findClosestPatternFlyVersion, + filterEnumeratedPatternFlyVersions, + disabled_findClosestPatternFlyVersion, + getPatternFlyVersionContext, + normalizeEnumeratedPatternFlyVersion, + type PatternFlyVersionContext +}; diff --git a/src/patternFly.search.ts b/src/patternFly.search.ts new file mode 100644 index 0000000..57cc1a2 --- /dev/null +++ b/src/patternFly.search.ts @@ -0,0 +1,94 @@ +import { fuzzySearch, type FuzzySearchResult } from './server.search'; +import { memo } from './server.caching'; +import { DEFAULT_OPTIONS } from './options.defaults'; +import { getPatternFlyMcpResources, type PatternFlyMcpResourceMetadata } from './patternFly.getResources'; + +/** + * Search result object returned by searchPatternFly. + * Includes additional metadata and URLs. + */ +interface SearchPatternFlyResult extends FuzzySearchResult, PatternFlyMcpResourceMetadata { + query: string; +} + +/** + * Search results object returned by searchPatternFly. + * Includes additional metadata and URLs. + * + * @interface SearchPatternFlyResults + * + * @property isSearchWildCardAll - Whether the search query matched all components + * @property {SearchPatternFlyResult | undefined} firstExactMatch - First exact match within fuzzy search results + * @property {SearchPatternFlyResult[]} exactMatches - All exact matches within fuzzy search results + * @property {SearchPatternFlyResult[]} searchResults - Fuzzy search results + */ +interface SearchPatternFlyResults { + isSearchWildCardAll: boolean, + firstExactMatch: SearchPatternFlyResult | undefined, + exactMatches: SearchPatternFlyResult[], + searchResults: SearchPatternFlyResult[] +} + +/** + * Search for PatternFly component documentation URLs using fuzzy search. + * + * @param searchQuery - Search query string + * @param settings - Optional settings object + * @param settings.resources - Object of multifaceted documentation entries to search. + * @param settings.allowWildCardAll - Allow a search query to match all components. Defaults to false. + * @returns Object containing search results and matched URLs + * - `isSearchWildCardAll`: Whether the search query matched all components + * - `firstExactMatch`: First exact match within fuzzy search results + * - `exactMatches`: All exact matches within fuzzy search results + * - `searchResults`: Fuzzy search results + */ +const searchPatternFly = async (searchQuery: string, { + resources = getPatternFlyMcpResources.memo(), + allowWildCardAll = false +} = {}): Promise => { + const updatedResources = await resources; + const isWildCardAll = searchQuery.trim() === '*' || searchQuery.trim().toLowerCase() === 'all' || searchQuery.trim() === ''; + const isSearchWildCardAll = allowWildCardAll && isWildCardAll; + let searchResults: FuzzySearchResult[] = []; + + if (isSearchWildCardAll) { + searchResults = updatedResources.keywordsIndex.map(name => ({ matchType: 'all', distance: 0, item: name } as FuzzySearchResult)); + } else { + searchResults = fuzzySearch(searchQuery, updatedResources.keywordsIndex, { + maxDistance: 3, + maxResults: 10, + isFuzzyMatch: true, + deduplicateByNormalized: true + }); + } + + const updatedSearchResults = searchResults.map((result: FuzzySearchResult) => { + const resource = updatedResources.resources.get(result.item); + + return { + ...result, + ...resource, + query: searchQuery + }; + }) as SearchPatternFlyResult[]; + + const exactMatches = updatedSearchResults.filter(result => result.matchType === 'exact' || result.matchType === 'all'); + + return { + isSearchWildCardAll, + firstExactMatch: exactMatches[0], + exactMatches: exactMatches, + searchResults: updatedSearchResults + }; +}; + +/** + * Memoized version of searchComponents. + */ +searchPatternFly.memo = memo(searchPatternFly, DEFAULT_OPTIONS.toolMemoOptions.searchPatternFlyDocs); + +export { + searchPatternFly, + type SearchPatternFlyResult, + type SearchPatternFlyResults +}; From b92bcf193b37e52593e65d7e745a0f33b7e78ae7 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Fri, 20 Feb 2026 19:12:24 -0500 Subject: [PATCH 2/7] fix: review updates --- src/patternFly.getResources.ts | 4 ++-- src/patternFly.helpers.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/patternFly.getResources.ts b/src/patternFly.getResources.ts index 145b980..ced2e3c 100644 --- a/src/patternFly.getResources.ts +++ b/src/patternFly.getResources.ts @@ -342,7 +342,7 @@ const getPatternFlyMcpResources = async (contextPathOverride?: string): Promise< }; /** - * Memoized version of getPatternFlyLocalDocs. + * Memoized version of getPatternFlyMcpResources. */ getPatternFlyMcpResources.memo = memo(getPatternFlyMcpResources); @@ -369,7 +369,7 @@ const getPatternFlyComponentSchema = async (componentName: string) => { }; /** - * Memoized version of getComponentSchema. + * Memoized version of getPatternFlyComponentSchema. */ getPatternFlyComponentSchema.memo = memo(getPatternFlyComponentSchema, DEFAULT_OPTIONS.toolMemoOptions.usePatternFlyDocs); diff --git a/src/patternFly.helpers.ts b/src/patternFly.helpers.ts index 5daa257..3a59b4a 100644 --- a/src/patternFly.helpers.ts +++ b/src/patternFly.helpers.ts @@ -49,7 +49,7 @@ findClosestPatternFlyVersion.memo = memo(findClosestPatternFlyVersion); * @returns The PatternFly version context, including the closest version, the latest version, and the available versions. * - `availableSemVer`: The list of available PatternFly SemVer versions, (e.g. "4.0.0", "5.0.0", "6.0.0") * - `availableVersions`: The list of available PatternFly `tag` versions, (e.g. "v4", "v5", "v6") - * - `availableSchemaVersions`: The list of available PatternFly `tag` schema versions, (e.g. "v6") + * - `availableSchemasVersions`: The list of available PatternFly `tag` schema versions, (e.g. "v6") * - `enumeratedVersions`: The list of available PatternFly `tag` and `display` versions, (e.g. "v4", "v5", "v6", "current", "latest") * - `envSemVer`: The "closest" or "detected" environment SemVer version detected in the project context. * - `envVersion`: The "closest" or "detected" environment PatternFly `tag` version detected in the project context, (e.g. "v4", "v5", "v6") @@ -145,8 +145,8 @@ normalizeEnumeratedPatternFlyVersion.memo = memo(normalizeEnumeratedPatternFlyVe * (e.g. "current", "v6", etc.) * * @param version - The version string to filter. - * @returns If version is provided returns the filtered version string array, or an empty array if the version is not recognized, - * otherwise returns all available versions. + * @returns If version is provided returns the filtered version string array, or all available versions if the version + * is not recognized. */ const filterEnumeratedPatternFlyVersions = async (version?: string) => { const { enumeratedVersions } = await getPatternFlyVersionContext.memo(); From 0bf6bae4e7642eadf8de0dba3bbed7d416a985d9 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Fri, 20 Feb 2026 19:27:46 -0500 Subject: [PATCH 3/7] fix: review updates --- src/options.defaults.ts | 1 - src/patternFly.getResources.ts | 8 +++++--- src/patternFly.helpers.ts | 4 ++-- src/patternFly.search.ts | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/options.defaults.ts b/src/options.defaults.ts index 2dabbd4..cc44bb1 100644 --- a/src/options.defaults.ts +++ b/src/options.defaults.ts @@ -176,7 +176,6 @@ interface ModeOptions { */ interface PatternFlyOptions { availableResourceVersions: ('6.0.0')[]; - // availableSearchVersions: ('current' | 'detected' | 'latest' | 'v3' | 'v4' | 'v5' | 'v6')[]; availableSearchVersions: ('current' | 'detected' | 'latest' | 'v6')[]; availableSchemasVersions: ('v6')[]; default: { diff --git a/src/patternFly.getResources.ts b/src/patternFly.getResources.ts index ced2e3c..6e5adfb 100644 --- a/src/patternFly.getResources.ts +++ b/src/patternFly.getResources.ts @@ -299,8 +299,8 @@ const getPatternFlyMcpResources = async (contextPathOverride?: string): Promise< byUri[uriSchemas]?.push(extendedEntry); } - byVersion[entry.version] ??= []; - byVersion[entry.version]?.push(extendedEntry); + byVersion[version] ??= []; + byVersion[version]?.push(extendedEntry); resource.urls.push(path); resource.versions[version].urls.push(path); @@ -363,7 +363,9 @@ const getPatternFlyComponentSchema = async (componentName: string) => { } return await getComponentSchema(updatedComponentName); - } catch {} + } catch (error) { + log.debug(`Failed to get component schemas for "${componentName}": ${formatUnknownError(error)}`); + } return undefined; }; diff --git a/src/patternFly.helpers.ts b/src/patternFly.helpers.ts index 3a59b4a..90f7e09 100644 --- a/src/patternFly.helpers.ts +++ b/src/patternFly.helpers.ts @@ -32,7 +32,7 @@ const findClosestPatternFlyVersion = async ( _contextPathOverride: string | undefined = undefined, options = getOptions() ): Promise => - options.patternflyOptions?.default?.latestSemVer; + options.patternflyOptions.default.latestSemVer; /** * Memoized version of findClosestPatternFlyVersion. @@ -62,7 +62,7 @@ const getPatternFlyVersionContext = async ( contextPathOverride: string | undefined = undefined, options = getOptions() ): Promise => { - const availableSemVer = options.patternflyOptions?.availableResourceVersions; + const availableSemVer = options.patternflyOptions?.availableResourceVersions || []; const availableVersions = availableSemVer?.map?.(version => { const majorVersion = semver.coerce(version)?.major; diff --git a/src/patternFly.search.ts b/src/patternFly.search.ts index 57cc1a2..9daa1f6 100644 --- a/src/patternFly.search.ts +++ b/src/patternFly.search.ts @@ -77,7 +77,7 @@ const searchPatternFly = async (searchQuery: string, { return { isSearchWildCardAll, firstExactMatch: exactMatches[0], - exactMatches: exactMatches, + exactMatches, searchResults: updatedSearchResults }; }; From ef4df20d9a5be3c9ea80fd01d5118ad5cb349c7c Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Fri, 20 Feb 2026 19:39:18 -0500 Subject: [PATCH 4/7] fix: review update --- src/patternFly.helpers.ts | 4 ++-- src/patternFly.search.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/patternFly.helpers.ts b/src/patternFly.helpers.ts index 90f7e09..42522d8 100644 --- a/src/patternFly.helpers.ts +++ b/src/patternFly.helpers.ts @@ -72,8 +72,8 @@ const getPatternFlyVersionContext = async ( const availableSchemasVersions = options.patternflyOptions?.availableSchemasVersions || []; const enumeratedVersions = Array.from(new Set([...options.patternflyOptions?.availableSearchVersions || [], ...availableVersions])); - const latestVersion = options.patternflyOptions?.default?.latestVersion; - const latestSchemasVersion = options.patternflyOptions?.default?.latestSchemasVersion; + const latestVersion = options.patternflyOptions.default.latestVersion; + const latestSchemasVersion = options.patternflyOptions.default.latestSchemasVersion; const envSemVer = await findClosestPatternFlyVersion.memo(contextPathOverride); const majorVersion = semver.coerce(envSemVer)?.major; diff --git a/src/patternFly.search.ts b/src/patternFly.search.ts index 9daa1f6..8b6beb8 100644 --- a/src/patternFly.search.ts +++ b/src/patternFly.search.ts @@ -83,7 +83,7 @@ const searchPatternFly = async (searchQuery: string, { }; /** - * Memoized version of searchComponents. + * Memoized version of searchPatternFly. */ searchPatternFly.memo = memo(searchPatternFly, DEFAULT_OPTIONS.toolMemoOptions.searchPatternFlyDocs); From a849409fb7683af3f372b8cc13b20b6a0b84b32f Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Fri, 20 Feb 2026 19:43:58 -0500 Subject: [PATCH 5/7] fix: review update --- src/patternFly.helpers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/patternFly.helpers.ts b/src/patternFly.helpers.ts index 42522d8..3d1d763 100644 --- a/src/patternFly.helpers.ts +++ b/src/patternFly.helpers.ts @@ -62,15 +62,15 @@ const getPatternFlyVersionContext = async ( contextPathOverride: string | undefined = undefined, options = getOptions() ): Promise => { - const availableSemVer = options.patternflyOptions?.availableResourceVersions || []; + const availableSemVer = options.patternflyOptions.availableResourceVersions; const availableVersions = availableSemVer?.map?.(version => { const majorVersion = semver.coerce(version)?.major; return majorVersion ? `v${majorVersion}` : undefined; }).filter(Boolean) as string[] || []; - const availableSchemasVersions = options.patternflyOptions?.availableSchemasVersions || []; - const enumeratedVersions = Array.from(new Set([...options.patternflyOptions?.availableSearchVersions || [], ...availableVersions])); + const availableSchemasVersions = options.patternflyOptions.availableSchemasVersions; + const enumeratedVersions = Array.from(new Set([...options.patternflyOptions.availableSearchVersions, ...availableVersions])); const latestVersion = options.patternflyOptions.default.latestVersion; const latestSchemasVersion = options.patternflyOptions.default.latestSchemasVersion; From 5dc70db723389678f1a6910f73432f92f4dec3ec Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Fri, 20 Feb 2026 19:46:13 -0500 Subject: [PATCH 6/7] fix: review update --- src/patternFly.helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/patternFly.helpers.ts b/src/patternFly.helpers.ts index 3d1d763..b5a3d00 100644 --- a/src/patternFly.helpers.ts +++ b/src/patternFly.helpers.ts @@ -63,7 +63,7 @@ const getPatternFlyVersionContext = async ( options = getOptions() ): Promise => { const availableSemVer = options.patternflyOptions.availableResourceVersions; - const availableVersions = availableSemVer?.map?.(version => { + const availableVersions = availableSemVer.map(version => { const majorVersion = semver.coerce(version)?.major; return majorVersion ? `v${majorVersion}` : undefined; From 94621e1b4ea67da97713b1ae2859ee1ec86180ab Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Fri, 20 Feb 2026 19:48:56 -0500 Subject: [PATCH 7/7] fix: review update --- src/patternFly.getResources.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/patternFly.getResources.ts b/src/patternFly.getResources.ts index 6e5adfb..3e042b8 100644 --- a/src/patternFly.getResources.ts +++ b/src/patternFly.getResources.ts @@ -75,7 +75,6 @@ type PatternFlyMcpResourcesByVersion = { */ type PatternFlyMcpResourceMetadata = { name: string; - // isSchemasAvailable: boolean; urls: string[]; urlsNoGuidance: string[]; urlsGuidance: string[];