Skip to main content
Glama
tylerbrabo98

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: Customer Health 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.

ParametersJSON Schema
NameRequiredDescriptionDefault
accountIdYesThe account's unique identifier, e.g. "acct_0007"

Output Schema

ParametersJSON Schema
NameRequiredDescription
usageYes
billingYes
supportYes
accountIdYes
companyNameYes

TDQS

A4.3/5.0
Behavior4/5

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

With no annotations, the description carries the full burden and does reasonably well: it discloses the breadth of data returned (full history and time series, implying a heavy response) and the error behavior when accountId does not exist. It does not state read-only status or any permission/rate considerations, leaving a gap for a data-heavy read tool.

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?

Three tightly written sentences with zero filler; the verb and resource come first, followed by contents, then the routing condition, then error behavior. Every sentence earns its place.

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

Completeness5/5

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

An output schema exists, so return values need not be spelled out, yet the description usefully previews the payload shape. For a single-parameter read tool with full schema coverage, nothing an agent needs to call it correctly is missing.

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 coverage is 100% and the single accountId parameter is already documented with an example format, so the schema does the heavy lifting. The description adds only an error condition for a nonexistent accountId, which is useful but not syntax or format detail beyond the schema.

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?

States a specific verb ('Fetch') and resource ('the complete raw record for one account'), then enumerates exactly what the record contains: billing/subscription, 90-day usage time series, and support ticket history. This lets an agent distinguish it from the summary-oriented sibling get_account_risk_summary without opening either schema.

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

Usage Guidelines4/5

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

Gives a clear selection condition: 'Use this when you need the underlying data behind a risk score, not just the summary,' which implicitly contrasts with the risk-summary sibling. It stops short of naming the alternative tool explicitly or stating any exclusions/prerequisites, so it falls just below the top band.

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

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
accountIdYesThe account's unique identifier, e.g. "acct_0007"

Output Schema

ParametersJSON Schema
NameRequiredDescription
driversYes
summaryYes
riskBandYes
accountIdYes
riskScoreYes
companyNameYes

TDQS

A4.4/5.0
Behavior4/5

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

With no annotations, the description carries the full burden and does well: it discloses that the result is a computed synthesis across billing/usage/support, that it includes explanatory drivers and a generated paragraph, and that a missing accountId yields a clear error. It omits cost/latency/auth considerations for what is presumably an expensive aggregation, keeping it out of 5 territory.

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?

Front-loaded with the core capability, then usage triggers, then the error contract — a logical ordering with no filler sentences. Slightly verbose in stacking three output claims, but each earns its place.

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

Completeness5/5

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

An output schema exists, so return-value detail is not the description's job. The description still covers purpose, synthesis inputs, when to invoke, and the error case, leaving nothing an agent needs in order to call this single-parameter tool correctly.

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

Parameters4/5

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

Schema coverage is 100% for the single accountId parameter, so the baseline is 3. The description adds real semantic value beyond the schema by specifying the failure mode — a clear error when the accountId does not exist — which tells the agent what to expect for invalid input.

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?

States a specific verb+resource pair (computes churn-risk score and band) plus the three signal sources it synthesizes, and enumerates its outputs (score, drivers, summary paragraph). The 'flagship synthesis tool' framing plus the multi-source computation clearly distinguishes it from list_accounts, get_account_details, and list_at_risk_accounts.

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

Usage Guidelines4/5

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

Gives explicit trigger phrases ('why is this account at risk?', 'give me a health summary for <company>'), which is clear usage context. It stops short of naming when NOT to use it or pointing to siblings (e.g., list_at_risk_accounts for a roster), so it lacks the exclusion/alternative half of a 5.

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

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
planNoOnly return accounts on this subscription plan
paymentStatusNoOnly return accounts with this billing payment status

Output Schema

ParametersJSON Schema
NameRequiredDescription
accountsYes

TDQS

A4/5.0
Behavior3/5

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

No annotations are present, so the description carries the full disclosure burden. It reveals what is returned and that filtering is optional, but says nothing about read-only semantics, result-set size limits, pagination, or ordering for what is clearly a collection endpoint.

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?

Three sentences, front-loaded with purpose and return fields, then capability, then routing. Every clause carries information and nothing is padded.

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?

With an output schema present, the response shape need not be explained, and the routing guidance to siblings is a genuine plus. The only gap is operational detail (pagination/limits) for a list endpoint expected to return the whole customer base.

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 coverage is 100% and both parameters are enum-constrained with their own descriptions, so the schema does the heavy lifting. The description only restates that filtering by plan and payment status is optional, adding no semantics beyond the schema.

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?

States a specific verb and resource ('List all customer accounts') and enumerates the returned fields (plan, MRR, payment status, churn-risk band), which cleanly separates it from the detail-oriented siblings.

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

Usage Guidelines4/5

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

Explicitly frames the use case as getting a customer-base overview before drilling down, and names get_account_details and get_account_risk_summary as the follow-on tools. It lacks an explicit statement of when NOT to use it, so it stops just short of a 5.

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

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMaximum number of accounts to return (default: all matches)
minBandNoMinimum risk band to include (default: 'watch')

Output Schema

ParametersJSON Schema
NameRequiredDescription
accountsYes

TDQS

A4.2/5.0
Behavior3/5

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

No annotations are supplied, so the description carries the full burden. It discloses the default threshold behavior, the inclusive band semantics, and descending sort order, which is genuinely useful. However, it says nothing about pagination, permissions, or result volume, which matters for a list endpoint with a limit parameter.

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?

Three sentences, each earning its place: scope/defaults first, use case second, follow-up routing third. Front-loaded with the most important information and free of filler.

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?

With an output schema present, return-value explanation is unnecessary, and the description covers scope, defaults, sorting, and the natural follow-up tool. The only gap is that no annotations exist to cover safety/permission characteristics and the description does not compensate for them.

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

Parameters4/5

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

Schema coverage is 100%, so the baseline is 3, but the description adds real meaning beyond the schema: it explains that a 'watch' minimum band therefore includes BOTH 'watch' and 'at_risk' accounts, clarifying the ordering semantics of the enum that the schema alone does not convey.

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?

States a specific verb (List) and resource (customer accounts) narrowed by a computed churn-risk band threshold, which cleanly separates it from the sibling list_accounts that has no risk filter. The default behavior and sort order are stated up front.

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

Usage Guidelines4/5

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

Explicitly frames the use case ('fastest way to answer which accounts are at risk this week?') and routes the agent to get_account_risk_summary when it needs score reasoning. It does not explicitly say when to prefer list_accounts or get_account_details, so it stops short of full when/when-not coverage.

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

Tool Schema Changelog

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

  1. 4 tool updatesv0.1.0
    • First observedget_account_details
    • First observedget_account_risk_summary
    • First observedlist_accounts
    • First observedlist_at_risk_accounts

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

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    B
    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
    24 npm
    1
    MIT
  • F
    license
    Not graded
    quality
    C
    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.
    -
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables AI assistants to query internal business data for insights into customers, revenue, subscriptions, sales, and churn through controlled, read-only MCP tools.
    -