#!/usr/bin/env node
/**
* Clockify MCP Server
*
* An MCP server that provides tools for interacting with the Clockify time tracking API.
*
* Configuration (in order of priority):
* 1. CLI argument: --api-key=YOUR_KEY or --api-key YOUR_KEY
* 2. Environment variable: CLOCKIFY_API_KEY
* 3. .env file in current directory: CLOCKIFY_API_KEY=your-key
*
* Get your API key from: https://app.clockify.me/user/preferences#advanced
*
* Usage with Claude Code:
* {
* "mcpServers": {
* "clockify": {
* "command": "npx",
* "args": ["@yikizi/clockify-mcp", "--api-key", "your-api-key"]
* }
* }
* }
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { ClockifyClient } from './clockify-client.js';
import { toolDefinitions, createToolHandlers } from './tools.js';
// ═══════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════
function getApiKey(): string | undefined {
// 1. Check CLI arguments: --api-key=VALUE or --api-key VALUE
const args = process.argv.slice(2);
for (let i = 0; i < args.length; i++) {
if (args[i].startsWith('--api-key=')) {
return args[i].split('=')[1];
}
if (args[i] === '--api-key' && args[i + 1]) {
return args[i + 1];
}
}
// 2. Check environment variable
if (process.env.CLOCKIFY_API_KEY) {
return process.env.CLOCKIFY_API_KEY;
}
// 3. Check .env file in current directory
const envPath = join(process.cwd(), '.env');
if (existsSync(envPath)) {
try {
const envContent = readFileSync(envPath, 'utf-8');
const match = envContent.match(/^CLOCKIFY_API_KEY=(.+)$/m);
if (match) {
return match[1].trim().replace(/^["']|["']$/g, ''); // Remove quotes if present
}
} catch {
// Ignore .env read errors
}
}
return undefined;
}
const CLOCKIFY_API_KEY = getApiKey();
if (!CLOCKIFY_API_KEY) {
console.error('Error: Clockify API key is required.');
console.error('');
console.error('Provide it via one of:');
console.error(' 1. CLI argument: --api-key=YOUR_KEY');
console.error(' 2. Environment var: CLOCKIFY_API_KEY=YOUR_KEY');
console.error(' 3. .env file: CLOCKIFY_API_KEY=YOUR_KEY');
console.error('');
console.error('Get your API key from: https://app.clockify.me/user/preferences#advanced');
process.exit(1);
}
// ═══════════════════════════════════════════════════════════
// SERVER SETUP
// ═══════════════════════════════════════════════════════════
const server = new McpServer({
name: 'clockify-mcp',
version: '1.0.0',
});
const clockifyClient = new ClockifyClient({ apiKey: CLOCKIFY_API_KEY });
const handlers = createToolHandlers(clockifyClient);
// ═══════════════════════════════════════════════════════════
// TOOL REGISTRATION
// ═══════════════════════════════════════════════════════════
for (const tool of toolDefinitions) {
const handlerName = tool.name.replace(/_([a-z])/g, (_, c) => c.toUpperCase()) as keyof typeof handlers;
const handler = handlers[handlerName];
if (!handler) {
console.error(`Warning: No handler found for tool ${tool.name}`);
continue;
}
server.registerTool(
tool.name,
{
description: tool.description,
inputSchema: tool.schema,
},
async (args: Record<string, unknown>) => {
try {
return await (handler as (args: Record<string, unknown>) => Promise<{ content: Array<{ type: 'text'; text: string }> }>)(args);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: 'text' as const, text: `Error: ${message}` }],
isError: true,
};
}
}
);
}
// ═══════════════════════════════════════════════════════════
// SERVER START
// ═══════════════════════════════════════════════════════════
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Clockify MCP server running on stdio');
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});