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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ exports[`collectionCallback should match snapshot for collection result 1`] = `
"records": [
{
"data": {
"card": [
"Card": [
{
"category": "css",
"contentType": "",
Expand Down Expand Up @@ -46,7 +46,7 @@ exports[`getPatternFlyApiRecords should attempt to convert expanded embedded rec
[
{
"data": {
"dolor": [
"Dolor": [
{
"category": "react",
"contentType": "text/markdown",
Expand Down
33 changes: 33 additions & 0 deletions src/__tests__/__snapshots__/server.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ exports[`runServer should allow server to be stopped, http stop server: diagnost
[
"Registered collection: patternfly-component-schemas",
],
[
"Registered collection: patternfly-api",
],
[
"Registered resource: patternfly-context",
],
Expand Down Expand Up @@ -89,6 +92,9 @@ exports[`runServer should allow server to be stopped, stdio stop server: diagnos
[
"Registered collection: patternfly-component-schemas",
],
[
"Registered collection: patternfly-api",
],
[
"Registered resource: patternfly-context",
],
Expand Down Expand Up @@ -157,6 +163,9 @@ exports[`runServer should attempt to run server, create transport, connect, and
[
"Registered collection: patternfly-component-schemas",
],
[
"Registered collection: patternfly-api",
],
[
"Registered resource: patternfly-context",
],
Expand Down Expand Up @@ -236,6 +245,9 @@ exports[`runServer should attempt to run server, disable SIGINT handler: diagnos
[
"Registered collection: patternfly-component-schemas",
],
[
"Registered collection: patternfly-api",
],
[
"Registered resource: patternfly-context",
],
Expand Down Expand Up @@ -310,6 +322,9 @@ exports[`runServer should attempt to run server, enable SIGINT handler explicitl
[
"Registered collection: patternfly-component-schemas",
],
[
"Registered collection: patternfly-api",
],
[
"Registered resource: patternfly-context",
],
Expand Down Expand Up @@ -389,6 +404,9 @@ exports[`runServer should attempt to run server, log warnings for experimental o
[
"Registered collection: patternfly-component-schemas",
],
[
"Registered collection: patternfly-api",
],
[
"Registered resource: patternfly-context",
],
Expand Down Expand Up @@ -483,6 +501,9 @@ exports[`runServer should attempt to run server, register a tool: diagnostics 1`
[
"Registered collection: patternfly-component-schemas",
],
[
"Registered collection: patternfly-api",
],
[
"Registered resource: patternfly-context",
],
Expand Down Expand Up @@ -570,6 +591,9 @@ exports[`runServer should attempt to run server, register multiple tools: diagno
[
"Registered collection: patternfly-component-schemas",
],
[
"Registered collection: patternfly-api",
],
[
"Registered resource: patternfly-context",
],
Expand Down Expand Up @@ -664,6 +688,9 @@ exports[`runServer should attempt to run server, use custom options: diagnostics
[
"Registered collection: patternfly-component-schemas",
],
[
"Registered collection: patternfly-api",
],
[
"Registered resource: patternfly-context",
],
Expand Down Expand Up @@ -743,6 +770,9 @@ exports[`runServer should attempt to run server, use default tools, http: diagno
[
"Registered collection: patternfly-component-schemas",
],
[
"Registered collection: patternfly-api",
],
[
"Registered resource: patternfly-context",
],
Expand Down Expand Up @@ -831,6 +861,9 @@ exports[`runServer should attempt to run server, use default tools, stdio: diagn
[
"Registered collection: patternfly-component-schemas",
],
[
"Registered collection: patternfly-api",
],
[
"Registered resource: patternfly-context",
],
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/collection.patternFlyApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ describe('collectionCallback', () => {
expect(keys.length).toBe(1);
const key: any = keys[0];

expect(key).toBe('button');
expect(key).toBe('Button');

expect(first).toMatchObject({
sourceId: `${BASE}/v1/components/Button/react`
Expand Down Expand Up @@ -284,7 +284,7 @@ describe('collectionCallback', () => {

const key: any = rec?.data ? Object.keys(rec.data)[0] : '';

expect(key).toBe('card');
expect(key).toBe('Card');
expect(rec?.data?.[key]).toContainEqual(expect.objectContaining({
displayName: 'Card',
category: 'overview'
Expand Down
14 changes: 7 additions & 7 deletions src/__tests__/collection.patternFlyApiHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -966,43 +966,43 @@ describe('extractApiName', () => {
description: 'components section returns normalized item name',
item: 'Button',
section: 'components',
expected: 'button'
expected: 'Button'
},
{
description: 'components section with uppercase and whitespace',
item: ' Card ',
section: ' Components ',
expected: 'card'
expected: 'Card'
},
{
description: 'overview item with custom section adds suffix',
item: 'overview',
section: 'utilities',
expected: 'utilities-overview'
expected: 'UtilitiesOverview'
},
{
description: 'item already prefixed with section avoids double prefix',
item: 'charts-pie',
section: 'charts',
expected: 'charts-pie'
expected: 'ChartsPie'
},
{
description: 'item already prefixed with uppercase section name',
item: 'Patterns-Gallery',
section: 'patterns',
expected: 'patterns-gallery'
expected: 'PatternsGallery'
},
{
description: 'non-prefixed item in custom section prefixes section',
item: 'pie',
section: 'charts',
expected: 'charts-pie'
expected: 'ChartsPie'
},
{
description: 'non-prefixed item in patterns section prefixes section',
item: 'gallery',
section: 'patterns',
expected: 'patterns-gallery'
expected: 'PatternsGallery'
}
])('should extract API name, $description', ({ item, section, expected }) => {
expect(extractApiName(item, section)).toBe(expected);
Expand Down
2 changes: 1 addition & 1 deletion src/collection.patternFlyApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ const contentMetadata = (record: ApiCrawler | ApiEmbeddedExpanded, options = get
normalizedFacet,
normalizedDetailType,
normalizedDetail,
...remaining.map(normalizeSlug)
...remaining.map(remainder => normalizeSlug(remainder))
].filter(Boolean).join('-');

const name = extractApiName(normalizedItem, normalizedSection);
Expand Down
48 changes: 32 additions & 16 deletions src/collection.patternFlyApiHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import {
contentType,
getInlinedCodeBlockCount,
isJson,
isJsonLike
isJsonLike,
stringToCase,
DEFAULT_ACRONYMS
} from './resource.helpers';

/**
Expand Down Expand Up @@ -195,12 +197,14 @@ const calculateContentQualityScore = (
* Transform a string.
*
* @param segment - Input string to normalize.
* @param [settings] - Optional settings
* @param [settings.acronyms] - Acronyms to avoid
* @returns Normalized slug.
*/
const normalizeSlug = (segment: string): string => {
const normalizeSlug = (segment: string, { acronyms = DEFAULT_ACRONYMS }: { acronyms?: string[] } = {}): string => {
let updatedSegment = segment;

if (/[A-Z]/.test(updatedSegment) && !/^(ai|css|html|mcp|cli|uxd|ui|api|faq|faqs|aria|rtl)$/i.test(updatedSegment)) {
if (/[A-Z]/.test(updatedSegment) && !new RegExp(`^(${acronyms.join('|')})$`, 'i').test(updatedSegment)) {
const split = updatedSegment.split(/(?=[A-Z])/);

if (split.every(val => /^[A-Z]/.test(val))) {
Expand All @@ -221,13 +225,14 @@ const normalizeSlug = (segment: string): string => {
*
* @param slug
* @param section
* @param [settings] - Optional settings
* @param [settings.acronyms] - Acronyms to avoid
*/
const formatSlugToTitle = (slug: string, section?: string): string => {
const formatSlugToTitle = (slug: string, section?: string, { acronyms = DEFAULT_ACRONYMS } = {}): string => {
if (!slug) {
return 'PatternFly API';
}

const acronyms = ['ai', 'css', 'html', 'mcp', 'cli', 'uxd', 'ui', 'api', 'faq', 'faqs', 'aria', 'rtl'];
const acronymRegex = new RegExp(`^(${acronyms.join('|')})$`, 'i');

const cleanSection = section
Expand Down Expand Up @@ -268,13 +273,23 @@ const formatSlugToTitle = (slug: string, section?: string): string => {
* @param [context.slug] - Optional slug used for fallback or secondary formatting of the display name.
* @param [context.category] - Optional category of content being processed (e.g., 'props', 'css', or 'doc').
* @param [context.section] - Optional section name used for refining the display name.
* @param [context.detail] - Optional detail string used for refining the display name.
* @param [context.detailType] - Optional detail type used for refining the display name.
* @returns Extracted or formatted display name for the API item.
*/
const extractApiDisplayName = (content?: string, context: { slug?: string; category?: string; section?: string; } = {}): string => {
const { slug = '', category = 'doc', section } = context || {};
const extractApiDisplayName = (
content?: string,
context: { slug?: string; category?: string; section?: string; detail?: string; detailType?: string } = {}
): string => {
const { slug = '', category = 'doc', section, detail = '', detailType = '' } = context || {};

const trimmed = content?.trim() || '';

// Use example detail
if (detailType === 'examples' && detail) {
return formatSlugToTitle(detail, section);
}

// Props JSON signature
if (category === 'props' && trimmed.startsWith('{')) {
try {
Expand Down Expand Up @@ -453,23 +468,24 @@ const extractApiDescription = (
* @returns Extracted entry name
*/
const extractApiName = (item: string, section: string): string => {
const normalizedItem = item.trim().toLowerCase();
const normalizedSection = section.trim().toLowerCase();
const normalizedItem = item.trim();
const normalizedSection = section.trim();
let updatedName = `${normalizedSection}-${normalizedItem}`;

if (normalizedSection === 'components') {
return normalizedItem;
if (normalizedSection.toLowerCase() === 'components') {
updatedName = normalizedItem;
}

if (normalizedItem === 'overview') {
return `${normalizedSection}-overview`;
if (normalizedItem.toLowerCase() === 'overview') {
updatedName = `${normalizedSection}-overview`;
}

// Prevent double-prefix
if (normalizedItem.startsWith(`${normalizedSection}-`)) {
return normalizedItem;
if (normalizedItem.toLowerCase().startsWith(`${normalizedSection.toLowerCase()}-`)) {
updatedName = normalizedItem;
}

return `${normalizedSection}-${normalizedItem}`;
return stringToCase(updatedName, { type: 'pascal' });
};

export {
Expand Down
2 changes: 1 addition & 1 deletion src/docs.filterWords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* @note It's tempting to remove category and section names from this list, don't. Instead, the search
* should be leveraging filters which allow for "section" and "category" specifically.
*/
const INDEX_BLOCKLIST_WORDS = ['patternfly', 'component', 'components', 'documentation', 'example', 'examples', 'view'];
const INDEX_BLOCKLIST_WORDS = ['patternfly', 'component', 'components', 'documentation', 'example', 'examples', 'type', 'types', 'view'];

/**
* Technical terms and acronyms that should be exempt from length and noise filtering.
Expand Down
4 changes: 3 additions & 1 deletion src/options.registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { patternFlySchemasIndexResource } from './resource.patternFlySchemasInde
import { patternFlySchemasTemplateResource } from './resource.patternFlySchemasTemplate';
import { patternFlyDocsCollection } from './collection.patternFlyDocs';
import { patternFlySchemasCollection } from './collection.patternFlySchemas';
import { patternFlyApiCollection } from './collection.patternFlyApi';

/**
* Built-in tools.
Expand Down Expand Up @@ -44,7 +45,8 @@ const builtinResources: McpResourceCreator[] = [
*/
const builtinCollections: McpCollectionCreator[] = [
patternFlyDocsCollection,
patternFlySchemasCollection
patternFlySchemasCollection,
patternFlyApiCollection
];

export { builtinCollections, builtinResources, builtinTools };
4 changes: 3 additions & 1 deletion src/patternFly.getResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,10 +492,12 @@ const getPatternFlyMcpResources = async (contextPathOverride?: string): Promise<
const { componentNamesIndex, byVersion: componentNamesByVersion, byDocs: componentNamesByDocs } = componentNames;

const originalDocs = patternFlyRecordsRegistry.get('patternfly-docs');
const apiCollection = patternFlyRecordsRegistry.get('patternfly-api');

const catalog = [
...originalDocs?.records?.flatMap(({ data }) => Object.entries(data as Record<string, unknown[]>)) || [],
...Array.from(componentNamesByDocs)
...Array.from(componentNamesByDocs),
...apiCollection?.records?.flatMap(({ data }) => Object.entries(data as Record<string, unknown[]>)) || []
];

const resources = new Map<string, PatternFlyMcpResourceMetadata>();
Expand Down
23 changes: 20 additions & 3 deletions src/patternFly.search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ const calculateRelevance = (
const normalizedName = normalizeString.memo(result.name);
const normalizedQuery = normalizeString.memo(query);

// Exact match
if (normalizedName === normalizedQuery) {
return 0;
}
Expand All @@ -200,16 +201,32 @@ const calculateRelevance = (
.map(entry => (entry.displayName ? normalizeString.memo(entry.displayName) : ''))
.filter(Boolean);

// Exact match
if (displayNames.some(name => name === normalizedQuery)) {
return 0;
}

if (normalizedName.includes(normalizedQuery) ||
displayNames.some(name => name.includes(normalizedQuery))) {
// Substring match
if (
normalizedName.includes(normalizedQuery) ||
normalizedQuery.includes(normalizedName) ||
displayNames.some(name => name.includes(normalizedQuery) || normalizedQuery.includes(name))
) {
return 1;
}

return 2;
// Multi-word name match
const queryTokens = normalizedQuery.split(' ').filter(token => token.length > 2);
const hasTokenMatch = queryTokens.some(token =>
normalizedName === token ||
displayNames.some(name => name === token));

if (hasTokenMatch) {
return 2;
}

// Everything else
return 3;
};

/**
Expand Down
Loading
Loading