Skip to content

Commit 06fdcdc

Browse files
authored
refactor(options): mode options, e2e testing mocks (#98)
* index, refactor modes to account for cli vs programmatic adds * options, mode, modeOptions added with focus on test mode * server.getResources, patch path checks, mock redirects for testing * testing, unified mocks for e2e stdio and http transport using test mode
1 parent e7063bf commit 06fdcdc

19 files changed

+568
-319
lines changed

CONTRIBUTING.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ Unit tests are located in the `__tests__` directory.
127127

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

130+
Contributors can run the MCP server in a specialized `test` mode against mock resources.
131+
132+
```bash
133+
npm run test:integration
134+
```
135+
136+
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.
137+
130138
## AI agent
131139

132140
### User Section

docs/development.md

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,28 @@ Complete guide to using the PatternFly MCP Server for development including CLI
1212

1313
### Available options
1414

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

2931
#### Notes
3032
- **HTTP transport mode** - By default, the server uses `stdio`. Use the `--http` flag to enable HTTP transport.
3133
- **Logging** - The server uses a `diagnostics_channel`-based logger that keeps STDIO stdout pure by default.
3234
- **Programmatic API** - The server can also be used programmatically with options. See [Programmatic Usage](#programmatic-usage) for more details.
33-
- **Tool Plugins** - The server can load external tool plugins at startup. See [Tool Plugins](#tool-plugins) for more details.
35+
- **Tool Plugins** - The server can load external tool plugins at startup. See [Tool Plugins](#tool-plugins) for more details.
36+
- **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.
3437

3538
### Basic use scenarios
3639

@@ -53,6 +56,10 @@ npx @patternfly/patternfly-mcp --http --port 3000 --allowed-origins "https://app
5356
```bash
5457
npx @patternfly/patternfly-mcp --tool ./first-tool.js --tool ./second-tool.ts
5558
```
59+
**Testing with a fixture server**:
60+
```bash
61+
npx @patternfly/patternfly-mcp --mode test --mode-test-url "http://localhost:3000"
62+
```
5663

5764
### Testing with MCP Inspector
5865

@@ -81,20 +88,21 @@ npx @modelcontextprotocol/inspector-cli \
8188

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

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

99107
#### Example usage
100108

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

127+
**Example: Programmatic test mode**
128+
```typescript
129+
import { start, type PfMcpInstance } from '@patternfly/patternfly-mcp';
130+
131+
const server: PfMcpInstance = await start({
132+
mode: 'test',
133+
modeOptions: {
134+
test: {
135+
baseUrl: 'http://my-fixture-server:3000'
136+
}
137+
}
138+
});
139+
```
140+
119141
### Server instance
120142

121143
The server instance exposes the following methods:

src/__tests__/__snapshots__/options.defaults.test.ts.snap

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ exports[`options defaults should return specific properties: defaults 1`] = `
55
"contextPath": "/",
66
"contextUrl": "file:///",
77
"docsPath": "/documentation",
8+
"docsPathSlug": "documentation:",
89
"http": {
910
"allowedHosts": [],
1011
"allowedOrigins": [],
@@ -21,8 +22,27 @@ exports[`options defaults should return specific properties: defaults 1`] = `
2122
},
2223
"maxDocsToLoad": 500,
2324
"maxSearchLength": 256,
25+
"mode": "programmatic",
26+
"modeOptions": {
27+
"cli": {},
28+
"programmatic": {},
29+
"test": {},
30+
},
2431
"name": "@patternfly/patternfly-mcp",
2532
"nodeVersion": 22,
33+
"patternflyOptions": {
34+
"availableResourceVersions": [
35+
"v6",
36+
],
37+
"default": {
38+
"defaultVersion": "v6",
39+
"versionStrategy": "highest",
40+
"versionWhitelist": [
41+
"@patternfly/react-core",
42+
"@patternfly/patternfly",
43+
],
44+
},
45+
},
2646
"pfExternal": "https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content",
2747
"pfExternalAccessibility": "https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/accessibility",
2848
"pfExternalChartsDesign": "https://raw.githubusercontent.com/patternfly/patternfly-org/fb05713aba75998b5ecf5299ee3c1a259119bd74/packages/documentation-site/patternfly-docs/content/design-guidelines/charts",

src/__tests__/__snapshots__/options.test.ts.snap

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ exports[`parseCliOptions should attempt to parse args with --allowed-hosts 1`] =
1616
"stderr": false,
1717
"transport": "stdio",
1818
},
19+
"modeOptions": {
20+
"cli": {},
21+
"programmatic": {},
22+
"test": {},
23+
},
1924
"pluginIsolation": undefined,
2025
"toolModules": [],
2126
}
@@ -37,6 +42,11 @@ exports[`parseCliOptions should attempt to parse args with --allowed-origins 1`]
3742
"stderr": false,
3843
"transport": "stdio",
3944
},
45+
"modeOptions": {
46+
"cli": {},
47+
"programmatic": {},
48+
"test": {},
49+
},
4050
"pluginIsolation": undefined,
4151
"toolModules": [],
4252
}
@@ -55,6 +65,11 @@ exports[`parseCliOptions should attempt to parse args with --http and --host 1`]
5565
"stderr": false,
5666
"transport": "stdio",
5767
},
68+
"modeOptions": {
69+
"cli": {},
70+
"programmatic": {},
71+
"test": {},
72+
},
5873
"pluginIsolation": undefined,
5974
"toolModules": [],
6075
}
@@ -73,6 +88,11 @@ exports[`parseCliOptions should attempt to parse args with --http and --port 1`]
7388
"stderr": false,
7489
"transport": "stdio",
7590
},
91+
"modeOptions": {
92+
"cli": {},
93+
"programmatic": {},
94+
"test": {},
95+
},
7696
"pluginIsolation": undefined,
7797
"toolModules": [],
7898
}
@@ -91,6 +111,11 @@ exports[`parseCliOptions should attempt to parse args with --http and invalid --
91111
"stderr": false,
92112
"transport": "stdio",
93113
},
114+
"modeOptions": {
115+
"cli": {},
116+
"programmatic": {},
117+
"test": {},
118+
},
94119
"pluginIsolation": undefined,
95120
"toolModules": [],
96121
}
@@ -107,6 +132,11 @@ exports[`parseCliOptions should attempt to parse args with --http flag 1`] = `
107132
"stderr": false,
108133
"transport": "stdio",
109134
},
135+
"modeOptions": {
136+
"cli": {},
137+
"programmatic": {},
138+
"test": {},
139+
},
110140
"pluginIsolation": undefined,
111141
"toolModules": [],
112142
}
@@ -123,6 +153,11 @@ exports[`parseCliOptions should attempt to parse args with --log-level flag 1`]
123153
"stderr": false,
124154
"transport": "stdio",
125155
},
156+
"modeOptions": {
157+
"cli": {},
158+
"programmatic": {},
159+
"test": {},
160+
},
126161
"pluginIsolation": undefined,
127162
"toolModules": [],
128163
}
@@ -139,6 +174,11 @@ exports[`parseCliOptions should attempt to parse args with --log-stderr flag and
139174
"stderr": true,
140175
"transport": "stdio",
141176
},
177+
"modeOptions": {
178+
"cli": {},
179+
"programmatic": {},
180+
"test": {},
181+
},
142182
"pluginIsolation": undefined,
143183
"toolModules": [],
144184
}
@@ -155,6 +195,11 @@ exports[`parseCliOptions should attempt to parse args with --tool 1`] = `
155195
"stderr": false,
156196
"transport": "stdio",
157197
},
198+
"modeOptions": {
199+
"cli": {},
200+
"programmatic": {},
201+
"test": {},
202+
},
158203
"pluginIsolation": undefined,
159204
"toolModules": [
160205
"my-tool",
@@ -174,6 +219,11 @@ exports[`parseCliOptions should attempt to parse args with --verbose flag 1`] =
174219
"stderr": false,
175220
"transport": "stdio",
176221
},
222+
"modeOptions": {
223+
"cli": {},
224+
"programmatic": {},
225+
"test": {},
226+
},
177227
"pluginIsolation": undefined,
178228
"toolModules": [],
179229
}
@@ -190,6 +240,11 @@ exports[`parseCliOptions should attempt to parse args with --verbose flag and --
190240
"stderr": false,
191241
"transport": "stdio",
192242
},
243+
"modeOptions": {
244+
"cli": {},
245+
"programmatic": {},
246+
"test": {},
247+
},
193248
"pluginIsolation": undefined,
194249
"toolModules": [],
195250
}
@@ -206,6 +261,11 @@ exports[`parseCliOptions should attempt to parse args with other arguments 1`] =
206261
"stderr": false,
207262
"transport": "stdio",
208263
},
264+
"modeOptions": {
265+
"cli": {},
266+
"programmatic": {},
267+
"test": {},
268+
},
209269
"pluginIsolation": undefined,
210270
"toolModules": [],
211271
}

src/__tests__/__snapshots__/server.getResources.test.ts.snap

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ exports[`promiseQueue should execute promises in order: allSettled 1`] = `
9191
]
9292
`;
9393

94-
exports[`resolveLocalPathFunction should return a consistent path, basic 1`] = `"/lorem-ipsum.md"`;
94+
exports[`resolveLocalPathFunction should return a consistent path, basic 1`] = `"/app/project/lorem-ipsum.md"`;
95+
96+
exports[`resolveLocalPathFunction should return a consistent path, documentation slug 1`] = `"/documentation/guidelines/README.md"`;
97+
98+
exports[`resolveLocalPathFunction should return a consistent path, relative path with valid navigation 1`] = `"/app/project/file.md"`;
9599

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

src/__tests__/index.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ describe('main', () => {
7979

8080
mockRunServer.mockRejectedValue(error);
8181

82-
await main();
82+
await expect(async () => main()).rejects.toThrow(error.message);
8383

8484
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to start server:', error);
8585
expect(processExitSpy).toHaveBeenCalledWith(1);
@@ -89,33 +89,33 @@ describe('main', () => {
8989
{
9090
description: 'parseCliOptions',
9191
error: new Error('Failed to parse CLI options'),
92-
message: 'Failed to start server:',
92+
message: 'Set options error, failed to start server:',
9393
method: main
9494
},
9595
{
9696
description: 'setOptions',
9797
error: new Error('Failed to set options'),
98-
message: 'Failed to start server:',
98+
message: 'Set options error, failed to start server:',
9999
method: main
100100
},
101101
{
102102
description: 'parseCliOptions, with start alias',
103103
error: new Error('Failed to parse CLI options'),
104-
message: 'Failed to start server:',
104+
message: 'Set options error, failed to start server:',
105105
method: start
106106
},
107107
{
108108
description: 'setOptions, with start alias',
109109
error: new Error('Failed to set options'),
110-
message: 'Failed to start server:',
110+
message: 'Set options error, failed to start server:',
111111
method: start
112112
}
113113
])('should handle errors, $description', async ({ error, message, method }) => {
114114
mockSetOptions.mockImplementation(() => {
115115
throw error;
116116
});
117117

118-
await method();
118+
await expect(async () => method()).rejects.toThrow(error.message);
119119

120120
expect(consoleErrorSpy).toHaveBeenCalledWith(message, error);
121121
expect(processExitSpy).toHaveBeenCalledWith(1);

0 commit comments

Comments
 (0)