UNPKG

@clicktime/mcp-server

Version:

ClickTime MCP Tech Demo for AI agents to interact with ClickTime API

185 lines (184 loc) • 7.51 kB
#!/usr/bin/env node // src/cli.ts import { Command } from 'commander'; import dotenv from 'dotenv'; import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; import { ClickTimeServer } from './server.js'; import { VERSION } from './constants.js'; // Load environment variables dotenv.config(); const program = new Command(); // Setup functions function getClaudeConfigPath() { const homeDir = os.homedir(); switch (process.platform) { case 'darwin': // macOS return path.join(homeDir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'); case 'win32': // Windows return path.join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json'); default: throw new Error(`Unsupported platform: ${process.platform}. Only macOS and Windows are supported.`); } } async function setupClaudeDesktop(token) { console.log('šŸš€ Setting up ClickTime MCP server for Claude Desktop...\n'); // Get Claude config path const configPath = getClaudeConfigPath(); console.log(`šŸ“ Claude config location: ${configPath}`); // Read or create config let config = {}; try { const configContent = await fs.readFile(configPath, 'utf8'); config = JSON.parse(configContent); console.log('šŸ“– Found existing Claude config'); } catch (error) { if (error.code === 'ENOENT') { console.log('šŸ“ Creating new Claude config file...'); await fs.mkdir(path.dirname(configPath), { recursive: true }); } else { throw new Error(`Failed to read config: ${error.message}`); } } // Add ClickTime MCP server if (!config.mcpServers) { config.mcpServers = {}; } // Security improvement: Use environment variable instead of CLI arg config.mcpServers.clicktime = { command: 'npx', args: ['@clicktime/mcp-server'], env: { CLICKTIME_API_TOKEN: token, }, }; // Write config await fs.writeFile(configPath, JSON.stringify(config, null, 2)); console.log('\nāœ… ClickTime MCP server configured successfully!'); console.log('šŸ”„ Please restart Claude Desktop to apply changes.'); console.log('\nšŸ’” You can now ask Claude things like:'); console.log(' • "Log 8 hours to project ABC-123 for Development work today"'); console.log(' • "Show me my time entries for this week"'); console.log(' • "Add a $25.50 lunch expense for today"'); } program .name('clicktime-mcp') .description('ClickTime MCP server for AI agents') .version(VERSION); // Add setup:claude command program .command('setup:claude [token]') .description('Setup Claude Desktop configuration automatically') .action(async (token) => { try { // Get token from argument or environment const apiToken = token || process.env.CLICKTIME_API_TOKEN; if (!apiToken) { console.error('āŒ ClickTime API token is required!\n'); console.error('Usage:'); console.error(' npx @clicktime/mcp-server@latest setup:claude YOUR_API_TOKEN'); console.error(' or set CLICKTIME_API_TOKEN environment variable\n'); console.error('šŸ” To get your ClickTime API token:'); console.error(' 1. Log in to ClickTime'); console.error(' 2. Click your name → My Preferences'); console.error(' 3. Go to Authentication Token tab'); console.error(' 4. Copy your token'); process.exit(1); } await setupClaudeDesktop(apiToken); } catch (error) { console.error('\nāŒ Setup failed:', error instanceof Error ? error.message : error); if (error instanceof Error && error.message.includes('API token')) { console.error('\nšŸ” To get your ClickTime API token:'); console.error(' 1. Log in to ClickTime'); console.error(' 2. Click your name → My Preferences'); console.error(' 3. Go to Authentication Token tab'); console.error(' 4. Copy your token'); } process.exit(1); } }); // Default command for MCP server program .option('-t, --token <token>', 'ClickTime API token') .option('-u, --url <url>', 'ClickTime API base URL', 'https://api.clicktime.com/v2') .option('-r, --read-only', 'Enable read-only mode', false) .option('-p, --allowed-paths <paths...>', 'Additional allowed directories for file access (comma-separated)') .action(async (options) => { // Get API token from CLI argument or environment variable const apiToken = options.token || process.env.CLICKTIME_API_TOKEN; if (!apiToken) { console.error('Error: ClickTime API token is required'); console.error('Provide it via:'); console.error(' --token <your_token>'); console.error(' or CLICKTIME_API_TOKEN environment variable'); console.error(''); console.error('To get your API token:'); console.error(' 1. Log in to your ClickTime account'); console.error(' 2. Go to Settings > API'); console.error(' 3. Generate or copy your API token'); process.exit(1); } // Validate API URL const baseUrl = options.url; if (!baseUrl.startsWith('http')) { console.error('Error: Invalid API URL. Must start with http:// or https://'); process.exit(1); } // Process allowed paths let allowedPaths; if (options.allowedPaths) { allowedPaths = Array.isArray(options.allowedPaths) ? options.allowedPaths : options.allowedPaths.split(',').map((p) => p.trim()); } // Create server configuration const config = { apiToken, baseUrl, readOnly: options.readOnly, }; // Print startup information to stderr (so it doesn't interfere with stdio protocol) console.error('=========================================='); console.error(` ClickTime MCP Server v${VERSION} `); console.error('=========================================='); console.error(`API URL: ${config.baseUrl}`); console.error(`Read-only mode: ${config.readOnly ? 'Enabled' : 'Disabled'}`); console.error('Token: [CONFIGURED]'); if (allowedPaths && allowedPaths.length > 0) { console.error(`Additional allowed paths: ${allowedPaths.join(', ')}`); } console.error(''); // Create and start server const server = new ClickTimeServer(config, allowedPaths); // Handle graceful shutdown process.on('SIGINT', async () => { console.error('\nReceived SIGINT, shutting down gracefully...'); await server.stop(); process.exit(0); }); process.on('SIGTERM', async () => { console.error('\nReceived SIGTERM, shutting down gracefully...'); await server.stop(); process.exit(0); }); // Handle uncaught errors process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection at:', promise, 'reason:', reason); process.exit(1); }); // Start the server server.start().catch((error) => { console.error('Failed to start server:', error); process.exit(1); }); }); program.parse();