reach-dispute-mcp
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@reach-dispute-mcpCustomer CUS-4471 says they paid but they're still suspended. What does the data show?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 checksAll 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:
/mcpreach-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." |
| "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 |
| 5550142201 | Paid, still suspended | RTJSM-41687 | 0.91 · auto-resolve |
| 5550163344 | Charged twice ($10.50) | REACHTS-12017 | 0.93 · auto-resolve |
| 5550178899 | Bill jumped to $335.84 | RTJSM-40005 | 0.90 · auto-resolve |
| 5550119055 | Promo credit not applied | REACHTS-12005 | 0.86 · auto-resolve |
| 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=devand 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 gateSix 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 |
| successful payment dated after |
| two ledger rows, same amount and category, < 24h apart, distinct transaction ids |
| valid unapplied credit while the bill shows no adjustment |
| bill decomposes exactly into plan + carry-forward + tax |
| 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 → escalateIt clamps with min(). There is no path that raises confidence.
Say these unprompted
Getting caught not saying them is far worse than saying them.
The data is synthetic. The scenarios are real tickets — numbers are in the table above.
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.
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.
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":
The interface has no write method.
DisputeDataProviderdeclares six methods, all reads. There is nothing to call.Every tool advertises
readOnlyHint=truein its MCP annotations — the host can see it, not just take the README's word.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.pyOr 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.pyreach-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 edit — check_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 checksdata/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 untouchedmcp==2.0.0(note: 2.0 usesMCPServer, not the 1.xFastMCP)boto3only needed forREACH_DATA_SOURCE=devNo
ANTHROPIC_API_KEY— Claude Code is the host and drives the loop
This server cannot be installed
Maintenance
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
- AlicenseBqualityCmaintenanceMCP server for the DataGate billing platform API, providing read-only tools to manage customers, invoices, products, agreements, sites, and payments.13MIT
- AlicenseAqualityBmaintenanceEnables AI agents to audit and analyze Zendesk instances through a read-only MCP interface, providing tools for triggers, automations, analytics, and cross-reference queries.74MIT
- Flicense-qualityBmaintenanceMCP 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.
- Flicense-qualityBmaintenanceAn 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.
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
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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