Skip to main content
Glama
hritiksahu

reach-dispute-mcp

by hritiksahu

Reach Dispute Resolver — MCP server

Read-only tools over the REACH billing system of record that diagnose customer billing disputes: find the contradiction behind the complaint, cite the exact rows it rests on, score confidence, and decide auto-resolve or escalate.

It is not a chatbot. It is a set of tools that plugs into Claude — or anything else that speaks MCP, including the DCAL team's existing chatbot.

The division of labour is the point. Deterministic Python finds the contradiction and sets the confidence score. The language model only turns that finding into a sentence a person can read. The model can lower confidence; it can never raise it.


Quick start

cd ~/Documents/Project/reach-dispute-mcp

.venv/bin/python check.py             # data layer          — 15 checks
.venv/bin/python check_mcp.py         # live MCP handshake  — 18 checks
.venv/bin/python check_detectors.py   # verdicts are right  — 28 checks

All three should end with all checks passed. That's 61 assertions and takes about three seconds — run them before demoing.

The server is already registered in ~/.claude.json. In Claude Code:

/mcp

reach-dispute should appear with 7 tools. If it doesn't, see Troubleshooting.


Related MCP server: Zendesk MCP Server

The 60-second demo

Two customers. That's the whole show — one the system can answer, one it knows it can't.

Setup (10s)

"This is an MCP server. Seven read-only tools over our billing data. Claude is just the client — the same tools could plug into the DCAL chatbot."

Run /mcp and show reach-dispute · 7 tools.

Case 1 — it can answer (25s)

Type:

Customer CUS-4471 says they paid but they're still suspended. What does the data show?

Watch it call get_customer_snapshot, list_ledger_transactions, then diagnose_dispute. Point at three things in the answer:

Point at

Say

The evidence rows

"Every claim traces to a row — table, key, field, value. It physically cannot cite a record it didn't read."

Confidence 0.91

"Java-style deterministic rules produced that number, not the model. The model can't change it."

auto_resolve

"Payment landed 22 July, suspension was 20 July, reconnection never ran. Two records disagreeing."

Case 2 — it knows it can't (25s)

Type:

CUS-6614 says you charged them $10.50 twice. Did you?

"This is RTJSM-41555, a real ticket that took three days and four people."

Point at

Say

Confidence 0.40 · escalate

"It does not tell the customer they're wrong."

The reason

"The second charge, if it exists, is in IPpay — which this server cannot read."

HPY202607021852176

"It hands L2 the transaction ID we do hold. Half-solved ticket instead of a cold start."

Close (5s)

"Same system, two customers. It correctly knows which one it can answer. Knowing where its knowledge stops is the product."


The five scenarios

Synthetic data. Real scenarios — each reconstructs the contradiction its ticket turned on.

Customer

Mobile

Scenario

Modelled on

Verdict

CUS-4471

5550142201

Paid, still suspended

RTJSM-41687

0.91 · auto-resolve

CUS-2210

5550163344

Charged twice ($10.50)

REACHTS-12017

0.93 · auto-resolve

CUS-8802

5550178899

Bill jumped to $335.84

RTJSM-40005

0.90 · auto-resolve

CUS-3390

5550119055

Promo credit not applied

REACHTS-12005

0.86 · auto-resolve

CUS-6614

5550188120

Gateway mismatch

RTJSM-41555

0.40 · escalate

Look them up by customer ID, mobile number, or email. Ask Claude "what dispute scenarios are available?" to list them.

CUS-8802 is a good second-choice demo — the bill decomposes exactly: 83.85 plan + 251.88 carried forward + 0.11 tax = 335.84, and it names the three failed payments that created the carry-forward. In the real ticket, everything needed to work that out was already in the description and nobody did the subtraction for seven days.


Pasting a real ticket

Claude extracts the parameters from ticket text. It needs:

  • Required — something identifying the customer: ID, mobile number, or email.

  • Helps a lot — the claimed amount and how many times, if the customer named them ("$10.50 twice"). This is what lets the rules detect "you say two, I see one" rather than just reporting what we hold.

  • Ignored — everything else. Harmless, just not used.

Running on fixtures, only the five customers above resolve. A real ticket needs REACH_DATA_SOURCE=dev and that customer to exist in the dev database.


How it works

Claude Code ──stdio──▶ server.py ──▶ tools/diagnose.py ──▶ data/provider.py
                        7 tools       4 detectors           fixtures | dev
                                      + decision gate

Six read tools fetch data. Each returns {data, evidence[]}, where evidence is the actual rows read as {table, key, field, value}.

diagnose_dispute is the seventh and the one that decides. Four detectors, each a comparison between two records:

Detector

The contradiction

paid_but_suspended

successful payment dated after lastSuspendedDate, no reconnect recorded

duplicate_charge

two ledger rows, same amount and category, < 24h apart, distinct transaction ids

credit_not_applied

valid unapplied credit while the bill shows no adjustment

bill_increase_explained

bill decomposes exactly into plan + carry-forward + tax

claim_exceeds_our_records

customer claims more charges than the ledger holds → unverifiable

The decision gate

Auto-resolve requires all of:

  • confidence ≥ 0.80

  • nothing flagged unverifiable

  • if the remedy moves money (refund, apply credit), amount ≤ $25

The money cap applies only to money-moving remedies. CUS-4471 is a $52.40 dispute and still auto-resolves, because the remedy is reconnect a line we wrongly suspended — that costs nothing and can't be the wrong call. Explaining a bill is likewise always safe.

The one-way lock

diagnose_dispute accepts a concern parameter. Supplying one caps confidence at 0.50 and forces escalation:

without concern:  0.91  → auto_resolve
with concern:     0.50  → escalate

It clamps with min(). There is no path that raises confidence.


Say these unprompted

Getting caught not saying them is far worse than saying them.

  1. The data is synthetic. The scenarios are real tickets — numbers are in the table above.

  2. It is not wired into JSM. You give it a customer, not a ticket. Deliberate: wiring is a day of plumbing that proves nothing about whether the diagnosis is right — and posting a comment to a live ticket would be a write, which breaks the read-only guarantee.

  3. No blanket time claim. In a 100-ticket sample the median was 2.45 days and 29% closed same-day. The claim is hours back on the 71% that don't, plus knowing whether the refunds we grant blind were actually owed.

  4. The fixtures and the detectors were written together, so of course they agree. This proves the wiring and the logic, not that detectors fire correctly on production data. One dev-database run is what settles that.


Read-only guarantees

Three layers, none of them "we promise":

  1. The interface has no write method. DisputeDataProvider declares six methods, all reads. There is nothing to call.

  2. Every tool advertises readOnlyHint=true in its MCP annotations — the host can see it, not just take the README's word.

  3. Evidence is read from the row, never passed alongside it. Citing a field that doesn't exist raises rather than emitting a blank.


Switching to real data

REACH_DATA_SOURCE=dev .venv/bin/python server.py

Or change env in the ~/.claude.json entry.

Working as of 11 Aug 2026. data/dynamo.py is implemented and the dev path is live. Run check_dynamo.py first — it verifies credentials, table names and index names, and tells you which layer is broken instead of dumping a botocore traceback.

Credentials are temporary session keys in ~/.aws/credentials with no SSO cache, so when they lapse they have to be re-pasted from the console — there is no aws sso login path. Export REACH_DEV_ACCOUNT_ID if you want the check to assert it reached the intended account. A read-only scoped role would be better than running this under admin.

Before pointing at dev, check whether that data is seeded or copied from prod — if it's a prod copy, real customer records land on screen during the demo.


Troubleshooting

reach-dispute missing from /mcp — restart Claude Code; the config is only read at startup. Then verify the entry in ~/.claude.json points at the venv python by absolute path.

Server won't start — run it directly and read stderr:

.venv/bin/python server.py

reach-dispute MCP server starting (data source: fixtures) then a wait for input is correct. It's a stdio server — there is no port to curl.

"No customer matches that identifier" — you're on fixtures and used an identifier outside the five above.

Checks fail after an editcheck_detectors.py names the scenario and the assertion. The fixtures and detectors are coupled by design; changing a fixture value will correctly break the detector test.


Layout

server.py             MCP server — tool registration and descriptions
data/
  provider.py         read-only contract; fixtures/dev switch
  records.py          Row and Evidence — the citation machinery
  fixtures.py         the five scenarios (data only, no logic)
tools/
  read_tools.py       the six read tools
  diagnose.py         four detectors + the decision gate
check.py              data layer          — 15 checks
check_mcp.py          live MCP handshake  — 18 checks
check_detectors.py    verdicts are right  — 28 checks

data/records.py is the smallest file and the load-bearing one: sixty-odd lines are the reason the model cannot cite a row it never read.


Environment

  • Python 3.12, isolated .venv — system Python untouched

  • mcp==2.0.0 (note: 2.0 uses MCPServer, not the 1.x FastMCP)

  • boto3 only needed for REACH_DATA_SOURCE=dev

  • No ANTHROPIC_API_KEY — Claude Code is the host and drives the loop

F
license - not found
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    B
    quality
    C
    maintenance
    MCP server for the DataGate billing platform API, providing read-only tools to manage customers, invoices, products, agreements, sites, and payments.
    13
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables AI agents to audit and analyze Zendesk instances through a read-only MCP interface, providing tools for triggers, automations, analytics, and cross-reference queries.
    74
    MIT
  • F
    license
    -
    quality
    B
    maintenance
    MCP server for investigating payment/webhook drift, classifying order status mismatches, detecting duplicate charges, and escalating findings for human review. It is read-only for payment state and does not automatically retry or correct transactions.
  • F
    license
    -
    quality
    B
    maintenance
    An MCP server that enables non-technical operations users to resolve common commerce-ops tickets, such as orders charged but failed, through an investigate, recommend, approve, and execute workflow with read-only and write tools.

View all related MCP servers

Related MCP Connectors

  • A paid remote MCP for AI SDK data query MCP, built to return verdicts, receipts, usage logs, and aud

  • Read-only MCP access to sessions, funnels, campaigns, errors, live visitors, and anomalies.

  • A paid remote MCP for hosted MCP server, built to return verdicts, receipts, usage logs, and audit-r

View all MCP Connectors

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/hritiksahu/reach-dispute-mcp'

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