From e6aa69589f177f2d422d11c64a1fea73b3fa4f42 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 30 Apr 2026 23:00:21 -0400 Subject: [PATCH 1/2] feat(cli,resources): pf-4059 catch cli start issues * docs, troubleshooting index, agent troubleshooting guidance * agent, allow checking for node versioning via mcp resource * build, add custom support url to package.json * cli, expose 3 levels of error with troubleshooting links * options, move getNodeMajorVersion to helpers, troubleshooting instructions * resources, patternFlyContext, expose env and support links --- docs/README.md | 1 + docs/usage.md | 2 + guidelines/README.md | 1 + guidelines/agent_behaviors.md | 10 ++- package.json | 3 + .../options.defaults.test.ts.snap | 6 +- .../__snapshots__/server.helpers.test.ts.snap | 2 + .../__snapshots__/server.test.ts.snap | 16 ++-- src/__tests__/options.defaults.test.ts | 26 +----- src/__tests__/options.helpers.test.ts | 48 +++++++++++ .../resource.patternFlyContext.test.ts | 17 ++-- src/__tests__/server.helpers.test.ts | 5 ++ src/cli.ts | 77 +++++++++++++++-- src/options.defaults.ts | 49 ++++++----- src/options.helpers.ts | 21 +++++ src/resource.patternFlyContext.ts | 77 ++++++++++++----- tests/e2e/cli.test.ts | 82 +++++++++++++++++++ 17 files changed, 350 insertions(+), 93 deletions(-) create mode 100644 src/__tests__/options.helpers.test.ts create mode 100644 src/options.helpers.ts create mode 100644 tests/e2e/cli.test.ts diff --git a/docs/README.md b/docs/README.md index 2353e0cf..019add3e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,6 +5,7 @@ Welcome to the PatternFly MCP Server documentation. This guide is organized by u ### 🚀 Usage - **[MCP Tools and Resources](./usage.md)**: Use built-in tools and resources like `searchPatternFlyDocs` and `usePatternFlyDocs`. - **[Client Configuration](./usage.md)**: Configure the server for your environment. +- **[Troubleshooting](./usage.md#troubleshooting)**: Steps for common setup problems. ### 🛠️ Developer reference - **[CLI Reference](./development.md#cli-usage)**: Reference of server options. diff --git a/docs/usage.md b/docs/usage.md index 25ab802c..ce8d919a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -180,6 +180,8 @@ These are **first-step checks** for common setup problems, not full diagnostics. > **Note on Operating Systems**: Our primary development and testing environments are **macOS and Linux**. While we provide instructions for **Windows**, these commands are run at your own discretion. If you are unsure, please verify them with your IT or system administrator before proceeding. +> **Agents**: PatternFly MCP server information is available internally through the `patternfly://context` MCP resource. + ### 1. Verify Node.js Version The PatternFly MCP server requires **Node.js 20 or higher**. diff --git a/guidelines/README.md b/guidelines/README.md index cdaa3a0d..228d056e 100644 --- a/guidelines/README.md +++ b/guidelines/README.md @@ -36,6 +36,7 @@ Agents should use these phrases as signals to consult specific documentation and | **"review development guide"** | Review `docs/development.md` for CLI, API, and plugin authoring. | | **"create an example tool plugin"** | Review `guidelines/agent_coding.md`, `docs/development.md`, `docs/examples/*`, and `src/*` for context, coding standards, and existing example formats. | | **"add documentation links"** / **"add doc entries"** / **"register docs"** / **"update docs.json"** / **"contribute to docs.json"** | Follow `guidelines/skills/add-docs-links/SKILL.md`: docs.json format, duplicate check, raw URL confirmation, then run unit tests and update meta. | +| **"troubleshoot server"** / **"debug server"** | Review `docs/usage.md#troubleshooting` and the PatternFly MCP server resource `patternfly://context` | ## Guidelines Processing Order diff --git a/guidelines/agent_behaviors.md b/guidelines/agent_behaviors.md index 6b28de45..04a4da24 100644 --- a/guidelines/agent_behaviors.md +++ b/guidelines/agent_behaviors.md @@ -46,15 +46,17 @@ For a detailed overview of the system design and roadmap, see [docs/architecture - **Confirmation Required**: Confirm success; summarize changes; explain impact; verify understanding. - **Guidance Review Scope**: Unless the user explicitly asks, do not make recommendations on improving guidance if all you're asked to do is review guidance. - **Environment Awareness**: - - Server execution requires **Node.js >= 20**. - - External tool plugins (`--tool`) require **Node.js >= 22** primarily for its robust **Permission Model** (`--experimental-permission`), which enables strict filesystem and network isolation. - - Always verify environment compatibility when proposing tools using modern Node.js features. + - Server and plugin execution requirements are defined in `package.json`. + - Always verify environment compatibility by checking `patternfly://context` or `package.json`. + - Proactively check for environment mismatches (e.g., Node.js version) if tools fail to load. - **Security Context**: - Default to `--plugin-isolation strict`. - - If a tool requires filesystem or network access beyond the sandbox, document the need for `--plugin-isolation none` explicitly. + - If a tool requires filesystem or network access beyond the sandbox, document the need for `--plugin-isolation none`. + - **Implicit Diagnostics**: If a tool call fails, the agent MUST proactively check `patternfly://context` to see if the user's environment meets requirements before requesting more technical details. - Warn users when a proposed solution requires disabling isolation. - **State Management**: Use `.agent/` directory for local guidance and state; maintain context; preserve session information. - **Security Awareness**: Be mindful of path traversal and isolation levels when working with external tools and resource loading. +- **Troubleshooting Reference**: When encountering environment or runtime issues, consult the [Troubleshooting section in docs/usage.md](../docs/usage.md#troubleshooting) for common fixes such as Node.js upgrades, cache resets, and Windows-specific symlink issues. ## 3. Trigger-Based Workflows diff --git a/package.json b/package.json index 84d1d9a9..21440c76 100644 --- a/package.json +++ b/package.json @@ -94,5 +94,8 @@ "bugs": { "url": "https://github.com/patternfly/patternfly-mcp/issues" }, + "support": { + "url": "https://github.com/patternfly/patternfly-mcp/blob/main/docs/usage.md#troubleshooting" + }, "homepage": "https://github.com/patternfly/patternfly-mcp#readme" } diff --git a/src/__tests__/__snapshots__/options.defaults.test.ts.snap b/src/__tests__/__snapshots__/options.defaults.test.ts.snap index 606d4cf5..4b202a0c 100644 --- a/src/__tests__/__snapshots__/options.defaults.test.ts.snap +++ b/src/__tests__/__snapshots__/options.defaults.test.ts.snap @@ -45,7 +45,9 @@ exports[`options defaults should return specific properties: defaults 1`] = ` "test": {}, }, "name": "@patternfly/patternfly-mcp", + "nodeEngine": ">=20.0.0", "nodeVersion": 22, + "nodeVersionPreferred": 22, "patternflyOptions": { "availableResourceVersions": [ "6.0.0", @@ -84,12 +86,14 @@ exports[`options defaults should return specific properties: defaults 1`] = ` "loadTimeoutMs": 5000, }, "pluginIsolation": "strict", + "repoBugs": "https://github.com/patternfly/patternfly-mcp/issues", "repoName": "patternfly-mcp", "repoResources": { "bugs": "https://github.com/patternfly/patternfly-mcp/issues", "git": "git+https://github.com/patternfly/patternfly-mcp.git", "homepage": "https://github.com/patternfly/patternfly-mcp#readme", }, + "repoSupport": "https://github.com/patternfly/patternfly-mcp/blob/main/docs/usage.md#troubleshooting", "resourceMemoOptions": { "default": { "cacheLimit": 3, @@ -112,7 +116,7 @@ exports[`options defaults should return specific properties: defaults 1`] = ` ", "serverInstanceOptions": { - "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development.", + "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development. Use patternfly://context for server environment and troubleshooting links if runtime issues occur.", }, "stats": { "reportIntervalMs": { diff --git a/src/__tests__/__snapshots__/server.helpers.test.ts.snap b/src/__tests__/__snapshots__/server.helpers.test.ts.snap index ae83ceb6..8bb32916 100644 --- a/src/__tests__/__snapshots__/server.helpers.test.ts.snap +++ b/src/__tests__/__snapshots__/server.helpers.test.ts.snap @@ -132,3 +132,5 @@ ipsum 3 true" `; + +exports[`stringJoin should join values, newline filtered empty 1`] = `""`; diff --git a/src/__tests__/__snapshots__/server.test.ts.snap b/src/__tests__/__snapshots__/server.test.ts.snap index 2f37de23..03eb8532 100644 --- a/src/__tests__/__snapshots__/server.test.ts.snap +++ b/src/__tests__/__snapshots__/server.test.ts.snap @@ -183,7 +183,7 @@ exports[`runServer should attempt to run server, create transport, connect, and "resources": {}, "tools": {}, }, - "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development.", + "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development. Use patternfly://context for server environment and troubleshooting links if runtime issues occur.", }, ], ], @@ -256,7 +256,7 @@ exports[`runServer should attempt to run server, disable SIGINT handler: diagnos "resources": {}, "tools": {}, }, - "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development.", + "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development. Use patternfly://context for server environment and troubleshooting links if runtime issues occur.", }, ], ], @@ -324,7 +324,7 @@ exports[`runServer should attempt to run server, enable SIGINT handler explicitl "resources": {}, "tools": {}, }, - "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development.", + "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development. Use patternfly://context for server environment and troubleshooting links if runtime issues occur.", }, ], ], @@ -403,7 +403,7 @@ exports[`runServer should attempt to run server, register a tool: diagnostics 1` "resources": {}, "tools": {}, }, - "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development.", + "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development. Use patternfly://context for server environment and troubleshooting links if runtime issues occur.", }, ], ], @@ -490,7 +490,7 @@ exports[`runServer should attempt to run server, register multiple tools: diagno "resources": {}, "tools": {}, }, - "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development.", + "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development. Use patternfly://context for server environment and troubleshooting links if runtime issues occur.", }, ], ], @@ -566,7 +566,7 @@ exports[`runServer should attempt to run server, use custom options: diagnostics "resources": {}, "tools": {}, }, - "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development.", + "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development. Use patternfly://context for server environment and troubleshooting links if runtime issues occur.", }, ], ], @@ -645,7 +645,7 @@ exports[`runServer should attempt to run server, use default tools, http: diagno "resources": {}, "tools": {}, }, - "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development.", + "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development. Use patternfly://context for server environment and troubleshooting links if runtime issues occur.", }, ], ], @@ -727,7 +727,7 @@ exports[`runServer should attempt to run server, use default tools, stdio: diagn "resources": {}, "tools": {}, }, - "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development.", + "instructions": "Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development. Use patternfly://context for server environment and troubleshooting links if runtime issues occur.", }, ], ], diff --git a/src/__tests__/options.defaults.test.ts b/src/__tests__/options.defaults.test.ts index 3e5e254d..2bcd697b 100644 --- a/src/__tests__/options.defaults.test.ts +++ b/src/__tests__/options.defaults.test.ts @@ -1,31 +1,7 @@ -import { DEFAULT_OPTIONS, getNodeMajorVersion } from '../options.defaults'; +import { DEFAULT_OPTIONS } from '../options.defaults'; describe('options defaults', () => { it('should return specific properties', () => { expect(DEFAULT_OPTIONS).toMatchSnapshot('defaults'); }); }); - -describe('getNodeMajorVersion', () => { - it('should get the current Node.js version', () => { - // Purposeful failure in the event the process.versions.node value is not available - expect(getNodeMajorVersion()).not.toBe(0); - }); - - it.each([ - { - description: 'number', - value: 1_000_000 - }, - { - description: 'string', - value: 'lorem ipsum' - }, - { - description: 'null', - value: null - } - ])('should handle basic failure, $description', ({ value }) => { - expect(getNodeMajorVersion(value as any)).toBe(0); - }); -}); diff --git a/src/__tests__/options.helpers.test.ts b/src/__tests__/options.helpers.test.ts new file mode 100644 index 00000000..75e04eeb --- /dev/null +++ b/src/__tests__/options.helpers.test.ts @@ -0,0 +1,48 @@ +import { getNodeMajorVersion } from '../options.helpers'; + +describe('getNodeMajorVersion', () => { + it('should get the current Node.js version', () => { + // Purposeful failure in the event the process.versions.node value is not available + expect(getNodeMajorVersion(process.versions.node)).not.toBe(0); + }); + + it.each([ + { + description: 'number failure', + value: 1_000_000, + expected: 0 + }, + { + description: 'string', + value: 'lorem ipsum', + expected: 0 + }, + { + description: 'null failure', + value: null, + expected: 0 + }, + { + description: 'undefined failure', + value: undefined, + expected: 0 + }, + { + description: 'NaN failure', + value: NaN, + expected: 0 + }, + { + description: 'operators', + value: '<=20', + expected: 20 + }, + { + description: 'operators and semver', + value: '<=20.0.1', + expected: 20 + } + ])('should handle, $description', ({ value, expected }) => { + expect(getNodeMajorVersion(value as any)).toBe(expected); + }); +}); diff --git a/src/__tests__/resource.patternFlyContext.test.ts b/src/__tests__/resource.patternFlyContext.test.ts index 3aa93f20..f5d5cf36 100644 --- a/src/__tests__/resource.patternFlyContext.test.ts +++ b/src/__tests__/resource.patternFlyContext.test.ts @@ -1,4 +1,7 @@ -import { patternFlyContextResource } from '../resource.patternFlyContext'; +import { + patternFlyContextResource, + resourceCallback +} from '../resource.patternFlyContext'; import { isPlainObject } from '../server.helpers'; describe('patternFlyContextResource', () => { @@ -18,7 +21,7 @@ describe('patternFlyContextResource', () => { }); }); -describe('patternFlyContextResource, callback', () => { +describe('resourceCallback', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -26,13 +29,13 @@ describe('patternFlyContextResource, callback', () => { it.each([ { description: 'default', - args: [] + expected: 'Troubleshooting' } - ])('should return context content, $description', async ({ args }) => { - const [_name, _uri, _config, callback] = patternFlyContextResource(); - const result = await callback(...args); + ])('should return context content, $description', async ({ expected }) => { + const result = await resourceCallback(undefined as any); expect(result.contents).toBeDefined(); - expect(Object.keys(result.contents[0])).toEqual(['uri', 'mimeType', 'text']); + expect(Object.keys(result.contents[0] as any)).toEqual(['uri', 'mimeType', 'text']); + expect(result.contents[0]?.text).toContain(expected); }); }); diff --git a/src/__tests__/server.helpers.test.ts b/src/__tests__/server.helpers.test.ts index 1dd38df2..18fe2a00 100644 --- a/src/__tests__/server.helpers.test.ts +++ b/src/__tests__/server.helpers.test.ts @@ -1001,6 +1001,11 @@ describe('stringJoin', () => { description: 'newline filtered', args: ['lorem', 'ipsum', 0, 1, 2, 3, true, false, null, undefined], settings: { sep: '\n', filterFalsyValues: true } + }, + { + description: 'newline filtered empty', + args: [false, null, undefined], + settings: { sep: '\n', filterFalsyValues: true } } ])('should join values, $description', ({ args, settings }) => { expect(stringJoin(args, settings)).toMatchSnapshot(); diff --git a/src/cli.ts b/src/cli.ts index 39d5827b..1fb46875 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,9 +1,74 @@ #!/usr/bin/env node -import { main } from './index'; +import packageJson from '../package.json'; +import { getNodeMajorVersion } from './options.helpers'; -main({ mode: 'cli' }).catch(error => { - // Use console.error, log.error requires initialization - console.error('Failed to start server:', error); - process.exit(1); -}); +/** + * CLI entry point with early error catching for environment and load-time issues. + */ +const run = async (): Promise => { + const appBugs = packageJson.bugs?.url; + const appName = packageJson.name; + const appSupport = packageJson.support?.url; + const appMinNodeMajorVersion = getNodeMajorVersion(packageJson.engines?.node); + const envNodeMajorVersion = getNodeMajorVersion(process.versions?.node || process.version); + + // Exit the process on error. + const processExit = (message: string, error: unknown): never => { + console.error(message, error instanceof Error ? error.message : error); + + if (appSupport) { + console.error(`\nFor help, visit the Troubleshooting Guide:\n${appSupport}`); + } + + if (appBugs) { + console.error(`\nTo report bugs:\n${appBugs}`); + } + console.error(''); + process.exit(1); + }; + + // Node.js confirmations + if (!envNodeMajorVersion || !appMinNodeMajorVersion || envNodeMajorVersion < appMinNodeMajorVersion) { + let error; + + if (!envNodeMajorVersion) { + // Environment not broadcasting version. Missing or falsy + error = new Error('Unable to determine environment Node.js version. Update Node.js and try again.'); + } else if (!appMinNodeMajorVersion) { + // Options or package.json engine been modified. Missing or falsy + error = new Error('Unable to determine server engine Node.js version requirements. Confirm engine available.'); + } else { + // Everything else + error = new Error( + `Node.js version ${envNodeMajorVersion} found but ${appMinNodeMajorVersion} or higher is required. Update Node.js and try again.` + ); + } + + processExit(`${appName} failed to start. Engine requirements not met.`, error); + + // Unreachable, processExit exits. Kept for readability. + return; + } + + let main: typeof import('./index').main; + + try { + const module = await import('./index'); + + main = module.main; + } catch (error) { + processExit(`Failed to load ${appName}`, error); + + // Unreachable, processExit exits. Kept for type satisfaction. + return; + } + + try { + await main({ mode: 'cli' }); + } catch (error) { + processExit(`${appName} encountered a runtime error`, error); + } +}; + +run(); diff --git a/src/options.defaults.ts b/src/options.defaults.ts index 66ac08f5..369ee198 100644 --- a/src/options.defaults.ts +++ b/src/options.defaults.ts @@ -2,6 +2,7 @@ import { basename, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import packageJson from '../package.json'; import { type ToolModule } from './server.toolsUser'; +import { getNodeMajorVersion } from './options.helpers'; /** * Application defaults, not all fields are user-configurable @@ -23,12 +24,16 @@ import { type ToolModule } from './server.toolsUser'; * - `test`: Testing or debugging mode. * @property {ModeOptions} modeOptions - Mode-specific options. * @property name - Name of the package. + * @property nodeEngine - Minimum Node.js version requirement from package.json. * @property nodeVersion - Node.js major version. + * @property nodeVersionPreferred = Preferred Node.js major version. Typically used for testing. * @property {PatternFlyOptions} patternflyOptions - PatternFly-specific options. * @property pluginIsolation - Isolation preset for external plugins. * @property {PluginHostOptions} pluginHost - Plugin host options. + * @property repoBugs - Bugs URL of the repository. * @property repoName - Name of the repository. * @property {RepoResources} repoResources - Repository resources. + * @property repoSupport - Troubleshooting URL of the repository. * @property {typeof RESOURCE_MEMO_OPTIONS} resourceMemoOptions - Resource-level memoization options. * @property resourceModules - Array for programmatic registration of resource provider modules, similar to `toolModules` but * for MCP resources and currently only internal. @@ -54,12 +59,16 @@ interface DefaultOptions { mode: 'cli' | 'programmatic' | 'test'; modeOptions: ModeOptions; name: string; + nodeEngine: string | undefined; nodeVersion: number; + nodeVersionPreferred: number; patternflyOptions: PatternFlyOptions; pluginIsolation: 'none' | 'strict'; pluginHost: PluginHostOptions; + repoBugs: string | undefined; repoName: string | undefined; repoResources: RepoResources; + repoSupport: string | undefined; resourceMemoOptions: Partial; resourceModules: unknown | unknown[]; separator: string; @@ -351,6 +360,15 @@ const MODE_OPTIONS: ModeOptions = { test: {} }; +/** + * The application's preferred Node.js major. Typically used for + * unit testing. + * + * @note Currently hardcoded, but once Node.js 20 support is removed + * we could consider populating this from package.json engine. + */ +const NODE_VERSION_PREFERRED = 22; + /** * Default plugin host options. */ @@ -413,7 +431,8 @@ const TOOL_MEMO_OPTIONS = { * Default server instance options. */ const SERVER_INSTANCE_OPTIONS: ServerInstanceOptions = { - instructions: 'Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development.' + instructions: + 'Use the PatternFly MCP when a user asks about: PatternFly, pf, pf docs, design tokens, design guidelines, accessibility, PatternFly components, and frontend development. Use patternfly://context for server environment and troubleshooting links if runtime issues occur.' }; /** @@ -470,25 +489,12 @@ const URL_REGEX = /^(https?:)\/\//i; /** * Available operational modes for the MCP server. - */ -const MODE_LEVELS: DefaultOptions['mode'][] = ['cli', 'programmatic', 'test']; - -/** - * Get the current Node.js major version. * - * @param nodeVersion - * @returns Node.js major version. + * @note Testing doesn't always use the expected mode + * - Unit tests default to `programmatic` mode + * - E2E tests generally use `test` mode */ -const getNodeMajorVersion = (nodeVersion = process.versions.node) => { - const updatedNodeVersion = nodeVersion || '0.0.0'; - const major = Number.parseInt(updatedNodeVersion?.split?.('.')?.[0] || '0', 10); - - if (Number.isFinite(major)) { - return major; - } - - return 0; -}; +const MODE_LEVELS: DefaultOptions['mode'][] = ['cli', 'programmatic', 'test']; /** * Global default options. Base defaults before CLI/programmatic overrides. @@ -510,10 +516,14 @@ const DEFAULT_OPTIONS: DefaultOptions = { mode: 'programmatic', modeOptions: MODE_OPTIONS, name: packageJson.name, - nodeVersion: (process.env.NODE_ENV === 'local' && 22) || getNodeMajorVersion(), + nodeEngine: packageJson.engines?.node, + nodeVersion: (process.env.NODE_ENV === 'local' && NODE_VERSION_PREFERRED) || getNodeMajorVersion(process.versions.node), + nodeVersionPreferred: NODE_VERSION_PREFERRED, patternflyOptions: PATTERNFLY_OPTIONS, pluginIsolation: 'strict', pluginHost: PLUGIN_HOST_OPTIONS, + repoBugs: packageJson.bugs?.url, + repoSupport: packageJson.support?.url, repoName: basename(process.cwd() || '').trim(), repoResources: REPO_RESOURCES, resourceMemoOptions: RESOURCE_MEMO_OPTIONS, @@ -532,7 +542,6 @@ export { DEFAULT_OPTIONS, LOG_BASENAME, MODE_LEVELS, - getNodeMajorVersion, type DefaultOptions, type DefaultOptionsOverrides, type HttpOptions, diff --git a/src/options.helpers.ts b/src/options.helpers.ts new file mode 100644 index 00000000..ac98caac --- /dev/null +++ b/src/options.helpers.ts @@ -0,0 +1,21 @@ +/** + * Get the current Node.js major version. + * + * @note Do not use the semver package here. This is purposefully a light implementation + * meant to be shared externally without the overhead of additional packaging. + * + * @param nodeVersion + * @returns Node.js major version. + */ +const getNodeMajorVersion = (nodeVersion: unknown): number => { + if (typeof nodeVersion !== 'string') { + return 0; + } + + const sanitizedVersion = nodeVersion?.replace?.(/^[^0-9]+/, ''); + const major = Number.parseInt(sanitizedVersion.split('.')?.[0] || '0', 10); + + return Number.isFinite(major) ? major : 0; +}; + +export { getNodeMajorVersion }; diff --git a/src/resource.patternFlyContext.ts b/src/resource.patternFlyContext.ts index 2bccccdf..14633ab4 100644 --- a/src/resource.patternFlyContext.ts +++ b/src/resource.patternFlyContext.ts @@ -1,5 +1,6 @@ import { type McpResource } from './server'; import { stringJoin } from './server.helpers'; +import { getOptions, runWithOptions } from './options.context'; /** * Name of the resource. @@ -16,14 +17,14 @@ const URI_TEMPLATE = 'patternfly://context'; */ const CONFIG = { title: 'PatternFly Design System Context', - description: 'Information about the PatternFly design system and how to use this MCP server.', + description: 'Information about the PatternFly design system and how to use this MCP server, including environment and troubleshooting information.', mimeType: 'text/markdown' }; /** - * Resource creator for context. + * Resource callback for the documentation index. * - * @note Consider adding an environment snapshot here once contextual MCP tooling is available. + * @note Consider refining the environment snapshot here once contextual MCP tooling is available. * ``` * const environmentSnapshot = stringJoin.newline( * `### Environment Snapshot`, @@ -34,14 +35,16 @@ const CONFIG = { * ``` * * @param passedUri - URI of the resource. - * @returns {McpResource} The resource definition tuple + * @param options - Options for the resource. + * @returns The resource contents. */ -const patternFlyContextResource = (): McpResource => [ - NAME, - URI_TEMPLATE, - CONFIG, - async (passedUri: URL) => { - const context = `PatternFly is an open-source design system for building consistent, accessible user interfaces. +const resourceCallback = async (passedUri: URL, options = getOptions()) => { + const troubleshooting = stringJoin.newlineFiltered( + options.repoSupport && `- **Troubleshooting guidance:** ${options.repoSupport}`, + options.repoBugs && `- **Report bugs:** ${options.repoBugs}` + ); + + const context = `PatternFly is an open-source design system for building consistent, accessible user interfaces. **What is PatternFly?** PatternFly provides React components, design guidelines, and development tools for creating enterprise applications. It is used by Red Hat and other organizations to build consistent UIs with reusable components and design principles. @@ -56,18 +59,48 @@ PatternFly provides React components, design guidelines, and development tools f This MCP server provides tools and resources to access all PatternFly documentation resources ranging from design to development. - **MCP tools:** Can be used to search, fetch and display available documentation resources. - **MCP resources:** Can be used to list, filter and display available documentation resources. + +**Environment:** +- **MCP Server Mode:** ${options.mode} +- **MCP Server Version:** ${options.version || 'Unknown'} +- **Node.js Major Version:** ${options.nodeVersion || 'Unknown'} + +${(troubleshooting && stringJoin.newline('**Troubleshooting:**', troubleshooting)) || ''} `; - return { - contents: [ - { - uri: passedUri?.toString(), - mimeType: 'text/markdown', - text: stringJoin.basic(context) - } - ] - }; - } -]; + return { + contents: [ + { + uri: passedUri?.toString(), + mimeType: 'text/markdown', + text: stringJoin.basic(context) + } + ] + }; +}; -export { patternFlyContextResource, NAME, URI_TEMPLATE, CONFIG }; +/** + * Resource creator for context. + * + * @param options - Global options + * @returns {McpResource} The resource definition tuple + */ +const patternFlyContextResource = (options = getOptions()): McpResource => { + const callback: McpResource[3] = async uri => + runWithOptions(options, async () => resourceCallback(uri)); + + return [ + NAME, + URI_TEMPLATE, + CONFIG, + callback + ]; +}; + +export { + patternFlyContextResource, + resourceCallback, + NAME, + URI_TEMPLATE, + CONFIG +}; diff --git a/tests/e2e/cli.test.ts b/tests/e2e/cli.test.ts new file mode 100644 index 00000000..f5909fbc --- /dev/null +++ b/tests/e2e/cli.test.ts @@ -0,0 +1,82 @@ +/** + * Requires: npm run build prior to running Jest. + * - If typings are needed, use public types from dist to avoid type identity mismatches between src and dist + * - We're unable to mock fetch for stdio since it runs in a separate process, so we run a server and use that path for mocking external URLs. + */ +import { resolve } from 'node:path'; +import { spawn } from 'node:child_process'; +import { startServer } from './utils/stdioTransportClient'; + +describe('CLI', () => { + const cliPath = resolve(process.cwd(), 'dist/cli.js'); + + it('should start and respond successfully', async () => { + const client = await startServer({ + serverPath: cliPath, + args: ['--mode', 'test'] + }); + + const response = await client.send({ + method: 'tools/list', + params: {} + }); + + expect(response?.result?.tools).toBeDefined(); + expect(Array.isArray(response.result.tools)).toBe(true); + + await client.close(); + }); + + it.each([ + { + description: 'basic', + expected: 'Node.js version 14 found', + version: '14.0.0' + }, + { + description: 'undefined or missing', + expected: 'Unable to determine environment Node.js', + version: undefined + } + ])('should exit when node version check fails, $description', async ({ expected, version }) => { + const child = spawn('node', [ + '--input-type=module', + '-e', + `Object.defineProperty(process.versions, 'node', {value: '${version}'}); import('./dist/cli.js')` + ]); + + let stderr = ''; + + child.stderr.on('data', data => { + stderr += data.toString(); + }); + + const exitCode = await new Promise(resolve => { + child.on('close', resolve); + }); + + expect(exitCode).toBe(1); + expect(stderr).toContain('Troubleshooting Guide'); + expect(stderr).toContain('To report bugs'); + expect(stderr).toContain('Engine requirements not met'); + expect(stderr).toContain(expected); + }); + + it('should show troubleshooting links on runtime error', async () => { + const child = spawn('node', [cliPath, '--mode', 'test', '--http', '--port', '1']); + + let stderr = ''; + + child.stderr.on('data', data => { + stderr += data.toString(); + }); + + const exitCode = await new Promise(resolve => { + child.on('close', resolve); + }); + + expect(exitCode).toBe(1); + expect(stderr).toContain('Troubleshooting Guide'); + expect(stderr).toContain('To report bugs'); + }); +}); From f71a89b47e407385148b96e3388b02df1e3ea68f Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Mon, 4 May 2026 15:22:45 -0400 Subject: [PATCH 2/2] fix: review update --- src/cli.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 1fb46875..e7a963c0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,16 +15,27 @@ const run = async (): Promise => { // Exit the process on error. const processExit = (message: string, error: unknown): never => { - console.error(message, error instanceof Error ? error.message : error); + const errorMsg = error instanceof Error ? error.message : error; + const msg = [message]; + + if (errorMsg) { + msg.push(String(errorMsg)); + } if (appSupport) { - console.error(`\nFor help, visit the Troubleshooting Guide:\n${appSupport}`); + msg.push(`For help, visit the Troubleshooting Guide:\n${appSupport}`); } if (appBugs) { - console.error(`\nTo report bugs:\n${appBugs}`); + msg.push(`To report bugs:\n${appBugs}`); + } + + const finalMsg = msg.filter(Boolean).join('\n\n').trim(); + + if (finalMsg) { + console.error(`\n${finalMsg}\n`); } - console.error(''); + process.exit(1); };