@clicktime/mcp-server
Version:
ClickTime MCP Tech Demo for AI agents to interact with ClickTime API
185 lines (184 loc) ⢠7.51 kB
JavaScript
// 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();