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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ exports[`getPatternFlyComponentNames should return multiple organized facets: pr
"componentNamesIndex",
"componentNamesIndexMap",
"byVersion",
"byDocs",
]
`;

Expand Down
19 changes: 19 additions & 0 deletions src/__tests__/__snapshots__/tool.patternFlyDocs.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,22 @@ exports[`usePatternFlyDocsTool, callback should attempt to parse parameters, sin
exports[`usePatternFlyDocsTool, callback should attempt to parse parameters, with invalid urlList 1`] = `"# Content for invalid-path"`;

exports[`usePatternFlyDocsTool, callback should attempt to parse parameters, with name and actual path 1`] = `"# Content for documentation:chatbot/README.md"`;

exports[`usePatternFlyDocsTool, callback should have a specific markdown format: Button 1`] = `
[
{
"text": "# Content for components/ipsumButton.md
Source: components/ipsumButton.md

ipsum documentation content

---

# Content for components/loremButton.md
Source: components/loremButton.md

lorem documentation content",
"type": "text",
},
]
`;
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,65 @@ exports[`searchPatternFlyDocsTool should have a consistent return structure: str
}
`;

exports[`searchPatternFlyDocsTool, callback should parse parameters, default: search 1`] = `"# Search results for "Button". Showing 2 exact matches."`;
exports[`searchPatternFlyDocsTool, callback should have a specific markdown format: tooltip 1`] = `
[
{
"text": "# Search results for PatternFly version "v6" and "button". Showing 2 exact matches.
1. **button**:
"usePatternFlyDocs" resource parameter "name" and "URLs"
- **Name**: button
- **URLs**:
- [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)
- [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)
- [Button - (v6) - Examples for the button component.](https://raw.githubusercontent.com/patternfly/patternfly-react/476782a298df81cb78de7f3cd804f3ff8f33993c/packages/react-core/src/components/Button/examples/Button.md)
- **Resources**:
- **URI**: patternfly://docs/button?version=v6
- **JSON Schemas**: patternfly://schemas/button?version=v6

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."`;
2. **patterns**:
"usePatternFlyDocs" resource parameter "name" and "URLs"
- **Name**: patterns
- **URLs**:
- [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)
- [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)
- [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)
- [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)
- [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)
- [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)
- [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)
- [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)
- **Resources**:
- **URI**: patternfly://docs/patterns?version=v6

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."`;

exports[`searchPatternFlyDocsTool, callback should parse parameters, with explicit valid version: search 1`] = `"# Search results for "Button". Showing 2 exact matches."`;

exports[`searchPatternFlyDocsTool, callback should parse parameters, with lower case componentName: search 1`] = `"# Search results for "button". Showing 2 exact matches."`;
---


**Important**:
- Use the "usePatternFlyDocs" tool with the above names and URLs to fetch resource content.
- Use a search all ("*") to find all available resources.",
"type": "text",
},
]
`;

exports[`searchPatternFlyDocsTool, callback should parse parameters, default: search 1`] = `"# Search results for PatternFly version "v6" and "Button". Showing 2 exact matches."`;

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."`;

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."`;

exports[`searchPatternFlyDocsTool, callback should parse parameters, with explicit valid version: search 1`] = `"# Search results for PatternFly version "v6" and "Button". Showing 2 exact matches."`;

exports[`searchPatternFlyDocsTool, callback should parse parameters, with lower case componentName: search 1`] = `"# Search results for PatternFly version "v6" and "button". Showing 2 exact matches."`;

exports[`searchPatternFlyDocsTool, callback should parse parameters, with made up componentName: search 1`] = `"No PatternFly resources found matching "lorem ipsum dolor sit amet""`;

exports[`searchPatternFlyDocsTool, callback should parse parameters, with multiple words: search 1`] = `"# Search results for "Button Card Table". Showing 4 related matches."`;
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."`;

exports[`searchPatternFlyDocsTool, callback should parse parameters, with partial componentName: search 1`] = `"# Search results for "ton". Showing 5 related matches."`;
exports[`searchPatternFlyDocsTool, callback should parse parameters, with partial componentName: search 1`] = `"# Search results for PatternFly version "v6" and "ton". Showing 5 related matches."`;

exports[`searchPatternFlyDocsTool, callback should parse parameters, with trimmed componentName: search 1`] = `"# Search results for " Button ". Showing 2 exact matches."`;
exports[`searchPatternFlyDocsTool, callback should parse parameters, with trimmed componentName: search 1`] = `"# Search results for PatternFly version "v6" and " Button ". Showing 2 exact matches."`;

exports[`searchPatternFlyDocsTool, callback should parse parameters, with upper case componentName: search 1`] = `"# Search results for "BUTTON". Showing 2 exact matches."`;
exports[`searchPatternFlyDocsTool, callback should parse parameters, with upper case componentName: search 1`] = `"# Search results for PatternFly version "v6" and "BUTTON". Showing 2 exact matches."`;
18 changes: 18 additions & 0 deletions src/__tests__/tool.patternFlyDocs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,22 @@ describe('usePatternFlyDocsTool, callback', () => {
await expect(callback({ urlList: ['https://www.patternfly.org//missing.md'] })).rejects.toThrow(McpError);
await expect(callback({ urlList: ['https://www.patternfly.org/missing.md'] })).rejects.toThrow('Failed to fetch documentation');
});

it('should have a specific markdown format', async () => {
mockProcessDocs.mockResolvedValue([
{
path: 'components/loremButton.md',
content: 'lorem documentation content'
},
{
path: 'components/ipsumButton.md',
content: 'ipsum documentation content'
}
] as any);

const [_name, _schema, callback] = usePatternFlyDocsTool();
const result = await callback({ name: 'button' });

expect(result.content).toMatchSnapshot('Button');
});
});
7 changes: 7 additions & 0 deletions src/__tests__/tool.searchPatternFlyDocs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,11 @@ describe('searchPatternFlyDocsTool, callback', () => {
await expect(callback({ searchQuery })).rejects.toThrow(McpError);
await expect(callback({ searchQuery })).rejects.toThrow(error);
});

it('should have a specific markdown format', async () => {
const [_name, _schema, callback] = searchPatternFlyDocsTool();
const result = await callback({ searchQuery: 'button' });

expect(result.content).toMatchSnapshot('tooltip');
});
});
104 changes: 83 additions & 21 deletions src/patternFly.getResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,45 @@ import { INDEX_BLOCKLIST_WORDS, INDEX_NOISE_WORDS } from './docs.filterWords';
*/
type PatternFlyComponentSchema = Awaited<ReturnType<typeof getPatternFlyComponentSchema>>;

/**
* Map of PatternFly component names to their versioned metadata.
*
* Properties for each component include:
* - `isSchemasAvailable`: Boolean indicating whether schemas are available for the component.
* - `displayName`: String that defines the human-readable name of the component.
*/
type PatternFlyMcpComponentNamesByVersion = {
[name: string]: {
isSchemasAvailable: boolean;
displayName: string;
}
};

/**
* Documentation structure for PatternFly MCP component names. Intended to help
* merge with existing documents catalog.
*
* Unique Properties:
* - `path`: Non-existent, omitted from the base catalog documentation type.
* - `isSchemasAvailable`: A boolean flag that indicates whether schemas are available for the component.
*/
type PatternFlyMcpComponentNamesDoc = Omit<PatternFlyMcpDocsCatalogDoc, 'path'> & { path?: never; isSchemasAvailable: boolean };

/**
* Component names metadata.
*
* @interface PatternFlyMcpComponentNames
*
* @property componentNamesIndex - Component names index.
* @property componentNamesIndexMap - Component names index map.
* @property byVersion - Component names by version map.
* @property byDocs - Component names by docs map.
*/
interface PatternFlyMcpComponentNames {
componentNamesIndex: string[];
componentNamesIndexMap: Map<string, string>;
byVersion: Map<string, PatternFlyMcpComponentNamesByVersion>;
byDocs: Map<string, PatternFlyMcpComponentNamesDoc[]>;
}

/**
Expand Down Expand Up @@ -230,14 +258,27 @@ const getPatternFlyComponentNames = async (contextPathOverride?: string): Promis
const latestNamesIndex = [...Array.from(new Set([...pfComponentNames, 'Table'])).map(name => name.toLowerCase()).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))];
const latestNamesIndexMap = new Map(Array.from(new Set([...pfComponentNames, 'Table'])).map(name => [name.toLowerCase(), name]));
const latestByNameMap = new Map<string, { isSchemasAvailable: boolean, displayName: string }>();
const latestByDocsFormat = new Map<string, PatternFlyMcpComponentNamesDoc[]>();
const createDocEntry = (name: string, isSchemasAvailable: boolean) => ({
displayName: name,
description: `PatternFly React component: ${name}`,
pathSlug: `schemas-${name}`.toLowerCase(),
category: 'react',
section: 'components',
source: 'schemas',
version: latestSchemasVersion,
isSchemasAvailable
});

pfComponentNames.forEach(name => {
latestByNameMap.set(name.toLowerCase(), { isSchemasAvailable: true, displayName: name });
latestByDocsFormat.set(name.toLowerCase(), [createDocEntry(name, true)]);
});

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

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

byVersion.set(
Expand All @@ -248,7 +289,8 @@ const getPatternFlyComponentNames = async (contextPathOverride?: string): Promis
return {
componentNamesIndex: latestNamesIndex,
componentNamesIndexMap: latestNamesIndexMap,
byVersion
byVersion,
byDocs: latestByDocsFormat
};
};

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

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

Object.entries(originalDocs.docs).forEach(([docsName, entries]) => {
const name = docsName.toLowerCase();
const resource: PatternFlyMcpResourceMetadata = {
name,
isSchemasAvailable: undefined,
uri: undefined,
uriSchemas: undefined,
entries: [],
versions: {}
};
const catalog = [...Object.entries(originalDocs.docs), ...Array.from(componentNamesByDocs)];

catalog.forEach(([unifiedName, entries]) => {
const name = unifiedName.toLowerCase();

if (!resources.has(name)) {
// isSchemasAvailable, uri, and uriSchemas are contextually populated from the patternFly.search
// functions, search and filter. They are intended to be undefined here.
resources.set(name, {
name,
isSchemasAvailable: undefined,
uri: undefined,
uriSchemas: undefined,
entries: [],
versions: {}
});
}

const resource = resources.get(name) as PatternFlyMcpResourceMetadata;

entries.forEach(entry => {
const version = (entry.version || 'unknown').toLowerCase();
const isSchemasAvailable = versionContext.latestSchemasVersion === version && byVersionSchemaNames.get(version)?.[name]?.isSchemasAvailable;
const isSchemasAvailable = versionContext.latestSchemasVersion === version && componentNamesByVersion.get(version)?.[name]?.isSchemasAvailable;
const path = entry.path;
const uri = `patternfly://docs/${version}/${name}`;
const uri = `patternfly://docs/${name}?version=${version}`;

pathIndex.add(path);
if (path) {
pathIndex.add(path);
}

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

const displayCategory = setCategoryDisplayLabel(entry);
const displayName = entry.displayName || name;
const displayCategory = setCategoryDisplayLabel(entry as PatternFlyMcpDocsCatalogDoc);
let uriSchemas;

if (isSchemasAvailable) {
uriSchemas = `patternfly://schemas/${version}/${name}`;
uriSchemas = `patternfly://schemas/${name}?version=${version}`;

resource.versions[version].uriSchemas = uriSchemas;
}

const extendedEntry = { ...entry, name: docsName, displayCategory, uri, uriSchemas };
const extendedEntry = {
...entry,
name,
displayName,
displayCategory,
uri,
uriSchemas
} as (PatternFlyMcpDocsCatalogDoc & PatternFlyMcpDocsMeta);

byPath[path] = extendedEntry;
if (path) {
byPath[path] = extendedEntry;
}

byUri[uri] ??= [];
byUri[uri]?.push(extendedEntry);
Expand Down Expand Up @@ -434,8 +497,6 @@ const getPatternFlyMcpResources = async (contextPathOverride?: string): Promise<
resource.entries.push(extendedEntry);
resource.versions[version].entries.push(extendedEntry);
});

resources.set(name, resource);
});

Object.entries(byVersion).forEach(([_version, entries]) => {
Expand Down Expand Up @@ -504,6 +565,7 @@ export {
setCategoryDisplayLabel,
type PatternFlyMcpComponentNames,
type PatternFlyMcpComponentNamesByVersion,
type PatternFlyMcpComponentNamesDoc,
type PatternFlyComponentSchema,
type PatternFlyMcpAvailableResources,
type PatternFlyMcpResourceMetadata,
Expand Down
8 changes: 8 additions & 0 deletions src/patternFly.search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ interface SearchPatternFlyOptions {
/**
* Apply sequenced priority filters for predictable filtering, filter PatternFly data.
*
* @note It is tempting to apply a default version to this function. Do not. Architecture
* dictates that this function remains purely data-driven, apply default versions in the caller.
* See both MCP resources and tools for examples.
*
* @note This is a predictable filter, not a search. Use searchPatternFly for fuzzy search.
* - Has case-insensitive filtering for all fields
* - Exact "version" filtering only
Expand Down Expand Up @@ -200,6 +204,10 @@ filterPatternFly.memo = memo(filterPatternFly, DEFAULT_OPTIONS.resourceMemoOptio
/**
* Search for PatternFly component documentation URLs using fuzzy search.
*
* @note It is tempting to apply a default version to this function. Do not. Architecture
* dictates that this function remains purely data-driven, apply default versions in the caller.
* See both MCP resources and tools for examples.
*
* @note Uses `filterPatternFly` for additional filtering.
*
* @param searchQuery - Search query. Values are coerced to string for fuzzy search.
Expand Down
Loading
Loading