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
61 changes: 51 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The Model Context Protocol (MCP) is an open standard that enables AI assistants

## Prerequisites

- Node.js 18.0.0 or higher
- Node.js 20.0.0 or higher
- npm (or another Node package manager)

## Installation
Expand Down Expand Up @@ -74,7 +74,7 @@ These are the most relevant NPM scripts from package.json:

## Usage

The MCP server communicates over stdio and provides access to PatternFly documentation through the following tools. Both tools accept an argument named urlList which must be an array of strings. Each string is either:
The MCP server communicates over stdio and provides access to PatternFly documentation through the following tools. Both tools accept an argument named `urlList` which must be an array of strings. Each string is either:
- An external URL (e.g., a raw GitHub URL to a .md file), or
- A local file path (e.g., documentation/.../README.md). When running with the --docs-host flag, these paths are resolved under the llms-files directory instead.

Expand Down Expand Up @@ -118,7 +118,7 @@ Then, passing a local path such as react-core/6.0.0/llms.txt in urlList will loa

## MCP client configuration examples

Most MCP clients use a JSON configuration that tells the client how to start this server. The server itself does not read that JSON; it only reads CLI flags and environment variables. Below are examples you can adapt to your MCP client.
Most MCP clients use a JSON configuration to specify how to start this server. The server itself only reads CLI flags and environment variables, not the JSON configuration. Below are examples you can adapt to your MCP client.

### Minimal client config (npx)

Expand Down Expand Up @@ -197,30 +197,71 @@ npx @modelcontextprotocol/inspector-cli \
## Environment variables

- DOC_MCP_FETCH_TIMEOUT_MS: Milliseconds to wait before aborting an HTTP fetch (default: 15000)
- DOC_MCP_CLEAR_COOLDOWN_MS: Default cooldown value used in internal cache configuration. The current public API does not expose a clearCache tool.
- DOC_MCP_CLEAR_COOLDOWN_MS: Default cooldown value used in internal cache configuration. The current public API does not expose a `clearCache` tool.

## Programmatic usage (advanced)

The package provides programmatic access through the `start()` function (or `main()` as an alternative):
The package provides programmatic access through the `start()` function:

```typescript
import { start, main, type CliOptions } from '@patternfly/patternfly-mcp';
import { start, main, type CliOptions, type ServerInstance } from '@patternfly/patternfly-mcp';

// Use with default options (equivalent to CLI without flags)
await start();
const server = await start();

// Override CLI options programmatically
await start({ docsHost: true });
const serverWithOptions = await start({ docsHost: true });

// Multiple options can be overridden
await start({
const customServer = await start({
docsHost: true,
// Future CLI options can be added here
});

// TypeScript users can use the CliOptions type for type safety
const options: Partial<CliOptions> = { docsHost: true };
await start(options);
const typedServer = await start(options);

// Server instance provides shutdown control
console.log('Server running:', server.isRunning()); // true

// Graceful shutdown
await server.stop();
console.log('Server running:', server.isRunning()); // false
```

### ServerInstance Interface

The `start()` function returns a `ServerInstance` object with the following methods:

```typescript
interface ServerInstance {
/**
* Stop the server gracefully
*/
stop(): Promise<void>;

/**
* Check if server is running
*/
isRunning(): boolean;
}
```

**Usage Examples**:
```typescript
const server = await start();

// Check if server is running
if (server.isRunning()) {
console.log('Server is active');
}

// Graceful shutdown
await server.stop();

// Verify shutdown
console.log('Server running:', server.isRunning()); // false
```

## Returned content details
Expand Down
87 changes: 87 additions & 0 deletions src/__tests__/__snapshots__/server.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,69 @@ exports[`runServer should attempt to run server, create transport, connect, and
},
],
],
"process": [
[
"SIGINT",
[Function],
],
],
"registerTool": [],
}
`;

exports[`runServer should attempt to run server, disable SIGINT handler: console 1`] = `
{
"info": [],
"log": [
[
"PatternFly MCP server running on stdio",
],
],
"mcpServer": [
[
{
"name": "@patternfly/patternfly-mcp",
"version": "0.0.0",
},
{
"capabilities": {
"tools": {},
},
},
],
],
"process": [],
"registerTool": [],
}
`;

exports[`runServer should attempt to run server, enable SIGINT handler explicitly: console 1`] = `
{
"info": [],
"log": [
[
"PatternFly MCP server running on stdio",
],
],
"mcpServer": [
[
{
"name": "@patternfly/patternfly-mcp",
"version": "0.0.0",
},
{
"capabilities": {
"tools": {},
},
},
],
],
"process": [
[
"SIGINT",
[Function],
],
],
"registerTool": [],
}
`;
Expand Down Expand Up @@ -50,6 +113,12 @@ exports[`runServer should attempt to run server, register a tool: console 1`] =
},
],
],
"process": [
[
"SIGINT",
[Function],
],
],
"registerTool": [
[
"loremIpsum",
Expand Down Expand Up @@ -91,6 +160,12 @@ exports[`runServer should attempt to run server, register multiple tools: consol
},
],
],
"process": [
[
"SIGINT",
[Function],
],
],
"registerTool": [
[
"loremIpsum",
Expand Down Expand Up @@ -133,6 +208,12 @@ exports[`runServer should attempt to run server, use custom options: console 1`]
},
],
],
"process": [
[
"SIGINT",
[Function],
],
],
"registerTool": [],
}
`;
Expand Down Expand Up @@ -165,6 +246,12 @@ exports[`runServer should attempt to run server, use default tools: console 1`]
},
],
],
"process": [
[
"SIGINT",
[Function],
],
],
"registerTool": [
[
"usePatternFlyDocs",
Expand Down
15 changes: 13 additions & 2 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ describe('main', () => {
// Setup default mocks
mockParseCliOptions.mockReturnValue({ docsHost: false });
mockFreezeOptions.mockReturnValue({} as GlobalOptions);
mockRunServer.mockResolvedValue(undefined);
mockRunServer.mockResolvedValue({
stop: jest.fn().mockResolvedValue(undefined),
isRunning: jest.fn().mockReturnValue(true)
});
});

afterEach(() => {
Expand Down Expand Up @@ -105,6 +108,11 @@ describe('main', () => {

mockRunServer.mockImplementation(async () => {
callOrder.push('run');

return {
stop: jest.fn().mockResolvedValue(undefined),
isRunning: jest.fn().mockReturnValue(true)
};
});

await main();
Expand Down Expand Up @@ -161,7 +169,10 @@ describe('start alias', () => {
// Setup default mocks
mockParseCliOptions.mockReturnValue({ docsHost: false });
mockFreezeOptions.mockReturnValue({} as GlobalOptions);
mockRunServer.mockResolvedValue(undefined);
mockRunServer.mockResolvedValue({
stop: jest.fn().mockResolvedValue(undefined),
isRunning: jest.fn().mockReturnValue(true)
});
});

afterEach(() => {
Expand Down
29 changes: 26 additions & 3 deletions src/__tests__/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
const MockStdioServerTransport = StdioServerTransport as jest.MockedClass<typeof StdioServerTransport>;

describe('runServer', () => {
let mockServer: any;

Check warning on line 14 in src/__tests__/server.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (22.x)

Unexpected any. Specify a different type

Check warning on line 14 in src/__tests__/server.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (20.x)

Unexpected any. Specify a different type

Check warning on line 14 in src/__tests__/server.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (24.x)

Unexpected any. Specify a different type
let mockTransport: any;

Check warning on line 15 in src/__tests__/server.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (22.x)

Unexpected any. Specify a different type

Check warning on line 15 in src/__tests__/server.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (20.x)

Unexpected any. Specify a different type

Check warning on line 15 in src/__tests__/server.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (24.x)

Unexpected any. Specify a different type
let consoleInfoSpy: jest.SpyInstance;
let consoleLogSpy: jest.SpyInstance;
let consoleErrorSpy: jest.SpyInstance;
let processOnSpy: jest.SpyInstance;

beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -37,12 +38,16 @@
consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation();
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();

// Spy on process.on method
processOnSpy = jest.spyOn(process, 'on').mockImplementation();
});

afterEach(() => {
consoleInfoSpy.mockRestore();
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
processOnSpy.mockRestore();
});

it.each([
Expand Down Expand Up @@ -90,16 +95,34 @@
jest.fn()
])
]
},
{
description: 'disable SIGINT handler',
options: undefined,
tools: [],
enableSigint: false
},
{
description: 'enable SIGINT handler explicitly',
options: undefined,
tools: [],
enableSigint: true
}
])('should attempt to run server, $description', async ({ options, tools }) => {
await runServer(options as GlobalOptions, (tools && { tools }) || undefined);
])('should attempt to run server, $description', async ({ options, tools, enableSigint }) => {
const settings = {
...(tools && { tools }),
...(enableSigint !== undefined && { enableSigint })
};

await runServer(options as GlobalOptions, Object.keys(settings).length > 0 ? settings : undefined);

expect(MockStdioServerTransport).toHaveBeenCalled();
expect({
info: consoleInfoSpy.mock.calls,
registerTool: mockServer.registerTool.mock.calls,
mcpServer: MockMcpServer.mock.calls,
log: consoleLogSpy.mock.calls
log: consoleLogSpy.mock.calls,
process: processOnSpy.mock.calls
}).toMatchSnapshot('console');
});

Expand Down
11 changes: 6 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
#!/usr/bin/env node

import { freezeOptions, parseCliOptions, type CliOptions } from './options';
import { runServer } from './server';
import { runServer, type ServerInstance } from './server';

/**
* Main function - CLI entry point with optional programmatic overrides
*
* @param programmaticOptions - Optional programmatic options that override CLI options
* @returns {Promise<ServerInstance>} Server-instance with shutdown capability
*/
const main = async (programmaticOptions?: Partial<CliOptions>): Promise<void> => {
const main = async (programmaticOptions?: Partial<CliOptions>): Promise<ServerInstance> => {

Check warning on line 12 in src/index.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (22.x)

Expected to return a value at the end of async arrow function

Check warning on line 12 in src/index.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (20.x)

Expected to return a value at the end of async arrow function

Check warning on line 12 in src/index.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (24.x)

Expected to return a value at the end of async arrow function
try {
// Parse CLI options
const cliOptions = parseCliOptions();
Expand All @@ -19,8 +20,8 @@
// Freeze options to prevent further changes
freezeOptions(finalOptions);

// Create and run the server
await runServer();
// Create and return server-instance
return await runServer();
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
Expand All @@ -35,4 +36,4 @@
});
}

export { main, main as start, type CliOptions };
export { main, main as start, type CliOptions, type ServerInstance };
Loading
Loading