From ad1771c1cbc53a4dcc8dee1ed8c1b2c10f83f2a8 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:25:34 -0400 Subject: [PATCH 1/9] feat(componentSchemas): add component props lookup tool Co-Authored-By: CD Cabrera --- README.md | 13 ++ jest.setupTests.ts | 28 +++ package-lock.json | 19 ++ package.json | 1 + .../__snapshots__/server.test.ts.snap | 49 ++++ .../tool.componentSchemas.test.ts.snap | 214 ++++++++++++++++++ src/__tests__/tool.componentSchemas.test.ts | 77 +++++++ src/declarations.d.ts | 28 +++ src/server.ts | 8 +- src/tool.componentSchemas.ts | 104 +++++++++ tests/__snapshots__/mcp.test.ts.snap | 1 + 11 files changed, 539 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/__snapshots__/tool.componentSchemas.test.ts.snap create mode 100644 src/__tests__/tool.componentSchemas.test.ts create mode 100644 src/declarations.d.ts create mode 100644 src/tool.componentSchemas.ts diff --git a/README.md b/README.md index 46cb03f2..75efa1a9 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ The Model Context Protocol (MCP) is an open standard that enables AI assistants - **TypeScript**: Full type safety and modern JavaScript features - **PatternFly Documentation Access**: Browse, search, and retrieve PatternFly development rules +- **Component Schemas**: Access JSON Schema validation for PatternFly React components - **Comprehensive Rule Coverage**: Access setup, guidelines, components, charts, chatbot, and troubleshooting documentation - **Smart Search**: Find specific rules and patterns across all documentation - **Error Handling**: Robust error handling with proper MCP error codes @@ -194,6 +195,18 @@ npx @modelcontextprotocol/inspector-cli \ ]' ``` +componentSchemas (get component JSON Schema): + +```bash +npx @modelcontextprotocol/inspector-cli \ + --config ./mcp-config.json \ + --server patternfly-docs \ + --cli \ + --method tools/call \ + --tool-name componentSchemas \ + --tool-arg componentName='Button' +``` + ## Environment variables - DOC_MCP_FETCH_TIMEOUT_MS: Milliseconds to wait before aborting an HTTP fetch (default: 15000) diff --git a/jest.setupTests.ts b/jest.setupTests.ts index a2c6a3b6..a5cfdb16 100644 --- a/jest.setupTests.ts +++ b/jest.setupTests.ts @@ -1 +1,29 @@ // Shared helpers for all Jest tests + +/** + * Note: Mock @patternfly/patternfly-component-schemas/json to avoid top-level await issues in Jest + * - This package uses top-level await which Jest cannot handle without transformation. + * - Individual tests can override this mock if needed + */ +jest.mock('@patternfly/patternfly-component-schemas/json', () => ({ + componentNames: ['Button', 'Alert', 'Card', 'Modal', 'AlertGroup', 'Text', 'TextInput'], + getComponentSchema: jest.fn().mockImplementation((name: string) => { + if (name === 'Button') { + return Promise.resolve({ + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + title: 'Button Props', + description: 'Props for the Button component', + properties: { + variant: { type: 'string', enum: ['primary', 'secondary'] }, + size: { type: 'string', enum: ['sm', 'md', 'lg'] }, + children: { type: 'string', description: 'Content rendered inside the button' } + }, + required: ['children'], + additionalProperties: false + }); + } + + throw new Error(`Component "${name}" not found`); + }) +}), { virtual: true }); diff --git a/package-lock.json b/package-lock.json index 1eef5f74..233225bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.19.1", + "@patternfly/patternfly-component-schemas": "1.2.0", "fastest-levenshtein": "1.0.16", "zod": "3.25.76" }, @@ -2033,6 +2034,24 @@ "node": ">= 8" } }, + "node_modules/@patternfly/patternfly-component-schemas": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@patternfly/patternfly-component-schemas/-/patternfly-component-schemas-1.2.0.tgz", + "integrity": "sha512-kmACHTBHBnWbyPYaO9UyEAhgd0yOQcjDUT2N/GHmnQ5k6COsINLXcfkVg4olGgGHKR7J5nDfQbpSjZYYJL951g==", + "license": "MIT", + "dependencies": { + "zod": "^4.1.12" + } + }, + "node_modules/@patternfly/patternfly-component-schemas/node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", diff --git a/package.json b/package.json index 5f47a80d..4fba11c4 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.19.1", + "@patternfly/patternfly-component-schemas": "1.2.0", "fastest-levenshtein": "1.0.16", "zod": "3.25.76" }, diff --git a/src/__tests__/__snapshots__/server.test.ts.snap b/src/__tests__/__snapshots__/server.test.ts.snap index b2d1a14d..be65c001 100644 --- a/src/__tests__/__snapshots__/server.test.ts.snap +++ b/src/__tests__/__snapshots__/server.test.ts.snap @@ -227,6 +227,9 @@ exports[`runServer should attempt to run server, use default tools: console 1`] [ "Registered tool: fetchDocs", ], + [ + "Registered tool: componentSchemas", + ], ], "log": [ [ @@ -629,6 +632,52 @@ exports[`runServer should attempt to run server, use default tools: console 1`] }, [Function], ], + [ + "componentSchemas", + { + "description": "Get JSON Schema for a PatternFly React component. Returns prop definitions, types, and validation rules. Use this for structured component metadata, not documentation.", + "inputSchema": { + "componentName": ZodString { + "_def": { + "checks": [], + "coerce": false, + "description": "Name of the PatternFly component (e.g., "Button", "Table")", + "typeName": "ZodString", + }, + "and": [Function], + "array": [Function], + "brand": [Function], + "catch": [Function], + "default": [Function], + "describe": [Function], + "isNullable": [Function], + "isOptional": [Function], + "nullable": [Function], + "nullish": [Function], + "optional": [Function], + "or": [Function], + "parse": [Function], + "parseAsync": [Function], + "pipe": [Function], + "promise": [Function], + "readonly": [Function], + "refine": [Function], + "refinement": [Function], + "safeParse": [Function], + "safeParseAsync": [Function], + "spa": [Function], + "superRefine": [Function], + "transform": [Function], + "~standard": { + "validate": [Function], + "vendor": "zod", + "version": 1, + }, + }, + }, + }, + [Function], + ], ], } `; diff --git a/src/__tests__/__snapshots__/tool.componentSchemas.test.ts.snap b/src/__tests__/__snapshots__/tool.componentSchemas.test.ts.snap new file mode 100644 index 00000000..fbac109e --- /dev/null +++ b/src/__tests__/__snapshots__/tool.componentSchemas.test.ts.snap @@ -0,0 +1,214 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`componentSchemasTool should have a consistent return structure: structure 1`] = ` +[ + "componentSchemas", + { + "description": "Get JSON Schema for a PatternFly React component. Returns prop definitions, types, and validation rules. Use this for structured component metadata, not documentation.", + "inputSchema": { + "componentName": ZodString { + "_def": { + "checks": [], + "coerce": false, + "description": "Name of the PatternFly component (e.g., "Button", "Table")", + "typeName": "ZodString", + }, + "and": [Function], + "array": [Function], + "brand": [Function], + "catch": [Function], + "default": [Function], + "describe": [Function], + "isNullable": [Function], + "isOptional": [Function], + "nullable": [Function], + "nullish": [Function], + "optional": [Function], + "or": [Function], + "parse": [Function], + "parseAsync": [Function], + "pipe": [Function], + "promise": [Function], + "readonly": [Function], + "refine": [Function], + "refinement": [Function], + "safeParse": [Function], + "safeParseAsync": [Function], + "spa": [Function], + "superRefine": [Function], + "transform": [Function], + "~standard": { + "validate": [Function], + "vendor": "zod", + "version": 1, + }, + }, + }, + }, + [Function], +] +`; + +exports[`componentSchemasTool, callback should parse parameters, default 1`] = ` +{ + "content": [ + { + "text": "{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "title": "Button Props", + "description": "Props for the Button component", + "properties": { + "variant": { + "type": "string", + "enum": [ + "primary", + "secondary" + ] + }, + "size": { + "type": "string", + "enum": [ + "sm", + "md", + "lg" + ] + }, + "children": { + "type": "string", + "description": "Content rendered inside the button" + } + }, + "required": [ + "children" + ], + "additionalProperties": false +}", + "type": "text", + }, + ], +} +`; + +exports[`componentSchemasTool, callback should parse parameters, with lower case componentName 1`] = ` +{ + "content": [ + { + "text": "{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "title": "Button Props", + "description": "Props for the Button component", + "properties": { + "variant": { + "type": "string", + "enum": [ + "primary", + "secondary" + ] + }, + "size": { + "type": "string", + "enum": [ + "sm", + "md", + "lg" + ] + }, + "children": { + "type": "string", + "description": "Content rendered inside the button" + } + }, + "required": [ + "children" + ], + "additionalProperties": false +}", + "type": "text", + }, + ], +} +`; + +exports[`componentSchemasTool, callback should parse parameters, with trimmed componentName 1`] = ` +{ + "content": [ + { + "text": "{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "title": "Button Props", + "description": "Props for the Button component", + "properties": { + "variant": { + "type": "string", + "enum": [ + "primary", + "secondary" + ] + }, + "size": { + "type": "string", + "enum": [ + "sm", + "md", + "lg" + ] + }, + "children": { + "type": "string", + "description": "Content rendered inside the button" + } + }, + "required": [ + "children" + ], + "additionalProperties": false +}", + "type": "text", + }, + ], +} +`; + +exports[`componentSchemasTool, callback should parse parameters, with upper case componentName 1`] = ` +{ + "content": [ + { + "text": "{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "title": "Button Props", + "description": "Props for the Button component", + "properties": { + "variant": { + "type": "string", + "enum": [ + "primary", + "secondary" + ] + }, + "size": { + "type": "string", + "enum": [ + "sm", + "md", + "lg" + ] + }, + "children": { + "type": "string", + "description": "Content rendered inside the button" + } + }, + "required": [ + "children" + ], + "additionalProperties": false +}", + "type": "text", + }, + ], +} +`; diff --git a/src/__tests__/tool.componentSchemas.test.ts b/src/__tests__/tool.componentSchemas.test.ts new file mode 100644 index 00000000..6144b9c1 --- /dev/null +++ b/src/__tests__/tool.componentSchemas.test.ts @@ -0,0 +1,77 @@ +import { McpError } from '@modelcontextprotocol/sdk/types.js'; +import { componentSchemasTool } from '../tool.componentSchemas'; + +// Mock dependencies +jest.mock('../server.caching', () => ({ + memo: jest.fn(fn => fn) +})); + +describe('componentSchemasTool', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should have a consistent return structure', () => { + const tool = componentSchemasTool(); + + expect(tool).toMatchSnapshot('structure'); + }); +}); + +describe('componentSchemasTool, callback', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it.each([ + { + description: 'default', + componentName: 'Button' + }, + { + description: 'with trimmed componentName', + componentName: ' Button ' + }, + { + description: 'with lower case componentName', + componentName: 'button' + }, + { + description: 'with upper case componentName', + componentName: 'BUTTON' + } + ])('should parse parameters, $description', async ({ componentName }) => { + const [_name, _schema, callback] = componentSchemasTool(); + const result = await callback({ componentName }); + + expect(result).toMatchSnapshot(); + }); + + it.each([ + { + description: 'with missing or undefined componentName', + error: 'Missing required parameter: componentName', + componentName: undefined + }, + { + description: 'with null componentName', + error: 'Missing required parameter: componentName', + componentName: null + }, + { + description: 'with non-string componentName', + error: 'Missing required parameter: componentName', + componentName: 123 + }, + { + description: 'with non-existent component', + error: 'Component "NonExistentComponent" not found', + componentName: 'NonExistentComponent' + } + ])('should handle errors, $description', async ({ error, componentName }) => { + const [_name, _schema, callback] = componentSchemasTool(); + + await expect(callback({ componentName })).rejects.toThrow(McpError); + await expect(callback({ componentName })).rejects.toThrow(error); + }); +}); diff --git a/src/declarations.d.ts b/src/declarations.d.ts new file mode 100644 index 00000000..5cf89360 --- /dev/null +++ b/src/declarations.d.ts @@ -0,0 +1,28 @@ +// Type declarations for @patternfly/patternfly-component-schemas/json +// This file is needed because the package doesn't export TypeScript types. +// TODO: Remove this file once the package exports its own types. + +declare module '@patternfly/patternfly-component-schemas/json' { + + /** + * An array of all available PatternFly component names. + */ + export const componentNames: string[]; + + /** + * A function that retrieves the JSON schema for a given component. + * Returns the JSON Schema object directly from schemas.json + * + * @param componentName The name of the component to get the schema for. + * @return A promise that resolves with the JSON Schema object. + */ + export function getComponentSchema(componentName: string): Promise<{ + $schema: string; + type: string; + title: string; + description: string; + properties: Record; + additionalProperties?: boolean; + required?: string[]; + }>; +} diff --git a/src/server.ts b/src/server.ts index 16ff0e9b..9b95a2c6 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,11 +2,12 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { usePatternFlyDocsTool } from './tool.patternFlyDocs'; import { fetchDocsTool } from './tool.fetchDocs'; +import { componentSchemasTool } from './tool.componentSchemas'; import { OPTIONS } from './options'; type McpTool = [string, { description: string; inputSchema: any }, (args: any) => Promise]; -type McpToolCreator = () => McpTool; +type McpToolCreator = (options?: any) => McpTool; /** * Server instance with shutdown capability @@ -35,7 +36,8 @@ interface ServerInstance { const runServer = async (options = OPTIONS, { tools = [ usePatternFlyDocsTool, - fetchDocsTool + fetchDocsTool, + componentSchemasTool ], enableSigint = true }: { tools?: McpToolCreator[]; enableSigint?: boolean } = {}): Promise => { @@ -66,7 +68,7 @@ const runServer = async (options = OPTIONS, { ); tools.forEach(toolCreator => { - const [name, schema, callback] = toolCreator(); + const [name, schema, callback] = toolCreator(options); console.info(`Registered tool: ${name}`); server?.registerTool(name, schema, callback); diff --git a/src/tool.componentSchemas.ts b/src/tool.componentSchemas.ts new file mode 100644 index 00000000..f91a89af --- /dev/null +++ b/src/tool.componentSchemas.ts @@ -0,0 +1,104 @@ +import { z } from 'zod'; +import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; +import { componentNames, getComponentSchema } from '@patternfly/patternfly-component-schemas/json'; +import { type McpTool } from './server'; +import { OPTIONS } from './options'; +import { memo } from './server.caching'; +import { fuzzySearch } from './server.search'; + +/** + * Component schema type from @patternfly/patternfly-component-schemas + * This is the JSON Schema object returned directly from getComponentSchema + */ +type ComponentSchema = { + $schema: string; + type: string; + title: string; + description: string; + properties: Record; + additionalProperties?: boolean; + required?: string[]; +}; + +/** + * componentSchemas tool function (tuple pattern) + * + * @param options + */ +const componentSchemasTool = (options = OPTIONS): McpTool => { + const memoGetComponentSchema = memo( + async (componentName: string): Promise => getComponentSchema(componentName), + options.toolMemoOptions.fetchDocs // Use the same memo options as fetchDocs + ); + + const callback = async (args: any = {}) => { + const { componentName } = args; + + if (typeof componentName !== 'string') { + throw new McpError( + ErrorCode.InvalidParams, + `Missing required parameter: componentName (must be a string): ${componentName}` + ); + } + + // Trim componentName (user input) to handle accidental spaces, but don't trim componentNames + // (authoritative data) to preserve them as-is + const trimmedComponentName = componentName.trim(); + + // Try an exact match first (case-insensitive) + const exactMatch = componentNames.find(name => name.toLowerCase() === trimmedComponentName.toLowerCase()); + + if (exactMatch === undefined) { + const fuzzyResults = fuzzySearch(trimmedComponentName, componentNames, { + maxDistance: 3, + maxResults: 5 + }); + + const suggestions = fuzzyResults.map(result => result.item); + + const suggestionMessage = suggestions.length > 0 + ? `Did you mean "${suggestions.shift()}"?` + : 'No similar components found.'; + + throw new McpError( + ErrorCode.InvalidParams, + `Component "${trimmedComponentName}" not found. ${suggestionMessage}` + ); + } + + // Get schema using a memoized function + let componentSchema: ComponentSchema; + + try { + componentSchema = await memoGetComponentSchema(exactMatch); + } catch (error) { + throw new McpError( + ErrorCode.InternalError, + `Failed to fetch component schema: ${error}` + ); + } + + // Return schema as JSON string (schema is already the JSON Schema object) + return { + content: [ + { + type: 'text', + text: JSON.stringify(componentSchema, null, 2) + } + ] + }; + }; + + return [ + 'componentSchemas', + { + description: 'Get JSON Schema for a PatternFly React component. Returns prop definitions, types, and validation rules. Use this for structured component metadata, not documentation.', + inputSchema: { + componentName: z.string().describe('Name of the PatternFly component (e.g., "Button", "Table")') + } + }, + callback + ]; +}; + +export { componentSchemasTool }; diff --git a/tests/__snapshots__/mcp.test.ts.snap b/tests/__snapshots__/mcp.test.ts.snap index 7ae50bbe..01fff6b5 100644 --- a/tests/__snapshots__/mcp.test.ts.snap +++ b/tests/__snapshots__/mcp.test.ts.snap @@ -385,6 +385,7 @@ You can find documentation on PatternFly's components at [PatternFly All compone exports[`PatternFly MCP should expose expected tools and stable shape 1`] = ` { "toolNames": [ + "componentSchemas", "fetchDocs", "usePatternFlyDocs", ], From 94fa6a280ff6f3dbaf89842b9fbdd62e1208147f Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 6 Nov 2025 11:33:03 -0500 Subject: [PATCH 2/9] refactor: include fuzzy results --- src/tool.componentSchemas.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tool.componentSchemas.ts b/src/tool.componentSchemas.ts index f91a89af..482c92c6 100644 --- a/src/tool.componentSchemas.ts +++ b/src/tool.componentSchemas.ts @@ -51,7 +51,8 @@ const componentSchemasTool = (options = OPTIONS): McpTool => { if (exactMatch === undefined) { const fuzzyResults = fuzzySearch(trimmedComponentName, componentNames, { maxDistance: 3, - maxResults: 5 + maxResults: 5, + isFuzzyMatch: true }); const suggestions = fuzzyResults.map(result => result.item); From 1215be39e0160e3a8f28f9d9c59984023f05e23c Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 6 Nov 2025 11:44:08 -0500 Subject: [PATCH 3/9] refactor: move to helper defaults, annotations --- src/tool.componentSchemas.ts | 74 +++++++++++++++++------------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/src/tool.componentSchemas.ts b/src/tool.componentSchemas.ts index 482c92c6..eb71f433 100644 --- a/src/tool.componentSchemas.ts +++ b/src/tool.componentSchemas.ts @@ -41,53 +41,47 @@ const componentSchemasTool = (options = OPTIONS): McpTool => { ); } - // Trim componentName (user input) to handle accidental spaces, but don't trim componentNames - // (authoritative data) to preserve them as-is - const trimmedComponentName = componentName.trim(); + // 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 + }); - // Try an exact match first (case-insensitive) - const exactMatch = componentNames.find(name => name.toLowerCase() === trimmedComponentName.toLowerCase()); + const exact = results.find(r => r.matchType === 'exact'); - if (exactMatch === undefined) { - const fuzzyResults = fuzzySearch(trimmedComponentName, componentNames, { - maxDistance: 3, - maxResults: 5, - isFuzzyMatch: true - }); + if (exact) { + let componentSchema: ComponentSchema; - const suggestions = fuzzyResults.map(result => result.item); - - const suggestionMessage = suggestions.length > 0 - ? `Did you mean "${suggestions.shift()}"?` - : 'No similar components found.'; + try { + componentSchema = await memoGetComponentSchema(exact.item); + } catch (error) { + throw new McpError( + ErrorCode.InternalError, + `Failed to fetch component schema: ${error}` + ); + } - throw new McpError( - ErrorCode.InvalidParams, - `Component "${trimmedComponentName}" not found. ${suggestionMessage}` - ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(componentSchema, null, 2) + } + ] + }; } - // Get schema using a memoized function - let componentSchema: ComponentSchema; - - try { - componentSchema = await memoGetComponentSchema(exactMatch); - } catch (error) { - throw new McpError( - ErrorCode.InternalError, - `Failed to fetch component schema: ${error}` - ); - } + const suggestions = results.map(r => r.item).slice(0, 3); + const suggestionMessage = suggestions.length + ? `Did you mean ${suggestions.map(suggestion => `"${suggestion}"`).join(', ')}?` + : 'No similar components found.'; - // Return schema as JSON string (schema is already the JSON Schema object) - return { - content: [ - { - type: 'text', - text: JSON.stringify(componentSchema, null, 2) - } - ] - }; + throw new McpError( + ErrorCode.InvalidParams, + `Component "${componentName.trim()}" not found. ${suggestionMessage}` + ); }; return [ From ed1b41072bbf220077b5f0c88cdbe1909bcacd10 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 6 Nov 2025 12:37:50 -0500 Subject: [PATCH 4/9] refactor: clean up --- src/tool.componentSchemas.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tool.componentSchemas.ts b/src/tool.componentSchemas.ts index eb71f433..333f0131 100644 --- a/src/tool.componentSchemas.ts +++ b/src/tool.componentSchemas.ts @@ -49,7 +49,7 @@ const componentSchemasTool = (options = OPTIONS): McpTool => { deduplicateByNormalized: true }); - const exact = results.find(r => r.matchType === 'exact'); + const exact = results.find(result => result.matchType === 'exact'); if (exact) { let componentSchema: ComponentSchema; @@ -73,7 +73,7 @@ const componentSchemasTool = (options = OPTIONS): McpTool => { }; } - const suggestions = results.map(r => r.item).slice(0, 3); + const suggestions = results.map(result => result.item).slice(0, 3); const suggestionMessage = suggestions.length ? `Did you mean ${suggestions.map(suggestion => `"${suggestion}"`).join(', ')}?` : 'No similar components found.'; From 42a45b8f5890c77651482862d1906ba8cb1fd94d Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 6 Nov 2025 12:59:38 -0500 Subject: [PATCH 5/9] test: add case --- src/__tests__/tool.componentSchemas.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/__tests__/tool.componentSchemas.test.ts b/src/__tests__/tool.componentSchemas.test.ts index 6144b9c1..d5955e93 100644 --- a/src/__tests__/tool.componentSchemas.test.ts +++ b/src/__tests__/tool.componentSchemas.test.ts @@ -58,6 +58,11 @@ describe('componentSchemasTool, callback', () => { error: 'Missing required parameter: componentName', componentName: null }, + { + description: 'with empty componentName', + error: 'No similar components found', + componentName: '' + }, { description: 'with non-string componentName', error: 'Missing required parameter: componentName', From 083fcac2a52e2399181f02e72c0efd6d8c1d331c Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 6 Nov 2025 13:24:45 -0500 Subject: [PATCH 6/9] docs: minor annotation update --- src/tool.componentSchemas.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/tool.componentSchemas.ts b/src/tool.componentSchemas.ts index 333f0131..77ea5b68 100644 --- a/src/tool.componentSchemas.ts +++ b/src/tool.componentSchemas.ts @@ -23,7 +23,11 @@ type ComponentSchema = { /** * componentSchemas tool function (tuple pattern) * - * @param options + * 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. + * + * @param options - Optional configuration options (defaults to OPTIONS) + * @returns {McpTool} MCP tool tuple [name, schema, callback] */ const componentSchemasTool = (options = OPTIONS): McpTool => { const memoGetComponentSchema = memo( From 925a69c1a3c50b72305e1b31d6bd2d190f57a634 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 6 Nov 2025 13:54:51 -0500 Subject: [PATCH 7/9] fix: clean up for default options --- src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index 9b95a2c6..08b5cfda 100644 --- a/src/server.ts +++ b/src/server.ts @@ -68,7 +68,7 @@ const runServer = async (options = OPTIONS, { ); tools.forEach(toolCreator => { - const [name, schema, callback] = toolCreator(options); + const [name, schema, callback] = toolCreator(); console.info(`Registered tool: ${name}`); server?.registerTool(name, schema, callback); From 4851944e82b990c1dd6a05ea2ab2990cc4dc3184 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 6 Nov 2025 13:55:01 -0500 Subject: [PATCH 8/9] fix: typings --- src/tool.componentSchemas.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/tool.componentSchemas.ts b/src/tool.componentSchemas.ts index 77ea5b68..56968635 100644 --- a/src/tool.componentSchemas.ts +++ b/src/tool.componentSchemas.ts @@ -8,17 +8,10 @@ import { fuzzySearch } from './server.search'; /** * Component schema type from @patternfly/patternfly-component-schemas - * This is the JSON Schema object returned directly from getComponentSchema + * Derives from the getComponentSchema() return type. When the package exports + * official typings, this will stay in sync automatically. */ -type ComponentSchema = { - $schema: string; - type: string; - title: string; - description: string; - properties: Record; - additionalProperties?: boolean; - required?: string[]; -}; +type ComponentSchema = Awaited>; /** * componentSchemas tool function (tuple pattern) From 39d8003917f08692534785e2e50127ebdd2dd73a Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 6 Nov 2025 14:11:13 -0500 Subject: [PATCH 9/9] docs: be concise --- src/tool.componentSchemas.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/tool.componentSchemas.ts b/src/tool.componentSchemas.ts index 56968635..52561e44 100644 --- a/src/tool.componentSchemas.ts +++ b/src/tool.componentSchemas.ts @@ -7,9 +7,7 @@ import { memo } from './server.caching'; import { fuzzySearch } from './server.search'; /** - * Component schema type from @patternfly/patternfly-component-schemas - * Derives from the getComponentSchema() return type. When the package exports - * official typings, this will stay in sync automatically. + * Derive the component schema type from @patternfly/patternfly-component-schemas */ type ComponentSchema = Awaited>;