reach-dispute-mcp

reach-dispute-mcp

Provides read-only MCP tools to diagnose customer billing disputes by analyzing billing records, identifying contradictions, citing evidence, and scoring confidence to auto-resolve or escalate.

Category
访问服务器

README

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.


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

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选