Skip to main content
Glama
honeycombio
by honeycombio

run_query

Execute Honeycomb queries to analyze observability data with statistical calculations, filtering, and grouping for performance insights.

Instructions

Executes a Honeycomb query, returning results with statistical summaries.

CRITICAL RULE: For COUNT operations, NEVER include a "column" field in your calculation, even as null or undefined. Example: Use {"op": "COUNT"} NOT {"op": "COUNT", "column": "anything"}.

Additional Rules:

  1. All parameters must be at the TOP LEVEL (not nested inside a 'query' property)

  2. Field names must be exact - use 'op' (not 'operation'), 'breakdowns' (not 'group_by')

  3. Only use the exact operation names listed in the schema (e.g., use "P95" for 95th percentile, not "PERCENTILE")

  4. For all operations EXCEPT COUNT and CONCURRENCY, you must specify a "column" field

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
environmentYesThe Honeycomb environment to query
datasetYesThe dataset to query. Use __all__ to query across all datasets in the environment.
calculationsYes⚠️ CRITICAL RULE: For COUNT or CONCURRENCY operations, you MUST OMIT the 'column' field COMPLETELY - do not include it at all. For all other operations, the 'column' field is REQUIRED.
breakdownsNoMUST use field name 'breakdowns' (not 'group_by'). Columns to group results by.
filtersNoMUST use field name 'filters' (an array of filter objects). Pre-calculation filters for the query.
filter_combinationNoMUST use field name 'filter_combination' (not 'combine_filters'). How to combine filters: AND or OR. Default: AND.
ordersNoMUST use field name 'orders' (not 'sort' or 'order_by'). Array of sort configurations.
limitNoMUST use field name 'limit'. Maximum number of result rows to return.
time_rangeNoMUST use field name 'time_range' (with underscore). Relative time range in seconds from now.
start_timeNoMUST use field name 'start_time' (with underscore). Absolute start timestamp in seconds.
end_timeNoMUST use field name 'end_time' (with underscore). Absolute end timestamp in seconds.
granularityNoMUST use field name 'granularity'. Time resolution in seconds. 0 for auto.
havingsNoMUST use field name 'havings'. Post-calculation filters with same column rules as calculations.

Implementation Reference

  • Main handler for run_query tool: normalizes parameters, fixes common input errors, validates query, executes with retry logic using executeQuery helper, formats results, handles errors.
      handler: async (params: any) => {
        try {
          // Handle query object nesting - common mistake is to put params inside a 'query' property
          if (params.query && typeof params.query === 'object' && params.environment && params.dataset) {
            console.warn("Detected nested query object - pulling properties to top level");
            // Merge query properties into top level, but don't overwrite existing top-level properties
            for (const [key, value] of Object.entries(params.query)) {
              if (params[key] === undefined) {
                params[key] = value;
              }
            }
            
            // We've processed the query object, now delete it to avoid confusion
            delete params.query;
          }
          
          // Handle common field name mistakes
          if (params.group_by && !params.breakdowns) {
            params.breakdowns = params.group_by;
            delete params.group_by;
            console.warn("Detected 'group_by' field - renamed to 'breakdowns'");
          }
          
          // Handle order_by -> orders conversion
          if (params.order_by && !params.orders) {
            // Convert single order_by object to orders array
            if (!Array.isArray(params.order_by)) {
              params.orders = [params.order_by];
            } else {
              params.orders = params.order_by;
            }
            delete params.order_by;
            console.warn("Detected 'order_by' field - renamed to 'orders'");
          }
          
          // Handle having -> havings conversion
          if (params.having && !params.havings) {
            params.havings = params.having;
            delete params.having;
            console.warn("Detected 'having' field - renamed to 'havings'");
          }
          
          // Validate calculations array and field names
          if (params.calculations) {
            for (const calc of params.calculations) {
              // Handle operation -> op conversion if needed
              if (calc.operation && !calc.op) {
                calc.op = calc.operation;
                delete calc.operation;
                console.warn("Detected 'operation' field in calculation - renamed to 'op'");
              }
              
              // Handle field -> column conversion if needed
              if (calc.field && !calc.column) {
                calc.column = calc.field;
                delete calc.field;
                console.warn("Detected 'field' field in calculation - renamed to 'column'");
              }
              
              // We now rely on Zod schema refinements for validation of column rules
            }
          }
          
          // Validate parameters with our standard validation
          validateQuery(params);
          
          // Check if any calculations use HEATMAP
          const hasHeatmap = params.calculations.some((calc: any) => calc.op === "HEATMAP");
          
          // Execute the query with retry logic for transient API issues
          const maxRetries = 3;
          let lastError: unknown = null;
          
          for (let attempt = 1; attempt <= maxRetries; attempt++) {
            try {
              return await executeQuery(api, params, hasHeatmap);
            } catch (error) {
              lastError = error;
              console.error(`Query attempt ${attempt} failed: ${error instanceof Error ? error.message : String(error)}`);
              
              // Only retry if not the last attempt
              if (attempt < maxRetries) {
                console.error(`Retrying in ${attempt * 500}ms...`);
                await new Promise(resolve => setTimeout(resolve, attempt * 500));
              }
            }
          }
          
          // If we get here, all attempts failed
          throw lastError || new Error("All query attempts failed");
        } catch (error) {
          return handleToolError(error, "run_query", {
            environment: params.environment,
            dataset: params.dataset
          });
        }
      },
    };
  • Zod schema definition for run_query tool inputs with detailed descriptions, enums for ops, and superRefine validation to enforce rules like no 'column' for COUNT ops.
        schema: {
          environment: z.string().min(1).trim().describe("The Honeycomb environment to query"),
          dataset: z.string().min(1).trim().describe("The dataset to query. Use __all__ to query across all datasets in the environment."),
          calculations: z.array(z.object({
            op: z.enum([
              "COUNT",               
              "CONCURRENCY",         
              "SUM",                 
              "AVG",                 
              "COUNT_DISTINCT",      
              "MAX",                 
              "MIN",                 
              "P001",                
              "P01",                 
              "P05",                 
              "P10",                 
              "P20",                 
              "P25",                 
              "P50",                 
              "P75",                 
              "P80",                 
              "P90",                 
              "P95",                 
              "P99",                 
              "P999",                
              "RATE_AVG",            
              "RATE_SUM",            
              "RATE_MAX",            
              "HEATMAP",             
            ]).describe(`⚠️⚠️⚠️ CRITICAL RULES FOR OPERATIONS:
    
    1. FOR COUNT OPERATIONS:
       - NEVER include a "column" field
       - CORRECT: {"op": "COUNT"}
       - INCORRECT: {"op": "COUNT", "column": "anything"} 
       
    2. FOR PERCENTILES:
       - Use the exact P* operations (P95, P99, etc.)
       - CORRECT: {"op": "P95", "column": "duration_ms"}
       - INCORRECT: {"op": "PERCENTILE", "percentile": 95}
       
    3. ALL operations EXCEPT COUNT and CONCURRENCY REQUIRE a column field
    
    COMMON ERRORS TO AVOID:
    - DO NOT include "column" with COUNT or CONCURRENCY
    - DO NOT use "PERCENTILE" - use "P95", "P99", etc. instead
    - DO NOT misspell operation names`),
            column: z.string().min(1).trim().optional().describe("⚠️ CRITICAL: NEVER include this field when op is COUNT or CONCURRENCY. REQUIRED for all other operations."),
          }).superRefine((calculation, ctx) => {
            // Prevent column for COUNT or CONCURRENCY
            if ((calculation.op === "COUNT" || calculation.op === "CONCURRENCY") && calculation.column !== undefined) {
              ctx.addIssue({
                code: z.ZodIssueCode.custom,
                message: `ERROR: ${calculation.op} operations MUST NOT have a column field. Remove the "column" field entirely.`,
                path: ["column"]
              });
            }
            
            // Require column for all other operations
            if (!(calculation.op === "COUNT" || calculation.op === "CONCURRENCY") && calculation.column === undefined) {
              ctx.addIssue({
                code: z.ZodIssueCode.custom,
                message: `ERROR: ${calculation.op} operations REQUIRE a column field.`,
                path: ["column"]
              });
            }
          })).describe("⚠️ CRITICAL RULE: For COUNT or CONCURRENCY operations, you MUST OMIT the 'column' field COMPLETELY - do not include it at all. For all other operations, the 'column' field is REQUIRED."),
          breakdowns: z.array(z.string().min(1).trim()).optional().describe("MUST use field name 'breakdowns' (not 'group_by'). Columns to group results by."),
          filters: z.array(z.object({
            column: z.string().min(1).trim().describe("MUST use field name 'column'. Name of the column to filter on."),
            op: z.enum([
              "=", "!=", ">", ">=", "<", "<=", 
              "starts-with", "does-not-start-with", 
              "ends-with", "does-not-end-with",
              "exists", "does-not-exist", 
              "contains", "does-not-contain",
              "in", "not-in"
            ]).describe(`MUST use field name 'op'. Available operators:
    - Equality: "=", "!="
    - Comparison: ">", ">=", "<", "<="
    - String: "starts-with", "does-not-start-with", "ends-with", "does-not-end-with", "contains", "does-not-contain"
    - Existence: "exists", "does-not-exist"
    - Arrays: "in", "not-in" (use with array values)`),
            value: z.any().optional().describe("MUST use field name 'value'. Comparison value. Optional for exists operators. Use arrays for in/not-in.")
          })).optional().describe("MUST use field name 'filters' (an array of filter objects). Pre-calculation filters for the query."),
          filter_combination: z.enum(["AND", "OR"]).optional().describe("MUST use field name 'filter_combination' (not 'combine_filters'). How to combine filters: AND or OR. Default: AND."),
          orders: z.array(z.object({
            column: z.string().min(1).trim().describe("MUST use field name 'column'. Column to order by. Required when sorting by a column directly."),
            op: z.string().optional().describe("MUST use field name 'op' when provided. Operation to order by. Must match a calculation operation."),
            order: z.enum(["ascending", "descending"]).optional().describe("MUST use field name 'order' when provided. Available values: \"ascending\" (low to high) or \"descending\" (high to low).")
          })).optional().describe("MUST use field name 'orders' (not 'sort' or 'order_by'). Array of sort configurations."),
          limit: z.number().int().positive().optional().describe("MUST use field name 'limit'. Maximum number of result rows to return."),
          time_range: z.number().positive().optional().describe("MUST use field name 'time_range' (with underscore). Relative time range in seconds from now."),
          start_time: z.number().int().positive().optional().describe("MUST use field name 'start_time' (with underscore). Absolute start timestamp in seconds."),
          end_time: z.number().int().positive().optional().describe("MUST use field name 'end_time' (with underscore). Absolute end timestamp in seconds."),
          granularity: z.number().int().nonnegative().optional().describe("MUST use field name 'granularity'. Time resolution in seconds. 0 for auto."),
          havings: z.array(z.object({
            calculate_op: z.enum([
              "COUNT",               
              "CONCURRENCY",         
              "SUM",                 
              "AVG",                 
              "COUNT_DISTINCT",      
              "MAX",                 
              "MIN",                 
              "P001",                
              "P01",                 
              "P05",                 
              "P10",                 
              "P20",                 
              "P25",                 
              "P50",                 
              "P75",                 
              "P80",                 
              "P90",                 
              "P95",                 
              "P99",                 
              "P999",                
              "RATE_AVG",            
              "RATE_SUM",            
              "RATE_MAX"             
            ]).describe(`MUST use field name 'calculate_op'. Available operations:
    - NO COLUMN ALLOWED: COUNT, CONCURRENCY
    - REQUIRE COLUMN: SUM, AVG, COUNT_DISTINCT, MAX, MIN, P001, P01, P05, P10, P20, P25, P50, P75, P80, P90, P95, P99, P999, RATE_AVG, RATE_SUM, RATE_MAX`),
            column: z.string().min(1).trim().optional().describe("MUST use field name 'column'. NEVER use with COUNT/CONCURRENCY. REQUIRED for all other operations."),
            op: z.enum(["=", "!=", ">", ">=", "<", "<="]).describe("MUST use field name 'op'. Available comparison operators: \"=\", \"!=\", \">\", \">=\", \"<\", \"<=\""),
            value: z.number().describe("MUST use field name 'value'. Numeric threshold value to compare against.")
          })).optional().describe("MUST use field name 'havings'. Post-calculation filters with same column rules as calculations.")
        },
  • Registers the run_query tool (created via createRunQueryTool) along with others on the MCP server, including pre-handler validation for required environment and dataset.
    export function registerTools(server: McpServer, api: HoneycombAPI) {
      const tools = [
        // Dataset tools
        createListDatasetsTool(api),
        createListColumnsTool(api),
    
        // Query tools
        createRunQueryTool(api),
        createAnalyzeColumnsTool(api),
    
        // Board tools
        createListBoardsTool(api),
        createGetBoardTool(api),
    
        // Marker tools
        createListMarkersTool(api),
    
        // Recipient tools
        createListRecipientsTool(api),
    
        // SLO tools
        createListSLOsTool(api),
        createGetSLOTool(api),
    
        // Trigger tools
        createListTriggersTool(api),
        createGetTriggerTool(api),
        
        // Trace tools
        createTraceDeepLinkTool(api),
        
        // Instrumentation tools
        createInstrumentationGuidanceTool(api)
      ];
    
      // Register each tool with the server
      for (const tool of tools) {
        // Register the tool with the server using type assertion to bypass TypeScript's strict type checking
        (server as any).tool(
          tool.name,
          tool.description,
          tool.schema, 
          async (args: Record<string, any>, extra: any) => {
            try {
              // Validate and ensure required fields are present before passing to handler
              if (tool.name.includes("analyze_columns") && (!args.environment || !args.dataset || !args.columns)) {
                throw new Error("Missing required fields: environment, dataset, and columns are required");
              } else if (tool.name.includes("run_query") && (!args.environment || !args.dataset)) {
                throw new Error("Missing required fields: environment and dataset are required");
              }
              
              // Use type assertion to satisfy TypeScript's type checking
              const result = await tool.handler(args as any);
              
              // If the result already has the expected format, return it directly
              if (result && typeof result === 'object' && 'content' in result) {
                return result as any;
              }
              
              // Otherwise, format the result as expected by the SDK
              return {
                content: [
                  {
                    type: "text",
                    text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
                  },
                ],
              } as any;
            } catch (error) {
              // Format errors to match the SDK's expected format
              return {
                content: [
                  {
                    type: "text",
                    text: error instanceof Error ? error.message : String(error),
                  },
                ],
                isError: true,
              } as any;
            }
          }
        );
      }
    }
  • Helper function to execute the actual API query, simplify/process results, add summaries and metadata, and format as tool response.
    async function executeQuery(
      api: HoneycombAPI, 
      params: z.infer<typeof QueryToolSchema>,
      hasHeatmap: boolean
    ) {
      // Execute the query
      const result = await api.runAnalysisQuery(params.environment, params.dataset, params);
      
      try {
        // Simplify the response to reduce context window usage
        const simplifiedResponse = {
          results: result.data?.results || [],
          // Only include series data if heatmap calculation is present (it's usually large)
          ...(hasHeatmap ? { series: result.data?.series || [] } : {}),
          
          // Include a query URL if available 
          query_url: result.links?.query_url || null,
          
          // Add summary statistics for numeric columns
          summary: summarizeResults(result.data?.results || [], params),
          
          // Add query metadata for context
          metadata: {
            environment: params.environment,
            dataset: params.dataset,
            executedAt: new Date().toISOString(),
            resultCount: result.data?.results?.length || 0
          }
        };
        
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(simplifiedResponse, null, 2),
            },
          ],
        };
      } catch (processingError) {
        // Handle result processing errors separately to still return partial results
        console.error("Error processing query results:", processingError);
        
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({
                results: result.data?.results || [],
                query_url: result.links?.query_url || null,
                error: `Error processing results: ${processingError instanceof Error ? processingError.message : String(processingError)}`
              }, null, 2),
            },
          ],
        };
      }
    }

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/honeycombio/honeycomb-mcp'

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