// src/lib.ts import { Command, OptionValues } from "commander"; import * as fs from "node:fs"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { getVersion } from "./utils.js"; import { parseNameValuePair, addParsedSourceToTarget, normalizeAndAddDomain, logConfigSummary, } from "./cli_lib.js"; import { logger } from "./logger.js"; const VERSION = getVersion(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export interface CliConfig { docSources: Record; allowedDomains: Set; openApiSpecs: Record; } // Source loading functions /** @internal */ export function loadDefaultSources( defaultsPath: string ): Record { const defaultSources: Record = {}; try { const defaultsContent = fs.readFileSync(defaultsPath, "utf-8"); const lines = defaultsContent.split("\n"); for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine.startsWith("-")) { const content = trimmedLine.substring(1).trim(); const parsed = parseNameValuePair(content); if (parsed) { addParsedSourceToTarget(parsed, defaultSources, "defaults"); } } } } catch (error) { logger.error( `Failed to load default sources: ${error instanceof Error ? error.message : error}` ); } return defaultSources; } // Process different source types /** @internal */ export function processSourceOptions( sources: Record, singleOptions: string[] | undefined, singleFlagName: string, multipleOption: string | undefined, multipleFlagName: string ): Record { // Process individual options if (singleOptions && singleOptions.length > 0) { for (const option of singleOptions) { const parsed = parseNameValuePair(option); if (parsed) { addParsedSourceToTarget(parsed, sources, singleFlagName); } } } // Process space-separated option if (multipleOption) { const items = multipleOption.split(/\s+/); for (const item of items) { if (!item.trim()) continue; const parsed = parseNameValuePair(item.trim()); if (parsed) { addParsedSourceToTarget(parsed, sources, multipleFlagName); } } } return sources; } /** @internal */ export function processDomainOptions( allowDomainOptions: string[] | undefined, allowDomainsOption: string | undefined, denyDomainOptions: string[] | undefined, denyDomainsOption: string | undefined, docSources: Record, openApiSpecs: Record ): Set { const allowedDomains = new Set(); const deniedDomains = new Set(); let userSpecifiedDomains = false; // Process individual --allow-domain options if (allowDomainOptions && allowDomainOptions.length > 0) { userSpecifiedDomains = true; allowDomainOptions.forEach((domain) => { normalizeAndAddDomain(domain, allowedDomains, "--allow-domain"); }); } // Process space-separated --allow-domains option if (allowDomainsOption) { userSpecifiedDomains = true; const domainsList = allowDomainsOption.split(/\s+/); domainsList.forEach((domain) => { normalizeAndAddDomain(domain, allowedDomains, "--allow-domains"); }); } // Always add explicitly allowed domains from CLI flags if (userSpecifiedDomains) { logger.info("Using explicitly allowed domains from CLI flags"); } // Add inferred domains from sources (always add these in addition to any explicitly allowed domains) inferDomainsFromSources(openApiSpecs, allowedDomains); inferDomainsFromSources(docSources, allowedDomains); // Process individual --deny-domain options if (denyDomainOptions && denyDomainOptions.length > 0) { userSpecifiedDomains = true; denyDomainOptions.forEach((domain) => { normalizeAndAddDomain(domain, deniedDomains, "--deny-domain"); }); } // Process space-separated --deny-domains option if (denyDomainsOption) { userSpecifiedDomains = true; const domainsList = denyDomainsOption.split(/\s+/); domainsList.forEach((domain) => { normalizeAndAddDomain(domain, deniedDomains, "--deny-domains"); }); } // Remove denied domains from allowed domains deniedDomains.forEach((domain) => { allowedDomains.delete(domain); }); return allowedDomains; } /** @internal */ export function inferDomainsFromSources( sources: Record, allowedDomains: Set ): void { Object.values(sources).forEach((url) => { try { if (url.startsWith("http:") || url.startsWith("https:")) { const parsedUrl = new URL(url); const hostname = parsedUrl.hostname.toLowerCase(); allowedDomains.add(hostname); } } catch (e) { logger.error( `Failed to parse source URL '${url}' for domain inference. Error: ${ e instanceof Error ? e.message : String(e) }. Skipping.` ); } }); // Warning messages if (allowedDomains.size === 0 && Object.keys(sources).length > 0) { logger.warn( "Warning: No remote URLs configured or parsed, and no explicit domains allowed. Fetching might be restricted to local files only." ); } else if (allowedDomains.size === 0) { logger.warn( "No domains could be inferred from sources. Only local file access will be allowed." ); } } // Main option processing functions /** @internal */ export function getDocSources( options: OptionValues, includeDefaults: boolean ): Record { let docSources: Record = {}; let deprecatedDocSources: Record = {}; // Load defaults if needed if (includeDefaults) { const defaultsPath = path.resolve(__dirname, "static/defaults.md"); docSources = loadDefaultSources(defaultsPath); } // Process URL options docSources = processSourceOptions( docSources, options.llmsTxtSource, "--llms-txt-source", options.llmsTxtSources, "--llms-txt-sources" ); if (options?.url?.length > 0 || options?.urls?.length > 0) { logger.warn( "Warning: The --url and --urls options are deprecated. Use --llms-txt-source and --llms-txt-sources instead." ); } deprecatedDocSources = processSourceOptions( deprecatedDocSources, options.url, "--url", options.urls, "--urls" ); // Merge deprecated sources into docSources Object.assign(docSources, deprecatedDocSources); if (Object.keys(docSources).length === 0) { logger.warn( "No documentation sources provided. Use --source to add sources." ); } return docSources; } /** @internal */ export function getOpenApiSpecs(options: OptionValues): Record { let openApiSpecs: Record = {}; openApiSpecs = processSourceOptions( openApiSpecs, options.openapiSpecSource, "--openapi-spec-source", options.openapiSpecSources, "--openapi-spec-sources" ); if (Object.keys(openApiSpecs).length === 0) { logger.warn( "No OpenAPI specs provided. Use --openapi to add OpenAPI specs." ); } return openApiSpecs; } // Main CLI parsing function export function parseCliArgs(): CliConfig { const program = new Command(); program .name("SushiMCP") .description( "Starts SushiMCP, a dev tools model context protocol server that serves context on a roll." ) .version(VERSION) .option( // DEPRECATED: Use --llms-txt-source instead "--url ", "Specify a single documentation source (repeatable)", (value, previous: string[] = []) => previous.concat(value), [] ) .option( // DEPRECATED: Use --llms-txt-sources instead "--urls ", "Specify a list of llms.txt sources as a single space-separated string (e.g., 'name1:url1 name2:url2')" ) .option( "--llms-txt-source ", "Specify a single documentation source (repeatable)", (value, previous: string[] = []) => previous.concat(value), [] ) .option( "--llms-txt-sources ", "Specify a list of llms.txt sources as a single space-separated string (e.g., 'name1:url1 name2:url2')" ) .option( "--openapi-spec-source ", "Specify a single OpenAPI spec source (repeatable)", (value, previous: string[] = []) => previous.concat(value), [] ) .option( "--openapi-spec-sources ", "Specify a list of OpenAPI spec sources as a single space-separated string (e.g., 'name1:url1 name2:url2')" ) .option( "--no-defaults", "Do NOT include default documentation sources from src/defaults.md" ) .option( "--allow-domain ", "Allow fetching from a specific domain (repeatable, use '*' for all)", (value, previous: string[] = []) => previous.concat(value), [] ) .option( "--allow-domains ", "Allow fetching from a list of domains as a single space-separated string (e.g., 'domain1 domain2')" ) .option( "--deny-domain ", "Deny fetching from a specific domain (repeatable, use '*' for all)", (value, previous: string[] = []) => previous.concat(value), [] ) .option( "--deny-domains ", "Deny fetching from a list of domains as a single space-separated string (e.g., 'domain1 domain2')" ); program.parse(process.argv); const options = program.opts(); // Process all options const docSources = getDocSources(options, options.defaults); const openApiSpecs = getOpenApiSpecs(options); const allowedDomains = processDomainOptions( options.allowDomain, options.allowDomains, options.denyDomain, options.denyDomains, docSources, openApiSpecs ); // Create the final config const config = { allowedDomains, docSources, openApiSpecs }; // Log summary if in appropriate mode logConfigSummary(config); return config; } // Copyright (C) 2025 Christopher White // SPDX-License-Identifier: AGPL-3.0-or-later