Skip to main content
Glama

Customer Segmentation

customers_segment
Read-onlyIdempotent

Segment customers using RFM analysis into categories like Champions, At Risk, and Lost with actionable recommendations to improve retention and targeting.

Instructions

RFM (Recency, Frequency, Monetary) customer segmentation. Categorizes customers into segments: Champions, Loyal, Potential, At Risk, New, Hibernating, Lost — with actionable recommendations.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
store_idYesUUID of a connected store (returned by store_connect with action="connect" or visible in store_connect with action="list" / the store_overview resource)

Implementation Reference

  • Main handler for customers_segment tool. Validates store, retrieves customers and orders from storage, runs RFM segmentation via segmentCustomers(), and returns a SegmentSummary with aggregated segment stats and per-customer RFM results.
    export async function getCustomerSegments(storeId: string): Promise<SegmentSummary> {
      validateUUID(storeId, 'store');
      const store = await storage.getStoreById(storeId);
      if (!store) throw new NotFoundError('Store', storeId);
    
      const customers = await storage.getCustomers(storeId);
      const orders = await storage.getOrders(storeId);
      const results = segmentCustomers(customers, orders);
    
      // Build segment summary
      const segments: Record<string, { count: number; total_spent: number; avg_rfm: number }> = {};
      for (const r of results) {
        if (!segments[r.segment]) {
          segments[r.segment] = { count: 0, total_spent: 0, avg_rfm: 0 };
        }
        segments[r.segment].count++;
        segments[r.segment].total_spent += r.total_spent;
        segments[r.segment].avg_rfm += r.rfm_score;
      }
      for (const seg of Object.values(segments)) {
        seg.avg_rfm = seg.count > 0 ? Math.round((seg.avg_rfm / seg.count) * 100) / 100 : 0;
        seg.total_spent = Math.round(seg.total_spent * 100) / 100;
      }
    
      return {
        store_id: storeId,
        total_customers: results.length,
        segments,
        customers: results,
      };
    }
  • Determines RFM segment based on recency/frequency/monetary quintile scores. Maps to segments: champions, loyal, potential, new, at_risk, hibernating, lost.
    export function determineSegment(r: number, f: number, m: number): RFMSegment {
      const avg = (r + f + m) / 3;
    
      if (r >= 4 && f >= 4 && m >= 4) return 'champions';
      if (r >= 3 && f >= 4 && m >= 3) return 'loyal';
      if (r >= 4 && f <= 2) return 'new';
      if (r >= 3 && f >= 2 && avg >= 3) return 'potential';
      if (r <= 2 && f >= 3) return 'at_risk';
      if (r <= 2 && f <= 2 && m <= 2) return 'lost';
      return 'hibernating';
    }
  • Core RFM segmentation logic. Computes recency (days since last order), frequency (order count), monetary (total spent) for each customer, scores them via percentile quintiles, and returns sorted RFMResult array.
    export function segmentCustomers(customers: Customer[], orders: Order[]): RFMResult[] {
      if (customers.length === 0) return [];
    
      const now = Date.now();
    
      // Build per-customer order data from orders
      const customerOrders = new Map<string, { lastOrderTs: number; orderCount: number; totalSpent: number }>();
      for (const order of orders) {
        if (order.status === 'cancelled' || order.status === 'refunded') continue;
        if (!order.customer_id) continue;
    
        const existing = customerOrders.get(order.customer_id);
        const orderTs = new Date(order.created_at).getTime();
    
        if (existing) {
          existing.lastOrderTs = Math.max(existing.lastOrderTs, orderTs);
          existing.orderCount++;
          existing.totalSpent += order.total;
        } else {
          customerOrders.set(order.customer_id, {
            lastOrderTs: orderTs,
            orderCount: 1,
            totalSpent: order.total,
          });
        }
      }
    
      // Calculate raw RFM values
      const rfmRaw: Array<{ customer: Customer; recencyDays: number; frequency: number; monetary: number }> = [];
    
      for (const customer of customers) {
        const orderData = customerOrders.get(customer.id);
        const lastOrderTs = orderData?.lastOrderTs ?? (customer.last_order_at ? new Date(customer.last_order_at).getTime() : 0);
        const recencyDays = lastOrderTs > 0 ? Math.round((now - lastOrderTs) / MS_PER_DAY) : 999;
        const frequency = orderData?.orderCount ?? customer.total_orders;
        const monetary = orderData?.totalSpent ?? customer.total_spent;
    
        if (frequency === 0 && monetary === 0) continue; // Skip customers with no orders
    
        rfmRaw.push({ customer, recencyDays, frequency, monetary });
      }
    
      if (rfmRaw.length === 0) return [];
    
      // Score each dimension (1-5)
      const recencyScores = quintileScores(rfmRaw.map((r) => r.recencyDays), false);
      const frequencyScores = quintileScores(rfmRaw.map((r) => r.frequency), true);
      const monetaryScores = quintileScores(rfmRaw.map((r) => r.monetary), true);
    
      return rfmRaw.map((entry, i) => {
        const r = recencyScores[i];
        const f = frequencyScores[i];
        const m = monetaryScores[i];
        const segment = determineSegment(r, f, m);
    
        return {
          customer_id: entry.customer.id,
          customer_name: entry.customer.name,
          customer_email: entry.customer.email,
          recency_score: r,
          frequency_score: f,
          monetary_score: m,
          rfm_score: Math.round(((r + f + m) / 3) * 100) / 100,
          segment,
          total_orders: entry.frequency,
          total_spent: Math.round(entry.monetary * 100) / 100,
          last_order_days_ago: entry.recencyDays,
          recommended_action: segmentAction(segment),
        };
      }).sort((a, b) => b.rfm_score - a.rfm_score);
    }
  • Type definitions for RFM segmentation: RFMSegment enum (champions/loyal/potential/at_risk/new/hibernating/lost) and RFMResult schema with score fields and recommended_action.
    // ── RFM Segmentation ──────────────────────────────────────────────
    export const RFMSegmentSchema = z.enum([
      'champions', 'loyal', 'potential', 'at_risk', 'new', 'hibernating', 'lost',
    ]);
    export type RFMSegment = z.infer<typeof RFMSegmentSchema>;
    
    export const RFMResultSchema = z.object({
      customer_id: z.string(),
      customer_name: z.string(),
      customer_email: z.string(),
      recency_score: z.number().int().min(1).max(5),
      frequency_score: z.number().int().min(1).max(5),
      monetary_score: z.number().int().min(1).max(5),
      rfm_score: z.number(),
      segment: RFMSegmentSchema,
      total_orders: z.number().int(),
      total_spent: z.number(),
      last_order_days_ago: z.number().int(),
      recommended_action: z.string(),
    });
    export type RFMResult = z.infer<typeof RFMResultSchema>;
  • src/index.ts:226-243 (registration)
    Registration of the customers_segment tool with the MCP server, including title, description, inputSchema (store_id UUID), annotations, and handler that calls getCustomerSegments().
    // ── Tool: customers_segment ───────────────────────────────────────
    server.registerTool(
      'customers_segment',
      {
        title: 'Customer Segmentation',
        description: 'RFM (Recency, Frequency, Monetary) customer segmentation. Categorizes customers into segments: Champions, Loyal, Potential, At Risk, New, Hibernating, Lost — with actionable recommendations.',
        inputSchema: z.object({
          store_id: z.string().uuid().describe('UUID of a connected store (returned by store_connect with action="connect" or visible in store_connect with action="list" / the store_overview resource)'),
        }),
        annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
      },
      async ({ store_id }) => {
        try {
          const result = await getCustomerSegments(store_id);
          return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
        } catch (e) { return handleToolError(e); }
      }
    );
Behavior4/5

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

Annotations already indicate read-only, non-destructive, idempotent behavior. The description adds value by explaining the RFM methodology and the list of segments, beyond what annotations provide. However, it does not disclose return format or data limits.

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

Conciseness5/5

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

Single sentence with acronym expansion and a bullet-like list of segments. No fluff, front-loaded with key concept 'RFM segmentation'.

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?

For a simple one-parameter tool with rich annotations and full schema coverage, the description covers methodology and output categories. Lacks explanation of 'actionable recommendations' or output structure, but does not require extensive completeness.

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?

Schema description coverage is 100% with a detailed explanation of the store_id parameter. The tool description adds no additional parameter information, so baseline score of 3 is appropriate.

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 specifies RFM customer segmentation, names the exact segments (Champions, Loyal, etc.), and mentions actionable recommendations. It distinguishes from sibling 'customers_churn' by focusing on multi-category segmentation rather than churn prediction.

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

Usage Guidelines2/5

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

No guidance on when to use this tool vs alternatives like 'customers_churn' or other analytics tools. No prerequisites mentioned (e.g., store must be connected). The agent is left to infer context from the name and description.

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/enzoemir1/shopops-mcp'

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