Coding standards and architectural patterns for the PatternFly MCP project. This document emphasizes maintainability, performance, and pragmatic TypeScript usage.
High - This document should be processed when working with source code or implementing features.
See the Guidelines Index for all guidelines.
Adhere to the TypeScript coding conventions established for the project.
- Pragmatic Over Perfection: Focus on code and functionality. Types are for consumer ergonomics and quick sanity checks — not a blocker for implementation speed.
- Generics should be reserved: Use for public exported functions/typings. If they reduce readability, prefer concrete typings.
- Prefer
unknownoverany:unknownis the default at boundaries; add runtime guards and narrow.anyis still acceptable in testing or deserialization (IPC). - Prefer inference over explicit returns: Let inference work unless the function/type is part of the public surface.
- Style Preference (Nullish Coalescing): Align to the codebase's established style. Prefer logical OR (
||) over lazy application of the nullish coalescing operator (??). Use nullish coalescing when an active distinction fornull/undefined(e.g., distinguishing from0,"", orfalse) is logically required. - Avoid Over-Engineering: Do not use overabundant type guards or complex nested ternaries. Prefer clear, readable logic over opaque TypeScript patterns.
The project is strictly ESM. Agents MUST follow these rules:
- Exports:
- Internal Source Code (TypeScript): Favor named exports (e.g.,
export { foo }). - External Tool Plugins (JavaScript): MUST use
export defaultfor the tool definition.
- Internal Source Code (TypeScript): Favor named exports (e.g.,
- Explicit Extensions:
- Internal Source Code (TypeScript): Use extension-less imports for local modules (e.g.,
import { foo } from './foo'). - External Tool Plugins (JavaScript): All relative imports MUST include explicit file extensions (e.g.,
import { foo } from './foo.js') as they are loaded by the Node.js ESM runtime.
- Internal Source Code (TypeScript): Use extension-less imports for local modules (e.g.,
- No CommonJS: Do not use
require(),module.exports, or__dirname.
Specific modules allow bypassing strict typing to maintain momentum:
- Internal tool composition (
src/server.tools.ts): Use localized casts for inferred unions that don't narrow. Add a short comment explaining intent. - Schema conversion (
src/server.schema.ts): Returningz.ZodTypeAnyis fine. Avoid deep type plumbing. - Tools Host IPC (
src/server.toolsHost.ts):anyfor deserialized payloads is acceptable. Runtime checks and try/catch are the safety net. - Test fixtures and E2E clients: Use
// @ts-ignoreoras anywhere tests exercise built outputs or where typings aren’t the point under test.
The project follows a plugin-based architecture focused on stability and extensibility.
All tools and resources MUST follow the Creator Pattern for dependency injection and testability.
- Structure: Creator functions accept an optional
optionsparameter (defaults togetOptions()) and return a tool/resource tuple. - Options Hybrid Approach: Environment-dependent helpers should accept an optional
optionsparameter that defaults togetOptions(). This allows for explicit dependency injection in tests while maintaining ergonomics viaAsyncLocalStoragein production. Pure transforms should remain option-agnostic. - Internal Tools:
(options = getOptions()): McpTool-> Returns[name, schema, handler]. - Internal Resources:
(options = getOptions()): McpResource-> Returns[name, uri, config, handler]. - External Tool Plugins: Authored using the
createMcpToolhelper with an object configuration, exported asdefault. - Testing: Creators allow easy mocking:
const tool = usePatternFlyDocsTool(mockOptions).
- File Naming:
- Internal:
lowerCamelCasewith dot notation (e.g.,server.http.ts,tool.docs.ts). - Prefixes:
server.*(core),tool.*(built-in tools),resource.*(resources),options.*(config). - External/Examples:
lowerCamelCasewith descriptive prefixes (e.g.,toolPluginHello.js).
- Internal:
- Export Patterns:
- Internal (TS): Use named exports grouped at the end of the file.
- External (JS): Use default export for the primary tool definition.
- Concurrency: Use
processDocsFunctionfor multi-file loading to leverage thepromiseQueue(sliding window pattern). RespectmaxDocsToLoad.
External tool plugins should follow this basic structure:
import { createMcpTool } from '@patternfly/patternfly-mcp';
export default createMcpTool({
name: 'myTool',
description: 'Tool description',
inputSchema: {
type: 'object',
properties: {
param1: { type: 'string', description: 'Parameter description' }
},
required: ['param1']
},
async handler({ param1 }) {
return {
content: [{ type: 'text', text: `Result: ${param1}` }]
};
}
});Expensive operations (network, I/O, schema processing) should be memoized by assigning a .memo property to the function. This allows easier testing of the original function.
- Usage:
const result = await getComponentSchema.memo('Button'); - Pattern:
const expensiveFn = async (arg: string) => { /* ... */ };
expensiveFn.memo = memo(expensiveFn, {
cacheLimit: 10,
keyHash: (args) => args[0]
});- Note: Use
cacheErrors: falseif normalization should retry on subsequent attempts.
Use AsyncLocalStorage via helper functions to access session and global options without parameter drilling.
- Tool Pattern:
const myTool = (options = getOptions()): McpTool => {
const session = getSessionOptions();
return [name, schema, async (args) => { /* use options/session */ }];
};- Execution: Context is automatically preserved across async boundaries. Use
runWithSessionorrunWithOptionsonly when explicit overrides are required.
Always use McpError with appropriate ErrorCode for user-facing failures. When a resource is not found, provide suggestions.
Note on Error Codes: To avoid drift, we do not maintain a local list of MCP SDK ErrorCode values. Agents MUST analyze the current version of the @modelcontextprotocol/sdk package to identify correct codes. Avoid listing package-specific resources we do not control in agent documentation due to maintenance concerns.
const { exactMatches, searchResults } = searchComponents.memo(name);
if (exactMatches.length === 0) {
const suggestions = searchResults.map(r => r.item).slice(0, 3);
throw new McpError(
ErrorCode.InvalidParams,
`"${name}" not found. Did you mean ${suggestions.map(s => `"${s}"`).join(', ')}?`
);
}Centralized utilities in server.helpers.ts, server.schema.ts, and server.getResources.ts should be favored over re-implementation.
- Helpers: Use
stringJoin.newline(),freezeObject(),timeoutFunction(), andmergeObjects(). - Schemas: Use
normalizeInputSchema(schema)to convert JSON Schema or Zod shapes into valid Zod instances. - Type Guards: Use
isPlainObject(),isZodSchema(), andisErrorLike()for runtime narrowing. - Zod Detection: Robustly detect Zod schemas by checking for internal brands:
_deffor Zod v3 and_zodfor Zod v4. Avoid relying solely onparse()orsafeParse()methods. - Immutability: Options and Session objects are frozen; use
structuredClone()before modification.
Validate input at boundaries (handlers) and sanitize objects before merging.
const handler = async (args: unknown) => {
if (!isPlainObject(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Object required');
}
const { name } = args;
if (typeof name !== 'string' || !name.trim()) {
throw new McpError(ErrorCode.InvalidParams, 'Name string required');
}
return process(name.trim());
};Use return await ONLY when catching/translating errors in the current layer or when a finally block must execute before returning. Otherwise, return the promise directly.
The project favors a "sliding window" pattern for promise queues (see src/server.getResources.ts) over strict batching. This maintains a constant number of active requests rather than waiting for an entire batch to complete.
While the codebase emphasizes pragmatism, public APIs require comprehensive JSDoc for consumer ergonomics.
@param,@returns,@throws: Standard documentation for function signature.@property: Document properties for interfaces or classes.@alias: Used for stable aliased typings exposed to consumers.@template: Document generic type parameters.@example: Provide usage examples for complex functions.@note: Important implementation details or gotchas.
/**
* Fetches documentation for a PatternFly component.
*
* @param name - Component name (e.g., 'Button')
* @param options - Configuration
* @param options.maxDocs - Max items to load
* @returns Tool tuple [name, schema, handler]
* @throws {McpError} If component not found
* @example
* const tool = usePatternFlyDocsTool();
* const res = await tool[2]({ name: 'Button' });
*/- Internal Code: Use minimal JSDoc (description, key params, returns). Focus on "why" rather than "what".
- Conciseness: Keep descriptions brief but informative.
- Accuracy: Update JSDoc immediately when signatures change.
- Types: Use TS types in JSDoc ONLY when they add clarity or differ from the implementation.
Agents MUST validate all code outputs using the project's quality suite:
- Linting:
npm run test:lint(Ensures style consistency) - Type Checking:
npm run test:types(tsc validation) - Documentation:
npm run test:spell-docs(CSpell validation) - Testing:
npm run test(Unit) andnpm run test:integration(E2E)