From a7a0469b687908858064ab920fb07d8bb6324e3f Mon Sep 17 00:00:00 2001 From: Joachim Schuler Date: Wed, 6 May 2026 12:38:15 -0400 Subject: [PATCH 1/4] feat: Add support for project felt --- .../options.defaults.test.ts.snap | 2 + src/__tests__/aiGuidelines.catalog.test.ts | 40 +++++ .../catalog.expandGithubDirectory.test.ts | 37 ++++ src/__tests__/docs.json.test.ts | 26 ++- .../fixtures/githubAiGuidelinesDirectory.ts | 10 ++ src/__tests__/fixtures/mockGithubFetch.ts | 36 ++++ src/__tests__/patternFly.search.test.ts | 38 ++++ .../resource.patternFlyDocsTemplate.test.ts | 55 +++++- src/catalog.expandGithubDirectory.ts | 170 ++++++++++++++++++ src/docs.embedded.ts | 55 +++++- src/docs.json | 23 ++- src/options.defaults.ts | 4 +- src/patternFly.getResources.ts | 44 ++++- src/resource.patternFlyDocsTemplate.ts | 2 +- src/tool.patternFlyDocs.ts | 2 +- .../__snapshots__/httpTransport.test.ts.snap | 2 +- .../__snapshots__/stdioTransport.test.ts.snap | 8 +- tests/prompts.md | 97 ++++++++++ 18 files changed, 614 insertions(+), 37 deletions(-) create mode 100644 src/__tests__/aiGuidelines.catalog.test.ts create mode 100644 src/__tests__/catalog.expandGithubDirectory.test.ts create mode 100644 src/__tests__/fixtures/githubAiGuidelinesDirectory.ts create mode 100644 src/__tests__/fixtures/mockGithubFetch.ts create mode 100644 src/catalog.expandGithubDirectory.ts create mode 100644 tests/prompts.md diff --git a/src/__tests__/__snapshots__/options.defaults.test.ts.snap b/src/__tests__/__snapshots__/options.defaults.test.ts.snap index 4b202a0c..2205c1d4 100644 --- a/src/__tests__/__snapshots__/options.defaults.test.ts.snap +++ b/src/__tests__/__snapshots__/options.defaults.test.ts.snap @@ -74,6 +74,8 @@ exports[`options defaults should return specific properties: defaults 1`] = ` "https://patternfly.org", "https://github.com/patternfly", "https://raw.githubusercontent.com/patternfly", + "https://github.com/project-felt", + "https://raw.githubusercontent.com/project-felt", ], "urlWhitelistProtocols": [ "http", diff --git a/src/__tests__/aiGuidelines.catalog.test.ts b/src/__tests__/aiGuidelines.catalog.test.ts new file mode 100644 index 00000000..74325519 --- /dev/null +++ b/src/__tests__/aiGuidelines.catalog.test.ts @@ -0,0 +1,40 @@ +import { expandGithubDirectoryInCatalog } from '../catalog.expandGithubDirectory'; +import type { PatternFlyMcpDocsCatalogSource } from '../docs.embedded'; +import docsJson from '../docs.json'; +import { installGithubFetchMock, restoreNativeFetch } from './fixtures/mockGithubFetch'; + +const FELT_RAW_PREFIX = 'https://raw.githubusercontent.com/project-felt/ai-guidelines/'; + +const EXPECTED_FILENAMES = [ + 'ai-design-principles.md', + 'animation.md', + 'chatbot-avatars.md', + 'color.md', + 'iconography.md', + 'legal-requirements.md', + 'transparency-notices.md' +]; + +describe('AiGuidelines docs.json catalog', () => { + beforeAll(() => installGithubFetchMock()); + afterAll(() => restoreNativeFetch()); + + it('should expand directory stub into seven project-felt entries', async () => { + const expanded = await expandGithubDirectoryInCatalog(docsJson as PatternFlyMcpDocsCatalogSource); + const entries = expanded.docs.AiGuidelines; + + expect(entries).toBeDefined(); + expect(entries).toHaveLength(EXPECTED_FILENAMES.length); + expect(expanded.meta.totalDocs).toBe(331); + + for (const entry of entries ?? []) { + expect(entry.path.startsWith(FELT_RAW_PREFIX)).toBe(true); + expect(entry.path.endsWith('.md')).toBe(true); + expect(entry.version).toBe('v6'); + } + + const basenames = (entries ?? []).map(docEntry => docEntry.path.split('/').pop()); + + expect(basenames.sort()).toEqual([...EXPECTED_FILENAMES].sort()); + }); +}); diff --git a/src/__tests__/catalog.expandGithubDirectory.test.ts b/src/__tests__/catalog.expandGithubDirectory.test.ts new file mode 100644 index 00000000..3754ecfd --- /dev/null +++ b/src/__tests__/catalog.expandGithubDirectory.test.ts @@ -0,0 +1,37 @@ +import { expandGithubDirectoryInCatalog } from '../catalog.expandGithubDirectory'; +import type { PatternFlyMcpDocsCatalogSource } from '../docs.embedded'; + +describe('expandGithubDirectoryInCatalog', () => { + it('should reject expansion for repos outside the allowlist', async () => { + const catalog: PatternFlyMcpDocsCatalogSource = { + version: '1', + generated: new Date().toISOString(), + meta: { + totalEntries: 1, + totalDocs: 1, + source: 'test' + }, + docs: { + BadExpand: [ + { + displayName: 'Invalid', + description: 'Should fail', + pathSlug: 'invalid', + section: 'guidelines', + category: 'ai', + source: 'github', + version: 'v6', + expandGithubDirectory: { + owner: 'disallowed', + repo: 'vendor', + ref: 'main', + directoryPath: 'docs' + } + } + ] + } + }; + + await expect(expandGithubDirectoryInCatalog(catalog)).rejects.toThrow(/not enabled/); + }); +}); diff --git a/src/__tests__/docs.json.test.ts b/src/__tests__/docs.json.test.ts index 5e31750c..a53be847 100644 --- a/src/__tests__/docs.json.test.ts +++ b/src/__tests__/docs.json.test.ts @@ -1,5 +1,6 @@ import { distance } from 'fastest-levenshtein'; import docsJson from '../docs.json'; +import type { PatternFlyMcpDocsCatalogDocImport } from '../docs.embedded'; describe('docs.json', () => { it('should have a valid top-level generated timestamp (ISO date string)', () => { @@ -23,21 +24,28 @@ describe('docs.json', () => { let totalDocs = 0; Object.entries(docsJson.docs).forEach(([key, entries]) => { - entries.forEach(entry => { + entries.forEach((entry: PatternFlyMcpDocsCatalogDocImport) => { totalDocs += 1; - allLinks.add(entry.path); - const path = entry.path; + const linkKey = 'expandGithubDirectory' in entry + ? `catalog-expand:${entry.expandGithubDirectory.owner}/${entry.expandGithubDirectory.repo}:${entry.expandGithubDirectory.directoryPath}@${entry.expandGithubDirectory.ref}` + : entry.path; - if (!linkMap.has(path)) { - linkMap.set(path, []); + allLinks.add(linkKey); + + if (!linkMap.has(linkKey)) { + linkMap.set(linkKey, []); } - linkMap.get(path)?.push(`${key}: ${entry.displayName} (${entry.category})`); + linkMap.get(linkKey)?.push(`${key}: ${entry.displayName} (${entry.category})`); - if (entry.path.includes('documentation:')) { + if ('expandGithubDirectory' in entry) { + baseHashes.add(entry.expandGithubDirectory.ref); + } else if (entry.path.includes('documentation:')) { baseHashes.add('documentation:'); } else if (/^https:\/\/raw\.githubusercontent\.com\/patternfly\/[a-zA-Z0-9-]+\//.test(entry.path)) { baseHashes.add(entry.path.split(/\/patternfly\/[a-zA-Z0-9-]+\//)[1]?.split('/')[0]); + } else if (/^https:\/\/raw\.githubusercontent\.com\/project-felt\/[a-zA-Z0-9-]+\//.test(entry.path)) { + baseHashes.add(entry.path.split(/\/project-felt\/[a-zA-Z0-9-]+\//)[1]?.split('/')[0]); } else { baseHashes.add(`new-resource-${entry.path}`); } @@ -67,9 +75,9 @@ describe('docs.json', () => { * If you are updating `docs.json` with an agent confirm altering this value is acceptable * when you open your MR/PR. You may be asked to change your git hash to one of the * existing values and keep this value the same. - * 1 (v6 org) + 1 (v6 react) + 1 (v5 org) + 1 (codemods) + 1 (ai-helpers) + 1 (patternfly-cli) + * 1 (v6 org) + 1 (v6 react) + 1 (v5 org) + 1 (codemods) + 1 (ai-helpers) + 1 (patternfly-cli) + 1 (project-felt/ai-guidelines) */ - expect(baseHashes.size).toBe(6); + expect(baseHashes.size).toBe(7); /** * Confirm total docs count matches metadata diff --git a/src/__tests__/fixtures/githubAiGuidelinesDirectory.ts b/src/__tests__/fixtures/githubAiGuidelinesDirectory.ts new file mode 100644 index 00000000..83f11751 --- /dev/null +++ b/src/__tests__/fixtures/githubAiGuidelinesDirectory.ts @@ -0,0 +1,10 @@ +/** Mirrors GitHub Contents API listing for project-felt/ai-guidelines/content (unit tests + jest.setup). */ +export const GITHUB_AI_GUIDELINES_DIRECTORY_FIXTURE = [ + { name: 'ai-design-principles.md', type: 'file', download_url: 'https://raw.githubusercontent.com/project-felt/ai-guidelines/refs/heads/main/content/ai-design-principles.md' }, + { name: 'animation.md', type: 'file', download_url: 'https://raw.githubusercontent.com/project-felt/ai-guidelines/refs/heads/main/content/animation.md' }, + { name: 'chatbot-avatars.md', type: 'file', download_url: 'https://raw.githubusercontent.com/project-felt/ai-guidelines/refs/heads/main/content/chatbot-avatars.md' }, + { name: 'color.md', type: 'file', download_url: 'https://raw.githubusercontent.com/project-felt/ai-guidelines/refs/heads/main/content/color.md' }, + { name: 'iconography.md', type: 'file', download_url: 'https://raw.githubusercontent.com/project-felt/ai-guidelines/refs/heads/main/content/iconography.md' }, + { name: 'legal-requirements.md', type: 'file', download_url: 'https://raw.githubusercontent.com/project-felt/ai-guidelines/refs/heads/main/content/legal-requirements.md' }, + { name: 'transparency-notices.md', type: 'file', download_url: 'https://raw.githubusercontent.com/project-felt/ai-guidelines/refs/heads/main/content/transparency-notices.md' } +]; diff --git a/src/__tests__/fixtures/mockGithubFetch.ts b/src/__tests__/fixtures/mockGithubFetch.ts new file mode 100644 index 00000000..4c9aeaa1 --- /dev/null +++ b/src/__tests__/fixtures/mockGithubFetch.ts @@ -0,0 +1,36 @@ +import { GITHUB_AI_GUIDELINES_DIRECTORY_FIXTURE } from './githubAiGuidelinesDirectory'; + +const nativeFetch = globalThis.fetch; + +/** + * Mock fetch to intercept GitHub Contents API calls for ai-guidelines. + * Call in beforeAll/beforeEach of tests that trigger catalog expansion. + */ +export const installGithubFetchMock = () => { + globalThis.fetch = jest.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + const url = typeof input === 'string' + ? input + : input instanceof URL + ? input.href + : 'url' in input && typeof input.url === 'string' + ? input.url + : String(input); + + if (url.includes('api.github.com/repos/project-felt/ai-guidelines/contents/content')) { + return new Response(JSON.stringify(GITHUB_AI_GUIDELINES_DIRECTORY_FIXTURE), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } + + if (typeof nativeFetch === 'function') { + return nativeFetch(input as RequestInfo, init); + } + + throw new Error(`Unhandled fetch in test: ${url}`); + }) as typeof fetch; +}; + +export const restoreNativeFetch = () => { + globalThis.fetch = nativeFetch; +}; diff --git a/src/__tests__/patternFly.search.test.ts b/src/__tests__/patternFly.search.test.ts index b3a58179..0029d786 100644 --- a/src/__tests__/patternFly.search.test.ts +++ b/src/__tests__/patternFly.search.test.ts @@ -1,4 +1,5 @@ import { filterPatternFly, searchPatternFly } from '../patternFly.search'; +import { installGithubFetchMock, restoreNativeFetch } from './fixtures/mockGithubFetch'; describe('filterPatternFly', () => { it.each([ @@ -115,3 +116,40 @@ describe('searchPatternFly', () => { expect(totalPotentialMatches).toBeGreaterThanOrEqual(totalResults); }); }); + +describe('searchPatternFly, AiGuidelines catalog', () => { + beforeAll(() => installGithubFetchMock()); + afterAll(() => restoreNativeFetch()); + const feltPath = (paths: { path?: string }[]) => + paths.some(entry => typeof entry.path === 'string' && entry.path.includes('project-felt/ai-guidelines')); + + it('should exact-match resource aiguidelines for query AiGuidelines', async () => { + const { exactMatches, searchResults } = await searchPatternFly('AiGuidelines'); + + const byName = new Map(searchResults.map(result => [result.name, result])); + const aiGuidelinesResource = byName.get('aiguidelines'); + + expect(aiGuidelinesResource).toBeDefined(); + expect(exactMatches.some(match => match.name === 'aiguidelines')).toBe(true); + expect(feltPath(aiGuidelinesResource!.entries)).toBe(true); + expect(aiGuidelinesResource!.entries.length).toBeGreaterThanOrEqual(7); + }); + + it('should exact-match resource aiguidelines for query aiguidelines', async () => { + const { exactMatches } = await searchPatternFly('aiguidelines'); + + expect(exactMatches.some(match => match.name === 'aiguidelines')).toBe(true); + }); + + it.each([ + { description: 'transparency wording', search: 'transparency notices' }, + { description: 'chatbot avatars display name fragment', search: 'chatbot avatars' }, + { description: 'legal requirements description fragment', search: 'legal review' } + ])('should surface project-felt AiGuidelines URLs when searching $description', async ({ search }) => { + const { searchResults } = await searchPatternFly(search); + + const hitsFelt = searchResults.some(result => feltPath(result.entries)); + + expect(hitsFelt).toBe(true); + }); +}); diff --git a/src/__tests__/resource.patternFlyDocsTemplate.test.ts b/src/__tests__/resource.patternFlyDocsTemplate.test.ts index 48adffd5..2f6a4cfc 100644 --- a/src/__tests__/resource.patternFlyDocsTemplate.test.ts +++ b/src/__tests__/resource.patternFlyDocsTemplate.test.ts @@ -5,6 +5,23 @@ import { resourceCallback } from '../resource.patternFlyDocsTemplate'; import { isPlainObject } from '../server.helpers'; +import { GITHUB_AI_GUIDELINES_DIRECTORY_FIXTURE } from './fixtures/githubAiGuidelinesDirectory'; + +const getFetchUrl = (input: RequestInfo | URL): string => { + if (typeof input === 'string') { + return input; + } + + if (input instanceof URL) { + return input.href; + } + + if (typeof input === 'object' && input !== null && 'url' in input && typeof (input as Request).url === 'string') { + return (input as Request).url; + } + + return String(input); +}; jest.mock('node:fs/promises', () => ({ ...jest.requireActual('node:fs/promises'), @@ -74,10 +91,21 @@ describe('resourceCallback', () => { const mockContent = `Mock content for ${variables.name}`; mockReadFile.mockResolvedValue(mockContent); - mockFetch.mockResolvedValue({ - ok: true, - text: () => mockContent - } as any); + mockFetch.mockImplementation(async (input: RequestInfo | URL, _init?: RequestInit) => { + const urlStr = getFetchUrl(input); + + if (urlStr.includes('api.github.com/repos/project-felt/ai-guidelines')) { + return new Response(JSON.stringify(GITHUB_AI_GUIDELINES_DIRECTORY_FIXTURE), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } + + return { + ok: true, + text: async () => mockContent + } as Response; + }); const result = await resourceCallback( { href: `patternfly://docs/${variables.version}/${variables.name}` } as any, @@ -149,10 +177,21 @@ describe('resourceCallback', () => { const mockContent = `Mock content for ${variables.name}`; mockReadFile.mockResolvedValue(mockContent); - mockFetch.mockResolvedValue({ - ok: true, - text: () => mockContent - } as any); + mockFetch.mockImplementation(async (input: RequestInfo | URL, _init?: RequestInit) => { + const urlStr = getFetchUrl(input); + + if (urlStr.includes('api.github.com/repos/project-felt/ai-guidelines')) { + return new Response(JSON.stringify(GITHUB_AI_GUIDELINES_DIRECTORY_FIXTURE), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } + + return { + ok: true, + text: async () => mockContent + } as Response; + }); const uri = new URL('patternfly://docs/test'); diff --git a/src/catalog.expandGithubDirectory.ts b/src/catalog.expandGithubDirectory.ts new file mode 100644 index 00000000..6669506b --- /dev/null +++ b/src/catalog.expandGithubDirectory.ts @@ -0,0 +1,170 @@ +import type { + ExpandGithubDirectoryConfig, + PatternFlyMcpDocsCatalog, + PatternFlyMcpDocsCatalogDoc, + PatternFlyMcpDocsCatalogDocStub, + PatternFlyMcpDocsCatalogEntry, + PatternFlyMcpDocsCatalogSource +} from './docs.embedded'; +import { log } from './logger'; + +type GithubApiContentFile = { + name: string; + type: string; + download_url: string | null; +}; + +const GITHUB_API_USER_AGENT = 'patternfly-mcp-catalog-expansion'; + +/** Repos allowed to use directory expansion (defense in depth). */ +const EXPANSION_ALLOWLIST = new Set(['project-felt/ai-guidelines']); + +const assertExpansionAllowed = (owner: string, repo: string) => { + const slug = `${owner}/${repo}`; + + if (!EXPANSION_ALLOWLIST.has(slug)) { + throw new Error(`catalog: GitHub directory expansion is not enabled for ${slug}`); + } +}; + +const buildGithubContentsApiUrl = (owner: string, repo: string, directoryPath: string, ref: string) => { + const encodedPath = directoryPath.split('/').filter(Boolean).map(encodeURIComponent).join('/'); + const query = new URLSearchParams({ ref }); + + return `https://api.github.com/repos/${owner}/${repo}/contents/${encodedPath}?${query.toString()}`; +}; + +const displayNameFromMarkdownFile = (filename: string, category: string) => { + const base = filename.replace(/\.md$/i, ''); + const words = base.split('-').filter(Boolean).map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()); + const title = words.join(' '); + + return category === 'ai' ? `AI ${title}` : title; +}; + +const pathSlugFromFilename = (filename: string) => filename.replace(/\.md$/i, '').replace(/[^a-zA-Z0-9-]+/g, '-').toLowerCase(); + +/** + * List Markdown files (or files matching `includePattern`) in a GitHub directory via the Contents API. + * + * @param config - GitHub coordinates and optional filename pattern. + */ +const fetchGithubMarkdownFiles = async (config: ExpandGithubDirectoryConfig): Promise => { + assertExpansionAllowed(config.owner, config.repo); + + const url = buildGithubContentsApiUrl(config.owner, config.repo, config.directoryPath, config.ref); + const headers: Record = { + Accept: 'application/vnd.github+json', + 'User-Agent': GITHUB_API_USER_AGENT + }; + + if (process.env.GITHUB_TOKEN?.trim()) { + headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN.trim()}`; + } + + const response = await fetch(url, { headers }); + + if (!response.ok) { + const body = await response.text(); + + throw new Error(`catalog: GitHub API ${response.status} for ${url}: ${body.slice(0, 500)}`); + } + + const bodyText = await response.text(); + const data = JSON.parse(bodyText) as unknown; + + if (!Array.isArray(data)) { + throw new Error(`catalog: expected directory listing array from GitHub API for ${url}`); + } + + const includeRe = new RegExp(config.includePattern ?? '\\.md$', 'i'); + + return data.filter((item): item is GithubApiContentFile => { + if (!item || typeof item !== 'object') { + return false; + } + + const file = item as GithubApiContentFile; + + return file.type === 'file' && + typeof file.name === 'string' && + includeRe.test(file.name) && + typeof file.download_url === 'string' && + Boolean(file.download_url); + }); +}; + +/** + * Expand one stub row into concrete catalog docs (one per matched file). + * + * @param template - Catalog stub carrying shared metadata and `expandGithubDirectory`. + * @param config - Same as `template.expandGithubDirectory` (explicit for clarity). + */ +const expandCatalogDoc = async ( + template: PatternFlyMcpDocsCatalogDocStub, + config: ExpandGithubDirectoryConfig +): Promise => { + const files = await fetchGithubMarkdownFiles(config); + + if (files.length === 0) { + log.warn(`catalog: GitHub directory expansion returned no matching files for ${config.owner}/${config.repo}/${config.directoryPath}`); + + return []; + } + + return files.map(file => { + const displayName = displayNameFromMarkdownFile(file.name, template.category); + + return { + displayName, + description: `${template.description} (${file.name})`, + pathSlug: pathSlugFromFilename(file.name), + section: template.section, + category: template.category, + source: template.source, + version: template.version, + path: file.download_url! + }; + }); +}; + +/** + * Replace catalog rows that declare `expandGithubDirectory` with concrete per-file rows. + * Updates `meta.totalDocs` and `meta.totalEntries` to match the expanded structure. + * + * @param catalog - Loaded catalog (e.g. from `docs.json`), possibly containing stubs. + */ +const expandGithubDirectoryInCatalog = async (catalog: PatternFlyMcpDocsCatalogSource): Promise => { + const newDocs: PatternFlyMcpDocsCatalogEntry = {}; + + for (const [resourceKey, entries] of Object.entries(catalog.docs)) { + const expandedEntries: PatternFlyMcpDocsCatalogDoc[] = []; + + for (const entry of entries) { + if (!('expandGithubDirectory' in entry)) { + expandedEntries.push(entry); + continue; + } + + const nested = await expandCatalogDoc(entry, entry.expandGithubDirectory); + + expandedEntries.push(...nested); + } + + newDocs[resourceKey] = expandedEntries; + } + + const totalDocs = Object.values(newDocs).reduce((acc, list) => acc + list.length, 0); + + return { + ...catalog, + docs: newDocs, + meta: { + ...catalog.meta, + totalDocs, + totalEntries: Object.keys(newDocs).length + } + }; +}; + +export { expandGithubDirectoryInCatalog }; diff --git a/src/docs.embedded.ts b/src/docs.embedded.ts index 6c09d303..c24d9707 100644 --- a/src/docs.embedded.ts +++ b/src/docs.embedded.ts @@ -1,5 +1,16 @@ /** - * PatternFly JSON catalog doc + * Expand a GitHub directory into one catalog row per file (GitHub Contents API at runtime). + */ +type ExpandGithubDirectoryConfig = { + owner: string; + repo: string; + ref: string; + directoryPath: string; + includePattern?: string; +}; + +/** + * Resolved catalog row (concrete documentation URL). */ type PatternFlyMcpDocsCatalogDoc = { displayName: string; @@ -13,12 +24,35 @@ type PatternFlyMcpDocsCatalogDoc = { }; /** - * PatternFly JSON catalog documentation entries. + * Stub row expanded at startup via {@link ExpandGithubDirectoryConfig}. + */ +type PatternFlyMcpDocsCatalogDocStub = { + displayName: string; + description: string; + pathSlug: string; + section: string; + category: string; + source: string; + version: string; + expandGithubDirectory: ExpandGithubDirectoryConfig; +}; + +type PatternFlyMcpDocsCatalogDocImport = PatternFlyMcpDocsCatalogDoc | PatternFlyMcpDocsCatalogDocStub; + +/** + * PatternFly JSON catalog documentation entries (resolved). */ type PatternFlyMcpDocsCatalogEntry = { [key: string]: PatternFlyMcpDocsCatalogDoc[] }; +/** + * Catalog entries as stored in `docs.json` (may include expansion stubs). + */ +type PatternFlyMcpDocsCatalogEntryImport = { + [key: string]: PatternFlyMcpDocsCatalogDocImport[] +}; + /** * PatternFly documentation catalog. * @@ -39,6 +73,16 @@ interface PatternFlyMcpDocsCatalog { docs: PatternFlyMcpDocsCatalogEntry } +/** + * Shape of `docs.json` on disk (may include GitHub directory expansion stubs). + */ +interface PatternFlyMcpDocsCatalogSource { + version?: string; + generated?: string; + meta: PatternFlyMcpDocsCatalog['meta']; + docs: PatternFlyMcpDocsCatalogEntryImport; +} + /** * Fallback documentation for when the catalog is unavailable. * Points to the high-level entry points for PatternFly. @@ -109,7 +153,12 @@ const EMBEDDED_DOCS: PatternFlyMcpDocsCatalog = { export { EMBEDDED_DOCS, + type ExpandGithubDirectoryConfig, type PatternFlyMcpDocsCatalog, + type PatternFlyMcpDocsCatalogDoc, + type PatternFlyMcpDocsCatalogDocImport, + type PatternFlyMcpDocsCatalogDocStub, type PatternFlyMcpDocsCatalogEntry, - type PatternFlyMcpDocsCatalogDoc + type PatternFlyMcpDocsCatalogEntryImport, + type PatternFlyMcpDocsCatalogSource }; diff --git a/src/docs.json b/src/docs.json index f907b92b..7e4177cd 100644 --- a/src/docs.json +++ b/src/docs.json @@ -1,9 +1,9 @@ { "version": "1", - "generated": "2026-04-29T20:24:40.471Z", + "generated": "2026-05-05T00:00:00.000Z", "meta": { - "totalEntries": 130, - "totalDocs": 324, + "totalEntries": 131, + "totalDocs": 325, "source": "patternfly-mcp-internal" }, "docs": { @@ -3506,6 +3506,23 @@ "path": "https://raw.githubusercontent.com/patternfly/ai-helpers/aa2766e8d9cb2bc08c13e41106d75c9829bc001f/CONTRIBUTING.md", "version": "v6" } + ], + "AiGuidelines": [ + { + "displayName": "AI Guidelines (GitHub directory)", + "description": "project-felt/ai-guidelines", + "pathSlug": "ai-guidelines-directory", + "section": "guidelines", + "category": "ai", + "source": "github", + "version": "v6", + "expandGithubDirectory": { + "owner": "project-felt", + "repo": "ai-guidelines", + "ref": "main", + "directoryPath": "content" + } + } ] } } diff --git a/src/options.defaults.ts b/src/options.defaults.ts index 369ee198..57ad763d 100644 --- a/src/options.defaults.ts +++ b/src/options.defaults.ts @@ -477,7 +477,9 @@ const PATTERNFLY_OPTIONS: PatternFlyOptions = { urlWhitelist: [ 'https://patternfly.org', 'https://github.com/patternfly', - 'https://raw.githubusercontent.com/patternfly' + 'https://raw.githubusercontent.com/patternfly', + 'https://github.com/project-felt', + 'https://raw.githubusercontent.com/project-felt' ], urlWhitelistProtocols: ['http', 'https'] }; diff --git a/src/patternFly.getResources.ts b/src/patternFly.getResources.ts index c53b5a88..146589d3 100644 --- a/src/patternFly.getResources.ts +++ b/src/patternFly.getResources.ts @@ -12,14 +12,16 @@ import { log, formatUnknownError } from './logger'; import { EMBEDDED_DOCS, type PatternFlyMcpDocsCatalog, + type PatternFlyMcpDocsCatalogDoc, type PatternFlyMcpDocsCatalogEntry, - type PatternFlyMcpDocsCatalogDoc + type PatternFlyMcpDocsCatalogSource } from './docs.embedded'; import { INDEX_BLOCKLIST_WORDS, INDEX_EXCEPTION_WORDS, INDEX_NOISE_WORDS } from './docs.filterWords'; +import { expandGithubDirectoryInCatalog } from './catalog.expandGithubDirectory'; /** * Derive the component schema type from @patternfly/patternfly-component-schemas @@ -178,21 +180,53 @@ interface PatternFlyMcpAvailableResources extends PatternFlyVersionContext { * @returns PatternFly documentation catalog JSON, or fallback catalog if import fails. */ const getPatternFlyDocsCatalog = async (): Promise => { - let docsCatalog = EMBEDDED_DOCS; + let docsCatalog: PatternFlyMcpDocsCatalogSource = EMBEDDED_DOCS; let isFallback = false; try { if (process.env.NODE_ENV === 'local') { - docsCatalog = (await import('./docs.json', { with: { type: 'json' } })).default; + docsCatalog = (await import('./docs.json', { with: { type: 'json' } })).default as PatternFlyMcpDocsCatalogSource; } else { - docsCatalog = (await import('#docsCatalog', { with: { type: 'json' } })).default; + docsCatalog = (await import('#docsCatalog', { with: { type: 'json' } })).default as PatternFlyMcpDocsCatalogSource; } } catch (error) { isFallback = true; log.debug(`Failed to import docs catalog '#docsCatalog': ${formatUnknownError(error)}`, 'Using fallback docs catalog.'); } - return { ...docsCatalog, isFallback }; + if (isFallback) { + return { ...EMBEDDED_DOCS, isFallback }; + } + + try { + const resolvedCatalog = await expandGithubDirectoryInCatalog(docsCatalog); + + return { ...resolvedCatalog, isFallback }; + } catch (error) { + log.error(`Failed to expand GitHub directory catalog entries: ${formatUnknownError(error)}`, 'Serving catalog without expanded entries.'); + + // Strip stubs that have no `path` so downstream code doesn't break + const safeDocs: PatternFlyMcpDocsCatalogEntry = {}; + + for (const [key, entries] of Object.entries(docsCatalog.docs)) { + const concrete = entries.filter((e): e is PatternFlyMcpDocsCatalogDoc => 'path' in e && typeof e.path === 'string'); + + if (concrete.length > 0) { + safeDocs[key] = concrete; + } + } + + return { + ...docsCatalog, + docs: safeDocs, + meta: { + ...docsCatalog.meta, + totalDocs: Object.values(safeDocs).reduce((acc, list) => acc + list.length, 0), + totalEntries: Object.keys(safeDocs).length + }, + isFallback: true + }; + } }; /** diff --git a/src/resource.patternFlyDocsTemplate.ts b/src/resource.patternFlyDocsTemplate.ts index 46d869f1..bbe41149 100644 --- a/src/resource.patternFlyDocsTemplate.ts +++ b/src/resource.patternFlyDocsTemplate.ts @@ -120,7 +120,7 @@ const resourceCallback = async (passedUri: URL, variables: Record entry.path).filter(Boolean); + const matchedUrls = byEntry.map(entry => entry.path).filter((path): path is string => typeof path === 'string' && path.length > 0); if (matchedUrls.length > 0) { const processedDocs = await processDocsFunction.memo(matchedUrls); diff --git a/src/tool.patternFlyDocs.ts b/src/tool.patternFlyDocs.ts index 9340375d..9c4ab63f 100644 --- a/src/tool.patternFlyDocs.ts +++ b/src/tool.patternFlyDocs.ts @@ -110,7 +110,7 @@ const usePatternFlyDocsTool = (options = getOptions()): McpTool => { ErrorCode.InvalidParams ); - updatedUrlList.push(...exactMatches.flatMap(match => match.entries.map(entry => entry.path)).filter(Boolean)); + updatedUrlList.push(...exactMatches.flatMap(match => match.entries.map(entry => entry.path)).filter((path): path is string => typeof path === 'string' && path.length > 0)); } const docs: ProcessedDoc[] = []; diff --git a/tests/e2e/__snapshots__/httpTransport.test.ts.snap b/tests/e2e/__snapshots__/httpTransport.test.ts.snap index 46e46f49..66032946 100644 --- a/tests/e2e/__snapshots__/httpTransport.test.ts.snap +++ b/tests/e2e/__snapshots__/httpTransport.test.ts.snap @@ -31,7 +31,7 @@ Use these parameters to filter the PatternFly documentation index. | Parameter | Valid Values | Description | | :--- | :--- | :--- | -| \`category\` | \`accessibility\`, \`design-guidelines\`, \`design-tokens\`, \`development-guidelines\`, \`grammar\`, \`react\`, \`writing-guides\` | Filter by category | +| \`category\` | \`accessibility\`, \`ai\`, \`design-guidelines\`, \`design-tokens\`, \`development-guidelines\`, \`grammar\`, \`react\`, \`writing-guides\` | Filter by category | | \`section\` | \`ai\`, \`charts\`, \`components\`, \`content-design\`, \`extensions\`, \`foundations-and-styles\`, \`get-started\`, \`guidelines\`, \`layouts\`, \`patterns\`, \`resources\`, \`upgrade\` | Filter by section | | \`version\` | \`v4\`, \`v5\`, \`v6\` | Filter by version | diff --git a/tests/e2e/__snapshots__/stdioTransport.test.ts.snap b/tests/e2e/__snapshots__/stdioTransport.test.ts.snap index 098cc273..9dbcb7f8 100644 --- a/tests/e2e/__snapshots__/stdioTransport.test.ts.snap +++ b/tests/e2e/__snapshots__/stdioTransport.test.ts.snap @@ -31,7 +31,7 @@ Use these parameters to filter the PatternFly documentation index. | Parameter | Valid Values | Description | | :--- | :--- | :--- | -| \`category\` | \`accessibility\`, \`design-guidelines\`, \`design-tokens\`, \`development-guidelines\`, \`grammar\`, \`react\`, \`writing-guides\` | Filter by category | +| \`category\` | \`accessibility\`, \`ai\`, \`design-guidelines\`, \`design-tokens\`, \`development-guidelines\`, \`grammar\`, \`react\`, \`writing-guides\` | Filter by category | | \`section\` | \`ai\`, \`charts\`, \`components\`, \`content-design\`, \`extensions\`, \`foundations-and-styles\`, \`get-started\`, \`guidelines\`, \`layouts\`, \`patterns\`, \`resources\`, \`upgrade\` | Filter by section | | \`version\` | \`v4\`, \`v5\`, \`v6\` | Filter by version | @@ -147,10 +147,8 @@ exports[`Logging should allow setting logging options, default 1`] = `[]`; exports[`Logging should allow setting logging options, stderr 1`] = ` [ "[INFO]: Server logging enabled. -", - "[INFO]: Server stats enabled. -", - "[INFO]: No external resources loaded. +[INFO]: Server stats enabled. +[INFO]: No external resources loaded. ", "[INFO]: No external tools loaded. ", diff --git a/tests/prompts.md b/tests/prompts.md new file mode 100644 index 00000000..cc090d46 --- /dev/null +++ b/tests/prompts.md @@ -0,0 +1,97 @@ +# Prompt 1 — Search by resource key + +Using only the PatternFly MCP tools: call searchPatternFlyDocs with searchQuery "AiGuidelines". Summarize what URLs and names come back and confirm any raw.githubusercontent.com/project-felt/ai-guidelines URLs appear. + +# Prompt 2 — Search by distinctive phrase + +Using only the PatternFly MCP tools: call searchPatternFlyDocs with searchQuery "transparency notices". Then call searchPatternFlyDocs with searchQuery "chatbot avatars". For each, say whether the results include project-felt ai-guidelines raw markdown URLs. + +# Prompt 3 — Search by topic keywords + +Using only the PatternFly MCP tools: run searchPatternFlyDocs three times with searchQuery "ai design principles", then "animation", then "iconography". Report which hits map to project-felt ai-guidelines content. + +# Prompt 4 — Fetch one file by URL (recommended) + +Using only the PatternFly MCP tools: call searchPatternFlyDocs with searchQuery "ai design principles". From the results, take the raw GitHub URL for ai-design-principles.md, call usePatternFlyDocs with urlList containing only that URL, and paste the first markdown heading line from the response. + +# Prompt 5 — Fetch whole AiGuidelines bundle by name + +Using only the PatternFly MCP tools: call usePatternFlyDocs with name "AiGuidelines". Confirm the markdown mentions AI design principles, transparency, and legal requirements (quote one short line from the response for each theme if present). + +# Prompt 6 — Version filter (if your tools expose version) + +Using only the PatternFly MCP tools: call searchPatternFlyDocs with searchQuery "legal requirements" and version "v6". Then call usePatternFlyDocs with the URL list from that hit only (or name "AiGuidelines" if URLs are messy) and paste one sentence about legal review from the markdown. + +# Prompt 7 — After editing docs.json locally + +I changed src/docs.json and ran npm run build. Using only the PatternFly MCP tools, search for "felt" or "ai-guidelines" and fetch one matching doc with usePatternFlyDocs to prove the updated catalog is live. + + + + + + + +# Human-sounding MCP test prompts (no tool names) + +These are written so a good agent *should* fetch official PatternFly / Felt content instead of free-styling. They do **not** mention `searchPatternFlyDocs`, `usePatternFlyDocs`, or “MCP.” + +## Felt / `project-felt/ai-guidelines` (catalog: AiGuidelines) + +- What do the Felt AI guidelines say about **transparency notices** when the product uses generative AI? Cite the doc, not general advice. +- Summarize the **legal review** expectations for shipping an AI feature, per the **project-felt ai-guidelines** content. +- How should we treat **color** in UI for AI features? Pull guidance from the Felt AI guidelines, not PatternFly’s generic color page unless that’s the only source. +- What are the **chatbot avatar** rules (icons, launch affordances) in the Felt AI guidelines? +- List the main **AI design principles** from the Felt guidelines in short bullets, with one example each. +- What does the Felt material say about **animation** and **sparkle** effects for AI features? +- Compare **iconography** guidance for “AI” affordances (sparkles, etc.) in the Felt guidelines—what to use and what to avoid? + +## PatternFly (components / v6) + +- I’m building a **Button** in PatternFly v6. What are the **accessibility** must-dos for `Button` (keyboard, `aria-label`, disabled behavior)? +- For **DataList** in v6, what does the official doc say about **selection** or **row actions**—quote the relevant bit. +- What’s the difference between **primary** and **secondary** actions in a **Modal** flow, per PatternFly’s guidance for the **Modal** component? + +## “Doc-only” follow-ups (if answers feel generic) + +- In the Felt **transparency** doc, what should we show near an AI-generated answer? Be specific to that document. +- Per the Felt **legal** doc, when do we need **Product Legal** (or equivalent) sign-off before release? + +## Soft nudge (still no tool names) + +If the model answers from memory only, try: *“Open the official markdown in our PatternFly / Felt catalog and quote the first heading.”* + + + +## Neutral prompts (no product or doc names) + +### Transparency & disclosure + +- When we ship a feature that calls a generative model, what should users see **before** they start typing or get an answer—especially around **personal or sensitive** input? +- What’s the minimum we should do so it’s **obvious** the experience involves generated output—not just a tiny badge somewhere? +- For a **chat-style assistant** embedded in our app, what **persistent** reminders should sit near the input/output area about reviewing outputs? +- For **summaries** or **auto-generated blurbs** in search or results pages, how should we label them so users know they’re **model-generated**? +- Why might **labeling every AI-generated image** on a page be a bad idea? What’s the alternative approach suggested for broader notices? + +### Risk, review, and policy + +- Before we release customer-facing capabilities backed by generative models, what **review or consultation** steps does our internal material expect (named roles/processes as written)? +- What topics does our doc treat as needing **extra care** or **more indicators** when stakes are higher? + +### Visual design (icons, motion, color) + +- What rules do we follow for **sparkles**, **stars**, or similar **“AI” affordances** in icons—what to prefer and what to avoid? +- How should **motion** or **animation** be used around generative features so it helps recognition without being gratuitous? +- Are there **color** constraints or preferences when marking AI-related UI so it stays on-brand and accessible? + +### Chat UX & affordances + +- How should we handle **avatars**, **robot metaphors**, and **launch entry points** for an assistant so they’re clear but not misleading? + +### Principles & framing + +- What **design principles** does our internal doc give for experiences that include generative capabilities—framed as product goals, not implementation detail? + +### Doc-grounding nudge (still neutral) + +- Answer using **our internal markdown** only: quote the **first heading** and **one bullet** from the transparency section that applies to **virtual assistants**. From 809bbf9efc2c0be1c5c1d5fd0fdde5b688fa12f4 Mon Sep 17 00:00:00 2001 From: Joachim Schuler Date: Wed, 6 May 2026 12:43:08 -0400 Subject: [PATCH 2/4] remove a file --- tests/prompts.md | 97 ------------------------------------------------ 1 file changed, 97 deletions(-) delete mode 100644 tests/prompts.md diff --git a/tests/prompts.md b/tests/prompts.md deleted file mode 100644 index cc090d46..00000000 --- a/tests/prompts.md +++ /dev/null @@ -1,97 +0,0 @@ -# Prompt 1 — Search by resource key - -Using only the PatternFly MCP tools: call searchPatternFlyDocs with searchQuery "AiGuidelines". Summarize what URLs and names come back and confirm any raw.githubusercontent.com/project-felt/ai-guidelines URLs appear. - -# Prompt 2 — Search by distinctive phrase - -Using only the PatternFly MCP tools: call searchPatternFlyDocs with searchQuery "transparency notices". Then call searchPatternFlyDocs with searchQuery "chatbot avatars". For each, say whether the results include project-felt ai-guidelines raw markdown URLs. - -# Prompt 3 — Search by topic keywords - -Using only the PatternFly MCP tools: run searchPatternFlyDocs three times with searchQuery "ai design principles", then "animation", then "iconography". Report which hits map to project-felt ai-guidelines content. - -# Prompt 4 — Fetch one file by URL (recommended) - -Using only the PatternFly MCP tools: call searchPatternFlyDocs with searchQuery "ai design principles". From the results, take the raw GitHub URL for ai-design-principles.md, call usePatternFlyDocs with urlList containing only that URL, and paste the first markdown heading line from the response. - -# Prompt 5 — Fetch whole AiGuidelines bundle by name - -Using only the PatternFly MCP tools: call usePatternFlyDocs with name "AiGuidelines". Confirm the markdown mentions AI design principles, transparency, and legal requirements (quote one short line from the response for each theme if present). - -# Prompt 6 — Version filter (if your tools expose version) - -Using only the PatternFly MCP tools: call searchPatternFlyDocs with searchQuery "legal requirements" and version "v6". Then call usePatternFlyDocs with the URL list from that hit only (or name "AiGuidelines" if URLs are messy) and paste one sentence about legal review from the markdown. - -# Prompt 7 — After editing docs.json locally - -I changed src/docs.json and ran npm run build. Using only the PatternFly MCP tools, search for "felt" or "ai-guidelines" and fetch one matching doc with usePatternFlyDocs to prove the updated catalog is live. - - - - - - - -# Human-sounding MCP test prompts (no tool names) - -These are written so a good agent *should* fetch official PatternFly / Felt content instead of free-styling. They do **not** mention `searchPatternFlyDocs`, `usePatternFlyDocs`, or “MCP.” - -## Felt / `project-felt/ai-guidelines` (catalog: AiGuidelines) - -- What do the Felt AI guidelines say about **transparency notices** when the product uses generative AI? Cite the doc, not general advice. -- Summarize the **legal review** expectations for shipping an AI feature, per the **project-felt ai-guidelines** content. -- How should we treat **color** in UI for AI features? Pull guidance from the Felt AI guidelines, not PatternFly’s generic color page unless that’s the only source. -- What are the **chatbot avatar** rules (icons, launch affordances) in the Felt AI guidelines? -- List the main **AI design principles** from the Felt guidelines in short bullets, with one example each. -- What does the Felt material say about **animation** and **sparkle** effects for AI features? -- Compare **iconography** guidance for “AI” affordances (sparkles, etc.) in the Felt guidelines—what to use and what to avoid? - -## PatternFly (components / v6) - -- I’m building a **Button** in PatternFly v6. What are the **accessibility** must-dos for `Button` (keyboard, `aria-label`, disabled behavior)? -- For **DataList** in v6, what does the official doc say about **selection** or **row actions**—quote the relevant bit. -- What’s the difference between **primary** and **secondary** actions in a **Modal** flow, per PatternFly’s guidance for the **Modal** component? - -## “Doc-only” follow-ups (if answers feel generic) - -- In the Felt **transparency** doc, what should we show near an AI-generated answer? Be specific to that document. -- Per the Felt **legal** doc, when do we need **Product Legal** (or equivalent) sign-off before release? - -## Soft nudge (still no tool names) - -If the model answers from memory only, try: *“Open the official markdown in our PatternFly / Felt catalog and quote the first heading.”* - - - -## Neutral prompts (no product or doc names) - -### Transparency & disclosure - -- When we ship a feature that calls a generative model, what should users see **before** they start typing or get an answer—especially around **personal or sensitive** input? -- What’s the minimum we should do so it’s **obvious** the experience involves generated output—not just a tiny badge somewhere? -- For a **chat-style assistant** embedded in our app, what **persistent** reminders should sit near the input/output area about reviewing outputs? -- For **summaries** or **auto-generated blurbs** in search or results pages, how should we label them so users know they’re **model-generated**? -- Why might **labeling every AI-generated image** on a page be a bad idea? What’s the alternative approach suggested for broader notices? - -### Risk, review, and policy - -- Before we release customer-facing capabilities backed by generative models, what **review or consultation** steps does our internal material expect (named roles/processes as written)? -- What topics does our doc treat as needing **extra care** or **more indicators** when stakes are higher? - -### Visual design (icons, motion, color) - -- What rules do we follow for **sparkles**, **stars**, or similar **“AI” affordances** in icons—what to prefer and what to avoid? -- How should **motion** or **animation** be used around generative features so it helps recognition without being gratuitous? -- Are there **color** constraints or preferences when marking AI-related UI so it stays on-brand and accessible? - -### Chat UX & affordances - -- How should we handle **avatars**, **robot metaphors**, and **launch entry points** for an assistant so they’re clear but not misleading? - -### Principles & framing - -- What **design principles** does our internal doc give for experiences that include generative capabilities—framed as product goals, not implementation detail? - -### Doc-grounding nudge (still neutral) - -- Answer using **our internal markdown** only: quote the **first heading** and **one bullet** from the transparency section that applies to **virtual assistants**. From ec46f0a2bf377635d8c700e88cb9db3fd5f02eb0 Mon Sep 17 00:00:00 2001 From: Joachim Schuler Date: Wed, 6 May 2026 12:45:55 -0400 Subject: [PATCH 3/4] linting --- src/patternFly.getResources.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/patternFly.getResources.ts b/src/patternFly.getResources.ts index 146589d3..c2f03a93 100644 --- a/src/patternFly.getResources.ts +++ b/src/patternFly.getResources.ts @@ -209,7 +209,7 @@ const getPatternFlyDocsCatalog = async (): Promise 'path' in e && typeof e.path === 'string'); + const concrete = entries.filter((entry): entry is PatternFlyMcpDocsCatalogDoc => 'path' in entry && typeof entry.path === 'string'); if (concrete.length > 0) { safeDocs[key] = concrete; From 4893b11b3a45fceeac20a8956f93deff1c4611a8 Mon Sep 17 00:00:00 2001 From: Joachim Schuler Date: Wed, 6 May 2026 12:55:41 -0400 Subject: [PATCH 4/4] testing --- .../__snapshots__/stdioTransport.test.ts.snap | 39 +++++++------------ tests/e2e/utils/stdioTransportClient.ts | 2 +- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/tests/e2e/__snapshots__/stdioTransport.test.ts.snap b/tests/e2e/__snapshots__/stdioTransport.test.ts.snap index 9dbcb7f8..4cd2b340 100644 --- a/tests/e2e/__snapshots__/stdioTransport.test.ts.snap +++ b/tests/e2e/__snapshots__/stdioTransport.test.ts.snap @@ -149,32 +149,19 @@ exports[`Logging should allow setting logging options, stderr 1`] = ` "[INFO]: Server logging enabled. [INFO]: Server stats enabled. [INFO]: No external resources loaded. -", - "[INFO]: No external tools loaded. -", - "[INFO]: Registered resource: patternfly-context -", - "[INFO]: Registered resource: patternfly-components-index-meta -", - "[INFO]: Registered resource: patternfly-components-index -", - "[INFO]: Registered resource: patternfly-docs-index-meta -", - "[INFO]: Registered resource: patternfly-docs-index -", - "[INFO]: Registered resource: patternfly-docs-template -", - "[INFO]: Registered resource: patternfly-schemas-index-meta -", - "[INFO]: Registered resource: patternfly-schemas-index -", - "[INFO]: Registered resource: patternfly-schemas-template -", - "[INFO]: Registered tool: usePatternFlyDocs -", - "[INFO]: Registered tool: searchPatternFlyDocs -", - "[INFO]: @patternfly/patternfly-mcp server running on stdio transport +[INFO]: No external tools loaded. +[INFO]: Registered resource: patternfly-context +[INFO]: Registered resource: patternfly-components-index-meta +[INFO]: Registered resource: patternfly-components-index +[INFO]: Registered resource: patternfly-docs-index-meta +[INFO]: Registered resource: patternfly-docs-index +[INFO]: Registered resource: patternfly-docs-template +[INFO]: Registered resource: patternfly-schemas-index-meta +[INFO]: Registered resource: patternfly-schemas-index +[INFO]: Registered resource: patternfly-schemas-template +[INFO]: Registered tool: usePatternFlyDocs +[INFO]: Registered tool: searchPatternFlyDocs +[INFO]: @patternfly/patternfly-mcp server running on stdio transport ", ] `; diff --git a/tests/e2e/utils/stdioTransportClient.ts b/tests/e2e/utils/stdioTransportClient.ts index c6ef5812..1b3a439e 100644 --- a/tests/e2e/utils/stdioTransportClient.ts +++ b/tests/e2e/utils/stdioTransportClient.ts @@ -233,7 +233,7 @@ export const startServer = async ({ }, logs: () => [ - ...stderrLogs, + ...(stderrLogs.length > 0 ? [stderrLogs.join('')] : []), ...protocolLogs ], stderrLogs: () => stderrLogs.slice(),