Skip to main content
Glama
8bitgentleman

ActivityWatch MCP Server

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
NameRequiredDescriptionDefault
timeperiodsYesTime periods to query. Format: ['2024-10-28/2024-10-29'] where dates are in ISO format and joined with a slash
queryYesMUST BE A SINGLE STRING containing all query statements separated by semicolons. DO NOT split into multiple strings.
nameNoOptional name for the query (used for caching)

Implementation Reference

  • 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);
      }
    }
  • 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
      });
  • 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
        };
      }
    }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/8bitgentleman/activitywatch-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server