Skip to content

Commit 68222cb

Browse files
authored
feat: expose server-instance stop, status (#9)
1 parent 1913d18 commit 68222cb

File tree

6 files changed

+237
-31
lines changed

6 files changed

+237
-31
lines changed

README.md

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ The Model Context Protocol (MCP) is an open standard that enables AI assistants
1717

1818
## Prerequisites
1919

20-
- Node.js 18.0.0 or higher
20+
- Node.js 20.0.0 or higher
2121
- npm (or another Node package manager)
2222

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

7575
## Usage
7676

77-
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:
77+
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:
7878
- An external URL (e.g., a raw GitHub URL to a .md file), or
7979
- 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.
8080

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

119119
## MCP client configuration examples
120120

121-
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.
121+
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.
122122

123123
### Minimal client config (npx)
124124

@@ -197,30 +197,71 @@ npx @modelcontextprotocol/inspector-cli \
197197
## Environment variables
198198

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

202202
## Programmatic usage (advanced)
203203

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

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

209209
// Use with default options (equivalent to CLI without flags)
210-
await start();
210+
const server = await start();
211211

212212
// Override CLI options programmatically
213-
await start({ docsHost: true });
213+
const serverWithOptions = await start({ docsHost: true });
214214

215215
// Multiple options can be overridden
216-
await start({
216+
const customServer = await start({
217217
docsHost: true,
218218
// Future CLI options can be added here
219219
});
220220

221221
// TypeScript users can use the CliOptions type for type safety
222222
const options: Partial<CliOptions> = { docsHost: true };
223-
await start(options);
223+
const typedServer = await start(options);
224+
225+
// Server instance provides shutdown control
226+
console.log('Server running:', server.isRunning()); // true
227+
228+
// Graceful shutdown
229+
await server.stop();
230+
console.log('Server running:', server.isRunning()); // false
231+
```
232+
233+
### ServerInstance Interface
234+
235+
The `start()` function returns a `ServerInstance` object with the following methods:
236+
237+
```typescript
238+
interface ServerInstance {
239+
/**
240+
* Stop the server gracefully
241+
*/
242+
stop(): Promise<void>;
243+
244+
/**
245+
* Check if server is running
246+
*/
247+
isRunning(): boolean;
248+
}
249+
```
250+
251+
**Usage Examples**:
252+
```typescript
253+
const server = await start();
254+
255+
// Check if server is running
256+
if (server.isRunning()) {
257+
console.log('Server is active');
258+
}
259+
260+
// Graceful shutdown
261+
await server.stop();
262+
263+
// Verify shutdown
264+
console.log('Server running:', server.isRunning()); // false
224265
```
225266

226267
## Returned content details

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

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,69 @@ exports[`runServer should attempt to run server, create transport, connect, and
2121
},
2222
],
2323
],
24+
"process": [
25+
[
26+
"SIGINT",
27+
[Function],
28+
],
29+
],
30+
"registerTool": [],
31+
}
32+
`;
33+
34+
exports[`runServer should attempt to run server, disable SIGINT handler: console 1`] = `
35+
{
36+
"info": [],
37+
"log": [
38+
[
39+
"PatternFly MCP server running on stdio",
40+
],
41+
],
42+
"mcpServer": [
43+
[
44+
{
45+
"name": "@patternfly/patternfly-mcp",
46+
"version": "0.0.0",
47+
},
48+
{
49+
"capabilities": {
50+
"tools": {},
51+
},
52+
},
53+
],
54+
],
55+
"process": [],
56+
"registerTool": [],
57+
}
58+
`;
59+
60+
exports[`runServer should attempt to run server, enable SIGINT handler explicitly: console 1`] = `
61+
{
62+
"info": [],
63+
"log": [
64+
[
65+
"PatternFly MCP server running on stdio",
66+
],
67+
],
68+
"mcpServer": [
69+
[
70+
{
71+
"name": "@patternfly/patternfly-mcp",
72+
"version": "0.0.0",
73+
},
74+
{
75+
"capabilities": {
76+
"tools": {},
77+
},
78+
},
79+
],
80+
],
81+
"process": [
82+
[
83+
"SIGINT",
84+
[Function],
85+
],
86+
],
2487
"registerTool": [],
2588
}
2689
`;
@@ -50,6 +113,12 @@ exports[`runServer should attempt to run server, register a tool: console 1`] =
50113
},
51114
],
52115
],
116+
"process": [
117+
[
118+
"SIGINT",
119+
[Function],
120+
],
121+
],
53122
"registerTool": [
54123
[
55124
"loremIpsum",
@@ -91,6 +160,12 @@ exports[`runServer should attempt to run server, register multiple tools: consol
91160
},
92161
],
93162
],
163+
"process": [
164+
[
165+
"SIGINT",
166+
[Function],
167+
],
168+
],
94169
"registerTool": [
95170
[
96171
"loremIpsum",
@@ -133,6 +208,12 @@ exports[`runServer should attempt to run server, use custom options: console 1`]
133208
},
134209
],
135210
],
211+
"process": [
212+
[
213+
"SIGINT",
214+
[Function],
215+
],
216+
],
136217
"registerTool": [],
137218
}
138219
`;
@@ -165,6 +246,12 @@ exports[`runServer should attempt to run server, use default tools: console 1`]
165246
},
166247
],
167248
],
249+
"process": [
250+
[
251+
"SIGINT",
252+
[Function],
253+
],
254+
],
168255
"registerTool": [
169256
[
170257
"usePatternFlyDocs",

src/__tests__/index.test.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ describe('main', () => {
2626
// Setup default mocks
2727
mockParseCliOptions.mockReturnValue({ docsHost: false });
2828
mockFreezeOptions.mockReturnValue({} as GlobalOptions);
29-
mockRunServer.mockResolvedValue(undefined);
29+
mockRunServer.mockResolvedValue({
30+
stop: jest.fn().mockResolvedValue(undefined),
31+
isRunning: jest.fn().mockReturnValue(true)
32+
});
3033
});
3134

3235
afterEach(() => {
@@ -105,6 +108,11 @@ describe('main', () => {
105108

106109
mockRunServer.mockImplementation(async () => {
107110
callOrder.push('run');
111+
112+
return {
113+
stop: jest.fn().mockResolvedValue(undefined),
114+
isRunning: jest.fn().mockReturnValue(true)
115+
};
108116
});
109117

110118
await main();
@@ -161,7 +169,10 @@ describe('start alias', () => {
161169
// Setup default mocks
162170
mockParseCliOptions.mockReturnValue({ docsHost: false });
163171
mockFreezeOptions.mockReturnValue({} as GlobalOptions);
164-
mockRunServer.mockResolvedValue(undefined);
172+
mockRunServer.mockResolvedValue({
173+
stop: jest.fn().mockResolvedValue(undefined),
174+
isRunning: jest.fn().mockReturnValue(true)
175+
});
165176
});
166177

167178
afterEach(() => {

src/__tests__/server.test.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ describe('runServer', () => {
1616
let consoleInfoSpy: jest.SpyInstance;
1717
let consoleLogSpy: jest.SpyInstance;
1818
let consoleErrorSpy: jest.SpyInstance;
19+
let processOnSpy: jest.SpyInstance;
1920

2021
beforeEach(() => {
2122
jest.clearAllMocks();
@@ -37,12 +38,16 @@ describe('runServer', () => {
3738
consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation();
3839
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
3940
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
41+
42+
// Spy on process.on method
43+
processOnSpy = jest.spyOn(process, 'on').mockImplementation();
4044
});
4145

4246
afterEach(() => {
4347
consoleInfoSpy.mockRestore();
4448
consoleLogSpy.mockRestore();
4549
consoleErrorSpy.mockRestore();
50+
processOnSpy.mockRestore();
4651
});
4752

4853
it.each([
@@ -90,16 +95,34 @@ describe('runServer', () => {
9095
jest.fn()
9196
])
9297
]
98+
},
99+
{
100+
description: 'disable SIGINT handler',
101+
options: undefined,
102+
tools: [],
103+
enableSigint: false
104+
},
105+
{
106+
description: 'enable SIGINT handler explicitly',
107+
options: undefined,
108+
tools: [],
109+
enableSigint: true
93110
}
94-
])('should attempt to run server, $description', async ({ options, tools }) => {
95-
await runServer(options as GlobalOptions, (tools && { tools }) || undefined);
111+
])('should attempt to run server, $description', async ({ options, tools, enableSigint }) => {
112+
const settings = {
113+
...(tools && { tools }),
114+
...(enableSigint !== undefined && { enableSigint })
115+
};
116+
117+
await runServer(options as GlobalOptions, Object.keys(settings).length > 0 ? settings : undefined);
96118

97119
expect(MockStdioServerTransport).toHaveBeenCalled();
98120
expect({
99121
info: consoleInfoSpy.mock.calls,
100122
registerTool: mockServer.registerTool.mock.calls,
101123
mcpServer: MockMcpServer.mock.calls,
102-
log: consoleLogSpy.mock.calls
124+
log: consoleLogSpy.mock.calls,
125+
process: processOnSpy.mock.calls
103126
}).toMatchSnapshot('console');
104127
});
105128

src/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
#!/usr/bin/env node
22

33
import { freezeOptions, parseCliOptions, type CliOptions } from './options';
4-
import { runServer } from './server';
4+
import { runServer, type ServerInstance } from './server';
55

66
/**
77
* Main function - CLI entry point with optional programmatic overrides
88
*
99
* @param programmaticOptions - Optional programmatic options that override CLI options
10+
* @returns {Promise<ServerInstance>} Server-instance with shutdown capability
1011
*/
11-
const main = async (programmaticOptions?: Partial<CliOptions>): Promise<void> => {
12+
const main = async (programmaticOptions?: Partial<CliOptions>): Promise<ServerInstance> => {
1213
try {
1314
// Parse CLI options
1415
const cliOptions = parseCliOptions();
@@ -19,8 +20,8 @@ const main = async (programmaticOptions?: Partial<CliOptions>): Promise<void> =>
1920
// Freeze options to prevent further changes
2021
freezeOptions(finalOptions);
2122

22-
// Create and run the server
23-
await runServer();
23+
// Create and return server-instance
24+
return await runServer();
2425
} catch (error) {
2526
console.error('Failed to start server:', error);
2627
process.exit(1);
@@ -35,4 +36,4 @@ if (process.env.NODE_ENV !== 'local') {
3536
});
3637
}
3738

38-
export { main, main as start, type CliOptions };
39+
export { main, main as start, type CliOptions, type ServerInstance };

0 commit comments

Comments
 (0)