#!/usr/bin/env node
/**
* PartnerCore Proxy CLI
*
* Command-line interface for starting and managing the proxy server.
*/
import { Command } from 'commander';
import * as fs from 'fs';
import * as path from 'path';
import { createRequire } from 'module';
import { loadConfig, validateConfig, findALWorkspace } from './config/loader.js';
import { createLogger, getLogger } from './utils/logger.js';
import { ALExtensionManager } from './al/extension-manager.js';
import { ALLanguageServer } from './al/language-server.js';
import { CloudRelayClient } from './cloud/relay-client.js';
import { ToolRouter } from './router/tool-router.js';
import { PartnerCoreMcpServer } from './mcp/server.js';
const program = new Command();
// Read version from package.json using createRequire for ESM compatibility
const require = createRequire(import.meta.url);
const packageJson = require('../package.json') as { version: string };
const PROXY_VERSION = packageJson.version;
program
.name('partnercore')
.description('MCP Server for Business Central AL Development')
.version(PROXY_VERSION);
interface StartOptions {
workspace?: string;
verbose?: boolean;
cloud?: boolean;
}
// Default action: start server (when run without subcommand)
program
.option('-w, --workspace <path>', 'AL workspace root (containing app.json)')
.option('-v, --verbose', 'Enable verbose logging')
.option('--no-cloud', 'Disable cloud connection (local tools only)')
.action((options: StartOptions) => {
void startServer(options);
});
// Explicit 'start' subcommand (for backwards compatibility)
program
.command('start')
.description('Start the MCP server')
.option('-w, --workspace <path>', 'AL workspace root (containing app.json)')
.option('-v, --verbose', 'Enable verbose logging')
.option('--no-cloud', 'Disable cloud connection (local tools only)')
.action((options: StartOptions) => {
void startServer(options);
});
program
.command('check')
.description('Check configuration and connections')
.action(() => {
void checkConfiguration();
});
program
.command('download-extension')
.description('Download/update the AL Language extension')
.action(() => {
void downloadExtension();
});
program.parse();
/**
* Start the proxy server
*/
async function startServer(options: StartOptions): Promise<void> {
// Initialize logger
const logLevel = options.verbose ? 'debug' : 'info';
createLogger(logLevel);
const logger = getLogger();
logger.info('PartnerCore Proxy starting...');
// Load configuration
const config = loadConfig();
// Override workspace if provided via CLI option
if (options.workspace) {
config.al.workspaceRoot = options.workspace;
} else if (!config.al.workspaceRoot || !fs.existsSync(path.join(config.al.workspaceRoot, 'app.json'))) {
// Auto-detect workspace if not set or invalid
const detected = findALWorkspace();
if (detected) {
config.al.workspaceRoot = detected;
logger.info(`Auto-detected AL workspace: ${detected}`);
} else {
logger.warn('No AL workspace detected. Workspace will be auto-detected when tools are used.');
}
}
// Validate configuration
const validation = validateConfig(config);
if (!validation.valid) {
for (const error of validation.errors) {
logger.error(`Configuration error: ${error}`);
}
logger.error('');
logger.error('PartnerCore requires API_KEY to function.');
logger.error('Set the API_KEY environment variable in your MCP configuration:');
logger.error('');
logger.error(' "env": {');
logger.error(' "API_KEY": "your-api-key"');
logger.error(' }');
logger.error('');
process.exit(1);
}
// Log warnings separately
for (const warning of validation.warnings) {
logger.warn(`Configuration warning: ${warning}`);
}
// Initialize AL extension
logger.info('Initializing AL extension...');
const extensionManager = new ALExtensionManager(config.al);
const extensionInfo = await extensionManager.getExtension();
logger.info(`Using AL extension: ${extensionInfo.version}`);
// Initialize AL Language Server (use detected workspace or fallback to cwd)
const workspaceRoot = config.al.workspaceRoot || process.cwd();
logger.info(`Using workspace: ${workspaceRoot}`);
logger.info('Starting AL Language Server...');
const alServer = new ALLanguageServer(extensionInfo, workspaceRoot);
await alServer.initialize();
// Initialize Cloud Client
logger.info('Connecting to PartnerCore Cloud...');
const cloudClient = new CloudRelayClient({
cloudUrl: config.cloudUrl,
apiKey: config.apiKey,
});
const connected = await cloudClient.checkConnection();
if (connected) {
const apiKeyValid = await cloudClient.validateApiKey();
if (apiKeyValid) {
logger.info('Cloud connected and API key validated');
} else {
logger.error('');
logger.error('API key is invalid. Please check your API_KEY.');
logger.error('');
process.exit(1);
}
} else {
logger.warn('Cloud connection unavailable - cloud features may not work');
}
// Initialize Router (workspace will be auto-detected when tools are used)
const router = new ToolRouter(config.al.workspaceRoot || undefined);
router.setALServer(alServer);
router.setCloudClient(cloudClient);
// Initialize MCP Server
const server = new PartnerCoreMcpServer(router);
// Handle shutdown
const shutdown = async (): Promise<void> => {
logger.info('Shutting down...');
await server.stop();
await alServer.shutdown();
process.exit(0);
};
process.on('SIGINT', () => void shutdown());
process.on('SIGTERM', () => void shutdown());
// Start server
await server.start();
}
/**
* Check configuration and connections
*/
async function checkConfiguration(): Promise<void> {
createLogger('info');
const logger = getLogger();
logger.info('Checking PartnerCore Proxy configuration...\n');
const config = loadConfig();
// Check configuration
console.log('Configuration:');
console.log(` Cloud URL: ${config.cloudUrl}`);
console.log(` API Key: ${config.apiKey ? '(set)' : '(not set)'}`);
console.log(` Workspace: ${config.al.workspaceRoot}`);
console.log(` Extension Version: ${config.al.extensionVersion}`);
console.log();
// Validate configuration
const validation = validateConfig(config);
console.log('Validation:');
if (validation.valid) {
console.log(' ✓ Configuration valid');
} else {
for (const error of validation.errors) {
console.log(` ✗ ${error}`);
}
}
console.log();
// Check AL extension
console.log('AL Extension:');
const extensionManager = new ALExtensionManager(config.al);
try {
const extensionInfo = await extensionManager.getExtension();
console.log(` ✓ Found: ${extensionInfo.version}`);
console.log(` Path: ${extensionInfo.path}`);
} catch (error) {
console.log(` ✗ Not found: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
console.log();
// Check cloud connection
console.log('Cloud Connection:');
if (config.apiKey) {
const cloudClient = new CloudRelayClient({
cloudUrl: config.cloudUrl,
apiKey: config.apiKey,
});
const connected = await cloudClient.checkConnection();
if (connected) {
const apiKeyValid = await cloudClient.validateApiKey();
console.log(' ✓ Connected');
console.log(` API Key: ${apiKeyValid ? 'valid' : 'INVALID'}`);
if (!apiKeyValid) {
console.log(' ✗ API key validation failed - server will not start');
}
} else {
console.log(' ✗ Not connected');
}
} else {
console.log(' ✗ API_KEY is required but not set');
console.log(' Set the API_KEY environment variable to start the server');
}
}
/**
* Download the AL extension
*/
async function downloadExtension(): Promise<void> {
createLogger('info');
const logger = getLogger();
logger.info('Downloading AL Language extension...');
const config = loadConfig();
const extensionManager = new ALExtensionManager(config.al);
try {
const extensionInfo = await extensionManager.getExtension();
logger.info(`Downloaded: ${extensionInfo.version}`);
logger.info(`Path: ${extensionInfo.path}`);
} catch (error) {
logger.error('Failed to download extension:', error);
process.exit(1);
}
}