Skip to main content
Glama

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