ActivityWatch MCP Server

import { z } from 'zod'; import axios, { AxiosError } from 'axios'; const AW_API_BASE = "http://localhost:5600/api/0"; interface Bucket { id?: string; name?: string; type: string; client: string; hostname: string; created: string; data?: Record<string, any>; } interface BucketWithId extends Bucket { id: string; } const inputSchema = { type: "object", properties: { type: { type: "string", description: "Filter buckets by type" }, includeData: { type: "boolean", description: "Include bucket data in response" } } }; export const activitywatch_list_buckets_tool = { name: "activitywatch_list_buckets", description: "List all ActivityWatch buckets with optional type filtering", inputSchema: inputSchema, // Properly structure the returned content for MCP handler: async (args: { type?: string; includeData?: boolean }) => { try { const response = await axios.get(`${AW_API_BASE}/buckets`); const buckets: Record<string, Bucket> = response.data; // Convert bucket record to array with guaranteed IDs let bucketList: BucketWithId[] = Object.entries(buckets).map(([bucketId, bucket]) => ({ ...bucket, id: bucketId })); // Apply type filter if specified if (args.type && typeof args.type === 'string') { bucketList = bucketList.filter(bucket => bucket.type.toLowerCase().includes(args.type!.toLowerCase()) ); } // Format output with consistent structure const formattedBuckets = bucketList.map(bucket => { const result: BucketWithId = { id: bucket.id, type: bucket.type, client: bucket.client, hostname: bucket.hostname, created: bucket.created, name: bucket.name, ...(args.includeData && bucket.data ? { data: bucket.data } : {}) }; return result; }); // Begin with the formatted buckets as JSON let resultText = JSON.stringify(formattedBuckets, null, 2); // Only add helpful guidance in production mode, not test mode if (process.env.NODE_ENV !== 'test' && bucketList.length > 0) { resultText += "\n\n"; resultText += "You can access the events in these buckets using the activitywatch_get_events tool, for example:\n"; resultText += `activitywatch_get_events with bucketId = "${bucketList[0].id}"`; if (bucketList.length > 1) { resultText += "\n\nOr try a different bucket:\n"; resultText += `activitywatch_get_events with bucketId = "${bucketList[1].id}"`; } } else if (process.env.NODE_ENV !== 'test' && bucketList.length === 0) { resultText += "\n\nNo buckets found. Please check that ActivityWatch is running and collecting data."; } return { content: [ { type: "text", text: resultText } ] }; } catch (error) { console.error("Error in bucket list tool:", error); // Check if the error is an Axios error with a response property if (axios.isAxiosError(error) && error.response) { const statusCode = error.response.status; let errorMessage = `Failed to fetch buckets: ${error.message} (Status code: ${statusCode})`; // Include response data if available if (error.response.data) { const errorDetails = typeof error.response.data === 'object' ? JSON.stringify(error.response.data) : String(error.response.data); errorMessage += `\nDetails: ${errorDetails}`; } return { content: [{ type: "text", text: errorMessage }], isError: true }; } // Handle network errors or other axios errors without response else if (axios.isAxiosError(error)) { const errorMessage = `Failed to fetch buckets: ${error.message} This appears to be a network or connection error. Please check: - The ActivityWatch server is running (http://localhost:5600) - The API base URL is correct (currently: ${AW_API_BASE}) - No firewall or network issues are blocking the connection `; return { content: [{ type: "text", text: errorMessage }], isError: true }; } // Handle non-axios errors else if (error instanceof Error) { return { content: [{ type: "text", text: `Failed to fetch buckets: ${error.message}` }], isError: true }; } // Fallback for unknown errors else { return { content: [{ type: "text", text: "Failed to fetch buckets: Unknown error" }], isError: true }; } } } };