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
6 changes: 5 additions & 1 deletion docs/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,15 @@ Context management (also called **token-saver mode**) switches the server from t

| Mode | Registered tools | Typical workflow |
|-------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------|
| **Default** _(off)_ | `searchPatternFlyDocs`, `usePatternFlyDocs` | Search returns text with URLs and names; fetch content with `usePatternFlyDocs`. |
| **Default** _(off)_ | `searchPatternFlyDocs`, `usePatternFlyDocs` | Search returns text with URLs, names, and URIs; fetch content with `usePatternFlyDocs`. |
| **Context management** | `searchPatternFly` only | Search returns `resource_link` items; read content with `resources/read`. |

Built-in MCP resources (`patternfly://docs/...`, `patternfly://schemas/...`, indexes, and `patternfly://context`) remain available in both modes.

> **Important!**
>
> In **default** mode, `searchPatternFlyDocs` returns and `usePatternFlyDocs` accepts `patternfly://` URIs. This is a compatibility bridge for limited MCP clients. This is a transitional allowance: context management is the architecture the PatternFly MCP tools are moving towards, where URIs are returned as links and read through MCP resources. However, this does not mean the older technique of returning Markdown will go away; there is still a possible future where the PatternFly MCP retains the older Markdown response techniques to purposefully support limited and agentless MCP clients.

### Tool: searchPatternFly

Registered only when context management is enabled.
Expand Down
12 changes: 8 additions & 4 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ Core server tools provide a resource library for PatternFly. They are extensible

### Tool: searchPatternFlyDocs

Use this to search for PatternFly documentation URLs and component names. Accepts partial string matches or `*` to list all available components. From the content, you can select specific URLs and component names to use with `usePatternFlyDocs`.
Use this to search for PatternFly documentation URLs, `patternfly://` resource URIs, and component names. Accepts partial string matches or `*` to list all available components. From the content, you can select specific URLs, URIs, and component names to use with `usePatternFlyDocs`.

> **Transitional URI support**: The default tools also return and accept `patternfly://` URIs for compatibility. Using and passing URIs through these tools is supported as a compatibility bridge for the intended workflow; see [experimental context management](./experimental.md#contextmanagement) for details on the transitional allowance for limited MCP clients.
**Parameters:**
- `searchQuery`: `string` (required) - Full or partial component name to search for (e.g., "button", "table", "*" for all components)
Expand All @@ -32,13 +34,15 @@ Use this to search for PatternFly documentation URLs and component names. Accept

### Tool: usePatternFlyDocs

Fetch full documentation and component JSON schemas for specific PatternFly URLs or component names.
Fetch full documentation and component JSON schemas for specific PatternFly URLs, `patternfly://` URIs, or component names.

> **Feature**: This tool automatically detects if a URL belongs to a component (or if a "name" is provided) and appends its machine-readable JSON schema (props, types, validation) to the response, combining human-readable documentation with technical specifications.
**Parameters:** _Parameters are mutually exclusive. Provide either `name` OR `urlList`, not both._
- `name`: `string` (optional) - The name of the PatternFly component (e.g., "Button", "Modal"). **Recommended** for known component lookups.
- `urlList`: `string[]` (optional) - A list of specific documentation URLs discovered via `searchPatternFlyDocs` (max 15 at a time).
- `name`: `string` (optional) - A PatternFly component or resource name (e.g., `"Button"`, `"Modal"`), or a `patternfly://` URI (e.g., `"patternfly://docs/button"`). Names are **recommended** for known component lookups.
- `urlList`: `string[]` (optional) - A list of documentation URLs and/or `patternfly://` URIs from `searchPatternFlyDocs` (max 15 at a time).

> **Transitional URI support**: Prefer reading `patternfly://` URIs with MCP `resources/read` when your client supports it. Passing URIs through this tool is supported as a compatibility bridge for the intended workflow; see [experimental context management](./experimental.md#contextmanagement) for details on the transitional allowance for limited MCP clients.
**Example with name:**
```json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ ipsum documentation content
---
# Component Schema for button (v6)
# Component Schema for Button (v6)
This machine-readable JSON schema defines the component's props, types, and validation rules.
\`\`\`json
{
Expand Down
48 changes: 48 additions & 0 deletions src/__tests__/tool.patternFlyDocs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getPatternFlyComponentSchema, getPatternFlyMcpResources, setCategoryDis
import { searchPatternFly } from '../patternFly.search';
import { isPlainObject } from '../server.helpers';
import { usePatternFlyDocsTool } from '../tool.patternFlyDocs';
import { DEFAULT_OPTIONS } from '../options.defaults';

// Mock dependencies
jest.mock('../server.getResources');
Expand All @@ -17,6 +18,7 @@ const mockProcessDocs = processDocsFunction as jest.MockedFunction<typeof proces
const mockComponentSchema = getPatternFlyComponentSchema as jest.MockedFunction<typeof getPatternFlyComponentSchema>;
const mockGetResources = getPatternFlyMcpResources as jest.MockedFunction<typeof getPatternFlyMcpResources>;
const mockSearch = searchPatternFly as jest.MockedFunction<typeof searchPatternFly>;
const mockSchema = getPatternFlyComponentSchema as jest.MockedFunction<typeof getPatternFlyComponentSchema>;
const mockSetCategoryLabel = setCategoryDisplayLabel as jest.MockedFunction<typeof setCategoryDisplayLabel>;

describe('usePatternFlyDocsTool', () => {
Expand Down Expand Up @@ -176,4 +178,50 @@ describe('usePatternFlyDocsTool, callback', () => {

expect(result.content).toMatchSnapshot('Button');
});

it('usePatternFlyDocs resolves patternfly:// URIs in name', async () => {
mockSearch.mockResolvedValue({
exactMatches: [{
name: 'ToolbarFilter',
isSchemasAvailable: true,
entries: [{ name: 'ToolbarFilter', version: 'v6', path: '' }]
}],
searchResults: []
} as any);

mockSchema.mockResolvedValue({
name: 'ToolbarFilter',
schema: { type: 'object' }
} as any);

mockProcessDocs.mockResolvedValue([]);

const [_name, _schema, callback] = usePatternFlyDocsTool(DEFAULT_OPTIONS);
const result = await callback({ name: 'patternfly://docs/ToolbarFilter' }) as any;

expect(result.content[0].text).toContain('Component Schema for ToolbarFilter');
});

it('usePatternFlyDocs resolves patternfly:// URIs in urlList', async () => {
mockSearch.mockResolvedValue({
exactMatches: [{
name: 'ToolbarFilter',
isSchemasAvailable: true,
entries: [{ name: 'ToolbarFilter', version: 'v6', path: '' }]
}],
searchResults: []
} as any);

mockSchema.mockResolvedValue({
name: 'ToolbarFilter',
schema: { type: 'object' }
} as any);

mockProcessDocs.mockResolvedValue([]);

const [_name, _schema, callback] = usePatternFlyDocsTool(DEFAULT_OPTIONS);
const result = await callback({ urlList: ['patternfly://docs/ToolbarFilter'] }) as any;

expect(result.content[0].text).toContain('Component Schema for ToolbarFilter');
});
});
2 changes: 2 additions & 0 deletions src/__tests__/tool.searchPatternFlyDocs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,5 +143,7 @@ describe('searchPatternFlyDocsTool, callback', () => {
const result = await callback({ searchQuery: 'button' });

expect(result.content).toMatchSnapshot('Button');
expect(result.content[0].text).toContain('patternfly://docs/button');
expect(result.content[0].text).toContain('patternfly://schemas/button');
});
});
2 changes: 1 addition & 1 deletion src/docs.filterWords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const INDEX_BLOCKLIST_WORDS = ['patternfly', 'component', 'components', 'documen
* @note If "AI" starts producing noisy or overly broad matches in search, remove it from this
* list and consider adding it to the noise words or blocklist.
*/
const INDEX_EXCEPTION_WORDS = ['cli', 'css', 'ai', 'rtl', 'ltr'];
const INDEX_EXCEPTION_WORDS = ['cli', 'css', 'ai', 'rtl', 'ltr', 'theming'];

/**
* Noise words that are common and do not add significant value to search results.
Expand Down
13 changes: 13 additions & 0 deletions src/patternFly.support.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { isUrl } from './server.helpers';

/**
* Check if a value is a valid PatternFly URI.
*
* @param uri - URI to check
* @returns `true` if the string is a valid PatternFly URI.
*/
const isPatternFlyUri = (uri: unknown): uri is string => isUrl(uri, { allowedProtocols: ['patternfly'] });

export {
isPatternFlyUri
};
14 changes: 13 additions & 1 deletion src/resource.patternFlyDocsTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,10 @@ const resourceCallback = async (passedUri: URL, variables: Record<string, string
);
}

const hasSchemas = byEntry.some(entry => entry.uriSchemasId);

assertInput(
docs.length > 0,
docs.length > 0 || hasSchemas,
() => {
let suggestionMessage = '';

Expand All @@ -159,6 +161,16 @@ const resourceCallback = async (passedUri: URL, variables: Record<string, string
}
);

if (docs.length === 0 && hasSchemas) {
return {
contents: byEntry.filter(entry => entry.uriSchemasId).map(entry => ({
uri: entry.uriId,
mimeType: 'text/markdown',
text: `# ${entry.displayName}\n\nNo documentation is available for this component. But a [JSON schema is available](${entry.uriSchemasId}).`
}))
};
}

return {
contents: docs.map(({ uri, path, resolvedPath, content }) => ({
uri,
Expand Down
14 changes: 12 additions & 2 deletions src/server.assertions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import assert from 'node:assert';
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
import { isWhitelistedUrl, stringJoin } from './server.helpers';
import {
isUrl,
isWhitelistedUrl,
stringJoin
} from './server.helpers';
import { isPatternFlyUri } from './patternFly.support';
import { DEFAULT_OPTIONS, type WhitelistUrl } from './options.defaults';

/**
Expand Down Expand Up @@ -160,8 +165,13 @@ function assertInputUrlWhiteListed(

updatedInput.forEach(url => {
const isRemote = typeof url === 'string' && allowedProtocols.some(protocol => url.startsWith(protocol));
const isPfUri = isPatternFlyUri(url);

if (isRemote && !isWhitelistedUrl(url, whitelist, { allowedProtocols })) {
if (isRemote) {
if (!isWhitelistedUrl(url, whitelist, { allowedProtocols })) {
invalidUrls.push(url);
}
} else if (isUrl(url, { isStrict: false }) && !isPfUri) {
invalidUrls.push(url);
}
});
Expand Down
Loading
Loading