server-manager.tsโข9.31 kB
/**
* Server Manager
* Manages server startup, shutdown, and testing
*/
import chalk from 'chalk';
import ora from 'ora';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { ConfigManager } from '../config/config-manager.js';
import { AuthService } from '../auth/auth-service.js';
import { GoogleDriveMCPServer } from '../mcp/index.js';
import { ServerConfig } from '../types/config.js';
import { serverConfigToMCPConfig } from '../config/config-adapter.js';
import { registerCleanupHandler, triggerShutdown } from '../utils/process-cleanup.js';
export interface StartOptions {
port?: number | undefined;
logLevel?: string | undefined;
useStdio?: boolean;
}
export interface TestOptions {
authOnly?: boolean;
driveOnly?: boolean;
}
export class ServerManager {
private configManager: ConfigManager;
private server: GoogleDriveMCPServer | null = null;
private isShuttingDown = false;
constructor(configPath?: string) {
this.configManager = new ConfigManager(configPath);
this.setupSignalHandlers();
}
/**
* Start the MCP server
*/
async start(options: StartOptions = {}): Promise<void> {
console.log(chalk.blue.bold('๐ Starting Google Drive MCP Server...\n'));
// Load and validate configuration
const spinner = ora('Loading configuration...').start();
let config: ServerConfig;
try {
config = await this.configManager.loadConfig();
// Apply command line overrides
if (options.port) {
config.server.port = options.port;
}
if (options.logLevel) {
config.server.logLevel = options.logLevel as any;
}
spinner.succeed('Configuration loaded');
} catch (error) {
spinner.fail('Failed to load configuration');
throw error;
}
// Initialize authentication
const authSpinner = ora('Initializing authentication...').start();
try {
const authService = new AuthService();
await authService.initialize();
const status = await authService.validateAuthentication();
if (!status.isValid) {
authSpinner.fail('Authentication failed');
console.log(chalk.red(`Error: ${status.error}`));
if (status.needsSetup) {
console.log(chalk.yellow('Run "google-drive-mcp auth setup" to configure authentication'));
} else if (status.needsReauth) {
console.log(chalk.yellow('Run "google-drive-mcp auth refresh" to refresh tokens'));
}
throw new Error('Authentication not configured');
}
authSpinner.succeed('Authentication validated');
} catch (error) {
authSpinner.fail('Authentication initialization failed');
throw error;
}
// Create and start server
const serverSpinner = ora('Starting MCP server...').start();
try {
const mcpConfig = serverConfigToMCPConfig(config);
this.server = new GoogleDriveMCPServer(mcpConfig);
await this.server.start();
// Register cleanup handler
registerCleanupHandler('mcp-server', async () => {
if (this.server) {
await this.server.stop();
this.server = null;
}
}, 10); // High priority
serverSpinner.succeed('MCP server started');
} catch (error) {
serverSpinner.fail('Failed to start MCP server');
throw error;
}
// Connect transport
const transportSpinner = ora('Connecting transport...').start();
try {
if (options.useStdio !== false) {
// Use stdio transport (default for MCP)
const transport = new StdioServerTransport();
await this.server.getServer().connect(transport);
transportSpinner.succeed('Connected to stdio transport');
console.log(chalk.green.bold('\nโ
Google Drive MCP Server is running!'));
console.log(chalk.blue('Server is ready to accept MCP requests via stdio'));
console.log(chalk.gray('Press Ctrl+C to stop the server\n'));
// Keep the process alive
await this.waitForShutdown();
} else {
// HTTP transport mode (for testing/debugging)
transportSpinner.succeed('Server ready for HTTP connections');
console.log(chalk.green.bold('\nโ
Google Drive MCP Server is running!'));
console.log(chalk.blue(`Server listening on port ${config.server.port}`));
console.log(chalk.gray('Press Ctrl+C to stop the server\n'));
// Keep the process alive
await this.waitForShutdown();
}
} catch (error) {
transportSpinner.fail('Failed to connect transport');
throw error;
}
}
/**
* Test server functionality
*/
async test(options: TestOptions = {}): Promise<void> {
console.log(chalk.blue.bold('๐งช Testing Google Drive MCP Server...\n'));
// Load configuration
const configSpinner = ora('Loading configuration...').start();
let config: ServerConfig;
try {
config = await this.configManager.loadConfig();
configSpinner.succeed('Configuration loaded');
} catch (error) {
configSpinner.fail('Failed to load configuration');
throw error;
}
// Test authentication
if (!options.driveOnly) {
const authSpinner = ora('Testing authentication...').start();
try {
const authService = new AuthService();
await authService.initialize();
const status = await authService.validateAuthentication();
if (status.isValid) {
authSpinner.succeed('Authentication test passed');
} else {
authSpinner.fail('Authentication test failed');
console.log(chalk.red(`Error: ${status.error}`));
if (options.authOnly) {
return;
}
}
} catch (error) {
authSpinner.fail('Authentication test failed');
console.log(chalk.red(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
if (options.authOnly) {
return;
}
}
}
// Test Google Drive API
if (!options.authOnly) {
const driveSpinner = ora('Testing Google Drive API...').start();
try {
// Create a temporary server instance for testing
const mcpConfig = serverConfigToMCPConfig(config);
const testServer = new GoogleDriveMCPServer(mcpConfig);
await testServer.start();
// Test basic Drive API functionality
// This would typically involve making a simple API call
driveSpinner.succeed('Google Drive API test passed');
// Clean up test server
await testServer.stop();
} catch (error) {
driveSpinner.fail('Google Drive API test failed');
console.log(chalk.red(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
}
}
// Test MCP protocol
if (!options.authOnly && !options.driveOnly) {
const mcpSpinner = ora('Testing MCP protocol...').start();
try {
const mcpConfig = serverConfigToMCPConfig(config);
const testServer = new GoogleDriveMCPServer(mcpConfig);
await testServer.start();
// Test MCP server initialization
const server = testServer.getServer();
if (server) {
mcpSpinner.succeed('MCP protocol test passed');
} else {
mcpSpinner.fail('MCP protocol test failed');
}
// Clean up test server
await testServer.stop();
} catch (error) {
mcpSpinner.fail('MCP protocol test failed');
console.log(chalk.red(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
}
}
console.log(chalk.green.bold('\nโ
Testing completed!'));
}
/**
* Graceful shutdown
*/
async shutdown(): Promise<void> {
if (this.isShuttingDown) {
return;
}
this.isShuttingDown = true;
console.log(chalk.yellow('\n๐ Shutting down server...'));
// Use the process cleanup system for consistent shutdown
await triggerShutdown('manual');
}
/**
* Setup signal handlers for graceful shutdown
*/
private setupSignalHandlers(): void {
// The ProcessCleanup utility will handle all signal registration
// We just need to register our cleanup handler
registerCleanupHandler('server-manager', async () => {
if (this.server) {
const spinner = ora('Cleaning up server resources...').start();
try {
await this.server.stop();
this.server = null;
spinner.succeed('Server resources cleaned up');
console.log(chalk.green('โ
Goodbye!'));
} catch (error) {
spinner.fail('Error during cleanup');
console.error(chalk.red('Cleanup error:'), error instanceof Error ? error.message : error);
}
}
}, 20); // Lower priority than the main server cleanup
}
/**
* Wait for shutdown signal
*/
private async waitForShutdown(): Promise<void> {
return new Promise((resolve) => {
const checkShutdown = () => {
if (this.isShuttingDown) {
resolve();
} else {
setTimeout(checkShutdown, 100);
}
};
checkShutdown();
});
}
}