diff --git a/src/__tests__/__snapshots__/docs.chart.test.ts.snap b/src/__tests__/__snapshots__/docs.chart.test.ts.snap deleted file mode 100644 index 1e40b3f5..00000000 --- a/src/__tests__/__snapshots__/docs.chart.test.ts.snap +++ /dev/null @@ -1,36 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`docsChart should return specific properties 1`] = ` -{ - "CHART_DOCS": [ - "[@patternfly/Charts - Colors for Charts - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-charts/src/victory/components/ChartTheme/examples/ChartTheme.md)", - "[@patternfly/Charts - Area Chart - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/charts/area-chart/area-chart.md)", - "[@patternfly/Charts - Area Chart - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-charts/src/victory/components/ChartArea/examples/ChartArea.md)", - "[@patternfly/Charts - Bar Chart - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/charts/bar-chart/bar-chart.md)", - "[@patternfly/Charts - Bar Chart - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-charts/src/victory/components/ChartBar/examples/ChartBar.md)", - "[@patternfly/Charts - Box Plot Chart - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-charts/src/victory/components/ChartBoxPlot/examples/ChartBoxPlot.md)", - "[@patternfly/Charts - Bullet Chart - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/charts/bullet-chart/bullet-chart.md)", - "[@patternfly/Charts - Bullet Chart - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-charts/src/victory/components/ChartBullet/examples/ChartBullet.md)", - "[@patternfly/Charts - Donut Chart - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/charts/donut-chart/donut-chart.md)", - "[@patternfly/Charts - Donut Chart - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-charts/src/victory/components/ChartDonut/examples/ChartDonut.md)", - "[@patternfly/Charts - Donut Utilization Chart - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/charts/donut-utilization-chart/donut-utilization-chart.md)", - "[@patternfly/Charts - Donut Utilization Chart - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-charts/src/victory/components/ChartDonutUtilization/examples/ChartDonutUtilization.md)", - "[@patternfly/Charts - Line Chart - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/charts/line-chart/line-chart.md)", - "[@patternfly/Charts - Line Chart - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-charts/src/victory/components/ChartLine/examples/ChartLine.md)", - "[@patternfly/Charts - Pie Chart - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/charts/pie-chart/pie-chart.md)", - "[@patternfly/Charts - Pie Chart - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-charts/src/victory/components/ChartPie/examples/ChartPie.md)", - "[@patternfly/Charts - Scatter Chart - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/charts/scatter-chart/scatter-chart.md)", - "[@patternfly/Charts - Scatter Chart - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-charts/src/victory/components/ChartScatter/examples/ChartScatter.md)", - "[@patternfly/Charts - Sparkline Chart - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/charts/sparkline-chart/sparkline-chart.md)", - "[@patternfly/Charts - Sparkline Chart - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-charts/src/victory/components/Sparkline/examples/sparkline.md)", - "[@patternfly/Charts - Stack Chart - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/charts/stacked-chart/stacked-chart.md)", - "[@patternfly/Charts - Stack Chart - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-charts/src/victory/components/ChartStack/examples/ChartStack.md)", - "[@patternfly/Charts - Threshold Chart - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/charts/threshold-chart/threshold-chart.md)", - "[@patternfly/Charts - Threshold Chart - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-charts/src/victory/components/ChartThreshold/examples/ChartThreshold.md)", - "[@patternfly/Charts - Legend - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/charts/legend-chart/legend-chart.md)", - "[@patternfly/Charts - Legend - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-charts/src/victory/components/ChartLegend/examples/ChartLegend.md)", - "[@patternfly/Charts - Tooltip - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/charts/tooltip-chart/tooltip-chart.md)", - "[@patternfly/Charts - Tooltip - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-charts/src/victory/components/ChartTooltip/examples/ChartTooltip.md)", - ], -} -`; diff --git a/src/__tests__/__snapshots__/docs.component.test.ts.snap b/src/__tests__/__snapshots__/docs.component.test.ts.snap deleted file mode 100644 index c4207397..00000000 --- a/src/__tests__/__snapshots__/docs.component.test.ts.snap +++ /dev/null @@ -1,208 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`docsComponent should return specific properties 1`] = ` -{ - "COMPONENT_DOCS": [ - "[@patternfly/AboutModal - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/about-modal/about-modal.md)", - "[@patternfly/AboutModal - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/about-modal/about-modal.md)", - "[@patternfly/AboutModal - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/AboutModal/examples/AboutModal.md)", - "[@patternfly/Accordion - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/accordion/accordion.md)", - "[@patternfly/Accordion - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/accordion/accordion.md)", - "[@patternfly/Accordion - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Accordion/examples/Accordion.md)", - "[@patternfly/ActionList - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/action-list/action-list.md)", - "[@patternfly/ActionList - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/action-list/action-list.md)", - "[@patternfly/ActionList - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/ActionList/examples/ActionList.md)", - "[@patternfly/Alert - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/alert/alert.md)", - "[@patternfly/Alert - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/alert/alert.md)", - "[@patternfly/Alert - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Alert/examples/Alert.md)", - "[@patternfly/ApplicationLauncher - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/application-launcher/application-launcher.md)", - "[@patternfly/ApplicationLauncher - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/application-launcher/application-launcher.md)", - "[@patternfly/Avatar - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/avatar/avatar.md)", - "[@patternfly/Avatar - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/avatar/avatar.md)", - "[@patternfly/Avatar - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Avatar/examples/Avatar.md)", - "[@patternfly/BackToTop - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/back-to-top/back-to-top.md)", - "[@patternfly/BackToTop - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/BackToTop/examples/BackToTop.md)", - "[@patternfly/Backdrop - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/backdrop/backdrop.md)", - "[@patternfly/Backdrop - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/backdrop/backdrop.md)", - "[@patternfly/Backdrop - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Backdrop/examples/Backdrop.md)", - "[@patternfly/BackgroundImage - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/background-image/background-image.md)", - "[@patternfly/BackgroundImage - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/background-image/background-image.md)", - "[@patternfly/BackgroundImage - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/BackgroundImage/examples/BackgroundImage.md)", - "[@patternfly/Badge - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/badge/badge.md)", - "[@patternfly/Badge - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/badge/badge.md)", - "[@patternfly/Badge - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Badge/examples/Badge.md)", - "[@patternfly/Banner - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/banner/banner.md)", - "[@patternfly/Banner - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/banner/banner.md)", - "[@patternfly/Banner - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Banner/examples/Banner.md)", - "[@patternfly/Brand - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/brand/brand.md)", - "[@patternfly/Brand - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/brand/brand.md)", - "[@patternfly/Brand - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Brand/examples/Brand.md)", - "[@patternfly/Breadcrumb - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/breadcrumb/breadcrumb.md)", - "[@patternfly/Breadcrumb - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/breadcrumb/breadcrumb.md)", - "[@patternfly/Breadcrumb - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Breadcrumb/examples/Breadcrumb.md)", - "[@patternfly/Button - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/button/button.md)", - "[@patternfly/Button - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/button/button.md)", - "[@patternfly/Button - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Button/examples/Button.md)", - "[@patternfly/CalendarMonth - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/calendar-month/calendar-month.md)", - "[@patternfly/CalendarMonth - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/calendar-month/calendar-month.md)", - "[@patternfly/CalendarMonth - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/CalendarMonth/examples/CalendarMonth.md)", - "[@patternfly/Card - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/card/card.md)", - "[@patternfly/Card - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/card/card.md)", - "[@patternfly/Card - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Card/examples/Card.md)", - "[@patternfly/Checkbox - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/checkbox/checkbox.md)", - "[@patternfly/Checkbox - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/checkbox/checkbox.md)", - "[@patternfly/Checkbox - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Checkbox/examples/Checkbox.md)", - "[@patternfly/ChipDeprecated - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/chip/chip.md)", - "[@patternfly/ChipDeprecated - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/chip/chip.md)", - "[@patternfly/ClipboardCopy - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/clipboard-copy/clipboard-copy.md)", - "[@patternfly/ClipboardCopy - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/clipboard-copy/clipboard-copy.md)", - "[@patternfly/ClipboardCopy - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/ClipboardCopy/examples/ClipboardCopy.md)", - "[@patternfly/CodeBlock - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/code-block/code-block.md)", - "[@patternfly/CodeBlock - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/CodeBlock/examples/CodeBlock.md)", - "[@patternfly/CodeEditor - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/code-editor/code-editor.md)", - "[@patternfly/CodeEditor - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/code-editor/code-editor.md)", - "[@patternfly/Content - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/content/content.md)", - "[@patternfly/Content - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Content/examples/Content.md)", - "[@patternfly/DataList - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/data-list/data-list.md)", - "[@patternfly/DataList - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/DataList/examples/DataList.md)", - "[@patternfly/DatePicker - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/date-picker/date-picker.md)", - "[@patternfly/DatePicker - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/DatePicker/examples/DatePicker.md)", - "[@patternfly/DescriptionList - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/description-list/description-list.md)", - "[@patternfly/DescriptionList - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/DescriptionList/examples/DescriptionList.md)", - "[@patternfly/Divider - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/divider/divider.md)", - "[@patternfly/Divider - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Divider/examples/Divider.md)", - "[@patternfly/DragAndDrop - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/drag-and-drop/drag.md)", - "[@patternfly/Drawer - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/drawer/drawer.md)", - "[@patternfly/Drawer - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Drawer/examples/Drawer.md)", - "[@patternfly/Dropdown - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/dropdown/dropdown.md)", - "[@patternfly/Dropdown - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Dropdown/examples/Dropdown.md)", - "[@patternfly/DualListSelector - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/dual-list-selector/dual-list-selector.md)", - "[@patternfly/DualListSelector - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/DualListSelector/examples/DualListSelector.md)", - "[@patternfly/EmptyState - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/empty-state/empty-state.md)", - "[@patternfly/EmptyState - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/EmptyState/examples/EmptyState.md)", - "[@patternfly/ExpandableSection - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/expandable-section/expandable-section.md)", - "[@patternfly/ExpandableSection - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/expandable-section/expandable-section.md)", - "[@patternfly/ExpandableSection - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/ExpandableSection/examples/ExpandableSection.md)", - "[@patternfly/FileUpload - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/file-upload/file-upload.md)", - "[@patternfly/FileUpload - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/FileUpload/examples/FileUpload.md)", - "[@patternfly/Form - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/form/forms.md)", - "[@patternfly/Form - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Form/examples/Form.md)", - "[@patternfly/FormControl - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/form-control/form-control.md)", - "[@patternfly/FormSelect - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/form-select/form-select.md)", - "[@patternfly/FormSelect - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/FormSelect/examples/FormSelect.md)", - "[@patternfly/HelperText - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/helper-text/helper-text.md)", - "[@patternfly/HelperText - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/helper-text/helper-text.md)", - "[@patternfly/HelperText - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/HelperText/examples/HelperText.md)", - "[@patternfly/Hint - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/hint/hint.md)", - "[@patternfly/Hint - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Hint/examples/Hint.md)", - "[@patternfly/Icon - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Icon/examples/Icon.md)", - "[@patternfly/InlineEdit - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/inline-edit/inline-edit.md)", - "[@patternfly/InputGroup - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/input-group/input-group.md)", - "[@patternfly/InputGroup - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/InputGroup/examples/InputGroup.md)", - "[@patternfly/JumpLinks - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/jump-link/jump-link.md)", - "[@patternfly/JumpLinks - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/jump-links/jump-links.md)", - "[@patternfly/JumpLinks - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/JumpLinks/examples/JumpLinks.md)", - "[@patternfly/Label - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/label/label.md)", - "[@patternfly/Label - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/label/label.md)", - "[@patternfly/Label - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Label/examples/Label.md)", - "[@patternfly/List - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/list/list.md)", - "[@patternfly/List - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/List/examples/List.md)", - "[@patternfly/LoginPage - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/login-page/login-page.md)", - "[@patternfly/LoginPage - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/LoginPage/examples/LoginPage.md)", - "[@patternfly/Masthead - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/masthead/masthead.md)", - "[@patternfly/Masthead - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Masthead/examples/Masthead.md)", - "[@patternfly/Menu - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/menu/menu.md)", - "[@patternfly/Menu - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/menu/menu.md)", - "[@patternfly/Menu - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Menu/examples/Menu.md)", - "[@patternfly/MenuToggle - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/menu-toggle/menu-toggle.md)", - "[@patternfly/MenuToggle - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/menu-toggle/menu-toggle.md)", - "[@patternfly/MenuToggle - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/MenuToggle/examples/MenuToggle.md)", - "[@patternfly/Modal - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/modal/modal.md)", - "[@patternfly/Modal - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/modal/modal.md)", - "[@patternfly/Modal - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Modal/examples/Modal.md)", - "[@patternfly/Navigation - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/navigation/navigation.md)", - "[@patternfly/Navigation - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/navigation/navigation.md)", - "[@patternfly/NotificationBadge - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/notification-badge/notification-badge.md)", - "[@patternfly/NotificationBadge - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/NotificationBadge/examples/NotificationBadge.md)", - "[@patternfly/NotificationDrawer - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/notification-drawer/notification-drawer.md)", - "[@patternfly/NotificationDrawer - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/NotificationDrawer/examples/NotificationDrawer.md)", - "[@patternfly/NumberInput - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/number%20input/number-input.md)", - "[@patternfly/NumberInput - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/NumberInput/examples/NumberInput.md)", - "[@patternfly/OverflowMenu - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/overflow-menu/overflow-menu.md)", - "[@patternfly/OverflowMenu - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/OverflowMenu/examples/OverflowMenu.md)", - "[@patternfly/Page - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/page/page.md)", - "[@patternfly/Page - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/page/page.md)", - "[@patternfly/Page - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Page/examples/Page.md)", - "[@patternfly/Pagination - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/pagination/pagination.md)", - "[@patternfly/Pagination - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Pagination/examples/Pagination.md)", - "[@patternfly/Panel - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/panel/panel.md)", - "[@patternfly/Panel - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Panel/examples/Panel.md)", - "[@patternfly/Popover - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/popover/popover.md)", - "[@patternfly/Popover - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Popover/examples/Popover.md)", - "[@patternfly/Progress - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/progress/progress.md)", - "[@patternfly/Progress - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/progress/progress.md)", - "[@patternfly/Progress - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Progress/examples/Progress.md)", - "[@patternfly/ProgressStepper - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/progress-stepper/progress-stepper.md)", - "[@patternfly/ProgressStepper - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/progress-stepper/progress-stepper.md)", - "[@patternfly/ProgressStepper - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/ProgressStepper/examples/ProgressStepper.md)", - "[@patternfly/Radio - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/radio/radio.md)", - "[@patternfly/Radio - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/radio/radio.md)", - "[@patternfly/Radio - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Radio/examples/Radio.md)", - "[@patternfly/SearchInput - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/search-input/search-input.md)", - "[@patternfly/SearchInput - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/SearchInput/examples/SearchInput.md)", - "[@patternfly/Select - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/select/select.md)", - "[@patternfly/Select - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Select/examples/Select.md)", - "[@patternfly/Sidebar - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/sidebar/sidebar.md)", - "[@patternfly/Sidebar - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/sidebar/sidebar.md)", - "[@patternfly/Sidebar - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Sidebar/examples/Sidebar.md)", - "[@patternfly/SimpleList - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/simple-list/simple-list.md)", - "[@patternfly/SimpleList - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/SimpleList/examples/SimpleList.md)", - "[@patternfly/Skeleton - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/skeleton/skeleton.md)", - "[@patternfly/Skeleton - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/skeleton/skeleton.md)", - "[@patternfly/Skeleton - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Skeleton/examples/Skeleton.md)", - "[@patternfly/SkipToContent - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/skip-to-content/skip-to-content.md)", - "[@patternfly/SkipToContent - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/skip-to-content/skip-to-content.md)", - "[@patternfly/SkipToContent - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/SkipToContent/examples/SkipToContent.md)", - "[@patternfly/Slider - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/slider/slider.md)", - "[@patternfly/Slider - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Slider/examples/Slider.md)", - "[@patternfly/Spinner - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/spinner/spinner.md)", - "[@patternfly/Spinner - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Spinner/examples/Spinner.md)", - "[@patternfly/Switch - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/switch/switch.md)", - "[@patternfly/Switch - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/switch/switch.md)", - "[@patternfly/Switch - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Switch/examples/Switch.md)", - "[@patternfly/Table - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/table/table.md)", - "[@patternfly/Table - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-table/src/components/Table/examples/Table.md)", - "[@patternfly/Tabs - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/tabs/tabs.md)", - "[@patternfly/Tabs - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/tabs/tabs.md)", - "[@patternfly/Tabs - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Tabs/examples/Tabs.md)", - "[@patternfly/TextArea - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/text-area/text-area.md)", - "[@patternfly/TextArea - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/TextArea/examples/TextArea.md)", - "[@patternfly/TextInput - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/text-input/text-input.md)", - "[@patternfly/TextInput - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/TextInput/examples/TextInput.md)", - "[@patternfly/TextInputGroup - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/text-input-group/text-input-group.md)", - "[@patternfly/TextInputGroup - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/TextInputGroup/examples/TextInputGroup.md)", - "[@patternfly/TileDeprecated - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/tile/tile.md)", - "[@patternfly/TimePicker - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/time%20picker/time-picker.md)", - "[@patternfly/TimePicker - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/TimePicker/examples/TimePicker.md)", - "[@patternfly/Timestamp - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/timestamp/timestamp.md)", - "[@patternfly/Timestamp - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Timestamp/examples/Timestamp.md)", - "[@patternfly/Title - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/title/title.md)", - "[@patternfly/Title - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/title/title.md)", - "[@patternfly/Title - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Title/examples/Title.md)", - "[@patternfly/ToggleGroup - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/toggle-group/toggle-group.md)", - "[@patternfly/ToggleGroup - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/ToggleGroup/examples/ToggleGroup.md)", - "[@patternfly/Toolbar - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/toolbar/toolbar.md)", - "[@patternfly/Toolbar - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Toolbar/examples/Toolbar.md)", - "[@patternfly/Tooltip - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/tooltip/tooltip.md)", - "[@patternfly/Tooltip - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/tooltip/tooltip.md)", - "[@patternfly/Tooltip - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Tooltip/examples/Tooltip.md)", - "[@patternfly/TreeView - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/tree-view/tree-view.md)", - "[@patternfly/TreeView - Accessibility](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility/tree-view/tree-view.md)", - "[@patternfly/TreeView - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/TreeView/examples/TreeView.md)", - "[@patternfly/Truncate - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/truncate/truncate.md)", - "[@patternfly/Truncate - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Truncate/examples/Truncate.md)", - "[@patternfly/Wizard - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components/wizard/wizard.md)", - "[@patternfly/Wizard - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components/Wizard/examples/Wizard.md)", - ], -} -`; diff --git a/src/__tests__/__snapshots__/docs.layout.test.ts.snap b/src/__tests__/__snapshots__/docs.layout.test.ts.snap deleted file mode 100644 index 5fa8be77..00000000 --- a/src/__tests__/__snapshots__/docs.layout.test.ts.snap +++ /dev/null @@ -1,22 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`docsLayout should return specific properties 1`] = ` -{ - "LAYOUT_DOCS": [ - "[@patternfly/Bullseye - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/layouts/bullseye.md)", - "[@patternfly/Bullseye - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/layouts/Bullseye/examples/Bullseye.md)", - "[@patternfly/Flex - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/layouts/flex.md)", - "[@patternfly/Flex - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/layouts/Flex/examples/Flex.md)", - "[@patternfly/Gallery - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/layouts/gallery.md)", - "[@patternfly/Gallery - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/layouts/Gallery/examples/Gallery.md)", - "[@patternfly/Grid - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/layouts/grid.md)", - "[@patternfly/Grid - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/layouts/Grid/examples/Grid.md)", - "[@patternfly/Level - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/layouts/level.md)", - "[@patternfly/Level - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/layouts/Level/examples/Level.md)", - "[@patternfly/Split - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/layouts/split.md)", - "[@patternfly/Split - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/layouts/Split/examples/Split.md)", - "[@patternfly/Stack - Design Guidelines](https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/layouts/stack.md)", - "[@patternfly/Stack - Examples](https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/layouts/Stack/examples/Stack.md)", - ], -} -`; diff --git a/src/__tests__/__snapshots__/docs.local.test.ts.snap b/src/__tests__/__snapshots__/docs.local.test.ts.snap deleted file mode 100644 index d0d320cc..00000000 --- a/src/__tests__/__snapshots__/docs.local.test.ts.snap +++ /dev/null @@ -1,33 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`docsLocal should return specific properties 1`] = ` -{ - "getLocalDocs": [Function], -} -`; - -exports[`getLocalDocs should return local references when called, default 1`] = ` -[ - "[@patternfly/react-charts](/documentation/charts/README.md)", - "[@patternfly/react-chatbot](/documentation/chatbot/README.md)", - "[@patternfly/react-component-groups](/documentation/component-groups/README.md)", - "[@patternfly/react-components](/documentation/components/README.md)", - "[@patternfly/react-guidelines](/documentation/guidelines/README.md)", - "[@patternfly/react-resources](/documentation/resources/README.md)", - "[@patternfly/react-setup](/documentation/setup/README.md)", - "[@patternfly/react-troubleshooting](/documentation/troubleshooting/README.md)", -] -`; - -exports[`getLocalDocs should return local references when called, with custom docsPath 1`] = ` -[ - "[@patternfly/react-charts](custom/docs/path/charts/README.md)", - "[@patternfly/react-chatbot](custom/docs/path/chatbot/README.md)", - "[@patternfly/react-component-groups](custom/docs/path/component-groups/README.md)", - "[@patternfly/react-components](custom/docs/path/components/README.md)", - "[@patternfly/react-guidelines](custom/docs/path/guidelines/README.md)", - "[@patternfly/react-resources](custom/docs/path/resources/README.md)", - "[@patternfly/react-setup](custom/docs/path/setup/README.md)", - "[@patternfly/react-troubleshooting](custom/docs/path/troubleshooting/README.md)", -] -`; diff --git a/src/__tests__/__snapshots__/options.defaults.test.ts.snap b/src/__tests__/__snapshots__/options.defaults.test.ts.snap index 2163e1be..716948b2 100644 --- a/src/__tests__/__snapshots__/options.defaults.test.ts.snap +++ b/src/__tests__/__snapshots__/options.defaults.test.ts.snap @@ -20,8 +20,24 @@ exports[`options defaults should return specific properties: defaults 1`] = ` "stderr": false, "transport": "stdio", }, - "maxDocsToLoad": 500, - "maxSearchLength": 256, + "minMax": { + "docsToLoad": { + "max": 15, + "min": 0, + }, + "inputStrings": { + "max": 256, + "min": 1, + }, + "toolSearches": { + "max": 10, + "min": 0, + }, + "urlString": { + "max": 1500, + "min": 11, + }, + }, "mode": "programmatic", "modeOptions": { "cli": {}, @@ -52,24 +68,28 @@ exports[`options defaults should return specific properties: defaults 1`] = ` "@patternfly/patternfly", ], }, + "urlWhitelist": [ + "https://patternfly.org", + "https://github.com/patternfly", + "https://raw.githubusercontent.com/patternfly", + ], + "urlWhitelistProtocols": [ + "http", + "https", + ], }, - "pfExternal": "https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content", - "pfExternalAccessibility": "https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility", - "pfExternalChartsDesign": "https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/charts", - "pfExternalDesignComponents": "https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/components", - "pfExternalDesignLayouts": "https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/layouts", - "pfExternalExamplesCharts": "https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-charts/src/victory/components", - "pfExternalExamplesComponents": "https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/components", - "pfExternalExamplesLayouts": "https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-core/src/layouts", - "pfExternalExamplesTable": "https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/v6.4.0/packages/react-table/src/components", "pluginHost": { "gracePeriodMs": 2000, "invokeTimeoutMs": 10000, "loadTimeoutMs": 5000, }, "pluginIsolation": "strict", - "recommendedMaxDocsToLoad": 15, "repoName": "patternfly-mcp", + "repoResources": { + "bugs": "https://github.com/patternfly/patternfly-mcp/issues", + "git": "git+https://github.com/patternfly/patternfly-mcp.git", + "homepage": "https://github.com/patternfly/patternfly-mcp#readme", + }, "resourceMemoOptions": { "default": { "cacheLimit": 3, diff --git a/src/__tests__/__snapshots__/resource.patternFlyComponentsIndex.test.ts.snap b/src/__tests__/__snapshots__/resource.patternFlyComponentsIndex.test.ts.snap new file mode 100644 index 00000000..bff6b2df --- /dev/null +++ b/src/__tests__/__snapshots__/resource.patternFlyComponentsIndex.test.ts.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`patternFlyComponentsIndexResource should have a consistent return structure: structure 1`] = ` +{ + "config": true, + "handler": [Function], + "name": "patternfly-components-index", + "uri": "patternfly://components/index", +} +`; diff --git a/src/__tests__/__snapshots__/server.test.ts.snap b/src/__tests__/__snapshots__/server.test.ts.snap index d9331350..4d3f1325 100644 --- a/src/__tests__/__snapshots__/server.test.ts.snap +++ b/src/__tests__/__snapshots__/server.test.ts.snap @@ -18,6 +18,9 @@ exports[`runServer should allow server to be stopped, http stop server: diagnost [ "Registered resource: patternfly-context", ], + [ + "Registered resource: patternfly-components-index", + ], [ "Registered resource: patternfly-docs-index", ], @@ -71,6 +74,9 @@ exports[`runServer should allow server to be stopped, stdio stop server: diagnos [ "Registered resource: patternfly-context", ], + [ + "Registered resource: patternfly-components-index", + ], [ "Registered resource: patternfly-docs-index", ], @@ -124,6 +130,9 @@ exports[`runServer should attempt to run server, create transport, connect, and [ "Registered resource: patternfly-context", ], + [ + "Registered resource: patternfly-components-index", + ], [ "Registered resource: patternfly-docs-index", ], @@ -183,6 +192,9 @@ exports[`runServer should attempt to run server, disable SIGINT handler: diagnos [ "Registered resource: patternfly-context", ], + [ + "Registered resource: patternfly-components-index", + ], [ "Registered resource: patternfly-docs-index", ], @@ -237,6 +249,9 @@ exports[`runServer should attempt to run server, enable SIGINT handler explicitl [ "Registered resource: patternfly-context", ], + [ + "Registered resource: patternfly-components-index", + ], [ "Registered resource: patternfly-docs-index", ], @@ -296,6 +311,9 @@ exports[`runServer should attempt to run server, register a tool: diagnostics 1` [ "Registered resource: patternfly-context", ], + [ + "Registered resource: patternfly-components-index", + ], [ "Registered resource: patternfly-docs-index", ], @@ -366,6 +384,9 @@ exports[`runServer should attempt to run server, register multiple tools: diagno [ "Registered resource: patternfly-context", ], + [ + "Registered resource: patternfly-components-index", + ], [ "Registered resource: patternfly-docs-index", ], @@ -446,6 +467,9 @@ exports[`runServer should attempt to run server, use custom options: diagnostics [ "Registered resource: patternfly-context", ], + [ + "Registered resource: patternfly-components-index", + ], [ "Registered resource: patternfly-docs-index", ], @@ -505,6 +529,9 @@ exports[`runServer should attempt to run server, use default tools, http: diagno [ "Registered resource: patternfly-context", ], + [ + "Registered resource: patternfly-components-index", + ], [ "Registered resource: patternfly-docs-index", ], @@ -577,6 +604,9 @@ exports[`runServer should attempt to run server, use default tools, stdio: diagn [ "Registered resource: patternfly-context", ], + [ + "Registered resource: patternfly-components-index", + ], [ "Registered resource: patternfly-docs-index", ], diff --git a/src/__tests__/__snapshots__/tool.patternFlyDocs.test.ts.snap b/src/__tests__/__snapshots__/tool.patternFlyDocs.test.ts.snap index 7a83354a..bcc0f173 100644 --- a/src/__tests__/__snapshots__/tool.patternFlyDocs.test.ts.snap +++ b/src/__tests__/__snapshots__/tool.patternFlyDocs.test.ts.snap @@ -7,3 +7,11 @@ exports[`usePatternFlyDocsTool should have a consistent return structure: struct "schema": true, } `; + +exports[`usePatternFlyDocsTool, callback should attempt to parse parameters, multiple files, mock paths 1`] = `"# Content for components/button.md"`; + +exports[`usePatternFlyDocsTool, callback should attempt to parse parameters, single file, mock path 1`] = `"# Content for components/button.md"`; + +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`] = `"# Documentation for React Chatbot (v6) [AI Guidance]"`; diff --git a/src/__tests__/__snapshots__/tool.searchPatternFlyDocs.test.ts.snap b/src/__tests__/__snapshots__/tool.searchPatternFlyDocs.test.ts.snap index e64429e5..9b865e6b 100644 --- a/src/__tests__/__snapshots__/tool.searchPatternFlyDocs.test.ts.snap +++ b/src/__tests__/__snapshots__/tool.searchPatternFlyDocs.test.ts.snap @@ -8,22 +8,22 @@ exports[`searchPatternFlyDocsTool should have a consistent return structure: str } `; -exports[`searchPatternFlyDocsTool, callback should parse parameters, default: search 1`] = `"# Search results for "Button", 1 matches found:"`; +exports[`searchPatternFlyDocsTool, callback should parse parameters, default: search 1`] = `"# Search results for "Button". Showing 1 exact match."`; -exports[`searchPatternFlyDocsTool, callback should parse parameters, with "*" searchQuery all: search 1`] = `"# Search results for "all components", 8 matches found:"`; +exports[`searchPatternFlyDocsTool, callback should parse parameters, with "*" searchQuery all: search 1`] = `"# Search results for "all" resources. Only showing the first 10 results. There are 546 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 "all components", 8 matches found:"`; +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 546 potential match variations. Try searching with a more specific query."`; -exports[`searchPatternFlyDocsTool, callback should parse parameters, with empty searchQuery all: search 1`] = `"# Search results for "all components", 8 matches found:"`; +exports[`searchPatternFlyDocsTool, callback should parse parameters, with explicit valid version: search 1`] = `"# Search results for "Button". Showing 1 exact match."`; -exports[`searchPatternFlyDocsTool, callback should parse parameters, with lower case componentName: search 1`] = `"# Search results for "button", 1 matches found:"`; +exports[`searchPatternFlyDocsTool, callback should parse parameters, with lower case componentName: search 1`] = `"# Search results for "button". Showing 1 exact match."`; -exports[`searchPatternFlyDocsTool, callback should parse parameters, with made up componentName: search 1`] = `"No PatternFly documentation found matching "lorem ipsum dolor sit amet""`; +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", 3 matches found:"`; +exports[`searchPatternFlyDocsTool, callback should parse parameters, with multiple words: search 1`] = `"# Search results for "Button Card Table". Showing 3 related matches."`; -exports[`searchPatternFlyDocsTool, callback should parse parameters, with partial componentName: search 1`] = `"# Search results for "ton", 2 matches found:"`; +exports[`searchPatternFlyDocsTool, callback should parse parameters, with partial componentName: search 1`] = `"# Search results for "ton". Showing 4 related matches."`; -exports[`searchPatternFlyDocsTool, callback should parse parameters, with trimmed componentName: search 1`] = `"# Search results for " Button ", 1 matches found:"`; +exports[`searchPatternFlyDocsTool, callback should parse parameters, with trimmed componentName: search 1`] = `"# Search results for " Button ". Showing 1 exact match."`; -exports[`searchPatternFlyDocsTool, callback should parse parameters, with upper case componentName: search 1`] = `"# Search results for "BUTTON", 1 matches found:"`; +exports[`searchPatternFlyDocsTool, callback should parse parameters, with upper case componentName: search 1`] = `"# Search results for "BUTTON". Showing 1 exact match."`; diff --git a/src/__tests__/docs.chart.test.ts b/src/__tests__/docs.chart.test.ts deleted file mode 100644 index 7eceb123..00000000 --- a/src/__tests__/docs.chart.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as docsChart from '../docs.chart'; - -describe('docsChart', () => { - it('should return specific properties', () => { - expect(docsChart).toMatchSnapshot(); - }); -}); - diff --git a/src/__tests__/docs.component.test.ts b/src/__tests__/docs.component.test.ts deleted file mode 100644 index 9bc21302..00000000 --- a/src/__tests__/docs.component.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as docsComponent from '../docs.component'; - -describe('docsComponent', () => { - it('should return specific properties', () => { - expect(docsComponent).toMatchSnapshot(); - }); -}); - diff --git a/src/__tests__/docs.layout.test.ts b/src/__tests__/docs.layout.test.ts deleted file mode 100644 index 3e848e56..00000000 --- a/src/__tests__/docs.layout.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as docsLayout from '../docs.layout'; - -describe('docsLayout', () => { - it('should return specific properties', () => { - expect(docsLayout).toMatchSnapshot(); - }); -}); - diff --git a/src/__tests__/docs.local.test.ts b/src/__tests__/docs.local.test.ts deleted file mode 100644 index bba032b8..00000000 --- a/src/__tests__/docs.local.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as docsLocal from '../docs.local'; -import { getLocalDocs } from '../docs.local'; -import { type GlobalOptions } from '../options'; - -describe('docsLocal', () => { - it('should return specific properties', () => { - expect(docsLocal).toMatchSnapshot(); - }); -}); - -describe('getLocalDocs', () => { - it.each([ - { - description: 'default', - options: undefined - }, - { - description: 'with custom docsPath', - options: { docsPath: 'custom/docs/path' } - } - ])('should return local references when called, $description', ({ options }) => { - expect(getLocalDocs(options as GlobalOptions)).toMatchSnapshot(); - }); -}); - diff --git a/src/__tests__/options.assertions.test.ts b/src/__tests__/options.assertions.test.ts new file mode 100644 index 00000000..8492ee1b --- /dev/null +++ b/src/__tests__/options.assertions.test.ts @@ -0,0 +1,46 @@ +import { + assertProtocol +} from '../options.assertions'; + +describe('assertProtocol', () => { + it.each([ + { + description: 'invalid URL format', + urls: ['not-a-url'], + protocols: ['http', 'https'] + }, + { + description: 'disallowed protocol', + urls: ['ftp://example.com'], + protocols: ['http', 'https'] + }, + { + description: 'valid URL but disallowed specific protocol', + urls: ['http://example.com'], + protocols: ['https'] + }, + { + description: 'empty protocol list with valid URL', + urls: ['https://example.com'], + protocols: [] + } + ])('should throw an error for validation, $description', ({ urls, protocols }) => { + const errorMessage = 'Invalid URL protocol configuration'; + + expect(() => assertProtocol( + urls, + protocols + )).toThrow(errorMessage); + }); + + it('should pass for valid inputs', () => { + expect(() => assertProtocol( + ['https://patternfly.org', 'http://localhost', 'HTTP://127.0.0.1'], + ['HTTP', 'https'] + )).not.toThrow(); + }); + + it('should pass for an empty URL list', () => { + expect(() => assertProtocol([], ['http', 'https'])).not.toThrow(); + }); +}); diff --git a/src/__tests__/resource.patternFlyComponentsIndex.test.ts b/src/__tests__/resource.patternFlyComponentsIndex.test.ts new file mode 100644 index 00000000..687a730b --- /dev/null +++ b/src/__tests__/resource.patternFlyComponentsIndex.test.ts @@ -0,0 +1,62 @@ +import { McpError } from '@modelcontextprotocol/sdk/types.js'; +import { + patternFlyComponentsIndexResource, + resourceCallback +} from '../resource.patternFlyComponentsIndex'; +import { isPlainObject } from '../server.helpers'; + +describe('patternFlyComponentsIndexResource', () => { + it('should have a consistent return structure', () => { + const resource = patternFlyComponentsIndexResource(); + + expect({ + name: resource[0], + uri: resource[1], + config: isPlainObject(resource[2]), + handler: resource[3] + }).toMatchSnapshot('structure'); + }); +}); + +describe('resourceCallback', () => { + it.each([ + { + description: 'default', + variables: {}, + expected: '# PatternFly Component Names Index for "v6"' + }, + { + description: 'explicit valid version', + variables: { + version: 'v6' + }, + expected: '# PatternFly Component Names Index for "v6"' + }, + { + description: 'category', + variables: { + category: 'accessibility' + }, + expected: '?category=accessibility' + } + ])('should return context content, $description', async ({ variables, expected }) => { + const result = await resourceCallback(undefined as any, variables); + + expect(result.contents).toBeDefined(); + expect(Object.keys(result.contents[0] as any)).toEqual(['uri', 'mimeType', 'text']); + expect(result.contents[0]?.text).toContain(expected); + }); + + it.each([ + { + description: 'available version', + variables: { + version: 'v5' + }, + error: 'Invalid PatternFly version' + } + ])('should handle variable errors, $description', async ({ error, variables }) => { + await expect(resourceCallback(undefined as any, variables as any)).rejects.toThrow(McpError); + await expect(resourceCallback(undefined as any, variables as any)).rejects.toThrow(error); + }); +}); diff --git a/src/__tests__/resource.patternFlyDocsIndex.test.ts b/src/__tests__/resource.patternFlyDocsIndex.test.ts index c2bd15ca..c0b1f582 100644 --- a/src/__tests__/resource.patternFlyDocsIndex.test.ts +++ b/src/__tests__/resource.patternFlyDocsIndex.test.ts @@ -1,18 +1,11 @@ -import { patternFlyDocsIndexResource } from '../resource.patternFlyDocsIndex'; -import { getLocalDocs } from '../docs.local'; +import { McpError } from '@modelcontextprotocol/sdk/types.js'; +import { + patternFlyDocsIndexResource, + resourceCallback +} from '../resource.patternFlyDocsIndex'; import { isPlainObject } from '../server.helpers'; -// Mock dependencies -jest.mock('../docs.local'); - -const mockGetLocalDocs = getLocalDocs as jest.MockedFunction; - describe('patternFlyDocsIndexResource', () => { - beforeEach(() => { - jest.clearAllMocks(); - mockGetLocalDocs.mockReturnValue(['[@patternfly/react-guidelines](./guidelines/README.md)']); - }); - it('should have a consistent return structure', () => { const resource = patternFlyDocsIndexResource(); @@ -25,23 +18,60 @@ describe('patternFlyDocsIndexResource', () => { }); }); -describe('patternFlyDocsIndexResource, callback', () => { - beforeEach(() => { - jest.clearAllMocks(); - mockGetLocalDocs.mockReturnValue(['[@patternfly/react-guidelines](./guidelines/README.md)']); - }); - +describe('resourceCallback', () => { it.each([ { description: 'default', - args: [] + variables: {}, + expected: '# PatternFly Documentation Index for "v6"' + }, + { + description: 'explicit valid version', + variables: { + version: 'v6' + }, + expected: '# PatternFly Documentation Index for "v6"' + }, + { + description: 'category', + variables: { + category: 'accessibility' + }, + expected: '?category=accessibility' + }, + { + description: 'section', + variables: { + section: 'components' + }, + expected: '?section=components' + }, + { + description: 'category and section', + variables: { + category: 'accessibility', + section: 'components' + }, + expected: '?category=accessibility§ion=components' } - ])('should return context content, $description', async ({ args }) => { - const [_name, _uri, _config, callback] = patternFlyDocsIndexResource(); - const result = await callback(...args); + ])('should return context content, $description', async ({ variables, expected }) => { + const result = await resourceCallback(undefined as any, variables); expect(result.contents).toBeDefined(); - expect(Object.keys(result.contents[0])).toEqual(['uri', 'mimeType', 'text']); - expect(result.contents[0].text).toContain('[@patternfly/react-guidelines](./guidelines/README.md)'); + expect(Object.keys(result.contents[0] as any)).toEqual(['uri', 'mimeType', 'text']); + expect(result.contents[0]?.text).toContain(expected); + }); + + it.each([ + { + description: 'available version', + variables: { + version: 'v5' + }, + error: 'Invalid PatternFly version' + } + ])('should handle variable errors, $description', async ({ error, variables }) => { + await expect(resourceCallback(undefined as any, variables as any)).rejects.toThrow(McpError); + await expect(resourceCallback(undefined as any, variables as any)).rejects.toThrow(error); }); }); diff --git a/src/__tests__/resource.patternFlyDocsTemplate.test.ts b/src/__tests__/resource.patternFlyDocsTemplate.test.ts index 4ba9c449..e836a018 100644 --- a/src/__tests__/resource.patternFlyDocsTemplate.test.ts +++ b/src/__tests__/resource.patternFlyDocsTemplate.test.ts @@ -1,21 +1,17 @@ +import { readFile } from 'node:fs/promises'; import { McpError } from '@modelcontextprotocol/sdk/types.js'; -import { patternFlyDocsTemplateResource } from '../resource.patternFlyDocsTemplate'; -import { processDocsFunction } from '../server.getResources'; -import { searchComponents } from '../tool.searchPatternFlyDocs'; +import { + patternFlyDocsTemplateResource, + resourceCallback +} from '../resource.patternFlyDocsTemplate'; import { isPlainObject } from '../server.helpers'; -// Mock dependencies -jest.mock('../server.getResources'); -jest.mock('../tool.searchPatternFlyDocs'); -jest.mock('../server.caching', () => ({ - memo: jest.fn(fn => fn) -})); -jest.mock('../options.context', () => ({ - getOptions: jest.fn(() => ({})) +jest.mock('node:fs/promises', () => ({ + ...jest.requireActual('node:fs/promises'), + readFile: jest.fn() })); -const mockProcessDocs = processDocsFunction as jest.MockedFunction; -const mockSearchComponents = searchComponents as jest.MockedFunction; +const mockReadFile = readFile as jest.MockedFunction; describe('patternFlyDocsTemplateResource', () => { beforeEach(() => { @@ -34,101 +30,117 @@ describe('patternFlyDocsTemplateResource', () => { }); }); -describe('patternFlyDocsTemplateResource, callback', () => { +describe('resourceCallback', () => { + let mockFetch: jest.SpyInstance; + beforeEach(() => { jest.clearAllMocks(); + mockFetch = jest.spyOn(global, 'fetch'); + }); + + afterEach(() => { + mockFetch.mockRestore(); }); it.each([ { - description: 'default', - name: 'Button', - urls: ['components/button.md'], - result: 'Button documentation content' + description: 'no version', + variables: { + name: 'Button' + } }, { - description: 'with multiple matched URLs', - name: 'Card', - urls: ['components/card.md', 'components/card-examples.md'], - result: 'Card documentation content' + description: 'default', + variables: { + name: 'Button', + version: 'v6' + } }, { - description: 'with trimmed name', - name: ' Table ', - urls: ['components/table.md'], - result: 'Table documentation content' + description: 'with lowercased name', + variables: { + name: 'button', + version: 'v6' + } }, { - description: 'with lower case name', - name: 'button', - urls: ['components/button.md'], - result: 'Button documentation content' + description: 'with local documentation', + variables: { + name: 'chatbot', + version: 'v6' + } } - ])('should parse parameters and return documentation, $description', async ({ name, urls, result: mockResult }) => { - mockSearchComponents.mockReturnValue({ - isSearchWildCardAll: false, - firstExactMatch: undefined, - exactMatches: [{ urls } as any], - searchResults: [] - }); - mockProcessDocs.mockResolvedValue([{ content: mockResult }] as any); - - const [_name, _uri, _config, callback] = patternFlyDocsTemplateResource(); - const uri = new URL('patternfly://docs/Button'); - const variables = { name }; - const result = await callback(uri, variables); - - expect(mockSearchComponents).toHaveBeenCalledWith(name); - expect(mockProcessDocs).toHaveBeenCalledWith(urls); + ])('should attempt to return resource content, $description', async ({ variables }) => { + const mockContent = `Mock content for ${variables.name}`; + + mockReadFile.mockResolvedValue(mockContent); + mockFetch.mockResolvedValue({ + ok: true, + text: () => mockContent + } as any); + + const result = await resourceCallback( + { href: `patternfly://docs/${variables.version}/${variables.name}` } as any, + variables + ); expect(result.contents).toBeDefined(); - expect(Object.keys(result.contents[0])).toEqual(['uri', 'mimeType', 'text']); - expect(result.contents[0].text).toContain(mockResult); + expect(Object.keys(result.contents[0] as any)).toEqual(['uri', 'mimeType', 'text']); + expect(result.contents[0]?.text).toMatch(new RegExp(mockContent, 'i')); }); it.each([ + { + description: 'invalid version', + error: 'Invalid PatternFly version', + variables: { + name: 'Button', + version: 'v5' + } + }, { description: 'with missing or undefined name', - error: 'Missing required parameter: name must be a string', - variables: {} + error: 'must be a string', + variables: { + version: 'v6' + } }, { description: 'with null name', - error: 'Missing required parameter: name must be a string', - variables: { name: null } + error: 'must be a string', + variables: { + name: null, + version: 'v6' + } }, { description: 'with empty name', - error: 'Missing required parameter: name must be a string', - variables: { name: '' } + error: 'must be a string', + variables: { + name: '', + version: 'v6' + } }, { description: 'with non-string name', - error: 'Missing required parameter: name must be a string', - variables: { name: 123 } + error: 'must be a string', + variables: { + name: 123, + version: 'v6' + } } ])('should handle variable errors, $description', async ({ error, variables }) => { - const [_name, _uri, _config, callback] = patternFlyDocsTemplateResource(); - const uri = new URL('patternfly://docs/test'); + const mockContent = `Mock content for ${variables.name}`; - await expect(callback(uri, variables)).rejects.toThrow(McpError); - await expect(callback(uri, variables)).rejects.toThrow(error); - }); + mockReadFile.mockResolvedValue(mockContent); + mockFetch.mockResolvedValue({ + ok: true, + text: () => mockContent + } as any); + + const uri = new URL('patternfly://docs/test'); - it('should handle documentation loading errors', async () => { - mockSearchComponents.mockReturnValue({ - isSearchWildCardAll: false, - firstExactMatch: undefined, - exactMatches: [], - searchResults: [] - }); - mockProcessDocs.mockRejectedValue(new Error('File not found')); - - const [_name, _uri, _config, handler] = patternFlyDocsTemplateResource(); - const uri = new URL('patternfly://docs/Button'); - const variables = { name: 'Button' }; - - await expect(handler(uri, variables)).rejects.toThrow(McpError); - await expect(handler(uri, variables)).rejects.toThrow('No documentation found'); + await expect(resourceCallback(uri, variables as any)).rejects.toThrow(McpError); + await expect(resourceCallback(uri, variables as any)).rejects.toThrow(error); }); }); diff --git a/src/__tests__/resource.patternFlySchemasIndex.test.ts b/src/__tests__/resource.patternFlySchemasIndex.test.ts index 504efa0e..4ea5526b 100644 --- a/src/__tests__/resource.patternFlySchemasIndex.test.ts +++ b/src/__tests__/resource.patternFlySchemasIndex.test.ts @@ -1,11 +1,10 @@ -import { patternFlySchemasIndexResource } from '../resource.patternFlySchemasIndex'; +import { McpError } from '@modelcontextprotocol/sdk/types.js'; +import { + patternFlySchemasIndexResource, + resourceCallback +} from '../resource.patternFlySchemasIndex'; import { isPlainObject } from '../server.helpers'; -// Mock dependencies -jest.mock('../tool.searchPatternFlyDocs', () => ({ - componentNames: ['Button', 'Card', 'Table'] -})); - describe('patternFlySchemasIndexResource', () => { beforeEach(() => { jest.clearAllMocks(); @@ -23,7 +22,7 @@ describe('patternFlySchemasIndexResource', () => { }); }); -describe('patternFlySchemasIndexResource, callback', () => { +describe('resourceCallback', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -31,14 +30,34 @@ describe('patternFlySchemasIndexResource, callback', () => { it.each([ { description: 'default', - args: [] + variables: {}, + expected: '# PatternFly Component JSON Schemas Index for "v6"' + }, + { + description: 'explicit valid version', + variables: { + version: 'v6' + }, + expected: '# PatternFly Component JSON Schemas Index for "v6"' } - ])('should return component schemas index, $description', async ({ args }) => { - const [_name, _uri, _config, callback] = patternFlySchemasIndexResource(); - const result = await callback(...args); + ])('should return component schemas index, $description', async ({ variables, expected }) => { + const result = await resourceCallback(undefined as any, variables); expect(result.contents).toBeDefined(); - expect(Object.keys(result.contents[0])).toEqual(['uri', 'mimeType', 'text']); - expect(result.contents[0].text).toContain('# PatternFly Component Names Index'); + expect(Object.keys(result.contents[0] as any)).toEqual(['uri', 'mimeType', 'text']); + expect(result.contents[0]?.text).toContain(expected); + }); + + it.each([ + { + description: 'version', + variables: { + version: 'v5' + }, + error: 'Invalid PatternFly version' + } + ])('should handle variable errors, $description', async ({ error, variables }) => { + await expect(resourceCallback(undefined as any, variables as any)).rejects.toThrow(McpError); + await expect(resourceCallback(undefined as any, variables as any)).rejects.toThrow(error); }); }); diff --git a/src/__tests__/resource.patternFlySchemasTemplate.test.ts b/src/__tests__/resource.patternFlySchemasTemplate.test.ts index 8af3abaf..0f854533 100644 --- a/src/__tests__/resource.patternFlySchemasTemplate.test.ts +++ b/src/__tests__/resource.patternFlySchemasTemplate.test.ts @@ -1,22 +1,7 @@ import { McpError } from '@modelcontextprotocol/sdk/types.js'; -import { getComponentSchema } from '../tool.patternFlyDocs'; -import { patternFlySchemasTemplateResource } from '../resource.patternFlySchemasTemplate'; -import { searchComponents } from '../tool.searchPatternFlyDocs'; +import { patternFlySchemasTemplateResource, resourceCallback } from '../resource.patternFlySchemasTemplate'; import { isPlainObject } from '../server.helpers'; -// Mock dependencies -jest.mock('../tool.searchPatternFlyDocs'); -jest.mock('../tool.patternFlyDocs'); -jest.mock('../server.caching', () => ({ - memo: jest.fn(fn => fn) -})); -jest.mock('../options.context', () => ({ - getOptions: jest.fn(() => ({})) -})); - -const mockGetComponentSchema = getComponentSchema as jest.MockedFunction; -const mockSearchComponents = searchComponents as jest.MockedFunction; - describe('patternFlySchemasTemplateResource', () => { beforeEach(() => { jest.clearAllMocks(); @@ -34,71 +19,93 @@ describe('patternFlySchemasTemplateResource', () => { }); }); -describe('patternFlySchemasTemplateResource, callback', () => { +describe('resourceCallback', () => { beforeEach(() => { jest.clearAllMocks(); }); it.each([ + { description: 'no version', variables: { name: 'Button' } }, + { + description: 'default', + variables: { + name: 'Button', + version: 'v6' + } + }, + { + description: 'with lowercased name', + variables: { + name: 'button', + version: 'v6' + } + } + ])('should attempt to return resource content, $description', async ({ variables }) => { + const mockContent = '$schema'; + + const result = await resourceCallback( + { href: `patternfly://schemas/v6/${variables.name}` } as any, + variables + ); + + expect(result.contents).toBeDefined(); + expect(Object.keys(result.contents[0] as any)).toEqual(['uri', 'mimeType', 'text']); + expect(result.contents[0]?.text).toContain(mockContent); + }); + + it.each([ + { + description: 'invalid version', + error: 'Invalid PatternFly version', + variables: { + name: 'Button', + version: 'v5' + } + }, { description: 'with missing or undefined name', - error: 'Missing required parameter: name must be a string', + error: 'must be a string', variables: {} }, { description: 'with null name', - error: 'Missing required parameter: name must be a string', - variables: { name: null } + error: 'must be a string', + variables: { + name: null + } }, { description: 'with empty name', - error: 'Missing required parameter: name must be a string', - variables: { name: '' } + error: 'must be a string', + variables: { + name: '' + } }, { description: 'with non-string name', - error: 'Missing required parameter: name must be a string', - variables: { name: 123 } + error: 'must be a string', + variables: { + name: 123 + } + }, + { + description: 'non-existent name', + error: 'No component JSON schemas found', + variables: { + name: 'loremIpsum', + version: 'v6' + } + }, + { + description: 'found but no schema', + error: 'No component JSON schemas found', + variables: { + name: 'table', + version: 'v6' + } } ])('should handle variable errors, $description', async ({ error, variables }) => { - const [_name, _uri, _config, callback] = patternFlySchemasTemplateResource(); - const uri = new URL('patternfly://schemas/test'); - - await expect(callback(uri, variables)).rejects.toThrow(McpError); - await expect(callback(uri, variables)).rejects.toThrow(error); - }); - - it('should handle missing exact match and missing schema errors', async () => { - mockSearchComponents.mockReturnValue({ - isSearchWildCardAll: false, - firstExactMatch: undefined, - exactMatches: [], - searchResults: [] - }); - mockGetComponentSchema.mockReturnValue(undefined as any); - - const [_name, _uri, _config, handler] = patternFlySchemasTemplateResource(); - const uri = new URL('patternfly://schemas/DolorSitAmet'); - const variables = { name: 'DolorSitAmet' }; - - await expect(handler(uri, variables)).rejects.toThrow(McpError); - await expect(handler(uri, variables)).rejects.toThrow('Component "DolorSitAmet" not found'); - }); - - it('should handle exact match but missing schema errors', async () => { - mockSearchComponents.mockReturnValue({ - isSearchWildCardAll: false, - firstExactMatch: undefined, - exactMatches: [{ item: 'Button', urls: [] } as any], - searchResults: [] - }); - mockGetComponentSchema.mockReturnValue(undefined as any); - - const [_name, _uri, _config, handler] = patternFlySchemasTemplateResource(); - const uri = new URL('patternfly://schemas/DolorSitAmet'); - const variables = { name: 'Button' }; - - await expect(handler(uri, variables)).rejects.toThrow(McpError); - await expect(handler(uri, variables)).rejects.toThrow('Component "Button" found'); + await expect(resourceCallback(undefined as any, variables as any)).rejects.toThrow(McpError); + await expect(resourceCallback(undefined as any, variables as any)).rejects.toThrow(error); }); }); diff --git a/src/__tests__/server.getResources.test.ts b/src/__tests__/server.getResources.test.ts index 17d99fb3..53b4ed92 100644 --- a/src/__tests__/server.getResources.test.ts +++ b/src/__tests__/server.getResources.test.ts @@ -355,11 +355,7 @@ describe('processDocsFunction', () => { 'local-file.md', 'https://example.com/remote.md' ], - options: { - isHttp: false, - urlRegex: /^(https?:)\/\//i, - separator: '\n\n---\n\n' - }, + options: {}, fileMemoHits: 1, fetchMemoHits: 1 }, @@ -372,11 +368,7 @@ describe('processDocsFunction', () => { 'https://example.com/remote.md', 'https://example.com/remote.md' ], - options: { - isHttp: false, - urlRegex: /^(https?:)\/\//i, - separator: '\n\n---\n\n' - }, + options: {}, fileMemoHits: 1, fetchMemoHits: 1 }, @@ -388,15 +380,22 @@ describe('processDocsFunction', () => { ' ', 'file2.md' ], - options: { - isHttp: false, - urlRegex: /^(https?:)\/\//i, - separator: '\n\n---\n\n' - }, + options: {}, fileMemoHits: 2 } ])('should process local and remote inputs, $description', async ({ inputs, options, fileMemoHits = 0, fetchMemoHits = 0 }) => { - const result = await processDocsFunction(inputs, options as GlobalOptions); + const result = await processDocsFunction(inputs, { + isHttp: false, + minMax: { + docsToLoad: { + min: 1, + max: 10 + } + }, + separator: '\n\n---\n\n', + urlRegex: /^(https?:)\/\//i, + ...options + } as GlobalOptions); expect(result).toMatchSnapshot(); expect(readLocalFileFunction.memo).toHaveBeenCalledTimes(fileMemoHits); @@ -406,8 +405,14 @@ describe('processDocsFunction', () => { it('should handle errors gracefully', async () => { const mockOptions = { isHttp: false, - urlRegex: /^(https?:)\/\//i, - separator: '\n\n---\n\n' + minMax: { + docsToLoad: { + min: 1, + max: 10 + } + }, + separator: '\n\n---\n\n', + urlRegex: /^(https?:)\/\//i }; // Mock one success and one failure diff --git a/src/__tests__/server.helpers.test.ts b/src/__tests__/server.helpers.test.ts index 10d4bf0b..2746e91d 100644 --- a/src/__tests__/server.helpers.test.ts +++ b/src/__tests__/server.helpers.test.ts @@ -1,4 +1,5 @@ import { + buildSearchString, freezeObject, generateHash, hashCode, @@ -8,12 +9,57 @@ import { isReferenceLike, isUrl, isPath, + isWhitelistedUrl, mergeObjects, portValid, stringJoin, timeoutFunction } from '../server.helpers'; +describe('buildSearchString', () => { + it.each([ + { + description: 'basic', + values: { + lorem: 'ipsum' + }, + expected: 'lorem=ipsum' + }, + { + description: 'with prefix', + values: { + lorem: 'ipsum' + }, + options: { + prefix: true + }, + expected: '?lorem=ipsum' + }, + { + description: 'null, undefined, and with prefix', + values: { + lorem: 'ipsum', + dolor: undefined, + ipsum: null, + amet: 'dolor' + }, + options: { + prefix: true + }, + expected: '?amet=dolor&lorem=ipsum' + }, + { + description: 'basic encoding', + values: { + lorem: 'ipsum dolor' + }, + expected: 'lorem=ipsum+dolor' + } + ])('should build a search string, $description', ({ values, options, expected }) => { + expect(buildSearchString(values, options || {})).toBe(expected); + }); +}); + describe('freezeObject', () => { it.each([ { @@ -604,6 +650,109 @@ describe('isPath', () => { }); }); +describe('isWhitelistedUrl', () => { + it.each([ + { + description: 'exact host match, root path', + url: 'https://patternfly.org', + expected: true + }, + { + description: 'exact host match, sub path', + url: 'https://patternfly.org/v6/components/button', + expected: true + }, + { + description: 'subdomain match', + url: 'https://www.patternfly.org', + expected: true + }, + { + description: 'deeper subdomain match', + url: 'https://v6.docs.patternfly.org', + expected: true + }, + { + description: 'host match, exact allowed path prefix', + url: 'https://github.com/patternfly', + expected: true + }, + { + description: 'host match, sub path of allowed prefix', + url: 'https://github.com/patternfly/patternfly-react', + expected: true + }, + { + description: 'host match, deeper allowed path', + url: 'https://raw.githubusercontent.com/patternfly/patternfly-mcp/main/README.md', + expected: true + }, + { + description: 'host mismatch', + url: 'https://example.com', + expected: false + }, + { + description: 'host mismatch, whitelisted host in path', + url: 'https://example.com/patternfly.org', + expected: false + }, + { + description: 'host match, path mismatch (not a prefix)', + url: 'https://github.com/other-org/patternfly', + expected: false + }, + { + description: 'host match, path mismatch (different repo)', + url: 'https://raw.githubusercontent.com/patternfly/other-repo', + expected: false + }, + { + description: 'protocol mismatch (not in allowedProtocols)', + url: 'http://patternfly.org', + expected: false + }, + { + description: 'case-insensitive host match', + url: 'https://PATTERNFLY.ORG', + expected: true + }, + { + description: 'case-insensitive path match', + url: 'https://github.com/PATTERNFLY/README.md', + expected: true + }, + { + description: 'invalid URL string', + url: 'not-a-url', + expected: false + } + ])('should confirm if a URL is whitelisted, $description', ({ url, expected }) => { + const whitelist = [ + 'https://patternfly.org', + 'https://github.com/patternfly', + 'https://raw.githubusercontent.com/patternfly/patternfly-mcp' + ] as any[]; + + expect(isWhitelistedUrl(url, whitelist)).toBe(expected); + }); + + it('should support custom allowed protocols', () => { + const customWhitelist = ['ftp://files.patternfly.org'] as any; + const result = isWhitelistedUrl( + 'ftp://files.patternfly.org/file.txt', + customWhitelist, + { allowedProtocols: ['ftp'] } + ); + + expect(result).toBe(true); + }); + + it('should return false for empty whitelist', () => { + expect(isWhitelistedUrl('https://patternfly.org', [])).toBe(false); + }); +}); + describe('mergeObjects', () => { it.each([ { diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts index 9977f9f4..3ba00c6b 100644 --- a/src/__tests__/server.test.ts +++ b/src/__tests__/server.test.ts @@ -3,6 +3,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { runServer } from '../server'; import { log } from '../logger'; import { startHttpTransport, type HttpServerHandle } from '../server.http'; +import { DEFAULT_OPTIONS } from '../options.defaults'; // Mock dependencies jest.mock('@modelcontextprotocol/sdk/server/mcp.js'); @@ -143,12 +144,15 @@ describe('runServer', () => { } ])('should attempt to run server, $description', async ({ options, tools, enableSigint, transportMethod }) => { const settings = { - ...(tools && { tools }), - ...(enableSigint !== undefined && { enableSigint }), + ...tools && { tools }, + ...enableSigint !== undefined && { enableSigint }, allowProcessExit: false // Prevent process.exit in tests }; - const serverInstance = await runServer(options as any, Object.keys(settings).length > 0 ? settings : { allowProcessExit: false }); + const serverInstance = await runServer( + { minMax: DEFAULT_OPTIONS.minMax, patternflyOptions: DEFAULT_OPTIONS.patternflyOptions, ...options } as any, + Object.keys(settings).length > 0 ? settings : { allowProcessExit: false } + ); expect(transportMethod).toHaveBeenCalled(); expect(serverInstance.isRunning()).toBe(true); @@ -174,7 +178,10 @@ describe('runServer', () => { options: { isHttp: true } } ])('should allow server to be stopped, $description', async ({ options }) => { - const serverInstance = await runServer({ ...options, name: 'test-server' } as any, { allowProcessExit: false }); + const serverInstance = await runServer( + { minMax: DEFAULT_OPTIONS.minMax, patternflyOptions: DEFAULT_OPTIONS.patternflyOptions, ...options, name: 'test-server' } as any, + { allowProcessExit: false } + ); expect(serverInstance.isRunning()).toBe(true); diff --git a/src/__tests__/tool.patternFlyDocs.test.ts b/src/__tests__/tool.patternFlyDocs.test.ts index d73523a8..a1556b24 100644 --- a/src/__tests__/tool.patternFlyDocs.test.ts +++ b/src/__tests__/tool.patternFlyDocs.test.ts @@ -34,33 +34,45 @@ describe('usePatternFlyDocsTool, callback', () => { it.each([ { - description: 'default', - value: 'components/button.md', + description: 'single file, mock path', + processedValue: { + path: 'components/button.md', + content: 'single documentation content' + }, urlList: ['components/button.md'] }, { - description: 'multiple files', - value: 'combined docs content', + description: 'multiple files, mock paths', + processedValue: { + path: 'components/button.md', + content: 'combined documentation content' + }, urlList: ['components/button.md', 'components/card.md', 'components/table.md'] }, { description: 'with invalid urlList', - value: 'invalid path', + processedValue: { + path: 'invalid-path', + content: 'Failed to load' + }, urlList: ['invalid-url'] }, { - description: 'with name', - value: 'button content', - name: 'button' + description: 'with name and actual path', + processedValue: { + path: 'documentation:chatbot/README.md', + content: 'chatbot documentation content' + }, + name: 'chatbot' } - ])('should parse parameters, $description', async ({ value, urlList, name }) => { - mockProcessDocs.mockResolvedValue([{ content: value }] as any); + ])('should attempt to parse parameters, $description', async ({ processedValue, urlList, name }) => { + mockProcessDocs.mockResolvedValue([processedValue] as any); const [_name, _schema, callback] = usePatternFlyDocsTool(); const result = await callback({ urlList, name }); expect(mockProcessDocs).toHaveBeenCalledTimes(1); expect(result.content[0].text).toBeDefined(); - expect(result.content[0].text.startsWith('# Documentation from')).toBe(true); + expect(result.content[0].text.split('\n')[0]).toMatchSnapshot(); }); it.each([ @@ -81,7 +93,7 @@ describe('usePatternFlyDocsTool, callback', () => { }, { description: 'with empty files', - error: 'Provide either a string', + error: 'array must contain strings', urlList: ['components/button.md', '', ' ', 'components/card.md', 'components/table.md'] }, { @@ -91,7 +103,7 @@ describe('usePatternFlyDocsTool, callback', () => { }, { description: 'with empty strings in a urlList', - error: 'Provide either a string', + error: 'array must contain strings', urlList: ['', ' '] }, { @@ -111,7 +123,7 @@ describe('usePatternFlyDocsTool, callback', () => { mockProcessDocs.mockRejectedValue(new Error('File not found')); const [_name, _schema, callback] = usePatternFlyDocsTool(); - await expect(callback({ urlList: ['missing.md'] })).rejects.toThrow(McpError); - await expect(callback({ urlList: ['missing.md'] })).rejects.toThrow('Failed to fetch documentation'); + 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'); }); }); diff --git a/src/__tests__/tool.searchPatternFlyDocs.test.ts b/src/__tests__/tool.searchPatternFlyDocs.test.ts index a4c2e1e4..9579b396 100644 --- a/src/__tests__/tool.searchPatternFlyDocs.test.ts +++ b/src/__tests__/tool.searchPatternFlyDocs.test.ts @@ -45,6 +45,11 @@ describe('searchPatternFlyDocsTool, callback', () => { description: 'with upper case componentName', searchQuery: 'BUTTON' }, + { + description: 'with explicit valid version', + searchQuery: 'Button', + version: 'v6' + }, { description: 'with partial componentName', searchQuery: 'ton' @@ -64,10 +69,6 @@ describe('searchPatternFlyDocsTool, callback', () => { { description: 'with "all" searchQuery all', searchQuery: 'ALL' - }, - { - description: 'with empty searchQuery all', - searchQuery: '' } ])('should parse parameters, $description', async ({ searchQuery }) => { const [_name, _schema, callback] = searchPatternFlyDocsTool(); @@ -77,19 +78,24 @@ describe('searchPatternFlyDocsTool, callback', () => { }); it.each([ + { + description: 'with empty searchQuery', + error: '"searchQuery" must be a string from', + searchQuery: '' + }, { description: 'with missing or undefined searchQuery', - error: 'Missing required parameter: searchQuery', + error: '"searchQuery" must be a string from', searchQuery: undefined }, { description: 'with null searchQuery', - error: 'Missing required parameter: searchQuery', + error: '"searchQuery" must be a string from', searchQuery: null }, { description: 'with non-string searchQuery', - error: 'Missing required parameter: searchQuery', + error: '"searchQuery" must be a string from', searchQuery: 123 } ])('should handle errors, $description', async ({ error, searchQuery }) => { diff --git a/src/docs.chart.ts b/src/docs.chart.ts deleted file mode 100644 index d7c14dd5..00000000 --- a/src/docs.chart.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { PF_EXTERNAL_EXAMPLES_CHARTS, PF_EXTERNAL_CHARTS_DESIGN } from './options.defaults'; - -const CHART_DOCS = [ - `[@patternfly/Charts - Colors for Charts - Examples](${PF_EXTERNAL_EXAMPLES_CHARTS}/ChartTheme/examples/ChartTheme.md)`, - `[@patternfly/Charts - Area Chart - Design Guidelines](${PF_EXTERNAL_CHARTS_DESIGN}/area-chart/area-chart.md)`, - `[@patternfly/Charts - Area Chart - Examples](${PF_EXTERNAL_EXAMPLES_CHARTS}/ChartArea/examples/ChartArea.md)`, - `[@patternfly/Charts - Bar Chart - Design Guidelines](${PF_EXTERNAL_CHARTS_DESIGN}/bar-chart/bar-chart.md)`, - `[@patternfly/Charts - Bar Chart - Examples](${PF_EXTERNAL_EXAMPLES_CHARTS}/ChartBar/examples/ChartBar.md)`, - `[@patternfly/Charts - Box Plot Chart - Examples](${PF_EXTERNAL_EXAMPLES_CHARTS}/ChartBoxPlot/examples/ChartBoxPlot.md)`, - `[@patternfly/Charts - Bullet Chart - Design Guidelines](${PF_EXTERNAL_CHARTS_DESIGN}/bullet-chart/bullet-chart.md)`, - `[@patternfly/Charts - Bullet Chart - Examples](${PF_EXTERNAL_EXAMPLES_CHARTS}/ChartBullet/examples/ChartBullet.md)`, - `[@patternfly/Charts - Donut Chart - Design Guidelines](${PF_EXTERNAL_CHARTS_DESIGN}/donut-chart/donut-chart.md)`, - `[@patternfly/Charts - Donut Chart - Examples](${PF_EXTERNAL_EXAMPLES_CHARTS}/ChartDonut/examples/ChartDonut.md)`, - `[@patternfly/Charts - Donut Utilization Chart - Design Guidelines](${PF_EXTERNAL_CHARTS_DESIGN}/donut-utilization-chart/donut-utilization-chart.md)`, - `[@patternfly/Charts - Donut Utilization Chart - Examples](${PF_EXTERNAL_EXAMPLES_CHARTS}/ChartDonutUtilization/examples/ChartDonutUtilization.md)`, - `[@patternfly/Charts - Line Chart - Design Guidelines](${PF_EXTERNAL_CHARTS_DESIGN}/line-chart/line-chart.md)`, - `[@patternfly/Charts - Line Chart - Examples](${PF_EXTERNAL_EXAMPLES_CHARTS}/ChartLine/examples/ChartLine.md)`, - `[@patternfly/Charts - Pie Chart - Design Guidelines](${PF_EXTERNAL_CHARTS_DESIGN}/pie-chart/pie-chart.md)`, - `[@patternfly/Charts - Pie Chart - Examples](${PF_EXTERNAL_EXAMPLES_CHARTS}/ChartPie/examples/ChartPie.md)`, - `[@patternfly/Charts - Scatter Chart - Design Guidelines](${PF_EXTERNAL_CHARTS_DESIGN}/scatter-chart/scatter-chart.md)`, - `[@patternfly/Charts - Scatter Chart - Examples](${PF_EXTERNAL_EXAMPLES_CHARTS}/ChartScatter/examples/ChartScatter.md)`, - `[@patternfly/Charts - Sparkline Chart - Design Guidelines](${PF_EXTERNAL_CHARTS_DESIGN}/sparkline-chart/sparkline-chart.md)`, - `[@patternfly/Charts - Sparkline Chart - Examples](${PF_EXTERNAL_EXAMPLES_CHARTS}/Sparkline/examples/sparkline.md)`, - `[@patternfly/Charts - Stack Chart - Design Guidelines](${PF_EXTERNAL_CHARTS_DESIGN}/stacked-chart/stacked-chart.md)`, - `[@patternfly/Charts - Stack Chart - Examples](${PF_EXTERNAL_EXAMPLES_CHARTS}/ChartStack/examples/ChartStack.md)`, - `[@patternfly/Charts - Threshold Chart - Design Guidelines](${PF_EXTERNAL_CHARTS_DESIGN}/threshold-chart/threshold-chart.md)`, - `[@patternfly/Charts - Threshold Chart - Examples](${PF_EXTERNAL_EXAMPLES_CHARTS}/ChartThreshold/examples/ChartThreshold.md)`, - `[@patternfly/Charts - Legend - Design Guidelines](${PF_EXTERNAL_CHARTS_DESIGN}/legend-chart/legend-chart.md)`, - `[@patternfly/Charts - Legend - Examples](${PF_EXTERNAL_EXAMPLES_CHARTS}/ChartLegend/examples/ChartLegend.md)`, - `[@patternfly/Charts - Tooltip - Design Guidelines](${PF_EXTERNAL_CHARTS_DESIGN}/tooltip-chart/tooltip-chart.md)`, - `[@patternfly/Charts - Tooltip - Examples](${PF_EXTERNAL_EXAMPLES_CHARTS}/ChartTooltip/examples/ChartTooltip.md)` -]; - -export { CHART_DOCS }; diff --git a/src/docs.component.ts b/src/docs.component.ts deleted file mode 100644 index ca4f93f5..00000000 --- a/src/docs.component.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { PF_EXTERNAL_ACCESSIBILITY, PF_EXTERNAL_DESIGN_COMPONENTS, PF_EXTERNAL_EXAMPLES_REACT_CORE, PF_EXTERNAL_EXAMPLES_TABLE } from './options.defaults'; - -const COMPONENT_DOCS = [ - `[@patternfly/AboutModal - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/about-modal/about-modal.md)`, - `[@patternfly/AboutModal - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/about-modal/about-modal.md)`, - `[@patternfly/AboutModal - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/AboutModal/examples/AboutModal.md)`, - `[@patternfly/Accordion - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/accordion/accordion.md)`, - `[@patternfly/Accordion - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/accordion/accordion.md)`, - `[@patternfly/Accordion - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Accordion/examples/Accordion.md)`, - `[@patternfly/ActionList - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/action-list/action-list.md)`, - `[@patternfly/ActionList - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/action-list/action-list.md)`, - `[@patternfly/ActionList - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/ActionList/examples/ActionList.md)`, - `[@patternfly/Alert - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/alert/alert.md)`, - `[@patternfly/Alert - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/alert/alert.md)`, - `[@patternfly/Alert - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Alert/examples/Alert.md)`, - `[@patternfly/ApplicationLauncher - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/application-launcher/application-launcher.md)`, - `[@patternfly/ApplicationLauncher - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/application-launcher/application-launcher.md)`, - // `[@patternfly/ApplicationLauncher - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/ApplicationLauncher/examples/ApplicationLauncher.md)`, - `[@patternfly/Avatar - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/avatar/avatar.md)`, - `[@patternfly/Avatar - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/avatar/avatar.md)`, - `[@patternfly/Avatar - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Avatar/examples/Avatar.md)`, - `[@patternfly/BackToTop - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/back-to-top/back-to-top.md)`, - // `[@patternfly/BackToTop - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/back-to-top/back-to-top.md)`, - `[@patternfly/BackToTop - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/BackToTop/examples/BackToTop.md)`, - `[@patternfly/Backdrop - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/backdrop/backdrop.md)`, - `[@patternfly/Backdrop - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/backdrop/backdrop.md)`, - `[@patternfly/Backdrop - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Backdrop/examples/Backdrop.md)`, - `[@patternfly/BackgroundImage - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/background-image/background-image.md)`, - `[@patternfly/BackgroundImage - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/background-image/background-image.md)`, - `[@patternfly/BackgroundImage - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/BackgroundImage/examples/BackgroundImage.md)`, - `[@patternfly/Badge - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/badge/badge.md)`, - `[@patternfly/Badge - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/badge/badge.md)`, - `[@patternfly/Badge - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Badge/examples/Badge.md)`, - `[@patternfly/Banner - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/banner/banner.md)`, - `[@patternfly/Banner - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/banner/banner.md)`, - `[@patternfly/Banner - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Banner/examples/Banner.md)`, - `[@patternfly/Brand - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/brand/brand.md)`, - `[@patternfly/Brand - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/brand/brand.md)`, - `[@patternfly/Brand - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Brand/examples/Brand.md)`, - `[@patternfly/Breadcrumb - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/breadcrumb/breadcrumb.md)`, - `[@patternfly/Breadcrumb - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/breadcrumb/breadcrumb.md)`, - `[@patternfly/Breadcrumb - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Breadcrumb/examples/Breadcrumb.md)`, - `[@patternfly/Button - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/button/button.md)`, - `[@patternfly/Button - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/button/button.md)`, - `[@patternfly/Button - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Button/examples/Button.md)`, - `[@patternfly/CalendarMonth - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/calendar-month/calendar-month.md)`, - `[@patternfly/CalendarMonth - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/calendar-month/calendar-month.md)`, - `[@patternfly/CalendarMonth - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/CalendarMonth/examples/CalendarMonth.md)`, - `[@patternfly/Card - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/card/card.md)`, - `[@patternfly/Card - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/card/card.md)`, - `[@patternfly/Card - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Card/examples/Card.md)`, - `[@patternfly/Checkbox - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/checkbox/checkbox.md)`, - `[@patternfly/Checkbox - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/checkbox/checkbox.md)`, - `[@patternfly/Checkbox - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Checkbox/examples/Checkbox.md)`, - `[@patternfly/ChipDeprecated - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/chip/chip.md)`, - `[@patternfly/ChipDeprecated - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/chip/chip.md)`, - // `[@patternfly/ChipDeprecated - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/ChipDeprecated/examples/ChipDeprecated.md)`, - `[@patternfly/ClipboardCopy - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/clipboard-copy/clipboard-copy.md)`, - `[@patternfly/ClipboardCopy - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/clipboard-copy/clipboard-copy.md)`, - `[@patternfly/ClipboardCopy - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/ClipboardCopy/examples/ClipboardCopy.md)`, - `[@patternfly/CodeBlock - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/code-block/code-block.md)`, - // `[@patternfly/CodeBlock - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/code-block/code-block.md)`, - `[@patternfly/CodeBlock - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/CodeBlock/examples/CodeBlock.md)`, - `[@patternfly/CodeEditor - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/code-editor/code-editor.md)`, - `[@patternfly/CodeEditor - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/code-editor/code-editor.md)`, - // `[@patternfly/CodeEditor - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/CodeEditor/examples/CodeEditor.md)`, - `[@patternfly/Content - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/content/content.md)`, - // `[@patternfly/Content - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/content/content.md)`, - `[@patternfly/Content - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Content/examples/Content.md)`, - `[@patternfly/DataList - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/data-list/data-list.md)`, - // `[@patternfly/DataList - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/data-list/data-list.md)`, - `[@patternfly/DataList - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/DataList/examples/DataList.md)`, - `[@patternfly/DatePicker - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/date-picker/date-picker.md)`, - // `[@patternfly/DatePicker - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/date-picker/date-picker.md)`, - `[@patternfly/DatePicker - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/DatePicker/examples/DatePicker.md)`, - // `[@patternfly/DateTimePicker - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/date-time-picker/date-time-picker.md)`, - // `[@patternfly/DateTimePicker - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/date-time-picker/date-time-picker.md)`, - // `[@patternfly/DateTimePicker - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/DateTimePicker/examples/DateTimePicker.md)`, - `[@patternfly/DescriptionList - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/description-list/description-list.md)`, - // `[@patternfly/DescriptionList - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/description-list/description-list.md)`, - `[@patternfly/DescriptionList - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/DescriptionList/examples/DescriptionList.md)`, - `[@patternfly/Divider - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/divider/divider.md)`, - // `[@patternfly/Divider - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/divider/divider.md)`, - `[@patternfly/Divider - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Divider/examples/Divider.md)`, - `[@patternfly/DragAndDrop - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/drag-and-drop/drag.md)`, - // `[@patternfly/DragAndDrop - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/drag-and-drop/drag-and-drop.md)`, - // `[@patternfly/DragAndDrop - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/DragAndDrop/examples/DragAndDrop.md)`, - `[@patternfly/Drawer - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/drawer/drawer.md)`, - // `[@patternfly/Drawer - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/drawer/drawer.md)`, - `[@patternfly/Drawer - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Drawer/examples/Drawer.md)`, - `[@patternfly/Dropdown - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/dropdown/dropdown.md)`, - // `[@patternfly/Dropdown - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/dropdown/dropdown.md)`, - `[@patternfly/Dropdown - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Dropdown/examples/Dropdown.md)`, - `[@patternfly/DualListSelector - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/dual-list-selector/dual-list-selector.md)`, - // `[@patternfly/DualListSelector - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/dual-list-selector/dual-list-selector.md)`, - `[@patternfly/DualListSelector - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/DualListSelector/examples/DualListSelector.md)`, - `[@patternfly/EmptyState - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/empty-state/empty-state.md)`, - // `[@patternfly/EmptyState - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/empty-state/empty-state.md)`, - `[@patternfly/EmptyState - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/EmptyState/examples/EmptyState.md)`, - `[@patternfly/ExpandableSection - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/expandable-section/expandable-section.md)`, - `[@patternfly/ExpandableSection - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/expandable-section/expandable-section.md)`, - `[@patternfly/ExpandableSection - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/ExpandableSection/examples/ExpandableSection.md)`, - `[@patternfly/FileUpload - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/file-upload/file-upload.md)`, - // `[@patternfly/FileUpload - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/file-upload/file-upload.md)`, - `[@patternfly/FileUpload - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/FileUpload/examples/FileUpload.md)`, - `[@patternfly/Form - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/form/forms.md)`, - // `[@patternfly/Form - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/form/form.md)`, - `[@patternfly/Form - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Form/examples/Form.md)`, - `[@patternfly/FormControl - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/form-control/form-control.md)`, - // `[@patternfly/FormControl - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/form-control/form-control.md)`, - // `[@patternfly/FormControl - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/FormControl/examples/FormControl.md)`, - `[@patternfly/FormSelect - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/form-select/form-select.md)`, - // `[@patternfly/FormSelect - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/form-select/form-select.md)`, - `[@patternfly/FormSelect - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/FormSelect/examples/FormSelect.md)`, - `[@patternfly/HelperText - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/helper-text/helper-text.md)`, - `[@patternfly/HelperText - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/helper-text/helper-text.md)`, - `[@patternfly/HelperText - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/HelperText/examples/HelperText.md)`, - `[@patternfly/Hint - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/hint/hint.md)`, - // `[@patternfly/Hint - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/hint/hint.md)`, - `[@patternfly/Hint - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Hint/examples/Hint.md)`, - // `[@patternfly/Icon - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/icon/icon.md)`, - // `[@patternfly/Icon - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/icon/icon.md)`, - `[@patternfly/Icon - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Icon/examples/Icon.md)`, - `[@patternfly/InlineEdit - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/inline-edit/inline-edit.md)`, - // `[@patternfly/InlineEdit - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/inline-edit/inline-edit.md)`, - // `[@patternfly/InlineEdit - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/InlineEdit/examples/InlineEdit.md)`, - `[@patternfly/InputGroup - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/input-group/input-group.md)`, - // `[@patternfly/InputGroup - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/input-group/input-group.md)`, - `[@patternfly/InputGroup - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/InputGroup/examples/InputGroup.md)`, - `[@patternfly/JumpLinks - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/jump-link/jump-link.md)`, - `[@patternfly/JumpLinks - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/jump-links/jump-links.md)`, - `[@patternfly/JumpLinks - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/JumpLinks/examples/JumpLinks.md)`, - `[@patternfly/Label - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/label/label.md)`, - `[@patternfly/Label - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/label/label.md)`, - `[@patternfly/Label - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Label/examples/Label.md)`, - `[@patternfly/List - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/list/list.md)`, - // `[@patternfly/List - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/list/list.md)`, - `[@patternfly/List - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/List/examples/List.md)`, - `[@patternfly/LoginPage - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/login-page/login-page.md)`, - // `[@patternfly/LoginPage - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/login-page/login-page.md)`, - `[@patternfly/LoginPage - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/LoginPage/examples/LoginPage.md)`, - `[@patternfly/Masthead - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/masthead/masthead.md)`, - // `[@patternfly/Masthead - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/masthead/masthead.md)`, - `[@patternfly/Masthead - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Masthead/examples/Masthead.md)`, - `[@patternfly/Menu - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/menu/menu.md)`, - `[@patternfly/Menu - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/menu/menu.md)`, - `[@patternfly/Menu - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Menu/examples/Menu.md)`, - `[@patternfly/MenuToggle - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/menu-toggle/menu-toggle.md)`, - `[@patternfly/MenuToggle - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/menu-toggle/menu-toggle.md)`, - `[@patternfly/MenuToggle - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/MenuToggle/examples/MenuToggle.md)`, - `[@patternfly/Modal - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/modal/modal.md)`, - `[@patternfly/Modal - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/modal/modal.md)`, - `[@patternfly/Modal - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Modal/examples/Modal.md)`, - `[@patternfly/Navigation - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/navigation/navigation.md)`, - `[@patternfly/Navigation - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/navigation/navigation.md)`, - // `[@patternfly/Navigation - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Navigation/examples/Navigation.md)`, - `[@patternfly/NotificationBadge - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/notification-badge/notification-badge.md)`, - // `[@patternfly/NotificationBadge - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/notification-badge/notification-badge.md)`, - `[@patternfly/NotificationBadge - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/NotificationBadge/examples/NotificationBadge.md)`, - `[@patternfly/NotificationDrawer - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/notification-drawer/notification-drawer.md)`, - // `[@patternfly/NotificationDrawer - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/notification-drawer/notification-drawer.md)`, - `[@patternfly/NotificationDrawer - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/NotificationDrawer/examples/NotificationDrawer.md)`, - `[@patternfly/NumberInput - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/number%20input/number-input.md)`, - // `[@patternfly/NumberInput - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/number-input/number-input.md)`, - `[@patternfly/NumberInput - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/NumberInput/examples/NumberInput.md)`, - `[@patternfly/OverflowMenu - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/overflow-menu/overflow-menu.md)`, - // `[@patternfly/OverflowMenu - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/overflow-menu/overflow-menu.md)`, - `[@patternfly/OverflowMenu - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/OverflowMenu/examples/OverflowMenu.md)`, - `[@patternfly/Page - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/page/page.md)`, - `[@patternfly/Page - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/page/page.md)`, - `[@patternfly/Page - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Page/examples/Page.md)`, - `[@patternfly/Pagination - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/pagination/pagination.md)`, - // `[@patternfly/Pagination - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/pagination/pagination.md)`, - `[@patternfly/Pagination - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Pagination/examples/Pagination.md)`, - `[@patternfly/Panel - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/panel/panel.md)`, - // `[@patternfly/Panel - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/panel/panel.md)`, - `[@patternfly/Panel - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Panel/examples/Panel.md)`, - `[@patternfly/Popover - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/popover/popover.md)`, - // `[@patternfly/Popover - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/popover/popover.md)`, - `[@patternfly/Popover - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Popover/examples/Popover.md)`, - `[@patternfly/Progress - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/progress/progress.md)`, - `[@patternfly/Progress - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/progress/progress.md)`, - `[@patternfly/Progress - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Progress/examples/Progress.md)`, - `[@patternfly/ProgressStepper - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/progress-stepper/progress-stepper.md)`, - `[@patternfly/ProgressStepper - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/progress-stepper/progress-stepper.md)`, - `[@patternfly/ProgressStepper - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/ProgressStepper/examples/ProgressStepper.md)`, - `[@patternfly/Radio - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/radio/radio.md)`, - `[@patternfly/Radio - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/radio/radio.md)`, - `[@patternfly/Radio - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Radio/examples/Radio.md)`, - `[@patternfly/SearchInput - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/search-input/search-input.md)`, - // `[@patternfly/SearchInput - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/search-input/search-input.md)`, - `[@patternfly/SearchInput - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/SearchInput/examples/SearchInput.md)`, - `[@patternfly/Select - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/select/select.md)`, - // `[@patternfly/Select - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/select/select.md)`, - `[@patternfly/Select - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Select/examples/Select.md)`, - `[@patternfly/Sidebar - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/sidebar/sidebar.md)`, - `[@patternfly/Sidebar - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/sidebar/sidebar.md)`, - `[@patternfly/Sidebar - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Sidebar/examples/Sidebar.md)`, - `[@patternfly/SimpleList - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/simple-list/simple-list.md)`, - // `[@patternfly/SimpleList - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/simple-list/simple-list.md)`, - `[@patternfly/SimpleList - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/SimpleList/examples/SimpleList.md)`, - `[@patternfly/Skeleton - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/skeleton/skeleton.md)`, - `[@patternfly/Skeleton - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/skeleton/skeleton.md)`, - `[@patternfly/Skeleton - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Skeleton/examples/Skeleton.md)`, - `[@patternfly/SkipToContent - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/skip-to-content/skip-to-content.md)`, - `[@patternfly/SkipToContent - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/skip-to-content/skip-to-content.md)`, - `[@patternfly/SkipToContent - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/SkipToContent/examples/SkipToContent.md)`, - `[@patternfly/Slider - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/slider/slider.md)`, - // `[@patternfly/Slider - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/slider/slider.md)`, - `[@patternfly/Slider - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Slider/examples/Slider.md)`, - `[@patternfly/Spinner - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/spinner/spinner.md)`, - // `[@patternfly/Spinner - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/spinner/spinner.md)`, - `[@patternfly/Spinner - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Spinner/examples/Spinner.md)`, - `[@patternfly/Switch - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/switch/switch.md)`, - `[@patternfly/Switch - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/switch/switch.md)`, - `[@patternfly/Switch - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Switch/examples/Switch.md)`, - `[@patternfly/Table - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/table/table.md)`, - // `[@patternfly/Table - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/table/table.md)`, - `[@patternfly/Table - Examples](${PF_EXTERNAL_EXAMPLES_TABLE}/Table/examples/Table.md)`, - `[@patternfly/Tabs - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/tabs/tabs.md)`, - `[@patternfly/Tabs - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/tabs/tabs.md)`, - `[@patternfly/Tabs - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Tabs/examples/Tabs.md)`, - `[@patternfly/TextArea - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/text-area/text-area.md)`, - // `[@patternfly/TextArea - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/text-area/text-area.md)`, - `[@patternfly/TextArea - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/TextArea/examples/TextArea.md)`, - `[@patternfly/TextInput - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/text-input/text-input.md)`, - // `[@patternfly/TextInput - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/text-input/text-input.md)`, - `[@patternfly/TextInput - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/TextInput/examples/TextInput.md)`, - `[@patternfly/TextInputGroup - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/text-input-group/text-input-group.md)`, - // `[@patternfly/TextInputGroup - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/text-input-group/text-input-group.md)`, - `[@patternfly/TextInputGroup - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/TextInputGroup/examples/TextInputGroup.md)`, - `[@patternfly/TileDeprecated - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/tile/tile.md)`, - // `[@patternfly/TileDeprecated - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/tile-deprecated/tile-deprecated.md)`, - // `[@patternfly/TileDeprecated - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/TileDeprecated/examples/TileDeprecated.md)`, - `[@patternfly/TimePicker - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/time%20picker/time-picker.md)`, - // `[@patternfly/TimePicker - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/time-picker/time-picker.md)`, - `[@patternfly/TimePicker - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/TimePicker/examples/TimePicker.md)`, - `[@patternfly/Timestamp - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/timestamp/timestamp.md)`, - // `[@patternfly/Timestamp - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/timestamp/timestamp.md)`, - `[@patternfly/Timestamp - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Timestamp/examples/Timestamp.md)`, - `[@patternfly/Title - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/title/title.md)`, - `[@patternfly/Title - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/title/title.md)`, - `[@patternfly/Title - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Title/examples/Title.md)`, - `[@patternfly/ToggleGroup - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/toggle-group/toggle-group.md)`, - // `[@patternfly/ToggleGroup - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/toggle-group/toggle-group.md)`, - `[@patternfly/ToggleGroup - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/ToggleGroup/examples/ToggleGroup.md)`, - `[@patternfly/Toolbar - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/toolbar/toolbar.md)`, - // `[@patternfly/Toolbar - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/toolbar/toolbar.md)`, - `[@patternfly/Toolbar - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Toolbar/examples/Toolbar.md)`, - `[@patternfly/Tooltip - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/tooltip/tooltip.md)`, - `[@patternfly/Tooltip - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/tooltip/tooltip.md)`, - `[@patternfly/Tooltip - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Tooltip/examples/Tooltip.md)`, - `[@patternfly/TreeView - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/tree-view/tree-view.md)`, - `[@patternfly/TreeView - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/tree-view/tree-view.md)`, - `[@patternfly/TreeView - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/TreeView/examples/TreeView.md)`, - `[@patternfly/Truncate - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/truncate/truncate.md)`, - // `[@patternfly/Truncate - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/truncate/truncate.md)`, - `[@patternfly/Truncate - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Truncate/examples/Truncate.md)`, - `[@patternfly/Wizard - Design Guidelines](${PF_EXTERNAL_DESIGN_COMPONENTS}/wizard/wizard.md)`, - // `[@patternfly/Wizard - Accessibility](${PF_EXTERNAL_ACCESSIBILITY}/wizard/wizard.md)`, - `[@patternfly/Wizard - Examples](${PF_EXTERNAL_EXAMPLES_REACT_CORE}/Wizard/examples/Wizard.md)` -]; - -export { COMPONENT_DOCS }; diff --git a/src/docs.layout.ts b/src/docs.layout.ts deleted file mode 100644 index 68c409a1..00000000 --- a/src/docs.layout.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { PF_EXTERNAL_DESIGN_LAYOUTS, PF_EXTERNAL_EXAMPLES_LAYOUTS } from './options.defaults'; - -const LAYOUT_DOCS = [ - `[@patternfly/Bullseye - Design Guidelines](${PF_EXTERNAL_DESIGN_LAYOUTS}/bullseye.md)`, - `[@patternfly/Bullseye - Examples](${PF_EXTERNAL_EXAMPLES_LAYOUTS}/Bullseye/examples/Bullseye.md)`, - `[@patternfly/Flex - Design Guidelines](${PF_EXTERNAL_DESIGN_LAYOUTS}/flex.md)`, - `[@patternfly/Flex - Examples](${PF_EXTERNAL_EXAMPLES_LAYOUTS}/Flex/examples/Flex.md)`, - `[@patternfly/Gallery - Design Guidelines](${PF_EXTERNAL_DESIGN_LAYOUTS}/gallery.md)`, - `[@patternfly/Gallery - Examples](${PF_EXTERNAL_EXAMPLES_LAYOUTS}/Gallery/examples/Gallery.md)`, - `[@patternfly/Grid - Design Guidelines](${PF_EXTERNAL_DESIGN_LAYOUTS}/grid.md)`, - `[@patternfly/Grid - Examples](${PF_EXTERNAL_EXAMPLES_LAYOUTS}/Grid/examples/Grid.md)`, - `[@patternfly/Level - Design Guidelines](${PF_EXTERNAL_DESIGN_LAYOUTS}/level.md)`, - `[@patternfly/Level - Examples](${PF_EXTERNAL_EXAMPLES_LAYOUTS}/Level/examples/Level.md)`, - `[@patternfly/Split - Design Guidelines](${PF_EXTERNAL_DESIGN_LAYOUTS}/split.md)`, - `[@patternfly/Split - Examples](${PF_EXTERNAL_EXAMPLES_LAYOUTS}/Split/examples/Split.md)`, - `[@patternfly/Stack - Design Guidelines](${PF_EXTERNAL_DESIGN_LAYOUTS}/stack.md)`, - `[@patternfly/Stack - Examples](${PF_EXTERNAL_EXAMPLES_LAYOUTS}/Stack/examples/Stack.md)` -]; - -export { LAYOUT_DOCS }; diff --git a/src/docs.local.ts b/src/docs.local.ts deleted file mode 100644 index 0afcca13..00000000 --- a/src/docs.local.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { join } from 'node:path'; -import { getOptions } from './options.context'; - -/** - * Get local documentation paths - * - * @param options - */ -const getLocalDocs = (options = getOptions()) => [ - `[@patternfly/react-charts](${join(options.docsPath, 'charts', 'README.md')})`, - `[@patternfly/react-chatbot](${join(options.docsPath, 'chatbot', 'README.md')})`, - `[@patternfly/react-component-groups](${join(options.docsPath, 'component-groups', 'README.md')})`, - `[@patternfly/react-components](${join(options.docsPath, 'components', 'README.md')})`, - `[@patternfly/react-guidelines](${join(options.docsPath, 'guidelines', 'README.md')})`, - `[@patternfly/react-resources](${join(options.docsPath, 'resources', 'README.md')})`, - `[@patternfly/react-setup](${join(options.docsPath, 'setup', 'README.md')})`, - `[@patternfly/react-troubleshooting](${join(options.docsPath, 'troubleshooting', 'README.md')})` -]; - -export { getLocalDocs }; diff --git a/src/options.assertions.ts b/src/options.assertions.ts new file mode 100644 index 00000000..03ee76c5 --- /dev/null +++ b/src/options.assertions.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; +import { mcpAssert } from './server.assertions'; + +/** + * Validates that each URL in the provided list uses one of the allowed schemes. + * + * @param urls - The list of URLs to be validated. + * @param protocols - Allowed scheme names (e.g. `['http', 'https']`). Each URL’s scheme is compared + * after normalizing to include a trailing colon (e.g. `http` matches `http:`). + * + * @throws {Error} Throws an error if any URL does not use one of the specified protocols. + */ +const assertProtocol = (urls: string[], protocols: string[]) => { + const validate = z.array( + z.string().url().refine( + url => { + try { + const urlScheme = new URL(url).protocol; + + return protocols.some(protocol => { + const expected = protocol.endsWith(':') ? protocol : `${protocol}:`; + + return urlScheme === expected.toLowerCase(); + }); + } catch { + return false; + } + }, + { + message: `Protocol entries must use allowed protocols: ${protocols.join(', ')}` + } + ) + ); + + const result = validate.safeParse(urls); + + mcpAssert( + result.success, + () => `Invalid URL protocol configuration: ${result.error?.message || urls.map(url => url.slice(0, 50)).join(', ')}` + ); +}; + +export { assertProtocol }; diff --git a/src/options.context.ts b/src/options.context.ts index 9d21ffe1..21ff92a5 100644 --- a/src/options.context.ts +++ b/src/options.context.ts @@ -9,6 +9,7 @@ import { type StatsSession } from './options.defaults'; import { mergeObjects, freezeObject, isPlainObject, hashCode } from './server.helpers'; +import { assertProtocol } from './options.assertions'; /** * AsyncLocalStorage instance for a per-instance session state. @@ -84,6 +85,9 @@ const optionsContext = new AsyncLocalStorage(); */ const setOptions = (options?: DefaultOptionsOverrides): GlobalOptions => { const base = mergeObjects(DEFAULT_OPTIONS, options, { allowNullValues: false, allowUndefinedValues: false }); + + assertProtocol(base.patternflyOptions.urlWhitelist, base.patternflyOptions.urlWhitelistProtocols); + const baseLogging = isPlainObject(base.logging) ? base.logging : DEFAULT_OPTIONS.logging; const basePluginIsolation = ['strict', 'none'].includes(base.pluginIsolation) ? base.pluginIsolation : DEFAULT_OPTIONS.pluginIsolation; diff --git a/src/options.defaults.ts b/src/options.defaults.ts index cc44bb1a..cd2cf68f 100644 --- a/src/options.defaults.ts +++ b/src/options.defaults.ts @@ -16,9 +16,7 @@ import { type ToolModule } from './server.toolsUser'; * @property isHttp - Flag indicating whether the server is running in HTTP mode. * @property {HttpOptions} http - HTTP server options. * @property {LoggingOptions} logging - Logging options. - * @property maxDocsToLoad - Maximum number of docs to load. - * @property maxSearchLength - Maximum length for search strings. - * @property recommendedMaxDocsToLoad - Recommended maximum number of docs to load. + * @property {MinMax} minMax - Minimum and maximum ranges for various options. * @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. @@ -27,18 +25,10 @@ import { type ToolModule } from './server.toolsUser'; * @property name - Name of the package. * @property nodeVersion - Node.js major version. * @property {PatternFlyOptions} patternflyOptions - PatternFly-specific options. - * @property pfExternal - PatternFly external docs URL. - * @property pfExternalDesignComponents - PatternFly design guidelines' components' URL. - * @property pfExternalExamplesComponents - PatternFly examples' core components' URL. - * @property pfExternalExamplesLayouts - PatternFly examples' core layouts' URL. - * @property pfExternalExamplesCharts - PatternFly examples' charts' components' URL. - * @property pfExternalExamplesTable - PatternFly examples' table components' URL. - * @property pfExternalChartsDesign - PatternFly charts' design guidelines URL. - * @property pfExternalDesignLayouts - PatternFly design guidelines' layouts' URL. - * @property pfExternalAccessibility - PatternFly accessibility URL. * @property pluginIsolation - Isolation preset for external plugins. * @property {PluginHostOptions} pluginHost - Plugin host options. * @property repoName - Name of the repository. + * @property {RepoResources} repoResources - Repository resources. * @property {typeof RESOURCE_MEMO_OPTIONS} resourceMemoOptions - Resource-level memoization options. * @property separator - Default string delimiter. * @property {StatsOptions} stats - Stats options. @@ -57,9 +47,7 @@ interface DefaultOptions { http: HttpOptions; isHttp: boolean; logging: TLogOptions; - maxDocsToLoad: number; - maxSearchLength: number; - recommendedMaxDocsToLoad: number; + minMax: MinMax; mode: 'cli' | 'programmatic' | 'test'; modeOptions: ModeOptions; name: string; @@ -67,16 +55,8 @@ interface DefaultOptions { patternflyOptions: PatternFlyOptions; pluginIsolation: 'none' | 'strict'; pluginHost: PluginHostOptions; - pfExternal: string; - pfExternalDesignComponents: string; - pfExternalExamplesComponents: string; - pfExternalExamplesLayouts: string; - pfExternalExamplesCharts: string; - pfExternalExamplesTable: string; - pfExternalChartsDesign: string; - pfExternalDesignLayouts: string; - pfExternalAccessibility: string; repoName: string | undefined; + repoResources: RepoResources; resourceMemoOptions: Partial; resourceModules: unknown | unknown[]; separator: string; @@ -144,6 +124,35 @@ interface LoggingOptions { transport: 'stdio' | 'mcp'; } +/** + * Minimum and maximum ranges for various options. + * + * @interface MinMax + * + * @property urlString Minimum and maximum length for URL strings. + * @property toolSearches Minimum and maximum number of tool searches. + * @property inputStrings Minimum and maximum length for input strings. + * @property docsToLoad Minimum and maximum number of docs to load. + */ +interface MinMax { + urlString: { + min: number; + max: number; + } + toolSearches: { + min: number; + max: number; + } + inputStrings: { + min: number; + max: number; + } + docsToLoad: { + min: number; + max: number; + } +} + /** * Mode-specific options. * @@ -159,6 +168,11 @@ interface ModeOptions { } | undefined; } +/** + * A string that must start with a valid protocol. + */ +type WhitelistUrl = `${'http' | 'https'}://${string}`; + /** * PatternFly-specific options. * @@ -173,10 +187,12 @@ interface ModeOptions { * @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. + * @property {WhitelistUrl[]} urlWhitelist List of allowed URLs to fetch PatternFly resources from. + * @property urlWhitelistProtocols List of allowed URL protocols to validate against when fetching PatternFly resources. */ interface PatternFlyOptions { availableResourceVersions: ('6.0.0')[]; - availableSearchVersions: ('current' | 'detected' | 'latest' | 'v6')[]; + availableSearchVersions: ('current' | 'latest' | 'v6')[]; availableSchemasVersions: ('v6')[]; default: { latestSemVer: '6.0.0'; @@ -184,7 +200,9 @@ interface PatternFlyOptions { latestSchemasVersion: 'v6'; versionWhitelist: string[]; versionStrategy: 'highest' | 'lowest'; - } + }, + urlWhitelist: WhitelistUrl[]; + urlWhitelistProtocols: string[]; } /** @@ -200,6 +218,19 @@ interface PluginHostOptions { gracePeriodMs: number; } +/** + * Repo resources. + * + * @property bugs URL for bug reports. + * @property git URL for the repository. + * @property homepage URL for the project homepage. + */ +interface RepoResources { + bugs: string; + git: string; + homepage: string; +} + /** * Logging session options, non-configurable by the user. * @@ -275,6 +306,28 @@ const HTTP_OPTIONS: HttpOptions = { allowedHosts: [] }; +/** + * Minimum and maximum ranges for various options. + */ +const MIN_MAX: MinMax = { + urlString: { + min: 11, + max: 1500 + }, + toolSearches: { + min: 0, + max: 10 + }, + inputStrings: { + min: 1, + max: 256 + }, + docsToLoad: { + min: 0, + max: 15 + } +}; + /** * Mode-specific options. */ @@ -293,6 +346,15 @@ const PLUGIN_HOST_OPTIONS: PluginHostOptions = { gracePeriodMs: 2000 }; +/** + * Default repo resources. + */ +const REPO_RESOURCES: RepoResources = { + bugs: packageJson.bugs?.url || '', + git: packageJson.repository?.url || '', + homepage: packageJson.homepage || '' +}; + /** * Default separator for joining multiple document contents */ @@ -371,7 +433,13 @@ const PATTERNFLY_OPTIONS: PatternFlyOptions = { '@patternfly/patternfly' ], versionStrategy: 'highest' - } + }, + urlWhitelist: [ + 'https://patternfly.org', + 'https://github.com/patternfly', + 'https://raw.githubusercontent.com/patternfly' + ], + urlWhitelistProtocols: ['http', 'https'] }; /** @@ -384,68 +452,6 @@ const URL_REGEX = /^(https?:)\/\//i; */ const MODE_LEVELS: DefaultOptions['mode'][] = ['cli', 'programmatic', 'test']; -const PF_EXTERNAL_EXAMPLES_VERSION = 'v6.4.0'; - -/** - * PatternFly examples URL - */ -const PF_EXTERNAL_EXAMPLES = `https://raw.githubusercontent.com/patternfly/patternfly-react/refs/tags/${PF_EXTERNAL_EXAMPLES_VERSION}/packages`; - -/** - * PatternFly examples' core components' URL. - */ -const PF_EXTERNAL_EXAMPLES_REACT_CORE = `${PF_EXTERNAL_EXAMPLES}/react-core/src/components`; - -/** - * PatternFly examples' core layouts' URL. - */ -const PF_EXTERNAL_EXAMPLES_LAYOUTS = `${PF_EXTERNAL_EXAMPLES}/react-core/src/layouts`; - -/** - * PatternFly examples' table components' URL. - */ -const PF_EXTERNAL_EXAMPLES_TABLE = `${PF_EXTERNAL_EXAMPLES}/react-table/src/components`; - -/** - * PatternFly charts' components' URL - */ -const PF_EXTERNAL_EXAMPLES_CHARTS = `${PF_EXTERNAL_EXAMPLES}/react-charts/src/victory/components`; - -/** - * PatternFly docs version to use, commit hash. Tags don't exist, but branches for older versions do. - * - * @see @patternfly/documentation-framework@6.30.0 - */ -const PF_EXTERNAL_VERSION = 'fb05713aba75998b5ecf5299ee3c1a259119bd74'; - -/** - * PatternFly docs root URL - */ -const PF_EXTERNAL = `https://raw.githubusercontent.com/patternfly/patternfly-org/${PF_EXTERNAL_VERSION}/packages/documentation-site/patternfly-docs/content`; - -/** - * PatternFly design guidelines' components' URL - * Updated 2025-11-24: Moved from design-guidelines/components to components - */ -const PF_EXTERNAL_DESIGN_COMPONENTS = `${PF_EXTERNAL}/design-guidelines/components`; - -/** - * PatternFly design guidelines' layouts' URL - * Updated 2025-11-24: Moved from design-guidelines/layouts to foundations-and-styles/layouts - */ -const PF_EXTERNAL_DESIGN_LAYOUTS = `${PF_EXTERNAL}/design-guidelines/layouts`; - -/** - * PatternFly accessibility URL - * Updated 2025-11-24: Moved from accessibility to components/accessibility - */ -const PF_EXTERNAL_ACCESSIBILITY = `${PF_EXTERNAL}/accessibility`; - -/** - * PatternFly charts' design guidelines URL - */ -const PF_EXTERNAL_CHARTS_DESIGN = `${PF_EXTERNAL}/design-guidelines/charts`; - /** * Get the current Node.js major version. * @@ -479,27 +485,17 @@ const DEFAULT_OPTIONS: DefaultOptions = { isHttp: false, http: HTTP_OPTIONS, logging: LOGGING_OPTIONS, - maxDocsToLoad: 500, - maxSearchLength: 256, - recommendedMaxDocsToLoad: 15, + minMax: MIN_MAX, mode: 'programmatic', modeOptions: MODE_OPTIONS, name: packageJson.name, nodeVersion: (process.env.NODE_ENV === 'local' && 22) || getNodeMajorVersion(), patternflyOptions: PATTERNFLY_OPTIONS, - pfExternal: PF_EXTERNAL, - pfExternalDesignComponents: PF_EXTERNAL_DESIGN_COMPONENTS, - pfExternalExamplesComponents: PF_EXTERNAL_EXAMPLES_REACT_CORE, - pfExternalExamplesLayouts: PF_EXTERNAL_EXAMPLES_LAYOUTS, - pfExternalExamplesCharts: PF_EXTERNAL_EXAMPLES_CHARTS, - pfExternalExamplesTable: PF_EXTERNAL_EXAMPLES_TABLE, - pfExternalChartsDesign: PF_EXTERNAL_CHARTS_DESIGN, - pfExternalDesignLayouts: PF_EXTERNAL_DESIGN_LAYOUTS, - pfExternalAccessibility: PF_EXTERNAL_ACCESSIBILITY, pluginIsolation: 'strict', pluginHost: PLUGIN_HOST_OPTIONS, - resourceMemoOptions: RESOURCE_MEMO_OPTIONS, repoName: basename(process.cwd() || '').trim(), + repoResources: REPO_RESOURCES, + resourceMemoOptions: RESOURCE_MEMO_OPTIONS, stats: STATS_OPTIONS, resourceModules: [], toolMemoOptions: TOOL_MEMO_OPTIONS, @@ -514,18 +510,6 @@ export { LOG_BASENAME, DEFAULT_OPTIONS, MODE_LEVELS, - PF_EXTERNAL, - PF_EXTERNAL_VERSION, - PF_EXTERNAL_EXAMPLES, - PF_EXTERNAL_EXAMPLES_CHARTS, - PF_EXTERNAL_EXAMPLES_REACT_CORE, - PF_EXTERNAL_EXAMPLES_LAYOUTS, - PF_EXTERNAL_EXAMPLES_TABLE, - PF_EXTERNAL_EXAMPLES_VERSION, - PF_EXTERNAL_CHARTS_DESIGN, - PF_EXTERNAL_DESIGN_COMPONENTS, - PF_EXTERNAL_DESIGN_LAYOUTS, - PF_EXTERNAL_ACCESSIBILITY, getNodeMajorVersion, type DefaultOptions, type DefaultOptionsOverrides, @@ -536,5 +520,6 @@ export { type PatternFlyOptions, type PluginHostOptions, type StatsSession, + type WhitelistUrl, type XhrFetchOptions }; diff --git a/src/resource.patternFlyComponentsIndex.ts b/src/resource.patternFlyComponentsIndex.ts new file mode 100644 index 00000000..8240a293 --- /dev/null +++ b/src/resource.patternFlyComponentsIndex.ts @@ -0,0 +1,109 @@ +import { type McpResource } from './server'; +import { buildSearchString, stringJoin } from './server.helpers'; +import { assertInput, assertInputStringLength } from './server.assertions'; +import { getOptions, runWithOptions } from './options.context'; +import { normalizeEnumeratedPatternFlyVersion } from './patternFly.helpers'; +import { getPatternFlyMcpResources } from './patternFly.getResources'; +import { filterPatternFly } from './patternFly.search'; + +/** + * Name of the resource. + */ +const NAME = 'patternfly-components-index'; + +/** + * URI template for the resource. + */ +const URI_TEMPLATE = 'patternfly://components/index'; + +/** + * Resource configuration. + */ +const CONFIG = { + title: 'PatternFly Components Index', + description: 'A list of all PatternFly component names available for documentation retrieval', + mimeType: 'text/markdown' +}; + +/** + * Resource callback for the documentation index. + * + * @param passedUri - URI of the resource. + * @param variables - Variables for the resource. + * @param options - Options for the resource. + * @returns The resource contents. + */ +const resourceCallback = async (passedUri: URL, variables: Record, options = getOptions()) => { + const { version, category } = variables || {}; + const section = 'components'; + + if (version) { + assertInputStringLength(version, { + ...options.minMax.inputStrings, + inputDisplayName: 'version' + }); + } + + if (category) { + assertInputStringLength(category, { + ...options.minMax.inputStrings, + inputDisplayName: 'category' + }); + } + + const { availableVersions, latestVersion } = await getPatternFlyMcpResources.memo(); + const normalizedVersion = await normalizeEnumeratedPatternFlyVersion.memo(version); + + assertInput( + !version || Boolean(normalizedVersion), + `Invalid PatternFly version "${version?.trim()}". Available versions are: ${availableVersions.join(', ')}` + ); + + const updatedVersion = normalizedVersion || latestVersion; + const { byResource } = await filterPatternFly.memo({ version: updatedVersion, section, category }); + + const docsIndex = Array.from(byResource.entries()) + .sort(([_aUri, aData], [_bUri, bData]) => aData.name.localeCompare(bData.name)) + .map(([_name, data], index) => { + const searchString = buildSearchString({ + version: updatedVersion, + category + }, { prefix: true }); + + return `${index + 1}. [${data.name} (${updatedVersion})](${data.uri}${searchString || ''})`; + }); + + return { + contents: [{ + uri: passedUri?.toString() || 'patternfly://components/index', + mimeType: 'text/markdown', + text: stringJoin.newline( + `# PatternFly Component Names Index for "${updatedVersion}"`, + '', + '', + ...docsIndex || [] + ) + }] + }; +}; + +/** + * Resource creator for the component schemas index. + * + * @param options - Global options + * @returns {McpResource} The resource definition tuple + */ +const patternFlyComponentsIndexResource = (options = getOptions()): McpResource => [ + NAME, + URI_TEMPLATE, + CONFIG, + async (uri, variables) => runWithOptions(options, async () => resourceCallback(uri, variables)) +]; + +export { + patternFlyComponentsIndexResource, + resourceCallback, + NAME, + URI_TEMPLATE, + CONFIG +}; diff --git a/src/resource.patternFlyContext.ts b/src/resource.patternFlyContext.ts index fd306e4b..15cbbbd0 100644 --- a/src/resource.patternFlyContext.ts +++ b/src/resource.patternFlyContext.ts @@ -1,4 +1,5 @@ import { type McpResource } from './server'; +import { stringJoin } from './server.helpers'; /** * Name of the resource. @@ -22,6 +23,16 @@ const CONFIG = { /** * Resource creator for context. * + * @note Consider adding an environment snapshot here once contextual MCP tooling is available. + * ``` + * const environmentSnapshot = stringJoin.newline( + * `### Environment Snapshot`, + * `**PatternFly Version:** ${detectedVersion}`, + * `**Detected PatternFly SemVer:** ${detectedSemverVersion}`, + * `**Context Path**: ${detectedProjectPath}` + * ); + * ``` + * * @returns {McpResource} The resource definition tuple */ const patternFlyContextResource = (): McpResource => [ @@ -48,7 +59,7 @@ This MCP server provides tools to access PatternFly documentation, component sch { uri: 'patternfly://context', mimeType: 'text/markdown', - text: context + text: stringJoin.basic(context) } ] }; diff --git a/src/resource.patternFlyDocsIndex.ts b/src/resource.patternFlyDocsIndex.ts index b212cf92..901e9edf 100644 --- a/src/resource.patternFlyDocsIndex.ts +++ b/src/resource.patternFlyDocsIndex.ts @@ -1,9 +1,11 @@ -import { COMPONENT_DOCS } from './docs.component'; -import { LAYOUT_DOCS } from './docs.layout'; -import { CHART_DOCS } from './docs.chart'; -import { getLocalDocs } from './docs.local'; import { type McpResource } from './server'; import { stringJoin } from './server.helpers'; +import { assertInput, assertInputStringLength } from './server.assertions'; +import { buildSearchString } from './server.helpers'; +import { getPatternFlyMcpResources } from './patternFly.getResources'; +import { getOptions, runWithOptions } from './options.context'; +import { normalizeEnumeratedPatternFlyVersion } from './patternFly.helpers'; +import { filterPatternFly } from './patternFly.search'; /** * Name of the resource. @@ -20,46 +22,136 @@ const URI_TEMPLATE = 'patternfly://docs/index'; */ const CONFIG = { title: 'PatternFly Documentation Index', - description: 'A comprehensive list of PatternFly documentation links, organized by components, layouts, charts, and local files.', + description: 'A comprehensive list of PatternFly documentation links, organized by components, layouts, charts, and guidance files.', mimeType: 'text/markdown' }; +/** + * Resource callback for the documentation index. + * + * @param passedUri - URI of the resource. + * @param variables - Variables for the resource. + * @param options - Global options + * @returns The resource contents. + */ +const resourceCallback = async (passedUri: URL, variables: Record, options = getOptions()) => { + const { category, version, section } = variables || {}; + + if (version) { + assertInputStringLength(version, { + ...options.minMax.inputStrings, + inputDisplayName: 'version' + }); + } + + if (category) { + assertInputStringLength(category, { + ...options.minMax.inputStrings, + inputDisplayName: 'category' + }); + } + + if (section) { + assertInputStringLength(section, { + ...options.minMax.inputStrings, + inputDisplayName: 'section' + }); + } + + const { availableVersions, latestVersion } = await getPatternFlyMcpResources.memo(); + const normalizedVersion = await normalizeEnumeratedPatternFlyVersion.memo(version); + + assertInput( + !version || Boolean(normalizedVersion), + `Invalid PatternFly version "${version?.trim()}". Available versions are: ${availableVersions.join(', ')}` + ); + + const updatedVersion = normalizedVersion || latestVersion; + + const { byEntry } = await filterPatternFly.memo({ + version: updatedVersion, + category, + section + }); + + // Group by URI + const groupedByUri = new Map }>(); + + byEntry.forEach(entry => { + if (!groupedByUri.has(entry.uri)) { + groupedByUri.set(entry.uri, { + name: entry.name, + version: entry.version, + categories: new Set([entry.displayCategory]) + }); + } else { + groupedByUri.get(entry.uri)?.categories.add(entry.displayCategory); + } + }); + + // Generate the consolidated list, apply search/query string + const docsIndex = Array.from(groupedByUri.entries()) + .sort(([_aUri, aData], [_bUri, bData]) => aData.name.localeCompare(bData.name)) + .map(([uri, data], index) => { + const categoryList = Array.from(data.categories).join(', '); + const searchString = buildSearchString({ section, category }, { prefix: true }); + + return `${index + 1}. [${data.name} - ${categoryList} (${data.version})](${uri}${searchString || ''})`; + }); + + assertInput( + docsIndex.length > 0, + () => { + let suggestionMessage = ''; + + if (category || section) { + const variableList = [ + (category && 'category') || undefined, + (section && 'section') || undefined + ].filter(Boolean).join(' or '); + + suggestionMessage = ` Try using a different ${variableList} search.`; + } + + return `No documentation found for "${passedUri?.toString()}".${suggestionMessage}`; + } + ); + + const allDocs = stringJoin.newline( + `# PatternFly Documentation Index for "${updatedVersion}"`, + '', + '', + ...(docsIndex || []) + ); + + return { + contents: [ + { + uri: passedUri?.toString() || 'patternfly://docs/index', + mimeType: 'text/markdown', + text: allDocs + } + ] + }; +}; + /** * Resource creator for the documentation index. * + * @param options - Global options * @returns {McpResource} The resource definition tuple */ -const patternFlyDocsIndexResource = (): McpResource => [ +const patternFlyDocsIndexResource = (options = getOptions()): McpResource => [ NAME, URI_TEMPLATE, CONFIG, - async () => { - const allDocs = stringJoin.newline( - '# PatternFly Documentation Index', - '', - '## Components', - ...COMPONENT_DOCS, - '', - '## Layouts', - ...LAYOUT_DOCS, - '', - '## Charts', - ...CHART_DOCS, - '', - '## Local Documentation', - ...getLocalDocs() - ); - - return { - contents: [ - { - uri: 'patternfly://docs/index', - mimeType: 'text/markdown', - text: allDocs - } - ] - }; - } + async (uri, variables) => runWithOptions(options, async () => resourceCallback(uri, variables, options)) ]; -export { patternFlyDocsIndexResource, NAME, URI_TEMPLATE, CONFIG }; +export { + patternFlyDocsIndexResource, + resourceCallback, + NAME, + URI_TEMPLATE, + CONFIG +}; diff --git a/src/resource.patternFlyDocsTemplate.ts b/src/resource.patternFlyDocsTemplate.ts index c15d05fe..719cc9ac 100644 --- a/src/resource.patternFlyDocsTemplate.ts +++ b/src/resource.patternFlyDocsTemplate.ts @@ -2,10 +2,12 @@ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { type McpResource } from './server'; import { processDocsFunction } from './server.getResources'; -import { searchComponents } from './tool.searchPatternFlyDocs'; -import { getOptions } from './options.context'; -import { memo } from './server.caching'; import { stringJoin } from './server.helpers'; +import { assertInput, assertInputStringLength } from './server.assertions'; +import { getOptions, runWithOptions } from './options.context'; +import { getPatternFlyMcpResources } from './patternFly.getResources'; +import { normalizeEnumeratedPatternFlyVersion } from './patternFly.helpers'; +import { filterPatternFly } from './patternFly.search'; /** * Name of the resource template. @@ -15,7 +17,7 @@ const NAME = 'patternfly-docs-template'; /** * URI template for the resource. */ -const URI_TEMPLATE = new ResourceTemplate('patternfly://docs/{name}', { list: undefined }); +const URI_TEMPLATE = 'patternfly://docs/{name}'; /** * Resource configuration. @@ -27,93 +29,134 @@ const CONFIG = { }; /** - * Resource creator for the documentation template. + * Resource callback for the documentation template. * + * @param passedUri - URI of the resource. + * @param variables - Variables for the resource. * @param options - Global options - * @returns {McpResource} The resource definition tuple + * @returns The resource contents. */ -const patternFlyDocsTemplateResource = (options = getOptions()): McpResource => { - const memoProcess = memo(processDocsFunction, options?.toolMemoOptions?.usePatternFlyDocs); - - return [ - NAME, - URI_TEMPLATE, - CONFIG, - async (uri: URL, variables: Record) => { - const { name } = variables || {}; - - if (!name || typeof name !== 'string') { - throw new McpError( - ErrorCode.InvalidParams, - `Missing required parameter: name must be a string: ${name}` - ); - } - - if (name.length > options.maxSearchLength) { - throw new McpError( - ErrorCode.InvalidParams, - `Resource name exceeds maximum length of ${options.maxSearchLength} characters.` - ); - } - - const docResults = []; - const docs = []; - const { exactMatches, searchResults } = searchComponents.memo(name); - - if (exactMatches.length === 0 || exactMatches.every(match => match.urls.length === 0)) { - const suggestions = searchResults.map(searchResult => searchResult.item).slice(0, 3); - const suggestionMessage = suggestions.length - ? `Did you mean ${suggestions.map(suggestion => `"${suggestion}"`).join(', ')}?` - : 'No similar components found.'; - - throw new McpError( - ErrorCode.InvalidParams, - `No documentation found for component "${name.trim()}". ${suggestionMessage}` - ); - } - - try { - const exactMatchesUrls = exactMatches.flatMap(match => match.urls); - - if (exactMatchesUrls.length > 0) { - const processedDocs = await memoProcess(exactMatchesUrls); - - docs.push(...processedDocs); - } - } catch (error) { - throw new McpError( - ErrorCode.InternalError, - `Failed to fetch documentation: ${error}` - ); - } - - // Redundancy check, technically this should never happen, future proofing - if (docs.length === 0) { - throw new McpError( - ErrorCode.InvalidParams, - `Component "${name.trim()}" was found, but no documentation URLs are available for it.` - ); - } - - for (const doc of docs) { - docResults.push(stringJoin.newline( - `# Documentation from ${doc.resolvedPath || doc.path}`, - '', - doc.content - )); +const resourceCallback = async (passedUri: URL, variables: Record, options = getOptions()) => { + const { category, name, section, version } = variables || {}; + + if (version) { + assertInputStringLength(version, { + ...options.minMax.inputStrings, + inputDisplayName: 'version' + }); + } + + assertInputStringLength(name, { + ...options.minMax.inputStrings, + inputDisplayName: 'name' + }); + + if (section) { + assertInputStringLength(section, { + ...options.minMax.inputStrings, + inputDisplayName: 'section' + }); + } + + if (category) { + assertInputStringLength(category, { + ...options.minMax.inputStrings, + inputDisplayName: 'category' + }); + } + + const { availableVersions, latestVersion } = await getPatternFlyMcpResources.memo(); + const normalizedVersion = await normalizeEnumeratedPatternFlyVersion.memo(version); + + assertInput( + !version || Boolean(normalizedVersion), + `Invalid PatternFly version "${version?.trim()}". Available versions are: ${availableVersions.join(', ')}` + ); + + const updatedVersion = normalizedVersion || latestVersion; + const updatedName = name.trim(); + + const { byEntry } = await filterPatternFly.memo({ + version: updatedVersion, + name: updatedName, + category, + section + }); + + const docResults = []; + const docs = []; + + try { + const matchedUrls = byEntry.map(entry => entry.path).filter(Boolean); + + if (matchedUrls.length > 0) { + const processedDocs = await processDocsFunction.memo(matchedUrls); + + docs.push(...processedDocs); + } + } catch (error) { + throw new McpError( + ErrorCode.InternalError, + `Failed to fetch documentation: ${error}` + ); + } + + assertInput( + docs.length > 0, + () => { + let suggestionMessage = ''; + + if (category || section) { + const variableList = [ + (category && 'category') || undefined, + (section && 'section') || undefined + ].filter(Boolean).join(' or '); + + suggestionMessage = ` Try using a different ${variableList} search.`; } - return { - contents: [ - { - uri: uri.href, - mimeType: 'text/markdown', - text: docResults.join(options.separator) - } - ] - }; + return `"${updatedName}" was found, but no documentation URLs are available for it.${suggestionMessage}`; } - ]; + ); + + for (const doc of docs) { + docResults.push(stringJoin.newline( + `# Documentation from ${doc.resolvedPath || doc.path}`, + '', + doc.content + )); + } + + return { + contents: [ + { + uri: passedUri?.toString() || `patternfly://docs/${updatedVersion}/${updatedName}`, + mimeType: 'text/markdown', + text: docResults.join(options.separator) + } + ] + }; }; -export { patternFlyDocsTemplateResource, NAME, URI_TEMPLATE, CONFIG }; +/** + * Resource creator for the documentation template. + * + * @param options - Global options + * @returns {McpResource} The resource definition tuple + */ +const patternFlyDocsTemplateResource = (options = getOptions()): McpResource => [ + NAME, + new ResourceTemplate(URI_TEMPLATE, { + list: undefined + }), + CONFIG, + async (uri, variables) => runWithOptions(options, async () => resourceCallback(uri, variables, options)) +]; + +export { + patternFlyDocsTemplateResource, + resourceCallback, + NAME, + URI_TEMPLATE, + CONFIG +}; diff --git a/src/resource.patternFlySchemasIndex.ts b/src/resource.patternFlySchemasIndex.ts index bbb8ec0d..283ee8a6 100644 --- a/src/resource.patternFlySchemasIndex.ts +++ b/src/resource.patternFlySchemasIndex.ts @@ -1,6 +1,10 @@ -import { componentNames } from '@patternfly/patternfly-component-schemas/json'; import { type McpResource } from './server'; import { stringJoin } from './server.helpers'; +import { getOptions, runWithOptions } from './options.context'; +import { getPatternFlyMcpResources } from './patternFly.getResources'; +import { normalizeEnumeratedPatternFlyVersion } from './patternFly.helpers'; +import { filterPatternFly } from './patternFly.search'; +import { assertInput, assertInputStringLength } from './server.assertions'; /** * Name of the resource. @@ -22,26 +26,100 @@ const CONFIG = { }; /** - * Resource creator for the component schemas index. + * Resource callback for the documentation index. * - * @returns {McpResource} The resource definition tuple + * @param passedUri - URI of the resource. + * @param variables - Variables for the resource. + * @param options - Options for the resource callback. + * @returns The resource contents. */ -const patternFlySchemasIndexResource = (): McpResource => [ - NAME, - URI_TEMPLATE, - CONFIG, - async () => ({ +const resourceCallback = async (passedUri: URL, variables: Record, options = getOptions()) => { + const { version } = variables || {}; + + if (version) { + assertInputStringLength(version, { + ...options.minMax.inputStrings, + inputDisplayName: 'version' + }); + } + + const { availableSchemasVersions, latestSchemasVersion } = await getPatternFlyMcpResources.memo(); + const normalizedVersion = await normalizeEnumeratedPatternFlyVersion.memo(version); + + assertInput( + !version || Boolean(normalizedVersion), + `Invalid PatternFly version "${version?.trim()}". Available versions are: ${availableSchemasVersions.join(', ')}` + ); + + const updatedVersion = normalizedVersion || latestSchemasVersion; + + let docsIndex: string[] = []; + + if (availableSchemasVersions.includes(updatedVersion)) { + const { byResource } = await filterPatternFly.memo({ + version: updatedVersion + }); + + const groupedByUri = new Map(); + + byResource.forEach(resource => { + if (resource.uriSchemas) { + groupedByUri.set(resource.uriSchemas, { name: resource.name, version: updatedVersion }); + } + }); + + docsIndex = Array.from(groupedByUri.entries()) + .sort(([_aUri, aData], [_bUri, bData]) => aData.name.localeCompare(bData.name)) + .map(([uri, data], index) => `${index + 1}. [${data.name} (${data.version})](${uri})`); + } + + assertInput( + docsIndex.length > 0, + () => { + let suggestionMessage = ''; + + if (!availableSchemasVersions.includes(updatedVersion)) { + suggestionMessage = ` Component schemas are only available for PatternFly versions ${availableSchemasVersions.join(', ')}`; + } + + return `No component JSON schemas found for "${passedUri?.toString()}".${suggestionMessage}`; + } + ); + + return { contents: [{ - uri: 'patternfly://schemas/index', + uri: passedUri?.toString(), mimeType: 'text/markdown', text: stringJoin.newline( - '# PatternFly Component Names Index', + `# PatternFly Component JSON Schemas Index for "${updatedVersion}"`, '', '', - ...componentNames + ...docsIndex ) }] - }) + }; +}; + +/** + * Resource creator for the component schemas index. + * + * @note This resource is being considered for deprecation in favor of a more + * all encompassing resource, like "resource.patternFlyComponentsIndex." + * + * @param options - Global options + * @returns {McpResource} The resource definition tuple + */ +const patternFlySchemasIndexResource = (options = getOptions()): McpResource => [ + NAME, + URI_TEMPLATE, + CONFIG, + async (uri, variables) => runWithOptions(options, async () => resourceCallback(uri, variables, options)) ]; -export { patternFlySchemasIndexResource, NAME, URI_TEMPLATE, CONFIG }; +export { + patternFlySchemasIndexResource, + resourceCallback, + NAME, + URI_TEMPLATE, + CONFIG +}; diff --git a/src/resource.patternFlySchemasTemplate.ts b/src/resource.patternFlySchemasTemplate.ts index ac051bf6..237bc916 100644 --- a/src/resource.patternFlySchemasTemplate.ts +++ b/src/resource.patternFlySchemasTemplate.ts @@ -1,15 +1,14 @@ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; -import { componentNames as pfComponentNames } from '@patternfly/patternfly-component-schemas/json'; import { type McpResource } from './server'; -import { getOptions } from './options.context'; -import { getComponentSchema } from './tool.patternFlyDocs'; -import { searchComponents } from './tool.searchPatternFlyDocs'; - -/** - * Derive the component schema type from @patternfly/patternfly-component-schemas - */ -type ComponentSchema = Awaited>; +import { getOptions, runWithOptions } from './options.context'; +import { filterPatternFly } from './patternFly.search'; +import { + getPatternFlyComponentSchema, + getPatternFlyMcpResources, + type PatternFlyComponentSchema +} from './patternFly.getResources'; +import { normalizeEnumeratedPatternFlyVersion } from './patternFly.helpers'; +import { assertInput, assertInputStringLength } from './server.assertions'; /** * Name of the resource template. @@ -19,7 +18,7 @@ const NAME = 'patternfly-schemas-template'; /** * URI template for the resource. */ -const URI_TEMPLATE = new ResourceTemplate('patternfly://schemas/{name}', { list: undefined }); +const URI_TEMPLATE = 'patternfly://schemas/{name}'; /** * Resource configuration. @@ -31,69 +30,105 @@ const CONFIG = { }; /** - * Resource creator for the component schemas template. + * Resource callback for the documentation template. + * + * @note We temporarily use `DEFAULT_OPTIONS` `latestSchemasVersion` * + * @param passedUri - The URI of the resource. + * @param variables - The variables of the resource. * @param options - Global options - * @returns {McpResource} The resource definition tuple + * @returns The resource contents. */ -const patternFlySchemasTemplateResource = (options = getOptions()): McpResource => [ - NAME, - URI_TEMPLATE, - CONFIG, - async (uri: URL, variables: Record) => { - const { name } = variables || {}; - - if (!name || typeof name !== 'string') { - throw new McpError( - ErrorCode.InvalidParams, - `Missing required parameter: name must be a string: ${name}` - ); - } +const resourceCallback = async (passedUri: URL, variables: Record, options = getOptions()) => { + const { version, name } = variables || {}; + + if (version) { + assertInputStringLength(version, { + ...options.minMax.inputStrings, + inputDisplayName: 'version' + }); + } + + assertInputStringLength(name, { + ...options.minMax.inputStrings, + inputDisplayName: 'name' + }); + + const { availableSchemasVersions, latestSchemasVersion } = await getPatternFlyMcpResources.memo(); + const normalizedVersion = await normalizeEnumeratedPatternFlyVersion.memo(version); + + assertInput( + !version || Boolean(normalizedVersion), + `Invalid PatternFly version "${version?.trim()}". Available versions are: ${availableSchemasVersions.join(', ')}` + ); + + const updatedVersion = normalizedVersion || latestSchemasVersion; + const updatedName = name.trim(); + + const { byEntry } = await filterPatternFly.memo({ + version: updatedVersion, + name: updatedName + }); + + let result: PatternFlyComponentSchema | undefined; + const matchedSchemas: string[] = []; - if (name.length > options.maxSearchLength) { - throw new McpError( - ErrorCode.InvalidParams, - `Resource name exceeds maximum length of ${options.maxSearchLength} characters.` - ); + byEntry.forEach(result => { + if (result.uriSchemas) { + matchedSchemas.push(result.name); } + }); - const { exactMatches, searchResults } = searchComponents.memo(name, { names: pfComponentNames }); - let result: ComponentSchema | undefined = undefined; + if (matchedSchemas[0]) { + result = await getPatternFlyComponentSchema.memo(matchedSchemas[0]); + } - if (exactMatches.length > 0) { - for (const match of exactMatches) { - const schema = await getComponentSchema.memo(match.item); + assertInput( + matchedSchemas.length > 0 && result !== undefined, + () => { + let suggestionMessage = ''; - if (schema) { - result = schema; - break; - } + if (!availableSchemasVersions.includes(updatedVersion)) { + suggestionMessage = ` Component schemas are only available for PatternFly versions ${availableSchemasVersions.join(', ')}`; } - } - if (result === undefined) { - const suggestions = searchResults.map(searchResult => searchResult.item).slice(0, 3); - const suggestionMessage = suggestions.length - ? `Did you mean ${suggestions.map(suggestion => `"${suggestion}"`).join(', ')}?` - : 'No similar components found.'; - const foundNotFound = exactMatches.length ? 'found but JSON schema not available.' : 'not found.'; - - throw new McpError( - ErrorCode.InvalidParams, - `Component "${name.trim()}" ${foundNotFound} ${suggestionMessage}` - ); + return `No component JSON schemas found for "${passedUri?.toString()}".${suggestionMessage}`; } + ); - return { - contents: [ - { - uri: uri.href, - mimeType: 'application/json', - text: JSON.stringify(result, null, 2) - } - ] - }; - } + return { + contents: [ + { + uri: passedUri?.toString() || `patternfly://schemas/${updatedVersion}/${updatedName}`, + mimeType: 'application/json', + text: JSON.stringify(result, null, 2) + } + ] + }; +}; + +/** + * Resource creator for the component schemas template. + * + * @note This resource is being considered for deprecation in favor of a more + * all encompassing resource, like "resource.patternFlyComponentsTemplate." + * + * @param options - Global options + * @returns {McpResource} The resource definition tuple + */ +const patternFlySchemasTemplateResource = (options = getOptions()): McpResource => [ + NAME, + new ResourceTemplate(URI_TEMPLATE, { + list: undefined + }), + CONFIG, + async (uri, variables) => runWithOptions(options, async () => resourceCallback(uri, variables, options)) ]; -export { patternFlySchemasTemplateResource, NAME, URI_TEMPLATE, CONFIG }; +export { + patternFlySchemasTemplateResource, + resourceCallback, + NAME, + URI_TEMPLATE, + CONFIG +}; diff --git a/src/server.assertions.ts b/src/server.assertions.ts index 3c12fbef..a226dfdf 100644 --- a/src/server.assertions.ts +++ b/src/server.assertions.ts @@ -1,5 +1,7 @@ import assert from 'node:assert'; import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; +import { isWhitelistedUrl, stringJoin } from './server.helpers'; +import { DEFAULT_OPTIONS, type WhitelistUrl } from './options.defaults'; /** * MCP assert. Centralizes and throws an error if the validation fails. @@ -52,7 +54,7 @@ function assertInput( */ function assertInputString( input: unknown, - { inputDisplayName, message }: { inputDisplayName?: string, message?: string } = {} + { inputDisplayName, message }: { inputDisplayName?: string; message?: string; } = {} ): asserts input is string { const isValid = typeof input === 'string' && input.trim().length > 0; @@ -73,7 +75,7 @@ function assertInputString( */ function assertInputStringLength( input: unknown, - { max, min, inputDisplayName, message }: { max: number, min: number, inputDisplayName?: string, message?: string } + { max, min, inputDisplayName, message }: { max: number; min: number; inputDisplayName?: string; message?: string } ): asserts input is string { const isValid = typeof input === 'string' && input.trim().length <= max && input.trim().length >= min; @@ -94,7 +96,7 @@ function assertInputStringLength( */ function assertInputStringArrayEntryLength( input: unknown, - { max, min, inputDisplayName, message }: { max: number, min: number, inputDisplayName?: string, message?: string } + { max, min, inputDisplayName, message }: { max: number; min: number; inputDisplayName?: string; message?: string } ): asserts input is string[] { const isValid = Array.isArray(input) && input.every(entry => typeof entry === 'string' && entry.trim().length <= max && entry.trim().length >= min); @@ -115,7 +117,7 @@ function assertInputStringArrayEntryLength( function assertInputStringNumberEnumLike( input: unknown, values: unknown, - { inputDisplayName, message }: { inputDisplayName?: string, message?: string } = {} + { inputDisplayName, message }: { inputDisplayName?: string; message?: string } = {} ): asserts input is string | number { const hasArrayWithLength = Array.isArray(values) && values.length > 0; let updatedDescription; @@ -135,10 +137,52 @@ function assertInputStringNumberEnumLike( mcpAssert(isValid, updatedDescription, errorCode); } +/** + * Assert/validate that a given input URL string, or array of URL strings, is whitelisted against a provided list of URLs. + * + * @param input - Input URL string, or array of URL strings, to validate. + * @param {WhitelistUrl[]} whitelist - The list of allowed URLs to compare against. + * @param [options] - Validation options + * @param [options.allowedProtocols] - Optional list of allowed URL protocols to validate against. + * @param [options.inputDisplayName] - Optional display name for the input parameter, used in error messages. + * @param [options.message] - Optional custom error message to override the default message. + * @param [options.urlDisplayMaxLength] - Optional maximum length of an invalid URL to display in error messages + */ +function assertInputUrlWhiteListed( + input: unknown, + whitelist: WhitelistUrl[], + { allowedProtocols = DEFAULT_OPTIONS.patternflyOptions.urlWhitelistProtocols, inputDisplayName, message, urlDisplayMaxLength = 50 }: { + allowedProtocols?: string[]; inputDisplayName?: string; message?: string; urlDisplayMaxLength?: number + } = {} +): asserts input is string | string[] { + const updatedInput = Array.isArray(input) ? input : [input]; + const invalidUrls: unknown[] = []; + + updatedInput.forEach(url => { + const isRemote = typeof url === 'string' && allowedProtocols.some(protocol => url.startsWith(protocol)); + + if (isRemote && !isWhitelistedUrl(url, whitelist, { allowedProtocols })) { + invalidUrls.push(url); + } + }); + + mcpAssert( + invalidUrls.length === 0, + () => message || stringJoin.newline( + `Access denied: "${inputDisplayName || 'URL input'}" must be within the whitelisted URLs.`, + `Use official PatternFly documentation sources.`, + ...invalidUrls.map(invalid => `Invalid URL: ${String(invalid).slice(0, urlDisplayMaxLength)}...`) + ), + ErrorCode.InvalidParams + ); +} + export { + mcpAssert, assertInput, assertInputString, assertInputStringLength, assertInputStringArrayEntryLength, - assertInputStringNumberEnumLike + assertInputStringNumberEnumLike, + assertInputUrlWhiteListed }; diff --git a/src/server.getResources.ts b/src/server.getResources.ts index f74eac7f..b5e49671 100644 --- a/src/server.getResources.ts +++ b/src/server.getResources.ts @@ -290,7 +290,7 @@ const processDocsFunction = async ( const uniqueInputs = new Map( inputs.map(input => [normalizeString.memo(input), input.trim()]) ); - const list = Array.from(uniqueInputs.values()).slice(0, options.maxDocsToLoad).filter(Boolean); + const list = Array.from(uniqueInputs.values()).slice(0, options.minMax.docsToLoad.max).filter(Boolean); const settled = await promiseQueue(list); const docs: { content: string, path: string | undefined, resolvedPath: string | undefined, isSuccess: boolean }[] = []; diff --git a/src/server.helpers.ts b/src/server.helpers.ts index 02d55e8e..0aeeaa97 100644 --- a/src/server.helpers.ts +++ b/src/server.helpers.ts @@ -1,5 +1,36 @@ import { createHash, type BinaryToTextEncoding } from 'node:crypto'; import { extname, sep } from 'node:path'; +import { type WhitelistUrl } from './options.defaults'; + +/** + * Construct a search/query string from an object of key-value pairs, optionally filtering out + * specific values and adding a `?` prefix. + * + * @param values - An object containing key-value pairs to be converted into a query string. + * @param [options] - Configuration options for constructing the query string. + * @param [options.filter=[undefined, null]] - Array of values to filter out from the key-value pairs. + * @param [options.prefix=false] - Determines whether to prepend a "?" to the query string. + * @returns The constructed query string, optionally prefixed with "?", or `undefined` if no valid key-value pairs remain. + */ +const buildSearchString = ( + values: Record, + { filter = [undefined, null], prefix = false }: { filter?: unknown[], prefix?: boolean } = {} +) => { + let entries = Object.entries(values); + + if (filter) { + entries = entries.filter(([_key, value]) => !filter.includes(value)); + } + + if (!entries.length) { + return undefined; + } + + const entriesToString = entries.sort(([aKey], [bKey]) => aKey.localeCompare(bKey)).map(([key, value]) => [key, `${value}`]); + const searchParams = new URLSearchParams(Object.fromEntries(entriesToString)); + + return prefix ? `?${searchParams.toString()}` : searchParams.toString(); +}; /** * Check if a value is a valid port number. @@ -386,6 +417,50 @@ const generateHash = (anyValue: unknown): string => { return hashCode(stringify); }; +/** + * Check if a string URL matches a whitelist entry + * + * @param url - string URL to check + * @param {WhitelistUrl[]} whitelist - List of whitelist entries + * @param options - Options for URL validation + * @param options.allowedProtocols - List of allowed protocols for URL validation + * + * @returns `true` if the URL matches any whitelist entry + */ +const isWhitelistedUrl = (url: string, whitelist: WhitelistUrl[], { allowedProtocols = ['http', 'https'] } = {}) => { + if (typeof url !== 'string' || !isUrl(url, { allowedProtocols })) { + return false; + } + + try { + const { host, pathname, protocol } = new URL(url); + const updatedProtocol = protocol.toLowerCase(); + const updatedHost = host.toLowerCase(); + const updatedPath = pathname.toLowerCase(); + + return whitelist.some(entry => { + const listUrl = new URL(entry); + const listProtocol = listUrl.protocol.toLowerCase(); + const listHost = listUrl.host.toLowerCase(); + const listPath = listUrl.pathname.toLowerCase(); + + const protocolMatch = updatedProtocol === listProtocol; + const hostMatch = updatedHost === listHost || updatedHost.endsWith(`.${listHost}`); + let pathMatch = listPath === '/' || updatedPath === listPath; + + if (!pathMatch) { + const checkDir = (listPath.endsWith('/') && listPath) || `${listPath}/`; + + pathMatch = updatedPath.startsWith(checkDir); + } + + return protocolMatch && hostMatch && pathMatch; + }); + } catch { + return false; + } +}; + /** * Join an array of values with a separator, optionally filtering out falsy values. * @@ -463,6 +538,7 @@ const timeoutFunction = async ( }; export { + buildSearchString, freezeObject, generateHash, hashCode, @@ -474,6 +550,7 @@ export { isPromise, isReferenceLike, isUrl, + isWhitelistedUrl, mergeObjects, portValid, stringJoin, diff --git a/src/server.ts b/src/server.ts index c53ec599..10b59cf6 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,6 +3,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { usePatternFlyDocsTool } from './tool.patternFlyDocs'; import { searchPatternFlyDocsTool } from './tool.searchPatternFlyDocs'; import { componentSchemasTool } from './tool.componentSchemas'; +import { patternFlyComponentsIndexResource } from './resource.patternFlyComponentsIndex'; import { patternFlyContextResource } from './resource.patternFlyContext'; import { patternFlyDocsIndexResource } from './resource.patternFlyDocsIndex'; import { patternFlyDocsTemplateResource } from './resource.patternFlyDocsTemplate'; @@ -158,6 +159,7 @@ const builtinTools: McpToolCreator[] = [ */ const builtinResources: McpResourceCreator[] = [ patternFlyContextResource, + patternFlyComponentsIndexResource, patternFlyDocsIndexResource, patternFlyDocsTemplateResource, patternFlySchemasIndexResource, diff --git a/src/tool.componentSchemas.ts b/src/tool.componentSchemas.ts index f0075319..b5d18f04 100644 --- a/src/tool.componentSchemas.ts +++ b/src/tool.componentSchemas.ts @@ -1,20 +1,19 @@ import { z } from 'zod'; import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; -import { getComponentSchema } from '@patternfly/patternfly-component-schemas/json'; import { type McpTool } from './server'; import { getOptions } from './options.context'; -import { memo } from './server.caching'; -import { fuzzySearch } from './server.search'; -import { componentNames } from './tool.searchPatternFlyDocs'; - -/** - * Derive the component schema type from @patternfly/patternfly-component-schemas - */ -type ComponentSchema = Awaited>; +import { + getPatternFlyComponentSchema, + getPatternFlyMcpResources, + type PatternFlyComponentSchema +} from './patternFly.getResources'; +import { searchPatternFly } from './patternFly.search'; /** * componentSchemas tool function * + * @deprecated + * * Creates an MCP tool that retrieves JSON Schema for PatternFly React components. * Uses fuzzy search to handle typos and case variations, with related fallback suggestions. * @@ -22,11 +21,6 @@ type ComponentSchema = Awaited>; * @returns MCP tool tuple [name, schema, callback] */ const componentSchemasTool = (options = getOptions()): McpTool => { - const memoGetComponentSchema = memo( - async (componentName: string): Promise => getComponentSchema(componentName), - options?.toolMemoOptions?.usePatternFlyDocs - ); - const callback = async (args: any = {}) => { const { componentName } = args; @@ -37,28 +31,31 @@ const componentSchemasTool = (options = getOptions()): McpTool => { ); } - if (componentName.length > options.maxSearchLength) { + if (componentName.length > options.minMax.inputStrings.max) { throw new McpError( ErrorCode.InvalidParams, - `Component name exceeds maximum length of ${options.maxSearchLength} characters.` + `Component name exceeds maximum length of ${options.minMax.inputStrings.max} characters.` ); } - // Use fuzzySearch with `isFuzzyMatch` to handle exact and intentional suggestions in one pass - const { results } = fuzzySearch(componentName, componentNames, { - maxDistance: 3, - maxResults: 5, - isFuzzyMatch: true, - deduplicateByNormalized: true - }); + const { latestVersion } = await getPatternFlyMcpResources.memo(); + const { exactMatches, remainingMatches } = await searchPatternFly.memo( + componentName, + { version: latestVersion, section: 'components' }, + { maxDistance: 3, maxResults: 5 } + ); - const exact = results.find(result => result.matchType === 'exact'); + const exact = exactMatches.find(match => match.isSchemasAvailable === true); if (exact) { - let componentSchema: ComponentSchema; + let componentSchema: PatternFlyComponentSchema | undefined; try { - componentSchema = await memoGetComponentSchema(exact.item); + componentSchema = await getPatternFlyComponentSchema.memo(exact.item); + + if (componentSchema === undefined) { + throw new Error(`Component schema for "${exact.item}" doesn't exist.`); + } } catch (error) { throw new McpError( ErrorCode.InternalError, @@ -76,7 +73,7 @@ const componentSchemasTool = (options = getOptions()): McpTool => { }; } - const suggestions = results.map(result => result.item).slice(0, 3); + const suggestions = remainingMatches.map(result => result.item).slice(0, 3); const suggestionMessage = suggestions.length ? `Did you mean ${suggestions.map(suggestion => `"${suggestion}"`).join(', ')}?` : 'No similar components found.'; @@ -96,7 +93,7 @@ const componentSchemasTool = (options = getOptions()): McpTool => { Returns prop definitions, types, and validation rules. Use this for structured component metadata, not documentation.`, inputSchema: { - componentName: z.string().max(options.maxSearchLength).describe('Name of the PatternFly component (e.g., "Button", "Table")') + componentName: z.string().max(options.minMax.inputStrings.max).describe('Name of the PatternFly component (e.g., "Button", "Table")') } }, callback diff --git a/src/tool.patternFlyDocs.ts b/src/tool.patternFlyDocs.ts index 54c76adf..1655f99b 100644 --- a/src/tool.patternFlyDocs.ts +++ b/src/tool.patternFlyDocs.ts @@ -1,32 +1,19 @@ import { z } from 'zod'; import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; -import { getComponentSchema as pfGetComponentSchema } from '@patternfly/patternfly-component-schemas/json'; import { type McpTool } from './server'; -import { getOptions } from './options.context'; -import { processDocsFunction } from './server.getResources'; -import { memo } from './server.caching'; +import { processDocsFunction, type ProcessedDoc } from './server.getResources'; import { stringJoin } from './server.helpers'; -import { setComponentToDocsMap, searchComponents } from './tool.searchPatternFlyDocs'; -import { DEFAULT_OPTIONS } from './options.defaults'; -import { log } from './logger'; - -/** - * Get the component schema from @patternfly/patternfly-component-schemas. - * - * @param componentName - */ -const getComponentSchema = async (componentName: string) => { - try { - return await pfGetComponentSchema(componentName); - } catch {} - - return undefined; -}; - -/** - * Memoized version of getComponentSchema. - */ -getComponentSchema.memo = memo(getComponentSchema, DEFAULT_OPTIONS.toolMemoOptions.usePatternFlyDocs); +import { getOptions } from './options.context'; +import { searchPatternFly } from './patternFly.search'; +import { getPatternFlyMcpResources, getPatternFlyComponentSchema, setCategoryDisplayLabel } from './patternFly.getResources'; +import { normalizeEnumeratedPatternFlyVersion } from './patternFly.helpers'; +import { + assertInput, + assertInputStringLength, + assertInputStringArrayEntryLength, + assertInputStringNumberEnumLike, + assertInputUrlWhiteListed +} from './server.assertions'; /** * usePatternFlyDocs tool function @@ -35,74 +22,128 @@ getComponentSchema.memo = memo(getComponentSchema, DEFAULT_OPTIONS.toolMemoOptio * @returns MCP tool tuple [name, schema, callback] */ const usePatternFlyDocsTool = (options = getOptions()): McpTool => { - const memoProcess = memo(processDocsFunction, options?.toolMemoOptions?.usePatternFlyDocs); - const { getKey: getComponentToDocsKey } = setComponentToDocsMap.memo(); - const callback = async (args: any = {}) => { - const { urlList, name } = args; - const isUrlList = urlList && Array.isArray(urlList) && urlList.length > 0 && urlList.every(url => typeof url === 'string' && url.trim().length > 0); - const isName = typeof name === 'string' && name.trim().length > 0; - const hasUri = (isName && new RegExp('patternfly://', 'i').test(name)) || (isUrlList && urlList.some(url => new RegExp('patternfly://', 'i').test(url))); - - if (hasUri) { - throw new McpError( - ErrorCode.InvalidParams, + const { urlList, name, version } = args; + const isUrlList = Array.isArray(urlList) && urlList.length > 0; + const isName = typeof name === 'string'; + const isVersion = typeof version === 'string'; + + assertInput( + !((isUrlList && isName) || (!isUrlList && !isName)), + `Provide either a string "name" OR an array of strings "urlList".` + ); + + if (isName) { + assertInputStringLength(name, { + ...options.minMax.inputStrings, + inputDisplayName: 'name' + }); + + assertInput( + !(new RegExp('patternfly://', 'i').test(name)), stringJoin.basic( - 'Direct "patternfly://" URIs are not supported as tool inputs, and are intended to be used directly.', - 'Use a component "name" or provide a "urlList" of raw documentation URLs.' + 'Direct "patternfly://" URIs are not currently supported as tool inputs, and are intended to be used with MCP resources directly.', + 'Use a component or resource "name" or provide a "urlList" of raw documentation URLs.' ) ); } - if ((isUrlList && isName) || (!isUrlList && !isName)) { - throw new McpError( - ErrorCode.InvalidParams, - `Provide either a string "name" OR an array of strings "urlList".` - ); - } + if (isUrlList) { + assertInputStringArrayEntryLength(urlList, { + ...options.minMax.urlString, + inputDisplayName: 'urlList' + }); - if (isName && name.length > options.maxSearchLength) { - throw new McpError( - ErrorCode.InvalidParams, - `String "name" exceeds maximum length of ${options.maxSearchLength} characters.` + assertInput( + urlList.length <= options.minMax.docsToLoad.max, + `"urlList" must be an array with a maximum length of ${options.minMax.docsToLoad.max} items.` ); - } - const updatedUrlList = isUrlList ? urlList.slice(0, options.recommendedMaxDocsToLoad) : []; - - if (isUrlList && urlList.length > options.recommendedMaxDocsToLoad) { - log.warn( - `usePatternFlyDocs: urlList truncated from ${urlList.length} to ${options.recommendedMaxDocsToLoad} items.` + assertInput( + !urlList.some(url => new RegExp('patternfly://', 'i').test(url)), + stringJoin.basic( + 'Direct "patternfly://" URIs are not currently supported as tool inputs, and are intended to be used with MCP resources directly.', + 'Use a component or resource "name" or provide a "urlList" of raw documentation URLs.' + ) ); + + if (options.mode !== 'test') { + assertInputUrlWhiteListed( + urlList, + options.patternflyOptions.urlWhitelist, + { inputDisplayName: 'urlList' } + ); + } } - if (name) { - const { exactMatches, searchResults } = searchComponents.memo(name); + if (isVersion) { + assertInputStringLength(version, { + max: options.minMax.inputStrings.max, + min: 2, + inputDisplayName: 'version' + }); - if (exactMatches.length === 0 || exactMatches.every(match => match.urls.length === 0)) { - const suggestions = searchResults.map(result => result.item).slice(0, 3); - const suggestionMessage = suggestions.length - ? `Did you mean ${suggestions.map(suggestion => `"${suggestion}"`).join(', ')}?` - : 'No similar components found.'; + assertInputStringNumberEnumLike(version, options.patternflyOptions.availableSearchVersions, { + inputDisplayName: 'version' + }); + } - throw new McpError( - ErrorCode.InvalidParams, - `Component "${name.trim()}" not found. ${suggestionMessage}` - ); - } + const updatedUrlList: string[] = isUrlList ? urlList.slice(0, options.minMax.docsToLoad.max) : []; + const { latestSchemasVersion, byPath } = await getPatternFlyMcpResources.memo(); + const normalizedVersion = (await normalizeEnumeratedPatternFlyVersion(version)); + const updatedName = name?.trim(); + + if (updatedName) { + const { searchResults, exactMatches } = await searchPatternFly.memo(updatedName, { version: normalizedVersion }); + + assertInput( + exactMatches.length > 0 && exactMatches.every(match => match.entries.some(entry => Boolean(entry.path))), + () => { + const suggestions = searchResults.map(result => result.item).slice(0, 3); + const suggestionMessage = suggestions.length + ? `Did you mean ${suggestions.map(suggestion => `"${suggestion}"`).join(', ')}?` + : 'No similar resources found.'; + + return `Resource "${updatedName}" not found. ${suggestionMessage}`; + }, + ErrorCode.InvalidParams + ); - updatedUrlList.push(...exactMatches.flatMap(match => match.urls)); + updatedUrlList.push(...exactMatches.flatMap(match => match.entries.map(entry => entry.path)).filter(Boolean)); } - const docs = []; + const docs: ProcessedDoc[] = []; const schemasSeen = new Set(); const schemaResults = []; const docResults = []; try { - const processedDocs = await memoProcess(updatedUrlList); + const processedDocs = await processDocsFunction.memo(updatedUrlList); + const primaryDocs: ProcessedDoc[] = []; + const secondaryDocs: ProcessedDoc[] = []; + const tertiaryDocs: ProcessedDoc[] = []; + + processedDocs.forEach(doc => { + const docEntry = (doc.path && byPath[doc.path]) || undefined; + + if (docEntry) { + if (!normalizedVersion || docEntry.version === normalizedVersion) { + primaryDocs.push(doc); + } else { + secondaryDocs.push(doc); + } + } else { + tertiaryDocs.push(doc); + } + }); + + const sortByPath = (a: ProcessedDoc, b: ProcessedDoc) => (a.path || '').localeCompare(b.path || ''); - docs.push(...processedDocs); + docs.push( + ...primaryDocs.sort(sortByPath), + ...secondaryDocs.sort(sortByPath), + ...tertiaryDocs.sort(sortByPath) + ); } catch (error) { throw new McpError( ErrorCode.InternalError, @@ -111,40 +152,55 @@ const usePatternFlyDocsTool = (options = getOptions()): McpTool => { } if (docs.length === 0) { + const nameFilter = `**Name**: ${updatedName || '*'}`; + const versionFilter = `**PatternFly Version**: ${normalizedVersion || '*'}`; const urlListBlock = updatedUrlList.map((url: string, index: number) => ` ${index + 1}. ${url}`).join('\n'); + const urlListFilter = stringJoin.newline( + `**URL List**:`, + urlListBlock || ' - None' + ); return { content: [{ type: 'text', text: stringJoin.newline( - `No PatternFly documentation found for:`, - urlListBlock, + `No PatternFly resources found for:`, + nameFilter, + versionFilter, + urlListFilter, '', '---', '', '**Important**:', - ' - To browse all available components use "searchPatternFlyDocs" with a search all ("*").' + ' - To browse all available resources use "searchPatternFlyDocs" with a search all ("*").' ) }] }; } for (const doc of docs) { - const componentName = getComponentToDocsKey(doc.path); + const patternFlyEntry = doc.path ? byPath[doc.path] : undefined; + const entryName = patternFlyEntry?.name; + const entryVersion = patternFlyEntry?.version; + const entryVersionDisplay = (entryVersion && `(${entryVersion}) `) || ''; + const docTitle = patternFlyEntry + ? `# Documentation for ${patternFlyEntry.displayName || entryName} ${entryVersionDisplay}[${setCategoryDisplayLabel(patternFlyEntry)}]` + : `# Content for ${doc.path}`; docResults.push(stringJoin.newline( - `# Documentation${(componentName && ` for ${componentName}`) || ''} from ${doc.path || 'unknown'}`, + docTitle, + `Source: ${doc.path}`, '', doc.content )); - if (componentName && !schemasSeen.has(componentName)) { - schemasSeen.add(componentName); - const componentSchema = await getComponentSchema.memo(componentName); + if (latestSchemasVersion === entryVersion && entryName && !schemasSeen.has(entryName)) { + schemasSeen.add(entryName); + const componentSchema = await getPatternFlyComponentSchema.memo(entryName); if (componentSchema) { schemaResults.push(stringJoin.newline( - `# Component Schema for ${componentName}`, + `# Component Schema for ${entryName} ${entryVersionDisplay}`, `This machine-readable JSON schema defines the component's props, types, and validation rules.`, '```json', JSON.stringify(componentSchema, null, 2), @@ -167,18 +223,22 @@ const usePatternFlyDocsTool = (options = getOptions()): McpTool => { return [ 'usePatternFlyDocs', { - description: `Get markdown documentation and component JSON schemas for PatternFly components. + description: `Get markdown documentation and component JSON schemas for PatternFly resources and components. **Usage**: - 1. Input a component name (e.g., "Button") OR a list of up to ${options.recommendedMaxDocsToLoad} documentation URLs at a time (typically from searchPatternFlyDocs results). + 1. Input a component or resource name (e.g., "Button", "Writing") or a list of up to ${options.minMax.docsToLoad.max} documentation URLs at a time (typically from searchPatternFlyDocs results). **Returns**: - Markdown documentation - Component JSON schemas, if available `, inputSchema: { - urlList: z.array(z.string()).max(options.recommendedMaxDocsToLoad).optional().describe(`The list of URLs to fetch the documentation from (max ${options.recommendedMaxDocsToLoad} at a time`), - name: z.string().max(options.maxSearchLength).optional().describe('The name of a PatternFly component to fetch documentation for (e.g., "Button", "Table")') + urlList: z.array(z.string()).max(options.minMax.docsToLoad.max) + .optional().describe(`The list of URLs to fetch the documentation from (max ${options.minMax.docsToLoad.max} at a time`), + name: z.string().max(options.minMax.inputStrings.max) + .optional().describe('The name of a PatternFly component or resource to fetch documentation for (e.g., "Button", "Table", "Writing")'), + version: z.enum(options.patternflyOptions.availableSearchVersions) + .optional().describe(`Filter results by a specific PatternFly version (e.g. ${options.patternflyOptions.availableSearchVersions.map(value => `"${value}"`).join(', ')})`) } }, callback @@ -190,4 +250,4 @@ const usePatternFlyDocsTool = (options = getOptions()): McpTool => { */ usePatternFlyDocsTool.toolName = 'usePatternFlyDocs'; -export { usePatternFlyDocsTool, getComponentSchema }; +export { usePatternFlyDocsTool }; diff --git a/src/tool.searchPatternFlyDocs.ts b/src/tool.searchPatternFlyDocs.ts index 37b408c5..48062bc6 100644 --- a/src/tool.searchPatternFlyDocs.ts +++ b/src/tool.searchPatternFlyDocs.ts @@ -1,195 +1,11 @@ +import { ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; -import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; -import { componentNames as pfComponentNames } from '@patternfly/patternfly-component-schemas/json'; import { type McpTool } from './server'; -import { COMPONENT_DOCS } from './docs.component'; -import { LAYOUT_DOCS } from './docs.layout'; -import { CHART_DOCS } from './docs.chart'; -import { getLocalDocs } from './docs.local'; -import { fuzzySearch, type FuzzySearchResult } from './server.search'; -import { getOptions } from './options.context'; -import { memo } from './server.caching'; import { stringJoin } from './server.helpers'; -import { DEFAULT_OPTIONS } from './options.defaults'; - -/** - * List of component names to include in search results. - * - * @note The "table" component is manually added to the list because it's not currently included - * in the component schemas package. - */ -const componentNames = [...pfComponentNames, 'Table'].sort((a, b) => a.localeCompare(b)); - -/** - * Extract a component name from an internal documentation URL string - * - * @note This is reliant on the documentation URLs being in the accepted format. - * If the format changes, this will need to be updated. This is a short-term solution - * until we can move the internal links to a new format like: - * ``` - * { - * name: 'Charts', - * description: 'Colors for Charts', - * type: 'example', - * scope: '@patternfly', - * url: `${PF_EXTERNAL_EXAMPLES_CHARTS}/ChartTheme/examples/ChartTheme.md` - * } - * ``` - * - * @example - * extractComponentName('[@patternfly/ComponentName - Type](URL)'); - * - * @param docUrl - Documentation URL string - * @returns ComponentName or `null` if not found - */ -const extractComponentName = (docUrl: string): string | null => { - // Stop at space or closing bracket, allowing dashes in the name - const match = docUrl.match(/\[@patternfly\/([^\s\]]+)/); - const name = match && match[1] ? match[1].trim() : null; - - // Filter out known non-component patterns - if (name?.startsWith('react-')) { - return null; - } - - return name; -}; - -/** - * Extract a URL from an internal Markdown link. - * - * @note This is a short-term solution until we can move the internal links to a new format. - * - * @example - * extractUrl('[text](URL)'); - * - * @param docUrl - * @returns URL or original string if not a Markdown link - */ -const extractUrl = (docUrl: string): string => { - const match = docUrl.match(/]\(([^)]+)\)/); - - return match && match[1] ? match[1] : docUrl; -}; - -/** - * Build a map of component names relative to internal documentation URLs. - * - * @returns Map of component name -> array of URLs (Design Guidelines + Accessibility) - */ -const setComponentToDocsMap = () => { - const map = new Map(); - const allDocs = [...COMPONENT_DOCS, ...LAYOUT_DOCS, ...CHART_DOCS, ...getLocalDocs()]; - const getKey = (value?: string | undefined) => { - if (!value) { - return undefined; - } - - for (const [key, urls] of map) { - if (urls.includes(value)) { - return key; - } else { - const { results } = fuzzySearch(value, urls, { - deduplicateByNormalized: true - }); - - if (results.length) { - return key; - } - } - } - - return undefined; - }; - - allDocs.forEach(docUrl => { - const componentName = extractComponentName(docUrl); - - if (componentName) { - const url = extractUrl(docUrl); - const existing = map.get(componentName) || []; - - map.set(componentName, [...existing, url]); - } - }); - - return { - map, - getKey - }; -}; - -/** - * Memoized version of componentToDocsMap. - */ -setComponentToDocsMap.memo = memo(setComponentToDocsMap); - -/** - * Search for PatternFly component documentation URLs using fuzzy search. - * - * @param searchQuery - Search query string - * @param settings - Optional settings object - * @param settings.names - List of names to search. Defaults to all component names. - * @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 searchComponents = (searchQuery: string, { names = componentNames, allowWildCardAll = false } = {}) => { - const isWildCardAll = searchQuery.trim() === '*' || searchQuery.trim().toLowerCase() === 'all' || searchQuery.trim() === ''; - const isSearchWildCardAll = allowWildCardAll && isWildCardAll; - const { map: componentToDocsMap } = setComponentToDocsMap.memo(); - let searchResults: FuzzySearchResult[] = []; - - if (isSearchWildCardAll) { - searchResults = componentNames.map(name => ({ matchType: 'all', distance: 0, item: name } as FuzzySearchResult)); - } else { - const search = fuzzySearch(searchQuery, names, { - maxDistance: 3, - maxResults: 10, - isFuzzyMatch: true, - deduplicateByNormalized: true - }); - - searchResults = search.results; - } - - const extendResults = (results: FuzzySearchResult[] = []) => results.map(result => { - const isSchemasAvailable = pfComponentNames.includes(result.item); - const urls = componentToDocsMap.get(result.item) || []; - const matchedUrls = new Set(); - - urls.forEach(url => { - matchedUrls.add(url); - }); - - return { - ...result, - doc: `patternfly://docs/${result.item}`, - isSchemasAvailable, - schema: isSchemasAvailable ? `patternfly://schemas/${result.item}` : undefined, - urls: Array.from(matchedUrls) - }; - }); - - const exactMatches = searchResults.filter(result => result.matchType === 'exact'); - const extendedExactMatches = extendResults(exactMatches); - const extendedSearchResults = extendResults(searchResults); - - return { - isSearchWildCardAll, - firstExactMatch: extendedExactMatches[0], - exactMatches: extendedExactMatches, - searchResults: extendedSearchResults - }; -}; - -/** - * Memoized version of searchComponents. - */ -searchComponents.memo = memo(searchComponents, DEFAULT_OPTIONS.toolMemoOptions.searchPatternFlyDocs); +import { getOptions } from './options.context'; +import { searchPatternFly } from './patternFly.search'; +import { normalizeEnumeratedPatternFlyVersion } from './patternFly.helpers'; +import { assertInput, assertInputStringLength, assertInputStringNumberEnumLike } from './server.assertions'; /** * searchPatternFlyDocs tool function @@ -202,67 +18,122 @@ searchComponents.memo = memo(searchComponents, DEFAULT_OPTIONS.toolMemoOptions.s */ const searchPatternFlyDocsTool = (options = getOptions()): McpTool => { const callback = async (args: any = {}) => { - const { searchQuery } = args; + const { searchQuery, version } = args; + const isVersion = typeof version === 'string' && version.length > 0; - if (typeof searchQuery !== 'string') { - throw new McpError( - ErrorCode.InvalidParams, - `Missing required parameter: searchQuery must be a string: ${searchQuery}` - ); - } + assertInputStringLength(searchQuery, { + ...options.minMax.inputStrings, + inputDisplayName: 'searchQuery' + }); - if (searchQuery.length > options.maxSearchLength) { - throw new McpError( - ErrorCode.InvalidParams, - `Search query exceeds ${options.maxSearchLength} character max length.` - ); + if (isVersion) { + assertInputStringLength(version, { + max: options.minMax.inputStrings.max, + min: 2, + inputDisplayName: 'version' + }); + + assertInputStringNumberEnumLike(version, options.patternflyOptions.availableSearchVersions, { + inputDisplayName: 'version' + }); } - const { isSearchWildCardAll, searchResults } = searchComponents.memo(searchQuery, { allowWildCardAll: true }); + const normalizedVersion = await normalizeEnumeratedPatternFlyVersion(version); + + const { isSearchWildCardAll, exactMatches, remainingMatches, searchResults, totalPotentialMatches } = await searchPatternFly.memo( + searchQuery, + { version: normalizedVersion }, + { allowWildCardAll: true, maxResults: options.minMax.toolSearches.max } + ); + + assertInput( + !isSearchWildCardAll || (isSearchWildCardAll && searchResults.length > 0), + stringJoin.newline( + `Internal Search Error: The server failed to retrieve PatternFly resources for query "${searchQuery}"`, + 'Ensure documentation resources are loaded or restart the server.' + ), + ErrorCode.InternalError + ); if (!isSearchWildCardAll && searchResults.length === 0) { return { content: [{ type: 'text', text: stringJoin.newline( - `No PatternFly documentation found matching "${searchQuery}"`, - '', - '---', - '', + `No PatternFly resources found matching "${searchQuery}"`, + options.separator, '**Important**:', - ' - Use a search all ("*") to find all available components.' + ' - Use a search all ("*") to find all available resources.' ) }] }; } - const results = searchResults.map(result => { - const urlList = result.urls.map((url: string, index: number) => ` ${index + 1}. ${url}`).join('\n'); + // Default to parsing all remainingMatches + let parseResults = remainingMatches; + + // Focus the result set. If there are exact matches, use those. + if (isSearchWildCardAll || exactMatches.length > 0) { + parseResults = exactMatches; + + // Focus the result set. If there aren't any exactMatches use "distance 1" matches only. + } else if (searchResults.some(result => result.distance === 1)) { + parseResults = searchResults.filter(result => result.distance === 1); + } + + const searchTitlePatternFly = normalizedVersion ? `PatternFly version "${normalizedVersion}" and ` : ''; - return stringJoin.newline( - '', - `## ${result.item}`, - `**Match Type**: ${result.matchType}`, - `### "usePatternFlyDocs" tool documentation URLs`, - urlList.length ? urlList : ' - No URLs found', - `### Resources metadata`, - ` - **Component name**: ${result.item}`, - ` - **JSON Schemas**: ${result.isSchemasAvailable ? 'Available' : 'Not available'}` + let searchTitle = stringJoin.basic( + `# Search results for ${searchTitlePatternFly}"${searchQuery}".`, + `Showing ${parseResults.length} related ${parseResults.length === 1 ? 'match' : 'matches'}.` + ); + + if (isSearchWildCardAll) { + searchTitle = stringJoin.basic( + `# Search results for ${searchTitlePatternFly}"all" resources.`, + `Only showing the first ${parseResults.length} results. There are ${totalPotentialMatches} potential match variations.`, + `Try searching with a more specific query.` + ); + } else if (exactMatches.length > 0) { + searchTitle = stringJoin.basic( + `# Search results for ${searchTitlePatternFly}"${searchQuery}".`, + `Showing ${parseResults.length} exact ${parseResults.length === 1 ? 'match' : 'matches'}.` ); + } + + const results = parseResults.map((result, index) => { + const availableVersions = new Set(); + const urlList = result.entries.map(entry => { + availableVersions.add(entry.version); + + return ` - [${entry.displayName} - (${entry.version}) - ${entry.description}](${entry.path})`; + }); + + const uri = result.uri; + const uriSchemas = result.uriSchemas; + + return stringJoin.newlineFiltered( + `${index + 1}. **${result.name}**:`, + ` "usePatternFlyDocs" resource parameter "name" and "URLs"`, + ` - **Name**: ${result.name}`, + urlList.length ? ` - **URLs**:` : undefined, + urlList.length ? urlList.join('\n') : undefined, + uri || uriSchemas ? ` **Resources**:` : undefined, + uri ? ` - **URI**: ${uri}` : undefined, + uriSchemas ? ` - **JSON Schemas**: ${uriSchemas}` : undefined + ) + '\n'; }); return { content: [{ type: 'text', text: stringJoin.newline( - `# Search results for "${isSearchWildCardAll ? 'all components' : searchQuery}", ${searchResults.length} matches found:`, + searchTitle, ...results, - '', - '---', - '', + options.separator, '**Important**:', - ' - Use the "usePatternFlyDocs" tool with the above URLs to fetch documentation content.', - ' - Use a search all ("*") to find all available components.' + ' - Use the "usePatternFlyDocs" tool with the above names and URLs to fetch resource content.', + ' - Use a search all ("*") to find all available resources.' ) }] }; @@ -271,18 +142,24 @@ const searchPatternFlyDocsTool = (options = getOptions()): McpTool => { return [ 'searchPatternFlyDocs', { - description: `Search PatternFly components and get component names with documentation URLs. Supports case-insensitive partial and all ("*") matches. + description: `Search PatternFly resources and get component names with documentation and guidance URLs. Supports case-insensitive partial and all ("*") matches. **Usage**: - 1. Input a "searchQuery" to find PatternFly documentation URLs and component names. - 2. Use the returned component names OR URLs with the "usePatternFlyDocs" tool to get markdown documentation and component JSON schemas. + 1. Input a "searchQuery" to find PatternFly documentation and guideline URLs, and component names. + 2. Use the returned resource names OR URLs OR version with the "usePatternFlyDocs" tool to get markdown documentation, guidelines, and component JSON schemas. **Returns**: - - Component names that can be used with "usePatternFlyDocs" - - Documentation URLs that can be used with "usePatternFlyDocs" + - Component and resource names that can be used with "usePatternFlyDocs" + - Documentation and guideline URLs that can be used with "usePatternFlyDocs" `, inputSchema: { - searchQuery: z.string().max(options.maxSearchLength).describe('Full or partial component name to search for (e.g., "button", "table", "*")') + searchQuery: z.string() + .min(options.minMax.inputStrings.min) + .max(options.minMax.inputStrings.max) + .describe('Full or partial resource or component name to search for (e.g., "button", "react", "*")'), + version: z.enum(options.patternflyOptions.availableSearchVersions) + .optional() + .describe(`Filter results by a specific PatternFly version (e.g. ${options.patternflyOptions.availableSearchVersions.map(value => `"${value}"`).join(', ')})`) } }, callback @@ -291,4 +168,4 @@ const searchPatternFlyDocsTool = (options = getOptions()): McpTool => { searchPatternFlyDocsTool.toolName = 'searchPatternFlyDocs'; -export { searchPatternFlyDocsTool, searchComponents, setComponentToDocsMap, componentNames }; +export { searchPatternFlyDocsTool }; diff --git a/tests/e2e/__snapshots__/httpTransport.test.ts.snap b/tests/e2e/__snapshots__/httpTransport.test.ts.snap index 33ef65e8..4dd96cd1 100644 --- a/tests/e2e/__snapshots__/httpTransport.test.ts.snap +++ b/tests/e2e/__snapshots__/httpTransport.test.ts.snap @@ -3,6 +3,7 @@ exports[`Builtin resources, HTTP transport should expose expected resources and templates: resources 1`] = ` { "resourceNames": [ + "patternfly://components/index", "patternfly://context", "patternfly://docs/index", "patternfly://schemas/index", @@ -15,7 +16,8 @@ exports[`Builtin resources, HTTP transport should expose expected resources and `; exports[`Builtin tools, HTTP transport should concatenate headers and separator with two local files 1`] = ` -"# Documentation from documentation/guidelines/README.md +"# Documentation for React Components (v6) [AI Guidance] +Source: documentation:components/README.md # PatternFly Development Rules This is a generated offline fixture used by the MCP external URLs test. @@ -31,7 +33,8 @@ exports[`Builtin tools, HTTP transport should concatenate headers and separator --- -# Documentation from documentation:components/README.md +# Content for documentation/guidelines/README.md +Source: documentation/guidelines/README.md # PatternFly Development Rules This is a generated offline fixture used by the MCP external URLs test. @@ -47,7 +50,8 @@ exports[`Builtin tools, HTTP transport should concatenate headers and separator `; exports[`Builtin tools, HTTP transport should concatenate headers and separator with two remote files 1`] = ` -"# Documentation from https://www.patternfly.org/notARealPath/ChartLegend.md +"# Content for https://www.patternfly.org/notARealPath/AboutModal.md +Source: https://www.patternfly.org/notARealPath/AboutModal.md # Test Document @@ -55,7 +59,8 @@ This is a test document for mocking remote HTTP requests. --- -# Documentation from https://www.patternfly.org/notARealPath/AboutModal.md +# Content for https://www.patternfly.org/notARealPath/ChartLegend.md +Source: https://www.patternfly.org/notARealPath/ChartLegend.md # Test Document diff --git a/tests/e2e/__snapshots__/stdioTransport.test.ts.snap b/tests/e2e/__snapshots__/stdioTransport.test.ts.snap index eb170aec..cbd00944 100644 --- a/tests/e2e/__snapshots__/stdioTransport.test.ts.snap +++ b/tests/e2e/__snapshots__/stdioTransport.test.ts.snap @@ -3,6 +3,7 @@ exports[`Builtin resources, STDIO should expose expected resources and templates 1`] = ` { "resourceNames": [ + "patternfly://components/index", "patternfly://context", "patternfly://docs/index", "patternfly://schemas/index", @@ -15,7 +16,8 @@ exports[`Builtin resources, STDIO should expose expected resources and templates `; exports[`Builtin tools, STDIO should concatenate headers and separator with two local files 1`] = ` -"# Documentation from documentation/guidelines/README.md +"# Documentation for React Components (v6) [AI Guidance] +Source: documentation:components/README.md # PatternFly Development Rules This is a generated offline fixture used by the MCP external URLs test. @@ -31,7 +33,8 @@ exports[`Builtin tools, STDIO should concatenate headers and separator with two --- -# Documentation from documentation:components/README.md +# Content for documentation/guidelines/README.md +Source: documentation/guidelines/README.md # PatternFly Development Rules This is a generated offline fixture used by the MCP external URLs test. @@ -47,7 +50,17 @@ exports[`Builtin tools, STDIO should concatenate headers and separator with two `; exports[`Builtin tools, STDIO should concatenate headers and separator with two remote files 1`] = ` -"# Documentation from http://127.0.0.1:5010/notARealPath/README.md +"# Content for http://127.0.0.1:5010/notARealPath/AboutModal.md +Source: http://127.0.0.1:5010/notARealPath/AboutModal.md + +# Test Document + +This is a test document for mocking remote HTTP requests. + +--- + +# Content for http://127.0.0.1:5010/notARealPath/README.md +Source: http://127.0.0.1:5010/notARealPath/README.md # PatternFly Development Rules This is a generated offline fixture used by the MCP external URLs test. @@ -59,15 +72,7 @@ exports[`Builtin tools, STDIO should concatenate headers and separator with two ### 🚀 Setup & Environment - **Setup Rules** - Project initialization requirements - **Quick Start** - Essential setup steps - - **Environment Rules** - Development configuration - ---- - -# Documentation from http://127.0.0.1:5010/notARealPath/AboutModal.md - -# Test Document - -This is a test document for mocking remote HTTP requests." + - **Environment Rules** - Development configuration" `; exports[`Builtin tools, STDIO should expose expected tools and stable shape 1`] = ` @@ -93,6 +98,8 @@ exports[`Logging should allow setting logging options, stderr 1`] = ` "[INFO]: No external tools loaded. ", "[INFO]: Registered resource: patternfly-context +", + "[INFO]: Registered resource: patternfly-components-index ", "[INFO]: Registered resource: patternfly-docs-index ", diff --git a/tests/e2e/httpTransport.test.ts b/tests/e2e/httpTransport.test.ts index f1eec23c..feeb48ab 100644 --- a/tests/e2e/httpTransport.test.ts +++ b/tests/e2e/httpTransport.test.ts @@ -97,7 +97,7 @@ describe('Builtin tools, HTTP transport', () => { const response = await CLIENT?.send(req); const text = response?.result?.content?.[0]?.text || ''; - expect(text.startsWith('# Documentation')).toBe(true); + expect(text.includes('This is a generated offline fixture')).toBe(true); expect(text).toMatchSnapshot(); }); @@ -121,7 +121,7 @@ describe('Builtin tools, HTTP transport', () => { const response = await CLIENT.send(req); const text = response?.result?.content?.[0]?.text || ''; - expect(text.startsWith('# Documentation')).toBe(true); + expect(text.includes('This is a test document for mocking')).toBe(true); expect(text).toMatchSnapshot(); await CLIENT.close(); }); @@ -230,7 +230,7 @@ describe('Builtin resources, HTTP transport', () => { const content = response?.result.contents[0]; expect(content.uri).toBe('patternfly://schemas/index'); - expect(content.text).toContain('PatternFly Component Names Index'); + expect(content.text).toContain('PatternFly Component JSON Schemas'); }); }); diff --git a/tests/e2e/stdioTransport.test.ts b/tests/e2e/stdioTransport.test.ts index 424ad06b..eadf9a6b 100644 --- a/tests/e2e/stdioTransport.test.ts +++ b/tests/e2e/stdioTransport.test.ts @@ -95,7 +95,7 @@ describe('Builtin tools, STDIO', () => { const response = await CLIENT.send(req); const text = response?.result?.content?.[0]?.text || ''; - expect(text.startsWith('# Documentation')).toBe(true); + expect(text.includes('This is a generated offline fixture')).toBe(true); expect(text).toMatchSnapshot(); }); @@ -119,7 +119,7 @@ describe('Builtin tools, STDIO', () => { const response = await CLIENT.send(req, { timeoutMs: 10000 }); const text = response?.result?.content?.[0]?.text || ''; - expect(text.startsWith('# Documentation')).toBe(true); + expect(text.includes('This is a generated offline fixture')).toBe(true); expect(text).toMatchSnapshot(); }); }); @@ -228,7 +228,7 @@ describe('Builtin resources, STDIO', () => { const content = response?.result.contents[0]; expect(content.uri).toBe('patternfly://schemas/index'); - expect(content.text).toContain('PatternFly Component Names Index'); + expect(content.text).toContain('PatternFly Component JSON Schemas Index'); }); });