Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ Unit tests are located in the `__tests__` directory.

E2E tests are located in the root `./tests` directory.

Contributors can run the MCP server in a specialized `test` mode against mock resources.

```bash
npm run test:integration
```

This mode leverages the `--mode test` and `--mode-test-url` flags to redirect resource lookups to a fixture server instead of live or local resources.

## AI agent

### User Section
Expand Down
78 changes: 50 additions & 28 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,28 @@ Complete guide to using the PatternFly MCP Server for development including CLI

### Available options

| Flag | Description | Default |
|:--------------------------------------|:------------------------------------------------------|:---------------------|
| `--http` | Enable HTTP transport mode | `false` (stdio mode) |
| `--port <num>` | Port for HTTP transport | `8080` |
| `--host <string>` | Host to bind to | `127.0.0.1` |
| `--allowed-origins <origins>` | Comma-separated list of allowed CORS origins | `none` |
| `--allowed-hosts <hosts>` | Comma-separated list of allowed host headers | `none` |
| `--tool <path>` | Path to external Tool Plugin (repeatable) | `none` |
| `--plugin-isolation <none \| strict>` | Isolation preset for external tools-as-plugins | `strict` |
| `--log-stderr` | Enable terminal logging | `false` |
| `--log-protocol` | Forward logs to MCP clients | `false` |
| `--log-level <level>` | Set log level (`debug`, `info`, `warn`, `error`) | `info` |
| `--verbose` | Shortcut for `--log-level debug` | `false` |
| Flag | Description | Default |
|:--------------------------------------|:-------------------------------------------------|:---------------------|
| `--http` | Enable HTTP transport mode | `false` (stdio mode) |
| `--port <num>` | Port for HTTP transport | `8080` |
| `--host <string>` | Host to bind to | `127.0.0.1` |
| `--allowed-origins <origins>` | Comma-separated list of allowed CORS origins | `none` |
| `--allowed-hosts <hosts>` | Comma-separated list of allowed host headers | `none` |
| `--tool <path>` | Path to external Tool Plugin (repeatable) | `none` |
| `--plugin-isolation <none \| strict>` | Isolation preset for external tools-as-plugins | `strict` |
| `--log-stderr` | Enable terminal logging | `false` |
| `--log-protocol` | Forward logs to MCP clients | `false` |
| `--log-level <level>` | Set log level (`debug`, `info`, `warn`, `error`) | `info` |
| `--mode <mode>` | Operational mode (`cli`, `programmatic`, `test`) | `cli` |
| `--mode-test-url <url>` | Base URL for fixture/mock servers in `test` mode | `none` |
| `--verbose` | Shortcut for `--log-level debug` | `false` |

#### Notes
- **HTTP transport mode** - By default, the server uses `stdio`. Use the `--http` flag to enable HTTP transport.
- **Logging** - The server uses a `diagnostics_channel`-based logger that keeps STDIO stdout pure by default.
- **Programmatic API** - The server can also be used programmatically with options. See [Programmatic Usage](#programmatic-usage) for more details.
- **Tool Plugins** - The server can load external tool plugins at startup. See [Tool Plugins](#tool-plugins) for more details.
- **Tool Plugins** - The server can load external tool plugins at startup. See [Tool Plugins](#tool-plugins) for more details.
- **Test Mode** - When `--mode test` is used, the server redirects resource requests to the URL provided by `--mode-test-url`, enabling E2E testing without local filesystem access.

### Basic use scenarios

Expand All @@ -53,6 +56,10 @@ npx @patternfly/patternfly-mcp --http --port 3000 --allowed-origins "https://app
```bash
npx @patternfly/patternfly-mcp --tool ./first-tool.js --tool ./second-tool.ts
```
**Testing with a fixture server**:
```bash
npx @patternfly/patternfly-mcp --mode test --mode-test-url "http://localhost:3000"
```

### Testing with MCP Inspector

Expand Down Expand Up @@ -81,20 +88,21 @@ npx @modelcontextprotocol/inspector-cli \

The `start()` function accepts an optional `PfMcpOptions` object for programmatic configuration. Use these options to customize behavior, transport, and logging for embedded instances.

| Option | Type | Description | Default |
|:----------------------|:-----------------------------------------|:----------------------------------------------------------------------|:-------------------|
| `toolModules` | `ToolModule \| ToolModule[]` | Array of tool modules or paths to external tool plugins to be loaded. | `[]` |
| `isHttp` | `boolean` | Enable HTTP transport mode. | `false` |
| `http.port` | `number` | Port for HTTP transport. | `8080` |
| `http.host` | `string` | Host to bind to. | `127.0.0.1` |
| `http.allowedOrigins` | `string[]` | List of allowed CORS origins. | `[]` |
| `http.allowedHosts` | `string[]` | List of allowed host headers. | `[]` |
| `pluginIsolation` | `'none' \| 'strict'` | Isolation preset for external tools-as-plugins. | `'strict'` |
| `logging.level` | `'debug' \| 'info' \| 'warn' \| 'error'` | Set the logging level. | `'info'` |
| `logging.stderr` | `boolean` | Enable terminal logging to stderr. | `false` |
| `logging.protocol` | `boolean` | Forward logs to MCP clients. | `false` |
| `mode` | `'cli' \| 'programmatic' \| 'test'` | Specifies the operation mode. | `'programmatic'` |
| `docsPath` | `string` | Path to the documentation directory. | (Internal default) |
| Option | Type | Description | Default |
|:---------------------------|:-----------------------------------------|:----------------------------------------------------------------------|:-------------------|
| `toolModules` | `ToolModule \| ToolModule[]` | Array of tool modules or paths to external tool plugins to be loaded. | `[]` |
| `isHttp` | `boolean` | Enable HTTP transport mode. | `false` |
| `http.port` | `number` | Port for HTTP transport. | `8080` |
| `http.host` | `string` | Host to bind to. | `127.0.0.1` |
| `http.allowedOrigins` | `string[]` | List of allowed CORS origins. | `[]` |
| `http.allowedHosts` | `string[]` | List of allowed host headers. | `[]` |
| `pluginIsolation` | `'none' \| 'strict'` | Isolation preset for external tools-as-plugins. | `'strict'` |
| `logging.level` | `'debug' \| 'info' \| 'warn' \| 'error'` | Set the logging level. | `'info'` |
| `logging.stderr` | `boolean` | Enable terminal logging to stderr. | `false` |
| `logging.protocol` | `boolean` | Forward logs to MCP clients. | `false` |
| `mode` | `'cli' \| 'programmatic' \| 'test'` | Specifies the operation mode. | `'programmatic'` |
| `modeOptions.test.baseUrl` | `string` | Base URL for fixture/mock servers in `test` mode. | `undefined` |
| `docsPath` | `string` | Path to the documentation directory. | (Internal default) |

#### Example usage

Expand All @@ -116,6 +124,20 @@ const options: PfMcpOptions = {
const server: PfMcpInstance = await start(options);
```

**Example: Programmatic test mode**
```typescript
import { start, type PfMcpInstance } from '@patternfly/patternfly-mcp';

const server: PfMcpInstance = await start({
mode: 'test',
modeOptions: {
test: {
baseUrl: 'http://my-fixture-server:3000'
}
}
});
```

### Server instance

The server instance exposes the following methods:
Expand Down
20 changes: 20 additions & 0 deletions src/__tests__/__snapshots__/options.defaults.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ exports[`options defaults should return specific properties: defaults 1`] = `
"contextPath": "/",
"contextUrl": "file:///",
"docsPath": "/documentation",
"docsPathSlug": "documentation:",
"http": {
"allowedHosts": [],
"allowedOrigins": [],
Expand All @@ -21,8 +22,27 @@ exports[`options defaults should return specific properties: defaults 1`] = `
},
"maxDocsToLoad": 500,
"maxSearchLength": 256,
"mode": "programmatic",
"modeOptions": {
"cli": {},
"programmatic": {},
"test": {},
},
"name": "@patternfly/patternfly-mcp",
"nodeVersion": 22,
"patternflyOptions": {
"availableResourceVersions": [
"v6",
],
"default": {
"defaultVersion": "v6",
"versionStrategy": "highest",
"versionWhitelist": [
"@patternfly/react-core",
"@patternfly/patternfly",
],
},
},
"pfExternal": "https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content",
"pfExternalAccessibility": "https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility",
"pfExternalChartsDesign": "https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/charts",
Expand Down
60 changes: 60 additions & 0 deletions src/__tests__/__snapshots__/options.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ exports[`parseCliOptions should attempt to parse args with --allowed-hosts 1`] =
"stderr": false,
"transport": "stdio",
},
"modeOptions": {
"cli": {},
"programmatic": {},
"test": {},
},
"pluginIsolation": undefined,
"toolModules": [],
}
Expand All @@ -37,6 +42,11 @@ exports[`parseCliOptions should attempt to parse args with --allowed-origins 1`]
"stderr": false,
"transport": "stdio",
},
"modeOptions": {
"cli": {},
"programmatic": {},
"test": {},
},
"pluginIsolation": undefined,
"toolModules": [],
}
Expand All @@ -55,6 +65,11 @@ exports[`parseCliOptions should attempt to parse args with --http and --host 1`]
"stderr": false,
"transport": "stdio",
},
"modeOptions": {
"cli": {},
"programmatic": {},
"test": {},
},
"pluginIsolation": undefined,
"toolModules": [],
}
Expand All @@ -73,6 +88,11 @@ exports[`parseCliOptions should attempt to parse args with --http and --port 1`]
"stderr": false,
"transport": "stdio",
},
"modeOptions": {
"cli": {},
"programmatic": {},
"test": {},
},
"pluginIsolation": undefined,
"toolModules": [],
}
Expand All @@ -91,6 +111,11 @@ exports[`parseCliOptions should attempt to parse args with --http and invalid --
"stderr": false,
"transport": "stdio",
},
"modeOptions": {
"cli": {},
"programmatic": {},
"test": {},
},
"pluginIsolation": undefined,
"toolModules": [],
}
Expand All @@ -107,6 +132,11 @@ exports[`parseCliOptions should attempt to parse args with --http flag 1`] = `
"stderr": false,
"transport": "stdio",
},
"modeOptions": {
"cli": {},
"programmatic": {},
"test": {},
},
"pluginIsolation": undefined,
"toolModules": [],
}
Expand All @@ -123,6 +153,11 @@ exports[`parseCliOptions should attempt to parse args with --log-level flag 1`]
"stderr": false,
"transport": "stdio",
},
"modeOptions": {
"cli": {},
"programmatic": {},
"test": {},
},
"pluginIsolation": undefined,
"toolModules": [],
}
Expand All @@ -139,6 +174,11 @@ exports[`parseCliOptions should attempt to parse args with --log-stderr flag and
"stderr": true,
"transport": "stdio",
},
"modeOptions": {
"cli": {},
"programmatic": {},
"test": {},
},
"pluginIsolation": undefined,
"toolModules": [],
}
Expand All @@ -155,6 +195,11 @@ exports[`parseCliOptions should attempt to parse args with --tool 1`] = `
"stderr": false,
"transport": "stdio",
},
"modeOptions": {
"cli": {},
"programmatic": {},
"test": {},
},
"pluginIsolation": undefined,
"toolModules": [
"my-tool",
Expand All @@ -174,6 +219,11 @@ exports[`parseCliOptions should attempt to parse args with --verbose flag 1`] =
"stderr": false,
"transport": "stdio",
},
"modeOptions": {
"cli": {},
"programmatic": {},
"test": {},
},
"pluginIsolation": undefined,
"toolModules": [],
}
Expand All @@ -190,6 +240,11 @@ exports[`parseCliOptions should attempt to parse args with --verbose flag and --
"stderr": false,
"transport": "stdio",
},
"modeOptions": {
"cli": {},
"programmatic": {},
"test": {},
},
"pluginIsolation": undefined,
"toolModules": [],
}
Expand All @@ -206,6 +261,11 @@ exports[`parseCliOptions should attempt to parse args with other arguments 1`] =
"stderr": false,
"transport": "stdio",
},
"modeOptions": {
"cli": {},
"programmatic": {},
"test": {},
},
"pluginIsolation": undefined,
"toolModules": [],
}
Expand Down
6 changes: 5 additions & 1 deletion src/__tests__/__snapshots__/server.getResources.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ exports[`promiseQueue should execute promises in order: allSettled 1`] = `
]
`;

exports[`resolveLocalPathFunction should return a consistent path, basic 1`] = `"/lorem-ipsum.md"`;
exports[`resolveLocalPathFunction should return a consistent path, basic 1`] = `"/app/project/lorem-ipsum.md"`;

exports[`resolveLocalPathFunction should return a consistent path, documentation slug 1`] = `"/documentation/guidelines/README.md"`;

exports[`resolveLocalPathFunction should return a consistent path, relative path with valid navigation 1`] = `"/app/project/file.md"`;

exports[`resolveLocalPathFunction should return a consistent path, url, file 1`] = `"file://someDirectory/dolor-sit.md"`;

Expand Down
12 changes: 6 additions & 6 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe('main', () => {

mockRunServer.mockRejectedValue(error);

await main();
await expect(async () => main()).rejects.toThrow(error.message);

expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to start server:', error);
expect(processExitSpy).toHaveBeenCalledWith(1);
Expand All @@ -89,33 +89,33 @@ describe('main', () => {
{
description: 'parseCliOptions',
error: new Error('Failed to parse CLI options'),
message: 'Failed to start server:',
message: 'Set options error, failed to start server:',
method: main
},
{
description: 'setOptions',
error: new Error('Failed to set options'),
message: 'Failed to start server:',
message: 'Set options error, failed to start server:',
method: main
},
{
description: 'parseCliOptions, with start alias',
error: new Error('Failed to parse CLI options'),
message: 'Failed to start server:',
message: 'Set options error, failed to start server:',
method: start
},
{
description: 'setOptions, with start alias',
error: new Error('Failed to set options'),
message: 'Failed to start server:',
message: 'Set options error, failed to start server:',
method: start
}
])('should handle errors, $description', async ({ error, message, method }) => {
mockSetOptions.mockImplementation(() => {
throw error;
});

await method();
await expect(async () => method()).rejects.toThrow(error.message);

expect(consoleErrorSpy).toHaveBeenCalledWith(message, error);
expect(processExitSpy).toHaveBeenCalledWith(1);
Expand Down
Loading
Loading