diff --git a/src/__mocks__/utils.moduleResolver.ts b/src/__mocks__/utils.moduleResolver.ts
new file mode 100644
index 0000000..f7c8d90
--- /dev/null
+++ b/src/__mocks__/utils.moduleResolver.ts
@@ -0,0 +1,8 @@
+/**
+ * Mock implementation of utils.moduleResolver for Jest tests
+ * This avoids import.meta.resolve compatibility issues in test environment
+ */
+
+export const resolveModule = jest.fn((modulePath: string): string =>
+ // Default mock behavior - can be overridden in individual tests
+ `file://${modulePath}`);
diff --git a/src/__tests__/__snapshots__/server.test.ts.snap b/src/__tests__/__snapshots__/server.test.ts.snap
index 45ab916..af1157c 100644
--- a/src/__tests__/__snapshots__/server.test.ts.snap
+++ b/src/__tests__/__snapshots__/server.test.ts.snap
@@ -230,6 +230,15 @@ exports[`runServer should attempt to run server, use default tools: console 1`]
[
"Registered tool: componentSchemas",
],
+ [
+ "Registered tool: getAvailableModules",
+ ],
+ [
+ "Registered tool: getComponentSourceCode",
+ ],
+ [
+ "Registered tool: getReactUtilityClasses",
+ ],
],
"log": [
[
@@ -678,6 +687,309 @@ exports[`runServer should attempt to run server, use default tools: console 1`]
},
[Function],
],
+ [
+ "getAvailableModules",
+ {
+ "description": "Retrieves a list of available Patternfly react-core modules in the current environment.",
+ "inputSchema": {
+ "packageName": ZodDefault {
+ "_def": {
+ "defaultValue": [Function],
+ "description": "Name of the patternfly package to get modules for. For tables its always better to use the @patternfly/react-data-view package.",
+ "errorMap": [Function],
+ "innerType": ZodEnum {
+ "_def": {
+ "description": "Name of the patternfly package to get modules for. For tables its always better to use the @patternfly/react-data-view package.",
+ "typeName": "ZodEnum",
+ "values": [
+ "@patternfly/react-core",
+ "@patternfly/react-icons",
+ "@patternfly/react-table",
+ "@patternfly/react-data-view",
+ "@patternfly/react-component-groups",
+ ],
+ },
+ "and": [Function],
+ "array": [Function],
+ "brand": [Function],
+ "catch": [Function],
+ "default": [Function],
+ "describe": [Function],
+ "isNullable": [Function],
+ "isOptional": [Function],
+ "nullable": [Function],
+ "nullish": [Function],
+ "optional": [Function],
+ "or": [Function],
+ "parse": [Function],
+ "parseAsync": [Function],
+ "pipe": [Function],
+ "promise": [Function],
+ "readonly": [Function],
+ "refine": [Function],
+ "refinement": [Function],
+ "safeParse": [Function],
+ "safeParseAsync": [Function],
+ "spa": [Function],
+ "superRefine": [Function],
+ "transform": [Function],
+ "~standard": {
+ "validate": [Function],
+ "vendor": "zod",
+ "version": 1,
+ },
+ },
+ "typeName": "ZodDefault",
+ },
+ "and": [Function],
+ "array": [Function],
+ "brand": [Function],
+ "catch": [Function],
+ "default": [Function],
+ "describe": [Function],
+ "isNullable": [Function],
+ "isOptional": [Function],
+ "nullable": [Function],
+ "nullish": [Function],
+ "optional": [Function],
+ "or": [Function],
+ "parse": [Function],
+ "parseAsync": [Function],
+ "pipe": [Function],
+ "promise": [Function],
+ "readonly": [Function],
+ "refine": [Function],
+ "refinement": [Function],
+ "safeParse": [Function],
+ "safeParseAsync": [Function],
+ "spa": [Function],
+ "superRefine": [Function],
+ "transform": [Function],
+ "~standard": {
+ "validate": [Function],
+ "vendor": "zod",
+ "version": 1,
+ },
+ },
+ },
+ },
+ [Function],
+ ],
+ [
+ "getComponentSourceCode",
+ {
+ "description": "Retrieve a source code of a specified Patternfly react-core module in the current environment.",
+ "inputSchema": {
+ "componentName": ZodString {
+ "_def": {
+ "checks": [],
+ "coerce": false,
+ "description": "Name of the PatternFly component (e.g., "Button", "Table")",
+ "typeName": "ZodString",
+ },
+ "and": [Function],
+ "array": [Function],
+ "brand": [Function],
+ "catch": [Function],
+ "default": [Function],
+ "describe": [Function],
+ "isNullable": [Function],
+ "isOptional": [Function],
+ "nullable": [Function],
+ "nullish": [Function],
+ "optional": [Function],
+ "or": [Function],
+ "parse": [Function],
+ "parseAsync": [Function],
+ "pipe": [Function],
+ "promise": [Function],
+ "readonly": [Function],
+ "refine": [Function],
+ "refinement": [Function],
+ "safeParse": [Function],
+ "safeParseAsync": [Function],
+ "spa": [Function],
+ "superRefine": [Function],
+ "transform": [Function],
+ "~standard": {
+ "validate": [Function],
+ "vendor": "zod",
+ "version": 1,
+ },
+ },
+ "packageName": ZodDefault {
+ "_def": {
+ "defaultValue": [Function],
+ "description": "Name of the patternfly package to get component from. For tables its always better to use the @patternfly/react-data-view package.",
+ "errorMap": [Function],
+ "innerType": ZodOptional {
+ "_def": {
+ "description": "Name of the patternfly package to get component from. For tables its always better to use the @patternfly/react-data-view package.",
+ "errorMap": [Function],
+ "innerType": ZodEnum {
+ "_def": {
+ "typeName": "ZodEnum",
+ "values": [
+ "@patternfly/react-core",
+ "@patternfly/react-table",
+ "@patternfly/react-data-view",
+ "@patternfly/react-component-groups",
+ ],
+ },
+ "and": [Function],
+ "array": [Function],
+ "brand": [Function],
+ "catch": [Function],
+ "default": [Function],
+ "describe": [Function],
+ "isNullable": [Function],
+ "isOptional": [Function],
+ "nullable": [Function],
+ "nullish": [Function],
+ "optional": [Function],
+ "or": [Function],
+ "parse": [Function],
+ "parseAsync": [Function],
+ "pipe": [Function],
+ "promise": [Function],
+ "readonly": [Function],
+ "refine": [Function],
+ "refinement": [Function],
+ "safeParse": [Function],
+ "safeParseAsync": [Function],
+ "spa": [Function],
+ "superRefine": [Function],
+ "transform": [Function],
+ "~standard": {
+ "validate": [Function],
+ "vendor": "zod",
+ "version": 1,
+ },
+ },
+ "typeName": "ZodOptional",
+ },
+ "and": [Function],
+ "array": [Function],
+ "brand": [Function],
+ "catch": [Function],
+ "default": [Function],
+ "describe": [Function],
+ "isNullable": [Function],
+ "isOptional": [Function],
+ "nullable": [Function],
+ "nullish": [Function],
+ "optional": [Function],
+ "or": [Function],
+ "parse": [Function],
+ "parseAsync": [Function],
+ "pipe": [Function],
+ "promise": [Function],
+ "readonly": [Function],
+ "refine": [Function],
+ "refinement": [Function],
+ "safeParse": [Function],
+ "safeParseAsync": [Function],
+ "spa": [Function],
+ "superRefine": [Function],
+ "transform": [Function],
+ "~standard": {
+ "validate": [Function],
+ "vendor": "zod",
+ "version": 1,
+ },
+ },
+ "typeName": "ZodDefault",
+ },
+ "and": [Function],
+ "array": [Function],
+ "brand": [Function],
+ "catch": [Function],
+ "default": [Function],
+ "describe": [Function],
+ "isNullable": [Function],
+ "isOptional": [Function],
+ "nullable": [Function],
+ "nullish": [Function],
+ "optional": [Function],
+ "or": [Function],
+ "parse": [Function],
+ "parseAsync": [Function],
+ "pipe": [Function],
+ "promise": [Function],
+ "readonly": [Function],
+ "refine": [Function],
+ "refinement": [Function],
+ "safeParse": [Function],
+ "safeParseAsync": [Function],
+ "spa": [Function],
+ "superRefine": [Function],
+ "transform": [Function],
+ "~standard": {
+ "validate": [Function],
+ "vendor": "zod",
+ "version": 1,
+ },
+ },
+ },
+ },
+ [Function],
+ ],
+ [
+ "getReactUtilityClasses",
+ {
+ "description": "Retrieves a list of available Patternfly react-styles utility classes in the current environment.",
+ "inputSchema": {
+ "utilityName": ZodEnum {
+ "_def": {
+ "description": "Name of a set of utility classes to retrieve from @patternfly/react-styles.",
+ "typeName": "ZodEnum",
+ "values": [
+ "Accessibility",
+ "Alignment",
+ "BackgroundColor",
+ "BoxShadow",
+ "Display",
+ "Flex",
+ "Float",
+ "Sizing",
+ "Spacing",
+ "Text",
+ ],
+ },
+ "and": [Function],
+ "array": [Function],
+ "brand": [Function],
+ "catch": [Function],
+ "default": [Function],
+ "describe": [Function],
+ "isNullable": [Function],
+ "isOptional": [Function],
+ "nullable": [Function],
+ "nullish": [Function],
+ "optional": [Function],
+ "or": [Function],
+ "parse": [Function],
+ "parseAsync": [Function],
+ "pipe": [Function],
+ "promise": [Function],
+ "readonly": [Function],
+ "refine": [Function],
+ "refinement": [Function],
+ "safeParse": [Function],
+ "safeParseAsync": [Function],
+ "spa": [Function],
+ "superRefine": [Function],
+ "transform": [Function],
+ "~standard": {
+ "validate": [Function],
+ "vendor": "zod",
+ "version": 1,
+ },
+ },
+ },
+ },
+ [Function],
+ ],
],
}
`;
diff --git a/src/__tests__/__snapshots__/tool.getAvailableModules.test.ts.snap b/src/__tests__/__snapshots__/tool.getAvailableModules.test.ts.snap
new file mode 100644
index 0000000..295ebd0
--- /dev/null
+++ b/src/__tests__/__snapshots__/tool.getAvailableModules.test.ts.snap
@@ -0,0 +1,125 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[`getAvailableModulesTool should have a consistent return structure: structure 1`] = `
+[
+ "getAvailableModules",
+ {
+ "description": "Retrieves a list of available Patternfly react-core modules in the current environment.",
+ "inputSchema": {
+ "packageName": ZodDefault {
+ "_def": {
+ "defaultValue": [Function],
+ "description": "Name of the patternfly package to get modules for. For tables its always better to use the @patternfly/react-data-view package.",
+ "errorMap": [Function],
+ "innerType": ZodEnum {
+ "_def": {
+ "description": "Name of the patternfly package to get modules for. For tables its always better to use the @patternfly/react-data-view package.",
+ "typeName": "ZodEnum",
+ "values": [
+ "@patternfly/react-core",
+ "@patternfly/react-icons",
+ "@patternfly/react-table",
+ "@patternfly/react-data-view",
+ "@patternfly/react-component-groups",
+ ],
+ },
+ "and": [Function],
+ "array": [Function],
+ "brand": [Function],
+ "catch": [Function],
+ "default": [Function],
+ "describe": [Function],
+ "isNullable": [Function],
+ "isOptional": [Function],
+ "nullable": [Function],
+ "nullish": [Function],
+ "optional": [Function],
+ "or": [Function],
+ "parse": [Function],
+ "parseAsync": [Function],
+ "pipe": [Function],
+ "promise": [Function],
+ "readonly": [Function],
+ "refine": [Function],
+ "refinement": [Function],
+ "safeParse": [Function],
+ "safeParseAsync": [Function],
+ "spa": [Function],
+ "superRefine": [Function],
+ "transform": [Function],
+ "~standard": {
+ "validate": [Function],
+ "vendor": "zod",
+ "version": 1,
+ },
+ },
+ "typeName": "ZodDefault",
+ },
+ "and": [Function],
+ "array": [Function],
+ "brand": [Function],
+ "catch": [Function],
+ "default": [Function],
+ "describe": [Function],
+ "isNullable": [Function],
+ "isOptional": [Function],
+ "nullable": [Function],
+ "nullish": [Function],
+ "optional": [Function],
+ "or": [Function],
+ "parse": [Function],
+ "parseAsync": [Function],
+ "pipe": [Function],
+ "promise": [Function],
+ "readonly": [Function],
+ "refine": [Function],
+ "refinement": [Function],
+ "safeParse": [Function],
+ "safeParseAsync": [Function],
+ "spa": [Function],
+ "superRefine": [Function],
+ "transform": [Function],
+ "~standard": {
+ "validate": [Function],
+ "vendor": "zod",
+ "version": 1,
+ },
+ },
+ },
+ },
+ [Function],
+]
+`;
+
+exports[`getAvailableModulesTool, callback should handle modules with special characters in names 1`] = `
+{
+ "content": [
+ {
+ "text": "Button-Group;Data.Table;Nav_Item",
+ "type": "text",
+ },
+ ],
+}
+`;
+
+exports[`getAvailableModulesTool, callback should return empty string when modules map is empty 1`] = `
+{
+ "content": [
+ {
+ "text": "",
+ "type": "text",
+ },
+ ],
+}
+`;
+
+exports[`getAvailableModulesTool, callback should return modules list separated by semicolons 1`] = `
+{
+ "content": [
+ {
+ "text": "Button;Card;Table",
+ "type": "text",
+ },
+ ],
+}
+`;
diff --git a/src/__tests__/__snapshots__/tool.getComponentSourceCode.test.ts.snap b/src/__tests__/__snapshots__/tool.getComponentSourceCode.test.ts.snap
new file mode 100644
index 0000000..4324049
--- /dev/null
+++ b/src/__tests__/__snapshots__/tool.getComponentSourceCode.test.ts.snap
@@ -0,0 +1,185 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[`getComponentSourceCode should have a consistent return structure: structure 1`] = `
+[
+ "getComponentSourceCode",
+ {
+ "description": "Retrieve a source code of a specified Patternfly react-core module in the current environment.",
+ "inputSchema": {
+ "componentName": ZodString {
+ "_def": {
+ "checks": [],
+ "coerce": false,
+ "description": "Name of the PatternFly component (e.g., "Button", "Table")",
+ "typeName": "ZodString",
+ },
+ "and": [Function],
+ "array": [Function],
+ "brand": [Function],
+ "catch": [Function],
+ "default": [Function],
+ "describe": [Function],
+ "isNullable": [Function],
+ "isOptional": [Function],
+ "nullable": [Function],
+ "nullish": [Function],
+ "optional": [Function],
+ "or": [Function],
+ "parse": [Function],
+ "parseAsync": [Function],
+ "pipe": [Function],
+ "promise": [Function],
+ "readonly": [Function],
+ "refine": [Function],
+ "refinement": [Function],
+ "safeParse": [Function],
+ "safeParseAsync": [Function],
+ "spa": [Function],
+ "superRefine": [Function],
+ "transform": [Function],
+ "~standard": {
+ "validate": [Function],
+ "vendor": "zod",
+ "version": 1,
+ },
+ },
+ "packageName": ZodDefault {
+ "_def": {
+ "defaultValue": [Function],
+ "description": "Name of the patternfly package to get component from. For tables its always better to use the @patternfly/react-data-view package.",
+ "errorMap": [Function],
+ "innerType": ZodOptional {
+ "_def": {
+ "description": "Name of the patternfly package to get component from. For tables its always better to use the @patternfly/react-data-view package.",
+ "errorMap": [Function],
+ "innerType": ZodEnum {
+ "_def": {
+ "typeName": "ZodEnum",
+ "values": [
+ "@patternfly/react-core",
+ "@patternfly/react-table",
+ "@patternfly/react-data-view",
+ "@patternfly/react-component-groups",
+ ],
+ },
+ "and": [Function],
+ "array": [Function],
+ "brand": [Function],
+ "catch": [Function],
+ "default": [Function],
+ "describe": [Function],
+ "isNullable": [Function],
+ "isOptional": [Function],
+ "nullable": [Function],
+ "nullish": [Function],
+ "optional": [Function],
+ "or": [Function],
+ "parse": [Function],
+ "parseAsync": [Function],
+ "pipe": [Function],
+ "promise": [Function],
+ "readonly": [Function],
+ "refine": [Function],
+ "refinement": [Function],
+ "safeParse": [Function],
+ "safeParseAsync": [Function],
+ "spa": [Function],
+ "superRefine": [Function],
+ "transform": [Function],
+ "~standard": {
+ "validate": [Function],
+ "vendor": "zod",
+ "version": 1,
+ },
+ },
+ "typeName": "ZodOptional",
+ },
+ "and": [Function],
+ "array": [Function],
+ "brand": [Function],
+ "catch": [Function],
+ "default": [Function],
+ "describe": [Function],
+ "isNullable": [Function],
+ "isOptional": [Function],
+ "nullable": [Function],
+ "nullish": [Function],
+ "optional": [Function],
+ "or": [Function],
+ "parse": [Function],
+ "parseAsync": [Function],
+ "pipe": [Function],
+ "promise": [Function],
+ "readonly": [Function],
+ "refine": [Function],
+ "refinement": [Function],
+ "safeParse": [Function],
+ "safeParseAsync": [Function],
+ "spa": [Function],
+ "superRefine": [Function],
+ "transform": [Function],
+ "~standard": {
+ "validate": [Function],
+ "vendor": "zod",
+ "version": 1,
+ },
+ },
+ "typeName": "ZodDefault",
+ },
+ "and": [Function],
+ "array": [Function],
+ "brand": [Function],
+ "catch": [Function],
+ "default": [Function],
+ "describe": [Function],
+ "isNullable": [Function],
+ "isOptional": [Function],
+ "nullable": [Function],
+ "nullish": [Function],
+ "optional": [Function],
+ "or": [Function],
+ "parse": [Function],
+ "parseAsync": [Function],
+ "pipe": [Function],
+ "promise": [Function],
+ "readonly": [Function],
+ "refine": [Function],
+ "refinement": [Function],
+ "safeParse": [Function],
+ "safeParseAsync": [Function],
+ "spa": [Function],
+ "superRefine": [Function],
+ "transform": [Function],
+ "~standard": {
+ "validate": [Function],
+ "vendor": "zod",
+ "version": 1,
+ },
+ },
+ },
+ },
+ [Function],
+]
+`;
+
+exports[`getComponentSourceCode, callback successful component source retrieval should handle .tsx file when .ts file not found 1`] = `
+{
+ "content": [
+ {
+ "text": "export const Button = () => { return ; };",
+ "type": "text",
+ },
+ ],
+}
+`;
+
+exports[`getComponentSourceCode, callback successful component source retrieval should retrieve component source code successfully 1`] = `
+{
+ "content": [
+ {
+ "text": "export const Button = () => { return ; };",
+ "type": "text",
+ },
+ ],
+}
+`;
diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts
index 39ad80f..52a49d0 100644
--- a/src/__tests__/index.test.ts
+++ b/src/__tests__/index.test.ts
@@ -8,6 +8,7 @@ import { runServer } from '../server';
jest.mock('../options');
jest.mock('../options.context');
jest.mock('../server');
+jest.mock('../utils.moduleResolver'); // Mock the module resolver to avoid import.meta issues
const mockParseCliOptions = parseCliOptions as jest.MockedFunction;
const mockSetOptions = setOptions as jest.MockedFunction;
diff --git a/src/__tests__/options.context.test.ts b/src/__tests__/options.context.test.ts
index 38f6401..86ff4bb 100644
--- a/src/__tests__/options.context.test.ts
+++ b/src/__tests__/options.context.test.ts
@@ -6,6 +6,7 @@ import { getOptions, setOptions } from '../options.context';
// Mock dependencies
jest.mock('@modelcontextprotocol/sdk/server/mcp.js');
jest.mock('@modelcontextprotocol/sdk/server/stdio.js');
+jest.mock('../utils.moduleResolver'); // Mock the module resolver to avoid import.meta issues
const MockMcpServer = McpServer as jest.MockedClass;
const MockStdioServerTransport = StdioServerTransport as jest.MockedClass;
diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts
index 11c0196..25e9543 100644
--- a/src/__tests__/server.test.ts
+++ b/src/__tests__/server.test.ts
@@ -6,6 +6,7 @@ import { type GlobalOptions } from '../options';
// Mock dependencies
jest.mock('@modelcontextprotocol/sdk/server/mcp.js');
jest.mock('@modelcontextprotocol/sdk/server/stdio.js');
+jest.mock('../utils.moduleResolver'); // Mock the module resolver to avoid import.meta issues
const MockMcpServer = McpServer as jest.MockedClass;
const MockStdioServerTransport = StdioServerTransport as jest.MockedClass;
diff --git a/src/__tests__/tool.getAvailableModules.test.ts b/src/__tests__/tool.getAvailableModules.test.ts
new file mode 100644
index 0000000..d0762b5
--- /dev/null
+++ b/src/__tests__/tool.getAvailableModules.test.ts
@@ -0,0 +1,103 @@
+import { McpError } from '@modelcontextprotocol/sdk/types.js';
+import { getAvailableModulesTool } from '../tool.getAvailableModules';
+import { getLocalModulesMap } from '../utils.getLocalModulesMap';
+
+// Mock dependencies
+jest.mock('../utils.getLocalModulesMap');
+jest.mock('../utils.moduleResolver'); // Mock the module resolver to avoid import.meta issues
+jest.mock('../server.caching', () => ({
+ memo: jest.fn(fn => fn)
+}));
+
+const mockGetLocalModulesMap = getLocalModulesMap as jest.MockedFunction;
+
+describe('getAvailableModulesTool', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should have a consistent return structure', () => {
+ const tool = getAvailableModulesTool();
+
+ expect(tool).toMatchSnapshot('structure');
+ });
+});
+
+describe('getAvailableModulesTool, callback', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return modules list separated by semicolons', async () => {
+ const mockModulesMap = {
+ Button: '/path/to/button',
+ Card: '/path/to/card',
+ Table: '/path/to/table'
+ };
+
+ mockGetLocalModulesMap.mockResolvedValue(mockModulesMap);
+
+ const [, , callback] = getAvailableModulesTool();
+ const result = await callback({ packageName: '@patternfly/react-core' });
+
+ expect(mockGetLocalModulesMap).toHaveBeenCalledWith('@patternfly/react-core');
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should return empty string when modules map is empty', async () => {
+ mockGetLocalModulesMap.mockResolvedValue({});
+
+ const [, , callback] = getAvailableModulesTool();
+ const result = await callback({ packageName: '@patternfly/react-core' });
+
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should handle modules with special characters in names', async () => {
+ const mockModulesMap = {
+ 'Button-Group': '/path/to/button-group',
+ 'Data.Table': '/path/to/data-table',
+ Nav_Item: '/path/to/nav-item'
+ };
+
+ mockGetLocalModulesMap.mockResolvedValue(mockModulesMap);
+
+ const [, , callback] = getAvailableModulesTool();
+ const result = await callback({ packageName: '@patternfly/react-core' });
+
+ expect(result).toMatchSnapshot();
+ });
+
+ it.each([
+ {
+ description: 'with Error object',
+ error: new Error('Package not found')
+ },
+ {
+ description: 'with string error',
+ error: 'String error message'
+ },
+ {
+ description: 'with null error',
+ error: null
+ }
+ ])('should handle getLocalModulesMap errors, $description', async ({ error }) => {
+ mockGetLocalModulesMap.mockRejectedValue(error);
+
+ const [, , callback] = getAvailableModulesTool();
+
+ await expect(callback({ packageName: '@patternfly/react-core' })).rejects.toThrow(McpError);
+ await expect(callback({ packageName: '@patternfly/react-core' })).rejects.toThrow('Failed to retrieve available modules');
+ });
+
+ it('should always call with @patternfly/react-core package', async () => {
+ mockGetLocalModulesMap.mockResolvedValue({ Component: '/path' });
+
+ const [, , callback] = getAvailableModulesTool();
+
+ await callback({ packageName: '@patternfly/react-core' });
+
+ expect(mockGetLocalModulesMap).toHaveBeenCalledWith('@patternfly/react-core');
+ expect(mockGetLocalModulesMap).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/__tests__/tool.getComponentSourceCode.test.ts b/src/__tests__/tool.getComponentSourceCode.test.ts
new file mode 100644
index 0000000..6c0802a
--- /dev/null
+++ b/src/__tests__/tool.getComponentSourceCode.test.ts
@@ -0,0 +1,265 @@
+import { McpError } from '@modelcontextprotocol/sdk/types.js';
+import { getComponentSourceCode } from '../tool.getComponentSourceCode';
+import { getLocalModulesMap } from '../utils.getLocalModulesMap';
+import { verifyLocalPackage } from '../utils.verifyLocalPackage';
+import { readFileAsync } from '../utils.readFile';
+
+// Mock dependencies
+jest.mock('../utils.getLocalModulesMap');
+jest.mock('../utils.verifyLocalPackage');
+jest.mock('../utils.readFile');
+jest.mock('../utils.moduleResolver'); // Mock the module resolver to avoid import.meta issues
+jest.mock('../server.caching', () => ({
+ memo: jest.fn(fn => fn)
+}));
+
+const mockGetLocalModulesMap = getLocalModulesMap as jest.MockedFunction;
+const mockVerifyLocalPackage = verifyLocalPackage as jest.MockedFunction;
+const mockReadFileAsync = readFileAsync as jest.MockedFunction;
+
+describe('getComponentSourceCode', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should have a consistent return structure', () => {
+ const tool = getComponentSourceCode();
+
+ expect(tool).toMatchSnapshot('structure');
+ });
+});
+
+describe('getComponentSourceCode, callback', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('successful component source retrieval', () => {
+ it('should retrieve component source code successfully', async () => {
+ const mockPackageStatus = {
+ exists: true,
+ version: '1.0.0',
+ packageRoot: '/test/node_modules/@patternfly/react-core'
+ };
+ const mockModulesMap = {
+ Button: 'dist/dynamic/components/Button'
+ };
+ const mockIndexSource = `export * from './Button';`;
+ const mockComponentSource = `export const Button = () => { return ; };`;
+
+ mockVerifyLocalPackage.mockResolvedValue(mockPackageStatus);
+ mockGetLocalModulesMap.mockResolvedValue(mockModulesMap);
+ mockReadFileAsync
+ .mockResolvedValueOnce(mockIndexSource) // index.ts read
+ .mockResolvedValueOnce(mockComponentSource); // component file read
+
+ const [, , callback] = getComponentSourceCode();
+ const result = await callback({ componentName: 'Button' });
+
+ expect(mockVerifyLocalPackage).toHaveBeenCalledWith('@patternfly/react-core');
+ expect(mockGetLocalModulesMap).toHaveBeenCalledWith('@patternfly/react-core');
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should handle .tsx file when .ts file not found', async () => {
+ const mockPackageStatus = {
+ exists: true,
+ version: '1.0.0',
+ packageRoot: '/test/node_modules/@patternfly/react-core'
+ };
+ const mockModulesMap = {
+ Button: 'dist/dynamic/components/Button'
+ };
+ const mockIndexSource = `export * from './Button';`;
+ const mockComponentSource = `export const Button = () => { return ; };`;
+
+ mockVerifyLocalPackage.mockResolvedValue(mockPackageStatus);
+ mockGetLocalModulesMap.mockResolvedValue(mockModulesMap);
+ mockReadFileAsync
+ .mockResolvedValueOnce(mockIndexSource) // index.ts read
+ .mockRejectedValueOnce(new Error('File not found')) // .ts file fails
+ .mockResolvedValueOnce(mockComponentSource); // .tsx file succeeds
+
+ const [, , callback] = getComponentSourceCode();
+ const result = await callback({ componentName: 'Button' });
+
+ expect(result).toMatchSnapshot();
+ });
+ });
+
+ describe('parameter validation', () => {
+ it.each([
+ {
+ description: 'with missing componentName',
+ componentName: undefined,
+ error: 'Missing required parameter: componentName'
+ },
+ {
+ description: 'with null componentName',
+ componentName: null,
+ error: 'Missing required parameter: componentName'
+ },
+ {
+ description: 'with non-string componentName',
+ componentName: 123,
+ error: 'Missing required parameter: componentName'
+ },
+ {
+ description: 'with array componentName',
+ componentName: ['Button'],
+ error: 'Missing required parameter: componentName'
+ }
+ ])('should handle invalid parameters, $description', async ({ componentName, error }) => {
+ const [, , callback] = getComponentSourceCode();
+
+ await expect(callback({ componentName })).rejects.toThrow(McpError);
+ await expect(callback({ componentName })).rejects.toThrow(error);
+ });
+ });
+
+ describe('package verification errors', () => {
+ it('should handle package not found', async () => {
+ const mockPackageStatus = {
+ exists: false,
+ version: '',
+ packageRoot: '',
+ error: new Error('Package not found')
+ };
+
+ mockVerifyLocalPackage.mockResolvedValue(mockPackageStatus);
+
+ const [, , callback] = getComponentSourceCode();
+
+ await expect(callback({ componentName: 'Button' })).rejects.toThrow(McpError);
+ await expect(callback({ componentName: 'Button' })).rejects.toThrow('Package "@patternfly/react-core" not found locally');
+ });
+ });
+
+ describe('component resolution errors', () => {
+ it('should handle component not found in modules map', async () => {
+ const mockPackageStatus = {
+ exists: true,
+ version: '1.0.0',
+ packageRoot: '/test/node_modules/@patternfly/react-core'
+ };
+ const mockModulesMap = {
+ Card: 'dist/dynamic/components/Card'
+ };
+
+ mockVerifyLocalPackage.mockResolvedValue(mockPackageStatus);
+ mockGetLocalModulesMap.mockResolvedValue(mockModulesMap);
+
+ const [, , callback] = getComponentSourceCode();
+
+ await expect(callback({ componentName: 'Button' })).rejects.toThrow(McpError);
+ await expect(callback({ componentName: 'Button' })).rejects.toThrow('No valid path to "Button" found');
+ });
+
+ it('should handle import line not found in index file', async () => {
+ const mockPackageStatus = {
+ exists: true,
+ version: '1.0.0',
+ packageRoot: '/test/node_modules/@patternfly/react-core'
+ };
+ const mockModulesMap = {
+ Button: 'dist/dynamic/components/Button'
+ };
+ const mockIndexSource = `export * from './Card';`; // Different component - no Button export
+
+ mockVerifyLocalPackage.mockResolvedValue(mockPackageStatus);
+ mockGetLocalModulesMap.mockResolvedValue(mockModulesMap);
+ mockReadFileAsync.mockResolvedValue(mockIndexSource);
+
+ const [, , callback] = getComponentSourceCode();
+
+ await expect(callback({ componentName: 'Button' })).rejects.toThrow(McpError);
+ await expect(callback({ componentName: 'Button' })).rejects.toThrow('Failed to find source code for component "Button"');
+ });
+
+ it('should handle both .ts and .tsx files not found', async () => {
+ const mockPackageStatus = {
+ exists: true,
+ version: '1.0.0',
+ packageRoot: '/test/node_modules/@patternfly/react-core'
+ };
+ const mockModulesMap = {
+ Button: 'dist/dynamic/components/Button'
+ };
+ const mockIndexSource = `export * from './Button';`;
+
+ mockVerifyLocalPackage.mockResolvedValue(mockPackageStatus);
+ mockGetLocalModulesMap.mockResolvedValue(mockModulesMap);
+ mockReadFileAsync
+ .mockImplementation(filePath => {
+ // @ts-ignore
+ if (filePath.includes('index.ts')) {
+ return Promise.resolve(mockIndexSource);
+ }
+
+ return Promise.reject(new Error('File not found'));
+ });
+
+ const [, , callback] = getComponentSourceCode();
+
+ await expect(callback({ componentName: 'Button' })).rejects.toThrow(McpError);
+ await expect(callback({ componentName: 'Button' })).rejects.toThrow('Failed to read source code file for component "Button"');
+ });
+ });
+
+ describe('path handling', () => {
+ it('should correctly transform dist/dynamic path to src path', async () => {
+ const mockPackageStatus = {
+ exists: true,
+ version: '1.0.0',
+ packageRoot: '/test/node_modules/@patternfly/react-core'
+ };
+ const mockModulesMap = {
+ Button: 'dist/dynamic/components/Button'
+ };
+
+ mockVerifyLocalPackage.mockResolvedValue(mockPackageStatus);
+ mockGetLocalModulesMap.mockResolvedValue(mockModulesMap);
+
+ const [, , callback] = getComponentSourceCode();
+
+ // This will fail at readFileAsync but we can verify the path transformation logic
+ try {
+ await callback({ componentName: 'Button' });
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ } catch (error) {
+ // Expected to fail, but we can verify the calls
+ }
+
+ // Should read from src path, not dist path
+ expect(mockReadFileAsync).toHaveBeenCalledWith(
+ '/test/node_modules/@patternfly/react-core/src/components/Button/index.ts',
+ 'utf-8'
+ );
+ });
+ });
+
+ describe('always targets @patternfly/react-core', () => {
+ it('should always use @patternfly/react-core package', async () => {
+ const mockPackageStatus = {
+ exists: true,
+ version: '1.0.0',
+ packageRoot: '/test/path'
+ };
+
+ mockVerifyLocalPackage.mockResolvedValue(mockPackageStatus);
+ mockGetLocalModulesMap.mockResolvedValue({});
+
+ const [, , callback] = getComponentSourceCode();
+
+ try {
+ await callback({ componentName: 'Button' });
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ } catch (error) {
+ // Expected to fail due to empty modules map
+ }
+
+ expect(mockVerifyLocalPackage).toHaveBeenCalledWith('@patternfly/react-core');
+ expect(mockGetLocalModulesMap).toHaveBeenCalledWith('@patternfly/react-core');
+ });
+ });
+});
diff --git a/src/__tests__/utils.verifyLocalPackage.test.ts b/src/__tests__/utils.verifyLocalPackage.test.ts
new file mode 100644
index 0000000..b9935c4
--- /dev/null
+++ b/src/__tests__/utils.verifyLocalPackage.test.ts
@@ -0,0 +1,243 @@
+import { verifyLocalPackage } from '../utils.verifyLocalPackage';
+import { readJsonFile } from '../utils.readFile';
+import { resolveModule } from '../utils.moduleResolver';
+
+// Mock dependencies
+jest.mock('../utils.readFile');
+jest.mock('../utils.moduleResolver'); // This will use the __mocks__ version
+
+const mockReadJsonFile = readJsonFile as jest.MockedFunction;
+const mockResolveModule = resolveModule as jest.MockedFunction;
+
+describe('verifyLocalPackage', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ // Mock process.cwd to return a consistent value for testing
+ jest.spyOn(process, 'cwd').mockReturnValue('/test/project');
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ describe('input validation', () => {
+ it('should return error for undefined package name', async () => {
+ const result = await verifyLocalPackage(undefined as any);
+
+ expect(result).toEqual({
+ exists: false,
+ version: '',
+ packageRoot: '',
+ error: new Error('Invalid package name: undefined')
+ });
+ });
+
+ it('should return error for null package name', async () => {
+ const result = await verifyLocalPackage(null as any);
+
+ expect(result).toEqual({
+ exists: false,
+ version: '',
+ packageRoot: '',
+ error: new Error('Invalid package name: null')
+ });
+ });
+
+ it('should return error for empty string package name', async () => {
+ const result = await verifyLocalPackage('');
+
+ expect(result).toEqual({
+ exists: false,
+ version: '',
+ packageRoot: '',
+ error: new Error('Invalid package name: ')
+ });
+ });
+
+ it('should return error for non-string package name', async () => {
+ const result = await verifyLocalPackage(123 as any);
+
+ expect(result).toEqual({
+ exists: false,
+ version: '',
+ packageRoot: '',
+ error: new Error('Invalid package name: 123')
+ });
+ });
+ });
+
+ describe('successful package resolution', () => {
+ it('should resolve package successfully with version', async () => {
+ const packageData = { version: '1.2.3' };
+ const packagePath = 'file:///test/project/node_modules/@patternfly/react-core/package.json';
+
+ mockResolveModule.mockReturnValue(packagePath);
+ mockReadJsonFile.mockResolvedValue(packageData);
+
+ const result = await verifyLocalPackage('@patternfly/react-core');
+
+ expect(mockResolveModule).toHaveBeenCalledWith('/test/project/node_modules/@patternfly/react-core/package.json');
+ expect(mockReadJsonFile).toHaveBeenCalledWith('/test/project/node_modules/@patternfly/react-core/package.json');
+ expect(result).toEqual({
+ exists: true,
+ version: '1.2.3',
+ packageRoot: '/test/project/node_modules/@patternfly/react-core'
+ });
+ });
+
+ it('should handle package without version field', async () => {
+ const packageData = {};
+ const packagePath = 'file:///test/project/node_modules/some-package/package.json';
+
+ mockResolveModule.mockReturnValue(packagePath);
+ mockReadJsonFile.mockResolvedValue(packageData);
+
+ const result = await verifyLocalPackage('some-package');
+
+ expect(result).toEqual({
+ exists: true,
+ version: '',
+ packageRoot: '/test/project/node_modules/some-package'
+ });
+ });
+
+ it('should handle scoped packages correctly', async () => {
+ const packageData = { version: '2.0.0' };
+ const packagePath = 'file:///test/project/node_modules/@scope/package-name/package.json';
+
+ mockResolveModule.mockReturnValue(packagePath);
+ mockReadJsonFile.mockResolvedValue(packageData);
+
+ const result = await verifyLocalPackage('@scope/package-name');
+
+ expect(mockResolveModule).toHaveBeenCalledWith('/test/project/node_modules/@scope/package-name/package.json');
+ expect(result).toEqual({
+ exists: true,
+ version: '2.0.0',
+ packageRoot: '/test/project/node_modules/@scope/package-name'
+ });
+ });
+
+ it('should strip file:// protocol from paths', async () => {
+ const packageData = { version: '1.0.0' };
+ const packagePath = 'file:///some/path/node_modules/package/package.json';
+
+ mockResolveModule.mockReturnValue(packagePath);
+ mockReadJsonFile.mockResolvedValue(packageData);
+
+ const result = await verifyLocalPackage('package');
+
+ expect(mockReadJsonFile).toHaveBeenCalledWith('/some/path/node_modules/package/package.json');
+ expect(result.packageRoot).toBe('/some/path/node_modules/package');
+ });
+ });
+
+ describe('error handling', () => {
+ it('should handle module resolution throwing error', async () => {
+ const resolveError = new Error('Module not found');
+
+ mockResolveModule.mockImplementation(() => {
+ throw resolveError;
+ });
+
+ const result = await verifyLocalPackage('nonexistent-package');
+
+ expect(result).toEqual({
+ exists: false,
+ version: '',
+ packageRoot: '',
+ error: new Error('Error resolving package "nonexistent-package": Module not found')
+ });
+ });
+
+ it('should handle readJsonFile throwing error', async () => {
+ const packagePath = 'file:///test/project/node_modules/package/package.json';
+ const readError = new Error('Permission denied');
+
+ mockResolveModule.mockReturnValue(packagePath);
+ mockReadJsonFile.mockRejectedValue(readError);
+
+ const result = await verifyLocalPackage('package');
+
+ expect(result).toEqual({
+ exists: false,
+ version: '',
+ packageRoot: '',
+ error: new Error('Error resolving package "package": Permission denied')
+ });
+ });
+
+ it('should handle non-Error objects being thrown', async () => {
+ mockResolveModule.mockImplementation(() => {
+ throw 'String error';
+ });
+
+ const result = await verifyLocalPackage('package');
+
+ expect(result).toEqual({
+ exists: false,
+ version: '',
+ packageRoot: '',
+ error: new Error('Error resolving package "package": String error')
+ });
+ });
+
+ it('should handle null/undefined errors', async () => {
+ mockResolveModule.mockImplementation(() => {
+ throw null;
+ });
+
+ const result = await verifyLocalPackage('package');
+
+ expect(result).toEqual({
+ exists: false,
+ version: '',
+ packageRoot: '',
+ error: new Error('Error resolving package "package": null')
+ });
+ });
+ });
+
+ describe('path resolution', () => {
+ it('should use current working directory as project root', async () => {
+ const packageData = { version: '1.0.0' };
+
+ mockResolveModule.mockReturnValue('file:///test/project/node_modules/test/package.json');
+ mockReadJsonFile.mockResolvedValue(packageData);
+
+ await verifyLocalPackage('test');
+
+ expect(mockResolveModule).toHaveBeenCalledWith('/test/project/node_modules/test/package.json');
+ });
+
+ it('should handle different working directories', async () => {
+ jest.spyOn(process, 'cwd').mockReturnValue('/different/path');
+
+ const packageData = { version: '1.0.0' };
+
+ mockResolveModule.mockReturnValue('file:///different/path/node_modules/test/package.json');
+ mockReadJsonFile.mockResolvedValue(packageData);
+
+ await verifyLocalPackage('test');
+
+ expect(mockResolveModule).toHaveBeenCalledWith('/different/path/node_modules/test/package.json');
+ });
+ });
+
+ describe('return type structure', () => {
+ it('should always return object with required properties', async () => {
+ mockResolveModule.mockImplementation(() => {
+ throw new Error('Test error');
+ });
+
+ const result = await verifyLocalPackage('test');
+
+ expect(result).toHaveProperty('exists');
+ expect(result).toHaveProperty('version');
+ expect(result).toHaveProperty('packageRoot');
+ expect(typeof result.exists).toBe('boolean');
+ expect(typeof result.version).toBe('string');
+ expect(typeof result.packageRoot).toBe('string');
+ });
+ });
+});
diff --git a/src/server.ts b/src/server.ts
index a8b5493..06d1982 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -5,6 +5,9 @@ import { fetchDocsTool } from './tool.fetchDocs';
import { componentSchemasTool } from './tool.componentSchemas';
import { getOptions, runWithOptions } from './options.context';
import { type GlobalOptions } from './options';
+import { getAvailableModulesTool } from './tool.getAvailableModules';
+import { getComponentSourceCode } from './tool.getComponentSourceCode';
+import { getReactUtilityClasses } from './tool.getReactUtilityClasses';
type McpTool = [string, { description: string; inputSchema: any }, (args: any) => Promise];
@@ -38,7 +41,10 @@ const runServer = async (options = getOptions(), {
tools = [
usePatternFlyDocsTool,
fetchDocsTool,
- componentSchemasTool
+ componentSchemasTool,
+ getAvailableModulesTool,
+ getComponentSourceCode,
+ getReactUtilityClasses
],
enableSigint = true
}: { tools?: McpToolCreator[]; enableSigint?: boolean } = {}): Promise => {
diff --git a/src/tool.getAvailableModules.ts b/src/tool.getAvailableModules.ts
new file mode 100644
index 0000000..d391a6f
--- /dev/null
+++ b/src/tool.getAvailableModules.ts
@@ -0,0 +1,58 @@
+import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
+import { z } from 'zod';
+
+import { type McpTool } from './server';
+import { memo } from './server.caching';
+import { getLocalModulesMap } from './utils.getLocalModulesMap';
+
+export const getAvailableModulesTool = (): McpTool => {
+ const memoGetModulesMap = memo(
+ getLocalModulesMap
+ );
+
+ const callback = async (args: any = {}) => {
+ const { packageName } = args;
+
+ if (typeof packageName !== 'string') {
+ throw new McpError(
+ ErrorCode.InvalidParams,
+ `Missing required parameter: packageName (must be a string): ${packageName}`
+ );
+ }
+ let modulesList : string[] = [];
+
+ try {
+ // should be extended for other packages in the future
+ const modulesMap = await memoGetModulesMap(packageName);
+
+ // no need to return paths, just the module names, reduce the context size
+ modulesList = Object.keys(modulesMap);
+ } catch (error) {
+ throw new McpError(
+ ErrorCode.InternalError,
+ `Failed to retrieve available modules: ${error}`
+ );
+ }
+
+ return {
+ content: [
+ {
+ type: 'text',
+ // Modules separated by semicolons to save context space
+ text: modulesList.join(';')
+ }
+ ]
+ };
+ };
+
+ return [
+ 'getAvailableModules',
+ {
+ description: 'Retrieves a list of available Patternfly react-core modules in the current environment.',
+ inputSchema: {
+ packageName: z.enum(['@patternfly/react-core', '@patternfly/react-icons', '@patternfly/react-table', '@patternfly/react-data-view', '@patternfly/react-component-groups']).describe('Name of the patternfly package to get modules for. For tables its always better to use the @patternfly/react-data-view package.').default('@patternfly/react-core')
+ }
+ },
+ callback
+ ];
+};
diff --git a/src/tool.getComponentSourceCode.ts b/src/tool.getComponentSourceCode.ts
new file mode 100644
index 0000000..bdd7929
--- /dev/null
+++ b/src/tool.getComponentSourceCode.ts
@@ -0,0 +1,119 @@
+import path from 'node:path';
+import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
+import { z } from 'zod';
+
+import { type McpTool } from './server';
+import { memo } from './server.caching';
+import { getLocalModulesMap } from './utils.getLocalModulesMap';
+import { verifyLocalPackage } from './utils.verifyLocalPackage';
+import { readFileAsync } from './utils.readFile';
+
+export const getComponentSourceCode = (): McpTool => {
+ const memoGetModulesMap = memo(
+ getLocalModulesMap
+ );
+
+ const callback = async (args: any = {}) => {
+ const { componentName, packageName: packageNameArg } = args;
+ let fileContent: string;
+ const packageName = packageNameArg || '@patternfly/react-core';
+
+ if (typeof componentName !== 'string') {
+ throw new McpError(
+ ErrorCode.InvalidParams,
+ `Missing required parameter: componentName (must be a string): ${componentName}`
+ );
+ }
+
+ // should be extended for other packages in the future
+ const status = await verifyLocalPackage(packageName);
+
+ if (!status.exists) {
+ throw new McpError(
+ ErrorCode.InvalidParams,
+ `Package "${packageName}" not found locally. ${status.error ? status.error.message : ''}`
+ );
+ }
+ const modulesMap = await memoGetModulesMap(packageName);
+ const componentPath = modulesMap[componentName]?.replace(/^dist\/dynamic/, 'src');
+
+ if (!componentPath) {
+ throw new McpError(
+ ErrorCode.InvalidParams,
+ `No valid path to "${componentName}" found in available modules.`
+ );
+ }
+
+ const componentDir = `${status.packageRoot}/${componentPath}`;
+ // TODO: We can also try to get directly to the component file first before checking re-exports
+ const indexFile = path.join(componentDir, 'index.ts');
+
+ const indexSource = await readFileAsync(`${componentDir}/index.ts`, 'utf-8');
+
+ if (!indexSource) {
+ throw new McpError(
+ ErrorCode.InternalError,
+ `Failed to read index.ts for component "${componentName}".`
+ );
+ }
+
+ const lines = indexSource.split('\n');
+ // TODO: the modules map does not provide paths for everything, need to improve that
+ const searchPattern = new RegExp(`'\\./${componentName}';?$`);
+ const importPartial = lines.find(line => searchPattern.test(line));
+
+ if (typeof importPartial === 'undefined' || !importPartial) {
+ throw new McpError(
+ ErrorCode.InternalError,
+ `Failed to find source code for component "${componentName}".`
+ );
+ }
+
+ const importPath = importPartial.split('from')[1]?.trim().replace(/['";]/g, '');
+
+ if (!importPath) {
+ throw new McpError(
+ ErrorCode.InternalError,
+ `Failed to parse import path for component "${componentName}".`
+ );
+ }
+
+ const absolutePath = path.resolve(indexFile.replace('/index.ts', ''), importPath + '.ts');
+ const absolutePathX = path.resolve(indexFile.replace('/index.ts', ''), importPath + '.tsx');
+
+ try {
+ fileContent = await readFileAsync(absolutePath, 'utf-8');
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ } catch (error) {
+ try {
+ fileContent = await readFileAsync(absolutePathX, 'utf-8');
+ } catch (error) {
+ throw new McpError(
+ ErrorCode.InternalError,
+ `Failed to read source code file for component "${componentName}": ${error}`
+ );
+ }
+ }
+
+ return {
+ content: [
+ {
+ type: 'text',
+ text: fileContent
+ }
+ ]
+ };
+ };
+
+ return [
+ 'getComponentSourceCode',
+ {
+ description: 'Retrieve a source code of a specified Patternfly react-core module in the current environment.',
+ inputSchema: {
+ componentName: z.string().describe('Name of the PatternFly component (e.g., "Button", "Table")'),
+ packageName: z.enum(['@patternfly/react-core', '@patternfly/react-table', '@patternfly/react-data-view', '@patternfly/react-component-groups']).optional().describe('Name of the patternfly package to get component from. For tables its always better to use the @patternfly/react-data-view package.').default('@patternfly/react-core')
+ }
+ },
+ callback
+ ];
+};
diff --git a/src/tool.getReactUtilityClasses.ts b/src/tool.getReactUtilityClasses.ts
new file mode 100644
index 0000000..0614784
--- /dev/null
+++ b/src/tool.getReactUtilityClasses.ts
@@ -0,0 +1,69 @@
+import { z } from 'zod';
+import { type McpTool } from './server';
+import { verifyLocalPackage } from './utils.verifyLocalPackage';
+import { readFileAsync } from './utils.readFile';
+
+const utilityClassesMap: Record = {
+ Accessibility: 'css/utilities/Accessibility/accessibility.css',
+ Alignment: 'css/utilities/Alignment/alignment.css',
+ BackgroundColor: 'css/utilities/BackgroundColor/background-color.css',
+ BoxShadow: 'css/utilities/BoxShadow/box-shadow.css',
+ Display: 'css/utilities/Display/display.css',
+ Flex: 'css/utilities/Flex/flex.css',
+ Float: 'css/utilities/Float/float.css',
+ Sizing: 'css/utilities/Sizing/sizing.css',
+ Spacing: 'css/utilities/Spacing/spacing.css',
+ Text: 'css/utilities/Text/text.css'
+};
+
+const utilities = Object.keys(utilityClassesMap) as [string, ...string[]];
+
+export const getReactUtilityClasses = (): McpTool => {
+ const callback = async (args: any = {}) => {
+ const { utilityName } = args;
+ let fileContent: string;
+
+ if (typeof utilityName !== 'string' || !utilities.includes(utilityName)) {
+ throw new Error(
+ `Invalid or missing parameter: utilityName (must be one of: ${utilities.join(', ')}): ${utilityName}`
+ );
+ }
+ const status = await verifyLocalPackage('@patternfly/react-styles');
+
+ if (!status.exists) {
+ throw new Error(
+ `Package "@patternfly/react-styles" not found locally. ${status.error ? status.error.message : ''}`
+ );
+ }
+
+ const root = status.packageRoot;
+ const utilityPath = utilityClassesMap[utilityName];
+ const fullPath = `${root}/${utilityPath}`;
+
+ try {
+ fileContent = await readFileAsync(fullPath, 'utf-8');
+ } catch (error) {
+ throw new Error(
+ `Failed to read utility classes file for "${utilityName}" at path "${fullPath}". ${error instanceof Error ? error.message : ''}`
+ );
+ }
+
+ return {
+ content: [{
+ type: 'text',
+ text: fileContent
+ }]
+ };
+ };
+
+ return [
+ 'getReactUtilityClasses',
+ {
+ description: 'Retrieves a list of available Patternfly react-styles utility classes in the current environment.',
+ inputSchema: {
+ utilityName: z.enum(utilities).describe('Name of a set of utility classes to retrieve from @patternfly/react-styles.')
+ }
+ },
+ callback
+ ];
+};
diff --git a/src/utils.getLocalModulesMap.ts b/src/utils.getLocalModulesMap.ts
new file mode 100644
index 0000000..7cddf16
--- /dev/null
+++ b/src/utils.getLocalModulesMap.ts
@@ -0,0 +1,21 @@
+import { readJsonFile } from './utils.readFile';
+import { verifyLocalPackage } from './utils.verifyLocalPackage';
+
+export const getLocalModulesMap = async (packageName: string): Promise> => {
+ let modulesMap: Record = {};
+
+ const status = await verifyLocalPackage(packageName);
+
+ if (!status.exists) {
+ throw new Error(`Package "${packageName}" not found locally. ${status.error ? status.error.message : ''}`);
+ }
+
+ // exported map of module names to their paths
+ try {
+ modulesMap = await readJsonFile>(`${status.packageRoot}/dist/dynamic-modules.json`);
+ } catch (error) {
+ throw new Error(`Failed to import modules map from package "${packageName}": ${error}. Does the modules map exist?`);
+ }
+
+ return modulesMap;
+};
diff --git a/src/utils.moduleResolver.ts b/src/utils.moduleResolver.ts
new file mode 100644
index 0000000..f5328ea
--- /dev/null
+++ b/src/utils.moduleResolver.ts
@@ -0,0 +1,6 @@
+/**
+ * Utility for module resolution using import.meta.resolve
+ * This is separated into its own file to facilitate mocking in tests
+ */
+
+export const resolveModule = (modulePath: string): string => import.meta.resolve(modulePath);
diff --git a/src/utils.readFile.ts b/src/utils.readFile.ts
new file mode 100644
index 0000000..c78b04d
--- /dev/null
+++ b/src/utils.readFile.ts
@@ -0,0 +1,19 @@
+/**
+ * the bundler is stripping some parameters form dynamic imports which makes it impossible to import json files correctly
+ * WHen using const a = await import('path/to/file.json', { with: { type: 'json' } });
+ * the resulted JS bundle is stripped to
+ * const a = await import('path/to/file.json');
+ *
+ * This file is to workaround that issue by reading the file content directly
+ * */
+
+import { promisify } from 'node:util';
+import { readFile } from 'node:fs';
+
+export const readFileAsync = promisify(readFile);
+
+export const readJsonFile = async (filePath: string): Promise => {
+ const data = await readFileAsync(filePath, 'utf-8');
+
+ return JSON.parse(data) as T;
+};
diff --git a/src/utils.verifyLocalPackage.ts b/src/utils.verifyLocalPackage.ts
new file mode 100644
index 0000000..448ed92
--- /dev/null
+++ b/src/utils.verifyLocalPackage.ts
@@ -0,0 +1,48 @@
+import path from 'node:path';
+import { readJsonFile } from './utils.readFile';
+import { resolveModule } from './utils.moduleResolver';
+
+type VerifyLocalPackageStatus = {
+ exists: boolean;
+ version: string;
+ packageRoot: string;
+ error?: Error;
+};
+
+/**
+ * Verifies if a local package exists and retrieves its version and root path.
+ *
+ * @param packageName string - The name of the local package to verify.
+ * @returns {Promise} Verification status including existence, version, and root path.
+ */
+export const verifyLocalPackage = async (packageName: string) : Promise => {
+ const errorStatus : VerifyLocalPackageStatus = {
+ exists: false,
+ version: '',
+ packageRoot: ''
+ };
+
+ if (!packageName || typeof packageName !== 'string') {
+ return { ...errorStatus, error: new Error(`Invalid package name: ${packageName}`) };
+ }
+
+ // current working dir of agent
+ const projectRoot = process.cwd();
+
+ try {
+ // TODO: consider monorepo setup in the future
+ const pkgPath = resolveModule(`${projectRoot}/node_modules/${packageName}/package.json`);
+ const packageDir = path.dirname(pkgPath).replace(/^file:\/\//, '');
+ const data = await readJsonFile<{ version: string }>(pkgPath.replace(/^file:\/\//, ''));
+
+ return {
+ exists: true,
+ version: data.version || '',
+ packageRoot: packageDir
+ };
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err);
+
+ return { ...errorStatus, error: new Error(`Error resolving package "${packageName}": ${message}`) };
+ }
+};
diff --git a/tests/__snapshots__/stdioTransport.test.ts.snap b/tests/__snapshots__/stdioTransport.test.ts.snap
index 7c34c9f..0802532 100644
--- a/tests/__snapshots__/stdioTransport.test.ts.snap
+++ b/tests/__snapshots__/stdioTransport.test.ts.snap
@@ -387,6 +387,9 @@ exports[`PatternFly MCP, STDIO should expose expected tools and stable shape 1`]
"toolNames": [
"componentSchemas",
"fetchDocs",
+ "getAvailableModules",
+ "getComponentSourceCode",
+ "getReactUtilityClasses",
"usePatternFlyDocs",
],
}