Skip to main content
Glama
simonl77

Salesforce MCP Server

by simonl77

salesforce_aggregate_query

Execute SOQL queries with GROUP BY and aggregate functions to summarize and analyze Salesforce data. Group records by fields, calculate counts, sums, averages, and apply statistical analysis across grouped results.

Instructions

Execute SOQL queries with GROUP BY, aggregate functions, and statistical analysis. Use this tool for queries that summarize and group data rather than returning individual records.

NOTE: For regular queries without GROUP BY or aggregates, use salesforce_query_records instead.

This tool handles:

  1. GROUP BY queries (single/multiple fields, related objects, date functions)

  2. Aggregate functions: COUNT(), COUNT_DISTINCT(), SUM(), AVG(), MIN(), MAX()

  3. HAVING clauses for filtering grouped results

  4. Date/time grouping: CALENDAR_YEAR(), CALENDAR_MONTH(), CALENDAR_QUARTER(), FISCAL_YEAR(), FISCAL_QUARTER()

Examples:

  1. Count opportunities by stage:

    • objectName: "Opportunity"

    • selectFields: ["StageName", "COUNT(Id) OpportunityCount"]

    • groupByFields: ["StageName"]

  2. Analyze cases by priority and status:

    • objectName: "Case"

    • selectFields: ["Priority", "Status", "COUNT(Id) CaseCount", "AVG(Days_Open__c) AvgDaysOpen"]

    • groupByFields: ["Priority", "Status"]

  3. Count contacts by account industry:

    • objectName: "Contact"

    • selectFields: ["Account.Industry", "COUNT(Id) ContactCount"]

    • groupByFields: ["Account.Industry"]

  4. Quarterly opportunity analysis:

    • objectName: "Opportunity"

    • selectFields: ["CALENDAR_YEAR(CloseDate) Year", "CALENDAR_QUARTER(CloseDate) Quarter", "SUM(Amount) Revenue"]

    • groupByFields: ["CALENDAR_YEAR(CloseDate)", "CALENDAR_QUARTER(CloseDate)"]

  5. Find accounts with more than 10 opportunities:

    • objectName: "Opportunity"

    • selectFields: ["Account.Name", "COUNT(Id) OpportunityCount"]

    • groupByFields: ["Account.Name"]

    • havingClause: "COUNT(Id) > 10"

Important Rules:

  • All non-aggregate fields in selectFields MUST be included in groupByFields

  • Use whereClause to filter rows BEFORE grouping

  • Use havingClause to filter AFTER grouping (for aggregate conditions)

  • ORDER BY can only use fields from groupByFields or aggregate functions

  • OFFSET is not supported with GROUP BY in Salesforce

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
objectNameYesAPI name of the object to query
selectFieldsYesFields to select - mix of group fields and aggregates. Format: 'FieldName' or 'COUNT(Id) AliasName'
groupByFieldsYesFields to group by - must include all non-aggregate fields from selectFields
whereClauseNoWHERE clause to filter rows BEFORE grouping (cannot contain aggregate functions)
havingClauseNoHAVING clause to filter results AFTER grouping (use for aggregate conditions)
orderByNoORDER BY clause - can only use grouped fields or aggregate functions
limitNoMaximum number of grouped results to return

Implementation Reference

  • Executes the aggregate SOQL query: validates inputs (GROUP BY fields, WHERE/HAVING/ORDER BY), constructs the full SOQL with GROUP BY and aggregates, queries Salesforce, formats grouped results, handles common errors with helpful messages.
    export async function handleAggregateQuery(conn: any, args: AggregateQueryArgs) {
      const { objectName, selectFields, groupByFields, whereClause, havingClause, orderBy, limit } = args;
    
      try {
        // Validate GROUP BY contains all non-aggregate fields
        const groupByValidation = validateGroupByFields(selectFields, groupByFields);
        if (!groupByValidation.isValid) {
          return {
            content: [{
              type: "text",
              text: `Error: The following non-aggregate fields must be included in GROUP BY clause: ${groupByValidation.missingFields!.join(', ')}\n\n` +
                    `All fields in SELECT that are not aggregate functions (COUNT, SUM, AVG, etc.) must be included in GROUP BY.`
            }],
            isError: true,
          };
        }
    
        // Validate WHERE clause doesn't contain aggregates
        const whereValidation = validateWhereClause(whereClause);
        if (!whereValidation.isValid) {
          return {
            content: [{
              type: "text",
              text: whereValidation.error!
            }],
            isError: true,
          };
        }
    
        // Validate ORDER BY fields
        const orderByValidation = validateOrderBy(orderBy, groupByFields, selectFields);
        if (!orderByValidation.isValid) {
          return {
            content: [{
              type: "text",
              text: orderByValidation.error!
            }],
            isError: true,
          };
        }
    
        // Construct SOQL query
        let soql = `SELECT ${selectFields.join(', ')} FROM ${objectName}`;
        if (whereClause) soql += ` WHERE ${whereClause}`;
        soql += ` GROUP BY ${groupByFields.join(', ')}`;
        if (havingClause) soql += ` HAVING ${havingClause}`;
        if (orderBy) soql += ` ORDER BY ${orderBy}`;
        if (limit) soql += ` LIMIT ${limit}`;
    
        const result = await conn.query(soql);
        
        // Format the output
        const formattedRecords = result.records.map((record: any, index: number) => {
          const recordStr = selectFields.map(field => {
            const baseField = extractBaseField(field);
            const fieldParts = field.trim().split(/\s+/);
            const displayName = fieldParts.length > 1 ? fieldParts[fieldParts.length - 1] : baseField;
            
            // Handle nested fields in results
            if (baseField.includes('.')) {
              const parts = baseField.split('.');
              let value = record;
              for (const part of parts) {
                value = value?.[part];
              }
              return `    ${displayName}: ${value !== null && value !== undefined ? value : 'null'}`;
            }
            
            const value = record[baseField] || record[displayName];
            return `    ${displayName}: ${value !== null && value !== undefined ? value : 'null'}`;
          }).join('\n');
          return `Group ${index + 1}:\n${recordStr}`;
        }).join('\n\n');
    
        return {
          content: [{
            type: "text",
            text: `Aggregate query returned ${result.records.length} grouped results:\n\n${formattedRecords}`
          }],
          isError: false,
        };
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        
        // Provide more helpful error messages for common issues
        let enhancedError = errorMessage;
        if (errorMessage.includes('MALFORMED_QUERY')) {
          if (errorMessage.includes('GROUP BY')) {
            enhancedError = `Query error: ${errorMessage}\n\nCommon issues:\n` +
              `1. Ensure all non-aggregate fields in SELECT are in GROUP BY\n` +
              `2. Check that date functions match exactly between SELECT and GROUP BY\n` +
              `3. Verify field names and relationships are correct`;
          }
        }
    
        return {
          content: [{
            type: "text",
            text: `Error executing aggregate query: ${enhancedError}`
          }],
          isError: true,
        };
      }
    } 
  • Defines the tool specification including name, detailed description with examples, and input schema for parameters like objectName, selectFields (with aggregates), groupByFields, whereClause, havingClause, etc.
    export const AGGREGATE_QUERY: Tool = {
      name: "salesforce_aggregate_query",
      description: `Execute SOQL queries with GROUP BY, aggregate functions, and statistical analysis. Use this tool for queries that summarize and group data rather than returning individual records.
    
    NOTE: For regular queries without GROUP BY or aggregates, use salesforce_query_records instead.
    
    This tool handles:
    1. GROUP BY queries (single/multiple fields, related objects, date functions)
    2. Aggregate functions: COUNT(), COUNT_DISTINCT(), SUM(), AVG(), MIN(), MAX()
    3. HAVING clauses for filtering grouped results
    4. Date/time grouping: CALENDAR_YEAR(), CALENDAR_MONTH(), CALENDAR_QUARTER(), FISCAL_YEAR(), FISCAL_QUARTER()
    
    Examples:
    1. Count opportunities by stage:
       - objectName: "Opportunity"
       - selectFields: ["StageName", "COUNT(Id) OpportunityCount"]
       - groupByFields: ["StageName"]
    
    2. Analyze cases by priority and status:
       - objectName: "Case"
       - selectFields: ["Priority", "Status", "COUNT(Id) CaseCount", "AVG(Days_Open__c) AvgDaysOpen"]
       - groupByFields: ["Priority", "Status"]
    
    3. Count contacts by account industry:
       - objectName: "Contact"
       - selectFields: ["Account.Industry", "COUNT(Id) ContactCount"]
       - groupByFields: ["Account.Industry"]
    
    4. Quarterly opportunity analysis:
       - objectName: "Opportunity"
       - selectFields: ["CALENDAR_YEAR(CloseDate) Year", "CALENDAR_QUARTER(CloseDate) Quarter", "SUM(Amount) Revenue"]
       - groupByFields: ["CALENDAR_YEAR(CloseDate)", "CALENDAR_QUARTER(CloseDate)"]
    
    5. Find accounts with more than 10 opportunities:
       - objectName: "Opportunity"
       - selectFields: ["Account.Name", "COUNT(Id) OpportunityCount"]
       - groupByFields: ["Account.Name"]
       - havingClause: "COUNT(Id) > 10"
    
    Important Rules:
    - All non-aggregate fields in selectFields MUST be included in groupByFields
    - Use whereClause to filter rows BEFORE grouping
    - Use havingClause to filter AFTER grouping (for aggregate conditions)
    - ORDER BY can only use fields from groupByFields or aggregate functions
    - OFFSET is not supported with GROUP BY in Salesforce`,
      inputSchema: {
        type: "object",
        properties: {
          objectName: {
            type: "string",
            description: "API name of the object to query"
          },
          selectFields: {
            type: "array",
            items: { type: "string" },
            description: "Fields to select - mix of group fields and aggregates. Format: 'FieldName' or 'COUNT(Id) AliasName'"
          },
          groupByFields: {
            type: "array",
            items: { type: "string" },
            description: "Fields to group by - must include all non-aggregate fields from selectFields"
          },
          whereClause: {
            type: "string",
            description: "WHERE clause to filter rows BEFORE grouping (cannot contain aggregate functions)",
            optional: true
          },
          havingClause: {
            type: "string",
            description: "HAVING clause to filter results AFTER grouping (use for aggregate conditions)",
            optional: true
          },
          orderBy: {
            type: "string",
            description: "ORDER BY clause - can only use grouped fields or aggregate functions",
            optional: true
          },
          limit: {
            type: "number",
            description: "Maximum number of grouped results to return",
            optional: true
          }
        },
        required: ["objectName", "selectFields", "groupByFields"]
      }
    };
  • src/index.ts:45-63 (registration)
    Registers the tool in the MCP server's listTools response by including AGGREGATE_QUERY in the tools array.
    server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        SEARCH_OBJECTS, 
        DESCRIBE_OBJECT, 
        QUERY_RECORDS, 
        AGGREGATE_QUERY,
        DML_RECORDS,
        MANAGE_OBJECT,
        MANAGE_FIELD,
        MANAGE_FIELD_PERMISSIONS,
        SEARCH_ALL,
        READ_APEX,
        WRITE_APEX,
        READ_APEX_TRIGGER,
        WRITE_APEX_TRIGGER,
        EXECUTE_ANONYMOUS,
        MANAGE_DEBUG_LOGS
      ],
    }));
  • src/index.ts:101-117 (registration)
    Dispatches calls to 'salesforce_aggregate_query' by validating arguments, typing them as AggregateQueryArgs, and invoking the handleAggregateQuery function.
    case "salesforce_aggregate_query": {
      const aggregateArgs = args as Record<string, unknown>;
      if (!aggregateArgs.objectName || !Array.isArray(aggregateArgs.selectFields) || !Array.isArray(aggregateArgs.groupByFields)) {
        throw new Error('objectName, selectFields array, and groupByFields array are required for aggregate query');
      }
      // Type check and conversion
      const validatedArgs: AggregateQueryArgs = {
        objectName: aggregateArgs.objectName as string,
        selectFields: aggregateArgs.selectFields as string[],
        groupByFields: aggregateArgs.groupByFields as string[],
        whereClause: aggregateArgs.whereClause as string | undefined,
        havingClause: aggregateArgs.havingClause as string | undefined,
        orderBy: aggregateArgs.orderBy as string | undefined,
        limit: aggregateArgs.limit as number | undefined
      };
      return await handleAggregateQuery(conn, validatedArgs);
    }
  • Helper function to validate that all non-aggregate selectFields are present in groupByFields, ensuring SOQL compliance.
    function validateGroupByFields(selectFields: string[], groupByFields: string[]): { isValid: boolean; missingFields?: string[] } {
      const nonAggregateFields = extractNonAggregateFields(selectFields);
      const groupBySet = new Set(groupByFields.map(f => f.trim()));
      
      const missingFields = nonAggregateFields.filter(field => !groupBySet.has(field));
      
      return {
        isValid: missingFields.length === 0,
        missingFields
      };
    }
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It effectively describes what the tool does (execute aggregate queries), lists supported features (GROUP BY, aggregate functions, HAVING, date grouping), and provides important behavioral rules (e.g., 'All non-aggregate fields in selectFields MUST be included in groupByFields', 'OFFSET is not supported'). However, it doesn't mention performance characteristics, error handling, or authentication requirements, leaving some behavioral aspects uncovered.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with clear sections: purpose statement, usage guidance, feature list, examples, and important rules. While comprehensive, it's appropriately sized for a complex tool with many parameters and constraints. Some sentences in the rules section could be more concise, but overall it's front-loaded with critical information and each section adds value.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (7 parameters, aggregate query functionality) and lack of annotations/output schema, the description does an excellent job of explaining what the tool does, when to use it, and important constraints. The examples and rules provide crucial context for proper usage. The main gap is the absence of output format description, which would be helpful since there's no output schema, but the description compensates well with comprehensive functional coverage.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema description coverage is 100%, so the schema already documents all 7 parameters thoroughly. The description adds value through examples that illustrate how parameters work together (e.g., showing selectFields with aggregate functions and aliases, groupByFields matching non-aggregate fields, havingClause usage). However, it doesn't provide additional semantic context beyond what the schema descriptions already cover, meeting the baseline for high schema coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Execute SOQL queries with GROUP BY, aggregate functions, and statistical analysis.' It specifies the verb (execute), resource (SOQL queries), and scope (GROUP BY, aggregates, statistical analysis). It explicitly distinguishes from sibling 'salesforce_query_records' for regular queries without GROUP BY or aggregates, providing clear differentiation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit guidance on when to use this tool vs. alternatives: 'Use this tool for queries that summarize and group data rather than returning individual records' and 'For regular queries without GROUP BY or aggregates, use salesforce_query_records instead.' It clearly defines the context (summarize/group data) and names the specific alternative tool, meeting the highest criteria.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/simonl77/mcp-server-salesforce'

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