activitywatch_run_query
Query ActivityWatch time tracking data to analyze application usage, browsing history, and productivity patterns using custom time periods.
Instructions
Run a query in ActivityWatch's query language
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| timeperiods | Yes | Time periods to query. Format: ['2024-10-28/2024-10-29'] where dates are in ISO format and joined with a slash | |
| query | Yes | MUST BE A SINGLE STRING containing all query statements separated by semicolons. DO NOT split into multiple strings. | |
| name | No | Optional name for the query (used for caching) |
Implementation Reference
- src/query.ts:47-111 (handler)The handler function for the 'activitywatch_run_query' tool. It processes the input arguments (timeperiods and query), formats them appropriately, sends a POST request to the ActivityWatch query API endpoint, and returns the JSON response or handles errors.handler: async (args: { timeperiods: string[]; query: string[]; name?: string }) => { try { // Handle special test cases if we're in test mode if (process.env.NODE_ENV === 'test') { return handleTestCases(args); } // Process timeperiods to ensure correct format const formattedTimeperiods = []; // If we have exactly two timeperiods, combine them into a single time period string if (args.timeperiods.length === 2 && !args.timeperiods[0].includes('/') && !args.timeperiods[1].includes('/')) { formattedTimeperiods.push(`${args.timeperiods[0]}/${args.timeperiods[1]}`); } // Otherwise use the timeperiods as provided else { args.timeperiods.forEach(period => { if (period.includes('/')) { formattedTimeperiods.push(period); } else { formattedTimeperiods.push(period); } }); } // Format queries let queryString = args.query.join(' '); const formattedQueries = [queryString]; // Set up query data const queryData = { query: formattedQueries, timeperiods: formattedTimeperiods }; // Add an optional 'name' parameter to the URL if provided const urlParams = args.name ? `?name=${encodeURIComponent(args.name)}` : ''; try { // Make the request to the ActivityWatch query endpoint const response = await axios.post(`${AW_API_BASE}/query/${urlParams}`, queryData); // Create the response text with the query results const responseText = JSON.stringify(response.data, null, 2); return { content: [ { type: "text", text: responseText } ], isError: false }; } catch (error) { // Error handling return handleApiError(error); } } catch (error) { // General error handling return handleApiError(error); } }
- src/query.ts:9-41 (schema)The input schema for the 'activitywatch_run_query' tool, defining the expected parameters: timeperiods (array of date ranges), query (array with single query string), and optional name.const inputSchema = { type: "object", properties: { timeperiods: { type: "array", description: "Time periods to query. Format: ['2024-10-28/2024-10-29'] where dates are in ISO format and joined with a slash", items: { type: "string", pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}/[0-9]{4}-[0-9]{2}-[0-9]{2}$", description: "Time period in format 'start-date/end-date'" }, minItems: 1, maxItems: 10, examples: ["2024-10-28/2024-10-29"] }, query: { type: "array", description: "MUST BE A SINGLE STRING containing all query statements separated by semicolons. DO NOT split into multiple strings.", items: { type: "string", description: "Complete query with all statements in one string separated by semicolons" }, minItems: 1, maxItems: 1, examples: ["events = query_bucket('aw-watcher-window_UNI-qUxy6XHnLkk'); RETURN = events;"] }, name: { type: "string", description: "Optional name for the query (used for caching)" } }, required: ["timeperiods", "query"] };
- src/index.ts:54-58 (registration)Registration of the tool in the ListTools response handler, where it is listed among available tools with its name, description, and inputSchema.{ name: activitywatch_run_query_tool.name, description: activitywatch_run_query_tool.description, inputSchema: activitywatch_run_query_tool.inputSchema },
- src/index.ts:86-212 (registration)Dispatch logic in the CallTool request handler that matches the tool name and invokes its handler after argument validation and normalization.} else if (request.params.name === activitywatch_run_query_tool.name) { // For the query tool, we need to validate and normalize the args // First, log the raw arguments to debug format issues console.error(`\nRAW ARGS FROM MCP CLIENT:`); console.error(JSON.stringify(request.params.arguments, null, 2)); console.error(`\nTYPE: ${typeof request.params.arguments}`); console.error(`\nARRAY? ${Array.isArray(request.params.arguments)}`); // Make a mutable copy of the arguments let queryArgs = {...(request.params.arguments || {})}; // Try to see if this is JSON string that needs parsing if (typeof request.params.arguments === 'string') { try { const parsedArgs = JSON.parse(request.params.arguments); console.error(`Parsed string arguments into object:`); console.error(JSON.stringify(parsedArgs, null, 2)); queryArgs = parsedArgs; } catch (e) { console.error(`Failed to parse arguments string: ${e}`); } } // More diagnostic info if (queryArgs.query) { console.error(`Query type: ${typeof queryArgs.query}`); console.error(`Query array? ${Array.isArray(queryArgs.query)}`); console.error(`Query value: ${JSON.stringify(queryArgs.query, null, 2)}`); if (Array.isArray(queryArgs.query) && queryArgs.query.length > 0) { console.error(`First item type: ${typeof queryArgs.query[0]}`); console.error(`First item array? ${Array.isArray(queryArgs.query[0])}`); } } // Validate timeperiods if (!queryArgs.timeperiods) { return makeSafeToolResponse(() => ({ content: [{ type: "text", text: "Error: Missing required parameter 'timeperiods' (must be an array of date ranges)" }], isError: true }))(); } if (!Array.isArray(queryArgs.timeperiods)) { // Try to normalize a single string to an array if (typeof queryArgs.timeperiods === 'string') { queryArgs.timeperiods = [queryArgs.timeperiods]; console.error(`Normalized timeperiods from string to array: ${JSON.stringify(queryArgs.timeperiods)}`); } else { return makeSafeToolResponse(() => ({ content: [{ type: "text", text: "Error: 'timeperiods' must be an array of date ranges in format: ['2024-10-28/2024-10-29']" }], isError: true }))(); } } // Validate query if (!queryArgs.query) { return makeSafeToolResponse(() => ({ content: [{ type: "text", text: "Error: Missing required parameter 'query'" }], isError: true }))(); } // Handle different query formats if (!Array.isArray(queryArgs.query)) { // If it's a string, wrap it in an array if (typeof queryArgs.query === 'string') { queryArgs.query = [queryArgs.query]; console.error(`Normalized query from string to array: ${JSON.stringify(queryArgs.query)}`); } else { return makeSafeToolResponse(() => formatValidationError())(); } } // Check for double-wrapped array format (an issue with some MCP clients) if (Array.isArray(queryArgs.query) && queryArgs.query.length === 1 && Array.isArray(queryArgs.query[0])) { // Extract the inner array const innerArray = queryArgs.query[0]; console.error(`Detected double-wrapped query array from MCP client. Unwrapping...`); console.error(`Original: ${JSON.stringify(queryArgs.query)}`); if (Array.isArray(innerArray) && innerArray.length >= 1) { // If the inner array is itself an array, take its first element if (Array.isArray(innerArray[0])) { console.error(`Triple-nested array detected! Unwrapping multiple levels...`); queryArgs.query = innerArray[0] as unknown as string[]; } else { queryArgs.query = innerArray as unknown as string[]; } console.error(`Unwrapped: ${JSON.stringify(queryArgs.query)}`); } } // Special case: Check if we received an array of query lines that need to be combined if (Array.isArray(queryArgs.query) && queryArgs.query.length > 1) { // Check if they look like separate query statements const areQueryStatements = queryArgs.query.some(q => typeof q === 'string' && (q.includes('=') || q.trim().endsWith(';')) ); if (areQueryStatements) { // Join them into a single query string const combinedQuery = queryArgs.query.join(' '); queryArgs.query = [combinedQuery]; console.error(`Combined multiple query statements into a single string: ${combinedQuery}`); } } // Log the processed query console.error(`Processed query for execution: ${JSON.stringify({timeperiods: queryArgs.timeperiods, query: queryArgs.query})}`); return makeSafeToolResponse(activitywatch_run_query_tool.handler)({ timeperiods: queryArgs.timeperiods as string[], query: queryArgs.query as string[], name: typeof queryArgs.name === 'string' ? queryArgs.name : undefined });
- src/query.ts:187-227 (helper)Helper function to standardize error responses from API calls, formatting Axios errors and other exceptions into consistent tool response format.function handleApiError(error: any) { // 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 = `Query failed: ${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)) { return { content: [{ type: "text", text: `Query failed: ${error.message}` }], isError: true }; } // Handle non-axios errors else if (error instanceof Error) { return { content: [{ type: "text", text: `Query failed: ${error.message}` }], isError: true }; } // Fallback for unknown errors else { return { content: [{ type: "text", text: "Query failed: Unknown error" }], isError: true }; } }