Skip to main content
Glama
Customer Health MCP

Customer Health MCP

by tylerbrabo98

customer-health-mcp

Read the build log → A write-up covering the architecture, the scoring methodology, and three real bugs caught along the way.

An MCP (Model Context Protocol) server that lets an AI assistant answer customer-success questions like "which accounts are at risk of churning?" or "give me a health summary for Acme Corp", by synthesizing data shaped like three real B2B SaaS systems:

  1. Billing/subscription data, shaped like a Stripe subscription object (plan, MRR, payment status, downgrades)

  2. Support ticket data, shaped like an Intercom/Zendesk ticket (severity, status, open duration)

  3. Product usage data, shaped like a product analytics tool (daily active seats over 90 days, feature adoption, login recency)

This is a portfolio project: it runs with zero external API keys or databases, using deterministic synthetic data generated from a seed at startup. The goal is to demonstrate production-grade MCP server design (tool granularity, input validation, structured output, error handling, and an inspectable scoring methodology) on top of realistic (if fictional) data.

Why this exists

Customer success teams live at the intersection of three systems that rarely talk to each other: billing, support, and product usage. A churn signal is almost never visible in just one of them: an account can have healthy MRR and no open tickets while quietly not logging in for a month. This server models that synthesis problem directly: it doesn't just expose three raw data sources as tools, it also computes and explains a single composite risk score from all three.

Related MCP server: CSM Intelligence MCP Server

Architecture

┌────────────────────────────────────────┐
│               MCP Client               │
│     (Claude Desktop / Claude Code)     │
└────────────────────────────────────────┘
                     │
                     │  stdio · JSON-RPC
                     ▼
┌────────────────────────────────────────┐
│           data/generator.ts            │
│  seeded PRNG → 32 fictional accounts,  │
│         generated once at boot         │
└────────────────────────────────────────┘
                     │
                     ▼
┌────────────────────────────────────────┐
│      32 accounts, held in memory       │
│   billing + usage + support tickets    │
└────────────────────────────────────────┘
                     │
                     │  re-read on every tool call
                     ▼
┌────────────────────────────────────────┐
│          scoring/riskScore.ts          │
│  weights + thresholds → score, band,   │
│         drivers, never cached          │
└────────────────────────────────────────┘
                     │
                     ▼
┌────────────────────────────────────────┐
│          tools/ + resources/           │
│  list_accounts · get_account_details   │
│         list_at_risk_accounts          │
│        get_account_risk_summary        │
│       methodology://risk-scoring       │
└────────────────────────────────────────┘

Data flows one direction: the generator builds the in-memory dataset once at startup, the scorer derives a risk score from it on demand (never cached, so it's always consistent with the underlying data), and the tools are thin adapters that filter/shape that data and the scorer's output for MCP clients.

Setup

Requires Node.js 22.12+ or 24+ (this repo pins lts/* via .nvmrc).

npm install
npm run build

Run it directly to confirm it starts (it will sit waiting for stdio input; Ctrl+C to exit):

npm start

Run the test suite:

npm test

Print a quick human-readable dump of the generated dataset without starting the MCP server:

npm run sample-data

Reproducible data with --seed

The dataset is generated once at startup from a seed string, so the same seed always produces the same 32 accounts:

node dist/index.js --seed my-custom-seed

Omit --seed to use the built-in default seed.

Wiring into Claude Desktop

Add an entry to your claude_desktop_config.json (on macOS: ~/Library/Application Support/Claude/claude_desktop_config.json; on Windows: %APPDATA%\Claude\claude_desktop_config.json), pointing at the built dist/index.js with an absolute path:

{
  "mcpServers": {
    "customer-health": {
      "command": "node",
      "args": ["/absolute/path/to/customer-health-mcp/dist/index.js"]
    }
  }
}

Restart Claude Desktop after saving. You should see customer-health listed among the connected MCP servers, with its four tools and one resource available.

Example prompts

Once connected, try asking Claude things like:

  • "Which of our accounts are most at risk this week, and why?"

  • "Give me a health summary for Tidewater Insurance."

  • "List every enterprise account that's past due on payment."

  • "How is churn risk actually calculated here?" (Claude can read the methodology://risk-scoring resource to answer this precisely, instead of guessing.)

Design decisions

Tool granularity: raw data tools + one synthesis tool

Three of the four tools (list_accounts, get_account_details, list_at_risk_accounts) return data that's close to the underlying model: they filter, sort, and reshape, but they don't interpret. The fourth, get_account_risk_summary, is the "synthesis" tool: it's the only one that combines all three data sources into a single judgment call (a score, a band, and a plain-language explanation).

This split matters for how a calling model uses the server. Raw-data tools let the model verify a claim or dig into specifics ("show me the actual tickets"); the synthesis tool lets it answer a fuzzy question quickly without re-deriving the scoring logic itself in-context (which it would likely do inconsistently). Keeping them separate, rather than only exposing the synthesized view, also makes the scoring auditable: a model (or a human reading its answer) can always cross-check a risk summary against get_account_details for the same account.

Scoring methodology

computeRiskScore (in src/scoring/riskScore.ts) returns a 0-100 score, where higher means more risk, built from four independently-capped categories (usage trend/dormancy, seat adoption, support signal, and billing signal), each of which contributes plain-language "drivers" rather than just a number. Two choices are worth calling out:

  • Billing is the highest-leverage category. A canceled subscription alone is enough to reach the "at_risk" band, and a past-due payment alone is enough to reach "watch," because in practice a billing failure is a much harder churn signal than a soft dip in usage or a few open tickets. Usage and support signals still matter, but they combine more gradually.

  • Drivers are sorted by point contribution, not by category order. The first item in drivers is always whatever single signal contributed the most to the score, so a model summarizing "why" an account is at risk leads with the actual biggest factor rather than an arbitrary category ordering.

The exact weights and thresholds are documented in code (WEIGHTS/BAND_THRESHOLDS in riskScore.ts) and re-rendered live as the methodology://risk-scoring MCP resource, so the two can never drift out of sync with each other.

Project structure

src/
  data/
    types.ts        Account/BillingRecord/Ticket/UsageProfile types
    rng.ts           seeded PRNG utilities
    generator.ts     seeded synthetic data generation
    printSample.ts   `npm run sample-data` entrypoint
  scoring/
    riskScore.ts      composite health/risk scoring logic
    riskScore.test.ts unit tests (4+ scenarios)
  tools/
    shared.ts               shared accountId lookup/error helper
    listAccounts.ts
    getAccountDetails.ts
    listAtRiskAccounts.ts
    getAccountRiskSummary.ts
  resources/
    methodology.ts    exposes scoring methodology as an MCP resource
  server.ts            wires up tools/resources into an McpServer
  index.ts              stdio entrypoint (`--seed` flag)
tests/
  integration.test.ts  real client/server round trip over InMemoryTransport

Available Tools

4 tools
get_account_detailsGet full account detailA

Fetch the complete raw record for one account: its Stripe-style billing/subscription record, its full 90-day product-usage time series and feature adoption, and its full support ticket history. Use this when you need the underlying data behind a risk score, not just the summary. Returns a clear error if the accountId does not exist.

Inputs: accountId (required). Returns: usage, billing, support, accountId, +1 more

get_account_risk_summaryGet account risk summaryA

The flagship synthesis tool: computes an account's churn-risk score and band from its billing, usage, and support signals, lists the specific drivers behind the number, and generates a short human-readable summary paragraph. Use this when someone asks 'why is this account at risk?' or 'give me a health summary for '. Returns a clear error if the accountId does not exist.

Inputs: accountId (required). Returns: drivers, summary, riskBand, accountId, +2 more

list_accountsList customer accountsA

List all customer accounts with their plan, MRR, payment status, and computed churn-risk band. Supports optional filtering by plan and payment status. Use this to get an overview of the customer base before drilling into a specific account with get_account_details or get_account_risk_summary.

Inputs: plan, paymentStatus. Returns: accounts

list_at_risk_accountsList accounts at risk of churningA

List customer accounts whose computed churn-risk band is at or above a minimum threshold (defaults to 'watch', so both 'watch' and 'at_risk' accounts are included), sorted by risk score descending. This is the fastest way to answer 'which accounts are at risk this week?' For the reasoning behind a specific account's score, follow up with get_account_risk_summary.

Inputs: limit, minBand. Returns: accounts

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.2/5.0

    Scored across 4 tools

    Disambiguation4/5

    list_accounts and list_at_risk_accounts both return account lists and could be momentarily confused, but the descriptions make the filtering intent distinct (plan/payment vs risk band). get_account_details (raw data) vs get_account_risk_summary (synthesized score/drivers) are cleanly separated.

    Naming Consistency5/5

    All four tools follow a consistent snake_case verb_noun pattern using list_/get_ prefixes and predictable noun phrases. No mixing of conventions or stylistic outliers.

    Tool Count4/5

    Four read-only tools is well-scoped for a churn-risk analytics surface, with no redundant entries. It is slightly lean—no mutation or annotation tools—but each tool earns its place.

    Completeness4/5

    The read-only lifecycle is well covered: overview, at-risk ranking, raw detail, and risk synthesis with explicit follow-up pathways. Minor gaps exist (no way to list support tickets globally, no account-level write/annotation), but core workflows have no dead ends.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      C
      maintenance
      Enables AI agents to query customer churn risk, MRR at risk, and customer health profiles from Retain, and take actions like adding notes and marking alerts contacted.
      8
      32 npm
      1
      MIT
    • F
      license
      Not graded
      quality
      D
      maintenance
      Exposes simulated Salesforce CRM data as MCP tools for Claude Desktop, enabling customer success managers to prioritize at-risk accounts, review opportunities, cases, and Gong notes via natural language.
      -
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables AI assistants to securely query a synthetic health-insurance claims database through read-only, parameterized tools for claims, member histories, coverage rules, and year-to-date summaries. Includes API-key authentication, per-tool authorization, rate limiting, untrusted-text sanitization, and fail-closed audit logging.
      MIT