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
14 changes: 14 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ export default [
'@typescript-eslint': tseslint.plugin
},
rules: {
'@typescript-eslint/consistent-type-imports': [
2,
{
prefer: 'type-imports',
fixStyle: 'inline-type-imports',
disallowTypeAnnotations: false
}
],
'@typescript-eslint/consistent-type-exports': [
2,
{
fixMixedExportsWithInlineTypeSpecifier: true
}
],
'@typescript-eslint/no-explicit-any': 1,
'@typescript-eslint/explicit-function-return-type': 0,
'@typescript-eslint/no-unused-vars': ['error', {
Expand Down
50 changes: 24 additions & 26 deletions src/__tests__/__snapshots__/options.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ exports[`parseCliOptions should attempt to parse args with --allowed-hosts 1`] =
"localhost",
"127.0.0.1",
],
"allowedOrigins": undefined,
"host": undefined,
"port": undefined,
},
"isHttp": true,
"logging": {
Expand All @@ -27,13 +24,10 @@ exports[`parseCliOptions should attempt to parse args with --allowed-origins 1`]
{
"docsHost": false,
"http": {
"allowedHosts": undefined,
"allowedOrigins": [
"https://app.com",
"https://admin.app.com",
],
"host": undefined,
"port": undefined,
},
"isHttp": true,
"logging": {
Expand All @@ -49,7 +43,7 @@ exports[`parseCliOptions should attempt to parse args with --allowed-origins 1`]
exports[`parseCliOptions should attempt to parse args with --docs-host flag 1`] = `
{
"docsHost": true,
"http": undefined,
"http": {},
"isHttp": false,
"logging": {
"level": "info",
Expand All @@ -65,10 +59,7 @@ exports[`parseCliOptions should attempt to parse args with --http and --host 1`]
{
"docsHost": false,
"http": {
"allowedHosts": undefined,
"allowedOrigins": undefined,
"host": "0.0.0.0",
"port": undefined,
},
"isHttp": true,
"logging": {
Expand All @@ -85,10 +76,7 @@ exports[`parseCliOptions should attempt to parse args with --http and --port 1`]
{
"docsHost": false,
"http": {
"allowedHosts": undefined,
"allowedOrigins": undefined,
"host": undefined,
"port": undefined,
"port": 6000,
},
"isHttp": true,
"logging": {
Expand All @@ -101,15 +89,25 @@ exports[`parseCliOptions should attempt to parse args with --http and --port 1`]
}
`;

exports[`parseCliOptions should attempt to parse args with --http flag 1`] = `
exports[`parseCliOptions should attempt to parse args with --http and invalid --port 1`] = `
{
"docsHost": false,
"http": {
"allowedHosts": undefined,
"allowedOrigins": undefined,
"host": undefined,
"port": undefined,
"http": {},
"isHttp": true,
"logging": {
"level": "info",
"logger": "@patternfly/patternfly-mcp",
"protocol": false,
"stderr": false,
"transport": "stdio",
},
}
`;

exports[`parseCliOptions should attempt to parse args with --http flag 1`] = `
{
"docsHost": false,
"http": {},
"isHttp": true,
"logging": {
"level": "info",
Expand All @@ -124,7 +122,7 @@ exports[`parseCliOptions should attempt to parse args with --http flag 1`] = `
exports[`parseCliOptions should attempt to parse args with --log-level flag 1`] = `
{
"docsHost": false,
"http": undefined,
"http": {},
"isHttp": false,
"logging": {
"level": "warn",
Expand All @@ -139,7 +137,7 @@ exports[`parseCliOptions should attempt to parse args with --log-level flag 1`]
exports[`parseCliOptions should attempt to parse args with --log-stderr flag and --log-protocol flag 1`] = `
{
"docsHost": false,
"http": undefined,
"http": {},
"isHttp": false,
"logging": {
"level": "info",
Expand All @@ -154,7 +152,7 @@ exports[`parseCliOptions should attempt to parse args with --log-stderr flag and
exports[`parseCliOptions should attempt to parse args with --verbose flag 1`] = `
{
"docsHost": false,
"http": undefined,
"http": {},
"isHttp": false,
"logging": {
"level": "debug",
Expand All @@ -169,7 +167,7 @@ exports[`parseCliOptions should attempt to parse args with --verbose flag 1`] =
exports[`parseCliOptions should attempt to parse args with --verbose flag and --log-level flag 1`] = `
{
"docsHost": false,
"http": undefined,
"http": {},
"isHttp": false,
"logging": {
"level": "debug",
Expand All @@ -184,7 +182,7 @@ exports[`parseCliOptions should attempt to parse args with --verbose flag and --
exports[`parseCliOptions should attempt to parse args with other arguments 1`] = `
{
"docsHost": false,
"http": undefined,
"http": {},
"isHttp": false,
"logging": {
"level": "info",
Expand All @@ -199,7 +197,7 @@ exports[`parseCliOptions should attempt to parse args with other arguments 1`] =
exports[`parseCliOptions should attempt to parse args without --docs-host flag 1`] = `
{
"docsHost": false,
"http": undefined,
"http": {},
"isHttp": false,
"logging": {
"level": "info",
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ describe('main', () => {
mockParseCliOptions.mockImplementation(() => {
callOrder.push('parse');

return { docsHost: false, logging: defaultLogging } as unknown as CliOptions;
return { docsHost: false, logging: defaultLogging } as CliOptions;
});

mockSetOptions.mockImplementation(options => {
callOrder.push('set');

return Object.freeze({ ...DEFAULT_OPTIONS, ...options }) as unknown as GlobalOptions;
return Object.freeze({ ...DEFAULT_OPTIONS, ...options }) as GlobalOptions;
});

mockGetSessionOptions.mockReturnValue({
Expand Down
18 changes: 17 additions & 1 deletion src/__tests__/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ describe('parseCliOptions', () => {
},
{
description: 'with --http and --port',
args: ['node', 'script.js', '--http', '--port', '8080']
args: ['node', 'script.js', '--http', '--port', '6000']
},
{
description: 'with --http and invalid --port',
args: ['node', 'script.js', '--http', '--port', '0']
},
{
description: 'with --http and --host',
Expand All @@ -63,4 +67,16 @@ describe('parseCliOptions', () => {

expect(result).toMatchSnapshot();
});

it('parses from a provided argv independent of process.argv', () => {
const customArgv = ['node', 'cli', '--http', '--port', '3101'];
const result = parseCliOptions(customArgv);
expect(result.http?.port).toBe(3101);
});

it('trims spaces in list flags', () => {
const argv = ['node', 'cli', '--http', '--allowed-hosts', ' localhost , 127.0.0.1 '];
const result = parseCliOptions(argv);
expect(result.http?.allowedHosts).toEqual(['localhost', '127.0.0.1']);
});
});
10 changes: 3 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { parseCliOptions, type CliOptions, type DefaultOptions } from './options';
import {
getSessionOptions,
setOptions,
runWithSession
} from './options.context';
import { parseCliOptions, type CliOptions, type DefaultOptionsOverrides } from './options';
import { getSessionOptions, setOptions, runWithSession } from './options.context';
import {
runServer,
type ServerInstance,
Expand All @@ -24,7 +20,7 @@
* - `'programmatic'`: Functionality is invoked programmatically. Allows process exits.
* - `'test'`: Functionality is being tested. Does NOT allow process exits.
*/
type PfMcpOptions = Partial<DefaultOptions> & {
type PfMcpOptions = DefaultOptionsOverrides & {
mode?: 'cli' | 'programmatic' | 'test';
};

Expand Down Expand Up @@ -54,7 +50,7 @@
const main = async (
pfMcpOptions: PfMcpOptions = {},
pfMcpSettings: PfMcpSettings = {}
): Promise<ServerInstance> => {

Check warning on line 53 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

Check warning on line 53 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 53 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
const { mode, ...options } = pfMcpOptions;
const { allowProcessExit } = pfMcpSettings;

Expand Down
26 changes: 13 additions & 13 deletions src/options.context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';
import { type Session, type GlobalOptions } from './options';
import { DEFAULT_OPTIONS, LOG_BASENAME, type LoggingSession, type DefaultOptions } from './options.defaults';
import { type AppSession, type GlobalOptions, type DefaultOptionsOverrides } from './options';
import { DEFAULT_OPTIONS, LOG_BASENAME, type LoggingSession } from './options.defaults';
import { mergeObjects, freezeObject, isPlainObject } from './server.helpers';

/**
Expand All @@ -10,14 +10,14 @@ import { mergeObjects, freezeObject, isPlainObject } from './server.helpers';
* The `sessionContext` allows sharing a common context without explicitly
* passing it as a parameter.
*/
const sessionContext = new AsyncLocalStorage<Session>();
const sessionContext = new AsyncLocalStorage<AppSession>();

/**
* Initialize and return session data.
*
* @returns {Session} Immutable session with a session ID and channel name.
* @returns {AppSession} Immutable session with a session ID and channel name.
*/
const initializeSession = (): Session => {
const initializeSession = (): AppSession => {
const sessionId = (process.env.NODE_ENV === 'local' && '1234d567-1ce9-123d-1413-a1234e56c789') || randomUUID();
const channelName = `${LOG_BASENAME}:${sessionId}`;

Expand All @@ -27,10 +27,10 @@ const initializeSession = (): Session => {
/**
* Set and return the current session options.
*
* @param {Session} [session]
* @returns {Session}
* @param {AppSession} [session]
* @returns {AppSession}
*/
const setSessionOptions = (session: Session = initializeSession()) => {
const setSessionOptions = (session: AppSession = initializeSession()) => {
sessionContext.enterWith(session);

return session;
Expand All @@ -39,10 +39,10 @@ const setSessionOptions = (session: Session = initializeSession()) => {
/**
* Get the current session options or set a new session with defaults.
*/
const getSessionOptions = (): Session => sessionContext.getStore() || setSessionOptions();
const getSessionOptions = (): AppSession => sessionContext.getStore() || setSessionOptions();

const runWithSession = async <TReturn>(
session: Session,
session: AppSession,
callback: () => TReturn | Promise<TReturn>
) => {
const frozen = freezeObject(structuredClone(session));
Expand All @@ -61,10 +61,10 @@ const optionsContext = new AsyncLocalStorage<GlobalOptions>();
/**
* Set and freeze cloned options in the current async context.
*
* @param {Partial<DefaultOptions>} [options] - Optional options to set in context. Merged with DEFAULT_OPTIONS.
* @param {DefaultOptionsOverrides} [options] - Optional overrides merged with DEFAULT_OPTIONS.
* @returns {GlobalOptions} Cloned frozen default options object with session.
*/
const setOptions = (options?: Partial<DefaultOptions>): GlobalOptions => {
const setOptions = (options?: DefaultOptionsOverrides): GlobalOptions => {
const base = mergeObjects(DEFAULT_OPTIONS, options, { allowNullValues: false, allowUndefinedValues: false });
const baseLogging = isPlainObject(base.logging) ? base.logging : DEFAULT_OPTIONS.logging;
const merged: GlobalOptions = {
Expand Down Expand Up @@ -102,7 +102,7 @@ const getOptions = (): GlobalOptions => optionsContext.getStore() || setOptions(
/**
* Get logging options from the current context.
*
* @param {Session} [session] - Session options to use in context.
* @param {AppSession} [session] - Session options to use in context.
* @returns {LoggingSession} Logging options from context.
*/
const getLoggerOptions = (session = getSessionOptions()): LoggingSession => {
Expand Down
19 changes: 16 additions & 3 deletions src/options.defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ interface DefaultOptions<TLogOptions = LoggingOptions> {
contextPath: string;
docsHost: boolean;
docsPath: string;
http: HttpOptions | undefined;
http: HttpOptions;
isHttp: boolean;
llmsFilesPath: string;
logging: TLogOptions;
Expand All @@ -57,11 +57,22 @@ interface DefaultOptions<TLogOptions = LoggingOptions> {
version: string;
}

/**
* Overrides for default options.
*/
type DefaultOptionsOverrides = Partial<
Omit<DefaultOptions, 'http' | 'logging'>
> & {
http?: Partial<HttpOptions>;
logging?: Partial<LoggingOptions>;
};

/**
* Logging options.
*
* See `LOGGING_OPTIONS` for defaults.
*
* @interface LoggingOptions
* @default { level: 'debug', logger: packageJson.name, stderr: false, protocol: false, transport: 'stdio' }
*
* @property level Logging level.
* @property logger Logger name. Human-readable/configurable logger name used in MCP protocol messages. Isolated
Expand All @@ -82,8 +93,9 @@ interface LoggingOptions {
/**
* HTTP server options.
*
* See `HTTP_OPTIONS` for defaults.
*
* @interface HttpOptions
* @default { port: 8080, host: '127.0.0.1', allowedOrigins: [], allowedHosts: [] }
*
* @property port Port number.
* @property host Host name.
Expand Down Expand Up @@ -288,6 +300,7 @@ export {
LOG_BASENAME,
DEFAULT_OPTIONS,
type DefaultOptions,
type DefaultOptionsOverrides,
type HttpOptions,
type LoggingOptions,
type LoggingSession
Expand Down
Loading
Loading