diff --git a/README.md b/README.md index aca7f47b..f37780d9 100644 --- a/README.md +++ b/README.md @@ -201,20 +201,26 @@ npx @modelcontextprotocol/inspector-cli \ ## Programmatic usage (advanced) -The `runServer` function is exported via the package exports map. You can import it from the dedicated subpath or the package root. +The package provides programmatic access through the `start()` function (or `main()` as an alternative): -Example (ESM): +```typescript +import { start, main, type CliOptions } from '@patternfly/patternfly-mcp'; -```js -// Prefer the public export subpath -import { runServer } from '@patternfly/patternfly-mcp/server'; +// Use with default options (equivalent to CLI without flags) +await start(); -// Or from the package root (index.ts re-exports it) -import { runServer } from '@patternfly/patternfly-mcp'; +// Override CLI options programmatically +await start({ docsHost: true }); -// Starts the MCP server and listens on stdio -await runServer(); -// Server runs until interrupted (Ctrl+C) +// Multiple options can be overridden +await start({ + docsHost: true, + // Future CLI options can be added here +}); + +// TypeScript users can use the CliOptions type for type safety +const options: Partial = { docsHost: true }; +await start(options); ``` ## Returned content details diff --git a/package.json b/package.json index 941dd98d..f1628c8e 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,7 @@ "main": "dist/index.js", "type": "module", "exports": { - ".": "./dist/index.js", - "./server": "./dist/server.js" + ".": "./dist/index.js" }, "bin": { "patternfly-mcp": "dist/index.js", diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 0e89331a..05c06277 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -1,4 +1,4 @@ -import { main } from '../index'; +import { main, start, type CliOptions } from '../index'; import { parseCliOptions, freezeOptions, type GlobalOptions } from '../options'; import { runServer } from '../server'; @@ -111,5 +111,95 @@ describe('main', () => { expect(callOrder).toEqual(['parse', 'freeze', 'run']); }); + + it('should merge programmatic options with CLI options', async () => { + const cliOptions = { docsHost: false }; + const programmaticOptions = { docsHost: true }; + + mockParseCliOptions.mockReturnValue(cliOptions); + + await main(programmaticOptions); + + // Should merge CLI options with programmatic options (programmatic takes precedence) + expect(mockFreezeOptions).toHaveBeenCalledWith({ docsHost: true }); + }); + + it('should work with empty programmatic options', async () => { + const cliOptions = { docsHost: true }; + + mockParseCliOptions.mockReturnValue(cliOptions); + + await main({}); + + expect(mockFreezeOptions).toHaveBeenCalledWith({ docsHost: true }); + }); + + it('should work with undefined programmatic options', async () => { + const cliOptions = { docsHost: false }; + + mockParseCliOptions.mockReturnValue(cliOptions); + + await main(); + + expect(mockFreezeOptions).toHaveBeenCalledWith({ docsHost: false }); + }); +}); + +describe('start alias', () => { + let consoleErrorSpy: jest.SpyInstance; + let processExitSpy: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock process.exit to prevent actual exit + processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never); + + // Mock console.error + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + + // Setup default mocks + mockParseCliOptions.mockReturnValue({ docsHost: false }); + mockFreezeOptions.mockReturnValue({} as GlobalOptions); + mockRunServer.mockResolvedValue(undefined); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + processExitSpy.mockRestore(); + }); + + it('should be equivalent to main function', async () => { + const cliOptions = { docsHost: true }; + + mockParseCliOptions.mockReturnValue(cliOptions); + + await start(); + + expect(mockParseCliOptions).toHaveBeenCalled(); + expect(mockFreezeOptions).toHaveBeenCalledWith(cliOptions); + expect(mockRunServer).toHaveBeenCalled(); + }); + + it('should accept programmatic options like main', async () => { + const cliOptions = { docsHost: false }; + const programmaticOptions = { docsHost: true }; + + mockParseCliOptions.mockReturnValue(cliOptions); + + await start(programmaticOptions); + + expect(mockFreezeOptions).toHaveBeenCalledWith({ docsHost: true }); + }); +}); + +describe('type exports', () => { + it('should export CliOptions type', () => { + // This test ensures the type is properly exported + // TypeScript compilation will fail if the type is not available + const options: Partial = { docsHost: true }; + + expect(options).toBeDefined(); + }); }); diff --git a/src/index.ts b/src/index.ts index aff4401d..e75c303e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,23 @@ #!/usr/bin/env node -import { freezeOptions, parseCliOptions } from './options'; +import { freezeOptions, parseCliOptions, type CliOptions } from './options'; import { runServer } from './server'; /** - * Main function - CLI entry point + * Main function - CLI entry point with optional programmatic overrides + * + * @param programmaticOptions - Optional programmatic options that override CLI options */ -const main = async (): Promise => { +const main = async (programmaticOptions?: Partial): Promise => { try { - // Temporary parse for CLI options until we move to yargs or commander + // Parse CLI options const cliOptions = parseCliOptions(); + // Merge programmatic options with CLI options (programmatic takes precedence) + const finalOptions = { ...cliOptions, ...programmaticOptions }; + // Freeze options to prevent further changes - freezeOptions(cliOptions); + freezeOptions(finalOptions); // Create and run the server await runServer(); @@ -30,4 +35,4 @@ if (process.env.NODE_ENV !== 'local') { }); } -export { main, runServer }; +export { main, main as start, type CliOptions };