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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/__tests__/__snapshots__/options.defaults.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ exports[`options defaults should return specific properties: defaults 1`] = `
"transport": "stdio",
},
"maxDocsToLoad": 500,
"maxSearchLength": 256,
"name": "@patternfly/patternfly-mcp",
"nodeVersion": 22,
"pfExternal": "https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content",
Expand Down
3 changes: 3 additions & 0 deletions src/options.defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { type ToolModule } from './server.toolsUser';
* @property llmsFilesPath - Path to the LLMs files directory.
* @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 name - Name of the package.
* @property nodeVersion - Node.js major version.
Expand Down Expand Up @@ -52,6 +53,7 @@ interface DefaultOptions<TLogOptions = LoggingOptions> {
llmsFilesPath: string;
logging: TLogOptions;
maxDocsToLoad: number;
maxSearchLength: number;
recommendedMaxDocsToLoad: number;
name: string;
nodeVersion: number;
Expand Down Expand Up @@ -368,6 +370,7 @@ const DEFAULT_OPTIONS: DefaultOptions = {
llmsFilesPath: (process.env.NODE_ENV === 'local' && '/llms-files') || join(resolve(process.cwd()), 'llms-files'),
logging: LOGGING_OPTIONS,
maxDocsToLoad: 500,
maxSearchLength: 256,
recommendedMaxDocsToLoad: 15,
name: packageJson.name,
nodeVersion: (process.env.NODE_ENV === 'local' && 22) || getNodeMajorVersion(),
Expand Down
7 changes: 7 additions & 0 deletions src/resource.patternFlyDocsTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ const patternFlyDocsTemplateResource = (options = getOptions()): McpResource =>
);
}

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);
Expand Down
11 changes: 10 additions & 1 deletion src/resource.patternFlySchemasTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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';

Expand Down Expand Up @@ -32,9 +33,10 @@ const CONFIG = {
/**
* Resource creator for the component schemas template.
*
* @param options - Global options
* @returns {McpResource} The resource definition tuple
*/
const patternFlySchemasTemplateResource = (): McpResource => [
const patternFlySchemasTemplateResource = (options = getOptions()): McpResource => [
NAME,
URI_TEMPLATE,
CONFIG,
Expand All @@ -48,6 +50,13 @@ const patternFlySchemasTemplateResource = (): McpResource => [
);
}

if (name.length > options.maxSearchLength) {
throw new McpError(
ErrorCode.InvalidParams,
`Resource name exceeds maximum length of ${options.maxSearchLength} characters.`
);
}

const { exactMatches, searchResults } = searchComponents.memo(name, { names: pfComponentNames });
let result: ComponentSchema | undefined = undefined;

Expand Down
9 changes: 8 additions & 1 deletion src/tool.componentSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ const componentSchemasTool = (options = getOptions()): McpTool => {
);
}

if (componentName.length > options.maxSearchLength) {
throw new McpError(
ErrorCode.InvalidParams,
`Component name exceeds maximum length of ${options.maxSearchLength} characters.`
);
}

// Use fuzzySearch with `isFuzzyMatch` to handle exact and intentional suggestions in one pass
const results = fuzzySearch(componentName, componentNames, {
maxDistance: 3,
Expand Down Expand Up @@ -89,7 +96,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().describe('Name of the PatternFly component (e.g., "Button", "Table")')
componentName: z.string().max(options.maxSearchLength).describe('Name of the PatternFly component (e.g., "Button", "Table")')
}
},
callback
Expand Down
9 changes: 8 additions & 1 deletion src/tool.patternFlyDocs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ const usePatternFlyDocsTool = (options = getOptions()): McpTool => {
);
}

if (isName && name.length > options.maxSearchLength) {
throw new McpError(
ErrorCode.InvalidParams,
`String "name" exceeds maximum length of ${options.maxSearchLength} characters.`
);
}

const updatedUrlList = isUrlList ? urlList.slice(0, options.recommendedMaxDocsToLoad) : [];

if (isUrlList && urlList.length > options.recommendedMaxDocsToLoad) {
Expand Down Expand Up @@ -171,7 +178,7 @@ const usePatternFlyDocsTool = (options = getOptions()): McpTool => {
`,
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().optional().describe('The name of a PatternFly component to fetch documentation for (e.g., "Button", "Table")')
name: z.string().max(options.maxSearchLength).optional().describe('The name of a PatternFly component to fetch documentation for (e.g., "Button", "Table")')
}
},
callback
Expand Down
13 changes: 11 additions & 2 deletions src/tool.searchPatternFlyDocs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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';
Expand Down Expand Up @@ -194,9 +195,10 @@ searchComponents.memo = memo(searchComponents, DEFAULT_OPTIONS.toolMemoOptions.s
* Searches for PatternFly component documentation URLs using fuzzy search.
* Returns URLs only (does not fetch content). Use usePatternFlyDocs to fetch the actual content.
*
* @param options - Optional configuration options (defaults to OPTIONS)
* @returns MCP tool tuple [name, schema, callback]
*/
const searchPatternFlyDocsTool = (): McpTool => {
const searchPatternFlyDocsTool = (options = getOptions()): McpTool => {
const callback = async (args: any = {}) => {
const { searchQuery } = args;

Expand All @@ -207,6 +209,13 @@ const searchPatternFlyDocsTool = (): McpTool => {
);
}

if (searchQuery.length > options.maxSearchLength) {
throw new McpError(
ErrorCode.InvalidParams,
`Search query exceeds ${options.maxSearchLength} character max length.`
);
}

const { isSearchWildCardAll, searchResults } = searchComponents.memo(searchQuery, { allowWildCardAll: true });

if (!isSearchWildCardAll && searchResults.length === 0) {
Expand Down Expand Up @@ -271,7 +280,7 @@ const searchPatternFlyDocsTool = (): McpTool => {
- Documentation URLs that can be used with "usePatternFlyDocs"
`,
inputSchema: {
searchQuery: z.string().describe('Full or partial component name to search for (e.g., "button", "table", "*")')
searchQuery: z.string().max(options.maxSearchLength).describe('Full or partial component name to search for (e.g., "button", "table", "*")')
}
},
callback
Expand Down
Loading