reimburse-buddy
Expense-claim auditing MCP server: an agent can inspect claims, policy, evidence and calendars, run deterministic checks/triage, draft decisions, and use mock write tools for follow-ups.
List claims by status, fetch a claim’s trip/items/employee, and read travel policy.
Search an employee’s reimburse mailbox, calendar, and approval memos.
Run
check_claimwith evidence IDs and human decisions to get deterministic amounts, gap codes, and ignored-evidence explanations.Triage a batch into auto-cleared claims and exceptions, with a time-saved estimate.
Draft reviewer-ready decision notes in Indonesian, English, or both.
Mock-write actions: email an employee, request manager approval, update claim status, view the outbox, and reset demo state.
Read-only tools are safe to run freely; write tools are mock/in-memory and do not send real messages.
Click on "Deploy 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., "@reimburse-buddyreview claim CLM-2026-0110 and draft a decision for me"
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.
reimburse-buddy
An office agent for finance teams, built for the Tencent Cloud Hackathon 2026 (Office Agent track). It audits business-travel expense claims and carries out office requests (send minutes, schedule a follow-up) without ever acting on its own: a deterministic rule engine decides every amount, the language model only investigates and drafts, and every write waits for a human approval that is recorded in an append-only audit log.
This repository holds two versions that share one rule engine:
Version B (Challenge B submission): the web app in
web/plus the LangGraph agent inagent/. Finance reviews claims, starts agent investigations, turns requests into plans and approves each action on a confirmation page. Employees submit claims and can ask about their own claims.Version A: the WorkBuddy skill (
skills/claim-auditor/SKILL.md) and the MCP server insrc/reimburse_buddy/, described under Plug into WorkBuddy.
All data in this repository is synthetic. Company, people, hotels and emails are fictional. No real email or message is ever sent.
Architecture (version B)
Source: docs/architecture_diagram.py (regenerates the SVG; the PNG is rendered from it).
Web app (
web/reimburse_web/, FastAPI, server-rendered HTML, Indonesian labels inlabels.py): employee portal, finance triage and claim pages, request intake, plan confirmation, employee Q&A.Claim agent (
agent/reimburse_agent/graph.py, LangGraph, DeepSeek through any OpenAI-compatible API): runs as a subprocess of the web app (agent/investigate_worker.py), calls the MCP tools and the rule engine, and drafts the decision note.Plan graph (
agent/reimburse_agent/plan_graph.py, no LLM): turns a request into staged actions, asks which person is meant when a name is ambiguous, takes recipients only from the directory (EMP- IDs) and times only from the operating-rules tool. See docs/request-intake.md.Approval gate and executor are separate graph nodes, because LangGraph re-runs an interrupted node from the top. The gate logs the reviewer's answer; the executor acts only when the audit log holds an approval for the same
action_idandargs_hash, re-runscheck_claimfor claim writes, and otherwise refuses.Audit log (
agent/reimburse_agent/audit.py, migrations inagent/migrations/): append-only, uniqueevent_keyper event, Postgres in production (see Deployment notes).Prompt-injection detection (
agent/reimburse_agent/injection.py): rule-based HIGH/LOW detector, withheld spans blocked at staging, an echo check, aneeds_reviewflag and an allowlist for where action arguments may come from.Employee Q&A (flag
EMPLOYEE_QA): read-only, identity from the signed session. See docs/employee-qa.md.
Related MCP server: policy-mcp
How the claim agent works
Source: docs/agent-how-it-works.excalidraw (open it at excalidraw.com).
The LangGraph agent (
agent/reimburse_agent/graph.py) reads the claim and the policy, then callscheck_claim. Every amount and gap comes from that deterministic rule engine, never from the LLM.For each open gap it searches the mailbox with
search_for_gap: 2 searches before asking the employee and 1 after. If a document is still missing, it asks the employee with the fixed-templaterequest_missing_evidenceand checks the claim again.Read-only tools run freely. Every write or message stops at the approval gate, evidence requests included. A run may ask for at most 10 approvals; after that, gated calls are declined without asking.
Decisions that need judgment (
approve_full,approve_capped,reject) come only from the finance reviewer, through the web question form or a reply. The graph refuses decisions the model tries to set itself.Once the check is clean, or every gap has been searched and asked about, the graph drafts the decision note. Finance takes the final action.
Version B also offers
find_messages(employee_id, query, date_from, date_to)(agent only, not a WorkBuddy tool). It collects the employee's messages through the same mailbox search, ranks them with BM25 plus a small local multilingual embedding model (fused with reciprocal rank fusion), and returns the top 5. Each call counts as one search against the gap's budget. If the model cannot be loaded, ranking falls back to BM25 and the result says so.
What the evaluation shows so far (synthetic data, small n, no LLM runs in this round)
Retrieval was the bottleneck. With the whole mailbox in hand the no-LLM baseline passes all 19 hard cases (0 unsafe, 0 false approvals) and 5/5 on each large set; with BM25 ranking only it passes 17/19 (cases 11 and 12 need the paraphrase) and 2/5 and 3/5. What the LLM adds is so far untested and is measured on the reasoning track.
suite (baseline, $0) | ranked (BM25+embedding), 5 runs | BM25 only, 1 run | whole mailbox, unranked (control), 1 run |
hard, 19 cases | 93/95 (18 cases 5/5, case 13 3/5) | 17/19 (11, 12 fail) | 19/19 |
large set A (tuning), 5 | 14/25 (A-slang_reply 4/5, A-reschedule_later and A-quoted 0/5) | 2/5 | 5/5 |
large set B (held out), 5 | 10/25 (B-slang_reply, B-reschedule_later, B-quoted 0/5) | 3/5 | 5/5 |
reasoning track, 11 | 50/55 (R1x 0/5, one false approval per run) | 10/11 (R1c) | 10/11 (R1x, 1 false approval) |
Run 2026-10-28 on this branch (agent/eval/results/*-20261028T0*.json). The ranked column is five runs per case, not one: case 13 passes 3 times in 5, so a single run reports 19/19 or 18/19 depending on the draw, and the same case is what makes test_ranked_baseline_on_heuristic_hard_cases[13-True] fail now and then. The BM25 and whole-mailbox columns are single runs.
Against the 25 Oct run the hard set goes from 17/19 to 19/19 in whole-mailbox mode (two scoring bugs, docs/regression-hard-eval.md), and large A, large B and the reasoning track each had one case that only failed because the scripted reviewer declined the evidence request that delivers the case's receipt. Those four cases now get a reviewer that grants a document request, and in whole-mailbox mode the two large sets are back to 5/5 and the reasoning track to 10/11. In ranked mode the receipts in those cases are not retrievable (the reply is "wkwk maap telat": no shared term with the item), so they depend on the graph's post-ask hold, which fires 4 times in 5 on set A and never on set B. That is a retrieval/hold limit, not a gate failure, and it is not fixed here.
The whole-mailbox control passes everything because every synthetic receipt and schedule change carries structured fields (
claim_id,item_id,meeting_update) and the rule engine keeps only what matches. On these fixtures the loss comes from the top-5 cut, not from understanding. Real mail has no such tags.Large sets (
agent/eval/large_fixtures.py): 46-68 messages per employee over Aug-Oct 2026, hand-written paraphrases combined by a seeded RNG; set A and set B share no paraphrase. Needed message recall@5 / MRR (baseline query): set A lexical 0.2/0.2, ranked 0.2/0.12; set B lexical 0.4/0.12, ranked 0.2/0.15 (agent/eval/retrieval_metrics.py). Every large mailbox is flaggedpossibly_incomplete(the 36 probes reach 33-47 of 46-68 messages).Reasoning track (
agent/eval/reasoning_fixtures.py, labels infixtures/reasoning/labels.json, to be reviewed by Imam): the PR #30 fix (meeting updates applied by message date) cleared R1, so the false approvals drop from two to one. The one left is R1x (moved, then cancelled in free text): the cancellation is prose only,meeting_updatecannot express it, so the tag-based engine still sees a live move to 15:00-16:30 and approves the 16:00 checkout. Under BM25 R1x passes, but for the wrong reason — the cancellation message is not retrieved at all, so the agent routes it to the reviewer; that is luck, not understanding. R1c fails only under BM25 (its paraphrase is not lexically retrievable).Results:
agent/eval/results/{hard,large_a,large_b,reasoning}-baseline*.jsonandretrieval-*.json; each records a SHA-256 of the fixtures.
Prepared, not yet run: model screening and an optional judge
agent/eval/screen_models.pyscreens SumoPod models on the 30 hard, large and reasoning cases. It is a dry run by default.--liveneeds--max-usd(hard cap;--strict-capstops mid-model). It runs a preflight that forces each tool once, freezes the conditions (temperature 0, no parallel tool calls, the same token cap), retries infrastructure errors and drops a model after two in a row, reads the cost from the gateway header, ranks by unsafe actions, then false approvals, correct, protocol, cost and latency, adds the $0 baseline as a reference row, and runs a gateway control (deepseek-flash direct vs. via SumoPod). The dry-run estimate is about $0.81 (worst case $1.10), using third-party prices that have not been verified.agent/reimburse_agent/judge.pyis an optional Jev (jev-1.13.0) judge. It is off by default; setRB_JUDGE=jev. It only escalates: a yes or unsettled answer adds a review flag. It never closes a gap, picks evidence or changes an amount. Each request sends one candidate message. Code computes dates and weekdays. Questions are Nouls, never Choices. A failed call is recorded as an error, never as "no". All wording and thresholds are injudge_questions.py. Today the judge is wired only into the contradiction scan of a calendar-only late checkout, and it sees the 160-character snippet, not the full body.agent/eval/judge_set.jsonlhas 62 hand-written labelled pairs (formal Indonesian, slang Indonesian, English; 27 positives; hard negatives and injection attempts).agent/eval/judge_score.pycompares Jev with a flash model using structured output. It reports AUROC, Brier, miss rate at the review threshold, false escalations and repeatability, overall and per register. It is a dry run by default; the estimate for 3 repeats is about $0.017.
find_messages: setup and limits
Embedding model:
sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2, loaded by fastembed 0.9.1 (pinned) fromQdrant/paraphrase-multilingual-MiniLM-L12-v2-onnx-Qat commitfaf4aa42(pinned inretrieval.py). Install withuv sync --extra embed. The first run downloads about 240 MB intoRB_EMBED_CACHE, elseFASTEMBED_CACHE_PATH, else~/.cache/fastembed.RB_NO_EMBED=1skips the model: BM25 only, and the result'srankersays so.Scope: only the dedicated
reimburse@mailbox, limited to messages the employee sent or forwarded. Personal inboxes are never read.Limitation: the server offers only a substring search capped at 5 results, so
find_messagesbuilds its pool with 36 one-character probes (a-z, 0-9). Any probe that returns 5 may have hidden more, so the pool is flaggedpossibly_incomplete; that happens for any mailbox with 5 or more messages. A contradiction scan on an incomplete pool counts as unavailable and sends the item to review. A server-side list or paged search would remove the workaround.Guards: a late checkout resolved by the calendar alone gets a graph-side scan (event title and date over the trip window; a hit at similarity >= 0.45, tuned on set A, or a schedule-change word sends it to review). After an evidence request, a reply that arrived since the pre-request mailbox snapshot and that the latest
check_claimdid not consider holds the draft once per mailbox version; a repeat draft goes through, flagged "new evidence found but not used". The same check runs when the model ends in plain text.
Tools
Tool | Purpose |
| Claims waiting for review |
| Claim, trip, items and employee |
| Travel policy: city tiers, hotel caps, per diem, receipt rules |
| Search the shared reimburse@ mailbox that employees forward evidence to (boarding passes, receipts, schedule changes). Personal inboxes are never read. |
| (Mock write) Bulk-approve the claims triage cleared, only with explicit reviewer confirmation and the exact cleared list |
| Mock calendar |
| Hotel-cap exception memos |
| Deterministic rule check |
| Split a batch into auto-cleared claims and exceptions, with a time-saved estimate |
| Reviewer-ready decision note: |
| (Mock write) Email an employee; goes to the outbox |
| (Mock write) Ask the claimant's manager; returns a pending request |
| (Mock write) |
| Everything the agent sent or changed this session |
| (Mock write) Restore all in-memory state before a new take (replies, outbox, approvals, statuses) |
| (Version B) Ask the claim's owner for a missing document with a fixed template; refuses a repeat while the last request is unanswered |
The ten tools without a mark are always registered. The five (Mock write) tools are registered only
when REIMBURSE_ENABLE_WRITE_TOOLS=1, and request_missing_evidence only when
REIMBURSE_ENABLE_EVIDENCE_TOOL=1. The WorkBuddy config (.workbuddy/mcp.json) sets neither, so in
WorkBuddy the agent can only read and draft; version B's agent sets both (16 tools) and gates every
write, evidence requests included, behind a human approval.
Write tools are mocks: state lives in memory for one server process. send_message is a
closed loop: scripted replies in data/replies.json are the employee's answer and land in the reimburse@ mailbox right away,
so the agent can search_reimburse_mailbox and pass the reply's ID to check_claim.
Rules in check_claim:
hotel nightly cap by city tier (or a signed memo), and hotel nights inside the trip
per diem per trip day
receipt required; a receipt forwarded to the reimburse mailbox counts
boarding pass must match flight number, date and passenger name
expense dates within the trip
duplicates keyed on receipt number (no distinct receipt numbers means
duplicate_suspected, not auto-reject)late check-out justified by a business meeting in the same city that ends after the standard check-out time, with the requested check-out at most 2 hours after the meeting (fee capped at 50% of the nightly cap)
claim-level warning when an outbound flight has no return flight
Unresolved problems come back as gap codes (receipt_missing, boarding_pass_missing,
hotel_over_cap, hotel_nights_mismatch, late_checkout_unjustified, duplicate_suspected,
return_flight_missing). The skill maps each code to a tool. human_decisions
(approve_full / approve_capped / reject) can settle any item, in any category.
ignored_evidence lists IDs that did not count and why (unknown_id, belongs_to_other_employee, memo_unsigned,
no_matching_item). Wrong argument shapes return an error with a correct example, so the model can retry.
search_reimburse_mailbox is a ranked OR search (case-insensitive, QZ7520 = QZ 7520, identifiers weigh more) that
returns the top 5 summaries. Scripted replies react to English and Indonesian requests (receipt, struk,
kuitansi, nota, bukti, taksi). The decision note's Indonesian section is fully localised and lists ignored
evidence as an audit trail.
Employee Q&A (flag)
EMPLOYEE_QA=1 adds a read-only Q&A box on the employee claim page. It answers policy questions, says what an employee's own claim is missing and shows its status, cites a source for every answer and keeps a full audit trail. It uses a synthetic rulebook. See docs/employee-qa.md.
Demo scenarios
# | Claim | What happens |
Triage | batch |
|
Hero |
| Four problems in one claim. (a) Searching "boarding pass" only finds another trip; the airline email says "Check-in successful: QZ 7520", so the agent retries with the flight number. (b) The only hotel memo, |
1 |
| Flight has no boarding pass. Agent searches the reimburse mailbox, finds |
2a |
| Jakarta hotel over cap. Agent finds signed memo |
2b |
| Medan hotel over cap, no memo. Agent asks the reviewer; capped amount approved. |
3 |
| Late check-out fee. Calendar shows a contract signing until 15:30, so the fee is justified. A post-trip dinner is rejected and a suspected duplicate taxi goes to the reviewer. |
Self-approval | batch | One claim, |
Run them without WorkBuddy:
uv run python -m reimburse_buddy.demo boarding-pass # or triage, hero, hotel-memo, hotel-ask, late-checkout, allSetup
Requires Python 3.11+ and uv.
git clone https://github.com/imam-db/reimburse-buddy.git
cd reimburse-buddy
uv sync
uv run pytest # rule tests + a real MCP stdio round-trip
uv run --no-dev reimburse-buddy-mcp # starts the server on stdio (WorkBuddy normally does this)
(cd agent && uv sync && uv run pytest -q) # agent, plan graph, audit log, injection, Q&ACI runs both suites on every pull request and on pushes to main, with Tesseract and RapidOCR installed. The Postgres-marked tests stay skipped in CI because no database is available there.
Run the version B web app
uv sync && (cd agent && uv sync)
export DEEPSEEK_API_KEY=... # only for "Selidiki dengan agent"; never commit it
export REIMBURSE_AUTH_DATABASE_URL=... # app_users table (migration 007); unset outside production = login off
EMPLOYEE_QA=1 uv run uvicorn --app-dir web reimburse_web.app:app --port 8000
# open http://127.0.0.1:8000/ (sign in at /login)Without a model key everything except the agent investigation works: triage, claim pages, request intake and plan confirmation, and employee Q&A (offline router) need no LLM.
Login (email + password)
Accounts live in the Postgres table app_users (agent/migrations/007_app_users.sql): employee_id
(matches data/employees.json), lowercase email, a scrypt password_hash
(scrypt$n$r$p$salt$hash, stdlib hashlib.scrypt), role (employee, reviewer, finance),
is_active. The app role can only SELECT and UPDATE (last_login_at). With the gate on, every route
except /login, /logout and /healthz needs the signed rb_auth cookie: a browser navigation is
redirected to the form, anything else gets 401. Three roles
(ADR 0001): employee (Karyawan) opens /employee/* and lands on
/employee; reviewer (shown as Manager) opens /manager/* and lands on /manager; finance opens
/ and /admin/* and lands on /admin. A wrong-role navigation is redirected to the role's landing page,
any other method gets 403. POST /admin/reset is refused (403) when APP_ENV is prod/production.
Manager approvals (ADR 0003): on a finance claim page,
"Minta persetujuan atasan" sends one item to the claimant's own manager (one open request per item). The
manager answers once on /manager (Setujui / Tolak plus a note); another manager gets 404 and the owner
of the claim is refused. The answer is logged as approval / rejection (run approval_request:<id>,
actor human:<manager>) before the request changes, and an audit failure changes nothing (503). Finance
sees the answer next to the item. It is input only: the rule engine, the claim status and the item
decisions stay with finance.
Session:
issued_at|employee_id|role|nonceplus an HMAC, 12 hours,httponly,samesite=lax,securein production, checked on every request.auth.reviewer_ofreturns the employee_id, which is what makes a self-approval refusable.One message for every failure (unknown email, wrong password, inactive account): "Email atau kata sandi salah." An unknown email still runs a hash check against a dummy hash, so timing does not leak.
Five failures per IP per minute; after that the form refuses even the right password. Failures are written to the audit log as
access_denied(run_id=auth, actorsystem,payload.reason=bad_credentials), one event per IP and minute, without the email, password or IP.next=only accepts a local path, so the form cannot be used as an open redirect.GET /healthzanswersokwithout touching the audit log, the store or the LLM.Outside production the gate is on only when a user DSN is set (
REIMBURSE_AUTH_DATABASE_URL, elseREIMBURSE_AUDIT_DATABASE_URL); otherwise it is off, so local runs and the test suite behave as before. In production a missing DSN, an unreachable database or an unsetREIMBURSE_DEMO_SESSION_KEYstops startup withauth.AuthConfigError.
Create the table and the accounts (owner role, never from the app; the passwords are prompted with
getpass, never passed as arguments):
psql "$OWNER_DATABASE_URL" -f agent/migrations/007_app_users.sql
uv --directory web run python -m reimburse_web.seed_users --dsn "$OWNER_DATABASE_URL" --employee EMP-100 --role reviewerTo create every account in data/employees.json in one run, use --all. The role comes from each row: a manager in Finance becomes finance, any other manager reviewer, everyone else employee. --same-password (with --all) asks for one password and uses it for every account, which is a demo convenience only.
uv --directory web run python -m reimburse_web.seed_users --dsn "$OWNER_DATABASE_URL" --all--dry-run prints the accounts and their roles without asking for a password or connecting to the database, so it needs no --dsn:
uv --directory web run python -m reimburse_web.seed_users --all --dry-runWho may approve
The finance user signed in is the actor on every decision. Nobody may approve their own work. The scenes
below need the demo people seeded as finance (for example seed_users --employee EMP-101 --role finance
and the same for EMP-100); by default EMP-100 and EMP-101 are managers and EMP-102 is finance:
A plan whose
requesteris the reviewer cannot be approved (POST /admin/plans/<run_id>/decide); the approve radio is disabled on the confirmation page.Neither can a plan the reviewer submitted: the intake records
submitted_by(the signed-in reviewer), so picking a colleague as the requester does not make the plan approvable by its author. The page shows both lines:peminta / requesterandDiajukan oleh / submitted by.A claim owned by the reviewer cannot be approved: item decisions (
approve_full,approve_capped), a status change offon_hold,Accept evidence, an agent write (/resume) or an agent question answered with an approve decision (/reply).POST /admin/batch/approverefuses the whole batch and names the claim, rather than dropping it silently.
Rejecting, adding a note and putting your own claim on_hold stay available: they cannot approve
anything. A refusal changes no state and is written to the audit log as access_denied with actor
human:<reviewer>, payload.reason = "self_approval" and payload.blocked naming the rule that fired
(requester or submitted_by). The intake form defaults the requester to EMP-101, so the demo
reviewer is EMP-100 (the login form's default) and the third reviewer is EMP-102 (Finance): the demo
shows the refusal as EMP-100, then the approval as EMP-102.
The server checks are the guard; the hidden/disabled buttons are only there so the page does not offer
an action that will be refused. The plan gate in plan_graph.py is unchanged: this is a web-layer
refusal, so an external driver calling the graph directly (for example agent/plan_demo.py) is not
covered.
An agent write approved from the investigation page is attributed the same way: the web layer sends the
reviewer ID on the worker's stdin answer, the worker puts it on the graph's resume payload as
answer["reviewer"], and graph.decide() (unchanged) logs human:<reviewer>.
Daily LLM budget
One cap for the whole deployment, not one per claim, on a ledger of its own (web/reimburse_web/spend.py;
agent/migrations/005_llm_spend.sql). The day boundary is Asia/Jakarta (UTC+07:00 all year, no DST).
Before a worker starts, the run reserves its worst case:
REIMBURSE_AGENT_MAX_TOKENStokens all billed at the output price. Ifcommitted + worst case > LLM_DAILY_BUDGET_USD, the run is refused with "Batas biaya harian demo tercapai. Coba lagi besok." and alimitaudit event (payload.kind = daily_llm_budget). Check and insert happen under one lock (a file lock for the JSONL ledger,pg_advisory_xact_lockfor Postgres), so two concurrent starts cannot both take the last slot.When the worker reports tokens (
final, orerror/token_limit), the run settles the real cost from the input hit/miss and output split (the worker accumulatesgraph.message_usage(), so cache-hit input is billed at the hit price). A run that ends without a token report (cancel, deadline, crash) keeps its whole reservation: the ledger fails safe instead of assuming the money was not spent.The Jakarta day is taken once, when the run is reserved: a run that crosses midnight WIB settles on the day it was reserved, so its reservation is always replaced and never leaks into the next day's cap.
If Postgres dropped the connection (a Neon endpoint suspends when idle), the ledger reconnects and retries once; if it is still unreachable the run is refused with "Ledger biaya LLM tidak bisa dihubungi, jadi tidak ada run yang dijalankan." instead of a 500, and nothing is spawned.
Committed for a day = every settle, plus the reservation of each run that never settled.
The remaining amount is shown in the nav ("Sisa anggaran LLM hari ini: $x.xx") and the ledger is append-only: the app role has INSERT + SELECT on
llm_spend, never UPDATE or DELETE.In production a missing budget or price stops startup with
spend.SpendConfigError(and refuses LLM runs as a second line of defence). Outside production an unconfigured ledger simply means no cap.Not metered yet: the optional
LLMQAModel(Q&A with a real key) and the Jev judge. Both are off by default and the demo runs the offline Q&A router and no judge, so the demo cannot spend there.
Plan state after a restart
A request that is waiting — on a "who" question, or on the reviewer's approval — has to survive a deploy.
Two things hold it: the plan store (request, plan versions, decisions, mock office rows) and the plan
graph's checkpointer (the open interrupt). REIMBURSE_PLAN_BACKEND chooses where they live.
file(default outside production):REIMBURSE_PLAN_STORE(JSON files) and langgraph'sMemorySaver. A restart loses every open question and every pending approval; that is what the demo has always done.postgres(default in production):agent/migrations/006_plan_state.sqlplus the checkpoint tables langgraph creates. One pool serves the store, the checkpointer and the mock office, so a suspended Neon endpoint costs one reconnect:check=ConnectionPool.check_connectionreplaces a dead socket when it is taken from the pool, andprepare_threshold=Nonekeeps the connection safe behind a pooler.
Both backends implement one PlanStore protocol (agent/reimburse_agent/office/store.py), and both mock
offices are idempotent per action_id (INSERT ... ON CONFLICT DO NOTHING in Postgres), so a resumed run
that replays an approved action finds the row it already wrote instead of sending a second email.
/decide and /answer never fail silently. When the run cannot be resumed — the intake started it but the
checkpointer has no thread for it, which is what a restart on the file backend looks like — the page says
"Rencana ini tidak bisa dilanjutkan setelah server dimulai ulang. Gunakan Jalankan ulang." with a
Jalankan ulang button, and nothing is executed. A decision for a plan an external driver owns
(agent/plan_demo.py) is still only stored, as before.
Two limits of this step: the expiry sweep still treats store().runs() as live (see
plans.startup_sweep), so a run whose plan survived but whose thread did not is not expired — it shows the
message above instead; and the checkpoint tables belong to langgraph, so checkpoint_setup has to run once
per database (below).
Backup: the plan tables are now the only copy of a pending request. Back them up with the rest of the
database (plan_requests, plan_versions, plan_decisions, mock_office_rows and the checkpoint*
tables, or Neon's point-in-time restore). The audit log remains the record of what was approved and
executed; the plan tables are what lets a half-finished request be finished.
Environment variables
Secrets come from the environment (or a git-ignored .env.local) and are never committed.
Variable | Default | Meaning |
| unset |
|
| audit DSN | Postgres DSN for |
| random per process | HMAC key for the login session cookie ( |
| unset | Postgres DSN for the audit log (secret; app role, Neon direct endpoint) |
| unset | JSONL audit log path outside production; unset means in memory |
| unset | Model key for the claim agent (secret) |
|
| Any OpenAI-compatible endpoint |
| 250000 | Model tokens per claim run |
| 600 | Seconds of agent work per run (paused while an approval is pending) |
| 900 | Seconds a pending approval waits before it is declined |
| 3 | Agent runs started per minute per web process (one live run at a time) |
| unset | Global LLM budget per Asia/Jakarta day, in USD. Unset outside production means no cap; unset in production aborts startup |
| unset | Token prices in USD per million tokens, read from the provider's pricing page at deploy time (never hard-coded) |
|
| Dev/test spend ledger; production uses the Postgres table from migration 005 |
| 30 | Minutes before an unanswered staged action of a dead run is expired |
| off | Employee Q&A box and |
|
| Where plans, requests and per-run mock office state are stored (file backend) |
|
| Where plan state and the graph checkpointer live: |
|
| Postgres DSN for plan state and checkpoints (secret; app role). One database can serve audit, spend and plan state |
| bundled synthetic files | Contact directory and operating rules for request intake |
|
| Upload extraction: |
|
| Where uploaded files are kept |
|
| Synthetic dataset used by the rule engine and the MCP server |
Plug into WorkBuddy (version A)
Copy
.workbuddy/mcp.jsoninto your project's.workbuddy/folder (or merge it into~/.workbuddy/mcp.json) and replace/ABSOLUTE/PATH/TO/reimburse-buddywith your clone path. On Windows use a path likeC:\\Users\\you\\reimburse-buddy.Restart WorkBuddy and confirm the
reimburse-buddyserver shows its 10 tools.Add the skill in
skills/claim-auditor/SKILL.mdto WorkBuddy's skills. It covers the gap-code-to-tool mapping, the fallback search strategy, how to explainignored_evidence, batching questions to the reviewer, and post-decision actions.Try: "Triage batch 2026-W43." or "Review claim CLM-2026-0110."
To point the server at another dataset, set REIMBURSE_BUDDY_DATA=/path/to/data in the env block.
Deployment notes: audit log
Production refuses JSONL and in-memory audit logs. When APP_ENV is prod or production
(case-insensitive), the audit log must be Postgres:
export APP_ENV=prod
export REIMBURSE_AUDIT_DATABASE_URL='postgresql://user:pass@db.example:5432/reimburse'
export REIMBURSE_AUTH_DATABASE_URL='...' # optional: defaults to the audit DSN (secret)
export REIMBURSE_DEMO_SESSION_KEY='...' # stable HMAC key (secret)
export LLM_DAILY_BUDGET_USD=2.00 # cap for the whole deployment, per Asia/Jakarta day
export LLM_PRICE_INPUT_MISS_PER_MTOK=... # from the provider's pricing page on the day you deploy
export LLM_PRICE_INPUT_HIT_PER_MTOK=...
export LLM_PRICE_OUTPUT_PER_MTOK=...REIMBURSE_AUDIT_DATABASE_URLis required and must start withpostgres://orpostgresql://; the connection must open at startup.If it is missing, is not a Postgres URL, or the connection fails, the process exits with
audit.AuditConfigError(a subclass ofaudit.AuditError). There is no fallback toREIMBURSE_AUDIT_LOG(JSONL) or the in-memory log.The web app checks this in its FastAPI startup hook, so it fails before serving the first request.
agent/investigate_worker.pychecks it before running, andaudit.default_log()enforces it too.Production startup also refuses a missing or unreachable user database (
app_users), a missingREIMBURSE_DEMO_SESSION_KEYand a missingLLM_DAILY_BUDGET_USDor token price (auth.AuthConfigError/spend.SpendConfigError), so a deploy cannot come up with every route open or able to spend without a cap.The plan state is checked last, right before the expiry sweep: with
REIMBURSE_PLAN_BACKEND=postgres(the production default) a missing DSN, an unreachable database or a missing table aborts startup withstore.StoreConfigErrornaming what to run. There is no fallback to the file store, which would lose exactly the state this backend keeps.Apply migrations
001-006before the first request:005_llm_spend.sqladds the spend ledger the daily cap reads and writes,006_plan_state.sqlthe plan state (plan_requests,plan_versions,plan_decisions,mock_office_rows;reimburse_appgets SELECT, INSERT and UPDATE, never DELETE). Apply them in numeric order:002-004each rewrite theaudit_events.event_typeconstraint, so a later one applied first would silently narrow it.Then create the plan-graph checkpoint tables once per database, as the owner role (langgraph owns their shape, so this is not a migration). It prints PASS/FAIL per step, masks the DSN, and grants
reimburse_appSELECT, INSERT, UPDATE and DELETE on thecheckpoint*tables only — the audit log and the spend ledger stay append-only:cd agent NEON_OWNER_DSN='postgresql://owner:...@ep-...neon.tech/neondb' \ uv run python -m reimburse_agent.office.checkpoint_setupcheckpoint_setupis safe to re-run:PostgresSaver.setup()skips the migrations it already applied.Outside production (
APP_ENVunset or any other value), behaviour is unchanged: JSONL viaREIMBURSE_AUDIT_LOG, otherwise a fresh in-memory log.
After deploy: audit log self-test
Use Neon's direct (unpooled) endpoint for REIMBURSE_AUDIT_DATABASE_URL and connect with the
application role (INSERT + SELECT only), never the owner role. psycopg connects with
prepare_threshold=None, so it never creates server-side prepared statements even if a pooled/PgBouncer
endpoint is used by mistake.
Install the optional extra in the agent image and run the self-test as a Cloud Run job (same environment as the web/agent service):
cd agent
uv sync --extra postgres
uv run --extra postgres python -m reimburse_agent.audit_selftestIt prints one PASS/FAIL line per check — append + list roundtrip, duplicate event_key, UPDATE/DELETE/
TRUNCATE denied on audit_events, no unmatched executions, and prepare_threshold=None — and exits
non-zero if any check fails. The DSN and its password are masked in the output. There is intentionally no
HTTP route for this: the web simulation has no admin authentication, so it is CLI/job only.
Project layout
web/reimburse_web/ version B web app (FastAPI): app.py, auth.py + users.py (login), intake.py, plans.py,
plan_state.py (plan store + checkpointer backend), qa.py, upload_flow.py,
agent_bridge.py (starts the agent worker), spend.py (daily LLM budget),
labels.py (Indonesian UI labels)
agent/
reimburse_agent/ graph.py (claim agent), plan_graph.py, office/ (plan, directory, rules, mock tools,
store.py (file + Postgres plan store), checkpoint_setup.py), audit.py, injection.py,
qa/, retrieval.py, audit_selftest.py
investigate_worker.py agent subprocess used by the web app
migrations/ Postgres schemas: audit log (001-004), LLM spend ledger (005), plan state (006)
eval/ synthetic eval suites and results
docs/ architecture and flow diagrams, request intake, employee Q&A, UI notes
src/reimburse_buddy/
server.py MCP server (FastMCP, stdio)
rules.py deterministic rule engine
decision.py decision note renderer
data_store.py JSON data access + mock tools
models.py result types, Decimal helpers
triage.py batch triage
demo.py scripted scenarios
data/ synthetic employees, policy, claims, reimburse mailbox (inbox.json), calendar, memos
skills/claim-auditor/SKILL.md
.workbuddy/mcp.json
tests/Human in the loop
The agent proposes, the reviewer confirms. triage_claims is read-only; cleared claims change status only
through approve_cleared_batch(..., reviewer_confirmed=true) with the exact cleared list. Final status
changes, rejections, above-cap approvals, escalations and decision notices always wait for the reviewer.
Gathering evidence happens automatically; asking an employee for a missing document also waits for the
reviewer's approval.
In a real deployment the evidence source is a dedicated reimburse@ mailbox that employees forward documents to, plus connectors for calendar, HR and ERP. The mock MCP tools here stand in for those.
License
MIT
Web app details (web/)
Pages
web/ is the version B app: a two-sided FastAPI app that imports the same core (data_store, rules,
decision, triage). The rule engine computes every amount. The reviewer can investigate by hand or start
the claim agent ("Selidiki dengan agent"), which runs agent/investigate_worker.py as a subprocess and
calls the tools over MCP; only that button needs a model key.
Employee portal (
/employee), drop-first: enter the trip, drop all receipts and boarding passes at once (/employee/new), then check one draft card per item. Each card field shows where it came from: extracted (ocr,pdf_textorvision, with a confidence; low confidence is highlighted) orself_declared(typed or changed by the employee). Cards can be edited, merged, split or deleted, and files moved between them. One submit creates the claim and forwards each file to the reimburse@ mailbox, linked to its item (files are kept locally inweb/uploads/). The claim page lists evidence per item and has an "Add file" button for gaps.Finance admin (
/admin): read-only triage, bulk approval only with an explicit confirmation, and a claim page with findings, gap codes, ignored evidence, mailbox search, calendar, memos, item decisions, a bilingual note preview, and a status change that needs the reviewer's tick. Each item shows provenance badges for its fields. Each uploaded file is cross-checked against its item; any failed rule marks itneeds_reviewand keeps it out of the rule engine. Rules:unreadable_file,self_declared_only(the file does not show the doc type or amount),mismatch_with_file(item amount, date or flight number differs from the file),file_date_outside_trip,doc_type_mismatch,implausible_amount(per-category caps, override withREIMBURSE_PLAUSIBLE_MAX),boarding_pass_mismatch(passenger or flight on the pass) andduplicate_file(same file hash on another item or claim). Extraction confidence is only a display hint. Boarding pass metadata is taken from the file, never from the item. The item stays out of the engine until the reviewer clicks "Accept evidence" or "Reject evidence" (logged). Until then the item shows "pending review", its claim cannot be bulk-approved or moved paston_hold, and the employee sees "Menunggu verifikasi finance / Awaiting finance review". "Reset demo data" restores the seed data.
uv sync
uv run uvicorn --app-dir web reimburse_web.app:app --port 8000
# open http://127.0.0.1:8000/adminFile extraction
Extraction is pluggable (web/reimburse_web/extract.py). It only drafts fields; the rule engine still decides
every amount. The default reads files locally with Tesseract plus regex rules; PDFs use pdftotext, falling back
to rasterising with pdf2image. Photos are read with two page layouts and the better-supported parse wins.
The amount is chosen from labelled lines (SUBTOTAL, PPN, SERVICE, DISKON, TOTAL, TUNAI, KEMBALI): cash, change,
discount and tax lines are never the total, and a TOTAL that matches subtotal + tax + service - discount or
cash - change is preferred; no consistent TOTAL means low confidence. Dates with a 2-digit year (03/10/26) are
read. An unreadable file becomes an empty card for manual input. Only PDF, PNG and JPEG are accepted.
sudo apt-get install tesseract-ocr tesseract-ocr-ind poppler-utils # Debian/Ubuntu
brew install tesseract tesseract-lang poppler # macOSA second OCR engine, RapidOCR (PaddleOCR models on onnxruntime, CPU only,
Apache-2.0), is optional. REIMBURSE_EXTRACTOR picks the extractor:
Value | What runs |
| Tesseract only |
| Tesseract and RapidOCR on every photo. If they read a field differently (doc type, date, amount, merchant, flight number, nights, passenger), or only one of them reads it, the Tesseract value is kept but shown as low confidence, and the file goes to |
| RapidOCR only |
| the vision extractor below |
PDFs with a text layer are read by pdftotext in every mode. If RapidOCR is not installed, dual and rapidocr
fall back to Tesseract. RapidOCR rebuilds lines from its word boxes and splits a date glued to a time
(03-10-202609:12).
uv sync --group rapidocr # adds rapidocr + onnxruntime (models ship with the package)
REIMBURSE_EXTRACTOR=dual uv run uvicorn --app-dir web reimburse_web.app:app --port 8000RapidOCR is not in the default dev group, so uv sync and the MCP server never install it. Install it with
uv sync --group rapidocr; without it the RapidOCR test skips and dual / rapidocr fall back to Tesseract.
On 10 clean synthetic images we generated (38 fields), Tesseract read 37/38 fields and RapidOCR 38/38; dual mode flagged the one Tesseract error and no other file. This shows the pipeline works end to end. It says nothing about accuracy on real receipts.
Reproduce with uv run python web/sample_uploads/measure_accuracy.py ocr rapidocr dual. Timings are mean seconds
per file after a warm-up file, measured on a cloud VM with an Intel Xeon CPU (no GPU), and vary by machine:
Extractor | Tuning set (7 files, 26 fields) | Held-out set (3 files, 12 fields) | Files flagged for review |
Tesseract | 25/26, 1.22 s/file | 12/12, 0.67 s/file | n/a |
RapidOCR | 26/26, 0.48 s/file | 12/12, 0.41 s/file | n/a |
Dual | 25/26, 1.58 s/file | 12/12, 1.08 s/file | 1 (tuning: the blurry taxi merchant, the one Tesseract miss) |
An optional vision extractor calls any OpenAI-compatible chat completions API. It is off by default and the key is read from the environment only:
export REIMBURSE_EXTRACTOR=vision VISION_API_BASE=https://api.example.com/v1 VISION_MODEL=your-model
export VISION_API_KEY=... # never commit thisSynthetic sample uploads live in web/sample_uploads/ (make_samples.py regenerates them with Pillow,
truth.json holds the expected fields). web/sample_uploads/heldout/ is a separate set of tricky layouts
(TOTAL (3 item), cash larger than the total, 2-digit year, TOTAL DISKON last) not used to tune the parser.
uv run python web/sample_uploads/measure_accuracy.py reports both (see the table above). Both sets are synthetic and small.
Known limits before any public deploy: shared in-memory state (claims, drafts, reviewer decisions and agent runs live in one process, so a restart loses them and a second instance sees a different demo); one shared demo password rather than accounts, so the app cannot tell two reviewers apart beyond the ID chosen at login and there is no way to revoke one person's access; local uploads (basic 5 MB / type limits only); and a self-declared field is only flagged, not verified. The daily LLM cap covers the investigation worker only: the optional Q&A model and the Jev judge are not metered (both are off by default and the demo runs neither). Dual mode detects OCR misreads, not edited receipts; PDFs, single-engine reads and near-duplicate files are not cross-checked yet (planned after the hackathon).
Known limitation: the cross-checks compare the item with what OCR read from the same file, so a convincingly forged or edited image still passes; OCR heuristics are tuned on synthetic receipts and need real, anonymised receipts (and a vision fallback) before production use.
Available Tools
14 toolscheck_claimA
Run the deterministic policy rules on a claim. ALWAYS use this for any money figure.
evidence_ids: IDs you found (inbox MSG-..., calendar EVT-..., memo MEMO-...).
human_decisions: {item_id: approve_full | approve_capped | reject}; works for any item category.
Items with status needs_evidence list gap codes (e.g. boarding_pass_missing, hotel_over_cap,
late_checkout_unjustified, duplicate_suspected). ignored_evidence explains IDs that did not count.
| Name | Required | Description | Default |
|---|---|---|---|
| claim_id | Yes | ||
| evidence_ids | No | ||
| human_decisions | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations at all, the description carries the full burden and does a fair job: it discloses that items can come back with status needs_evidence, enumerates real gap codes (boarding_pass_missing, hotel_over_cap, late_checkout_unjustified, duplicate_suspected), and explains that ignored_evidence reports IDs that did not count. The remaining gap is whether calling it mutates claim state or is a pure evaluation, which matters a lot for a tool accepting human_decisions.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The mandate is front-loaded in the first two sentences, then the per-parameter notes are compact and each carries non-obvious information. No filler sentences and nothing repeated from the schema.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 3-param tool with no annotations and no output schema, the description supplies parameter formats and describes key result fields (gap codes, ignored_evidence, needs_evidence status), which is close to sufficient. It still leaves open whether the call persists decisions and how to interpret an item that is neither approved nor flagged.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%, so the description must compensate, and it largely does: evidence_ids is given concrete ID formats per source (MSG-, EVT-, MEMO-), and human_decisions is given its allowed vocabulary (approve_full | approve_capped | reject) plus the note that it works for any item category. claim_id itself is left entirely unexplained, which is the one uncovered parameter.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ("Run the deterministic policy rules on a claim") and implicitly distinguishes itself from get_policy (which supplies the rules) and draft_decision (which drafts output). The word "deterministic" also tells the agent the result is reproducible, not a model opinion.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
"ALWAYS use this for any money figure" is an explicit, unambiguous triggering condition that an agent can act on directly. It stops short of naming which sibling to avoid in a specific scenario (e.g. draft_decision vs check_claim), so it is clear context without full alternative routing.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
draft_decisionB
Draft the final decision note for a reviewer. Re-runs check_claim, so numbers stay consistent. language: "id" (Bahasa Indonesia), "en" or "both" (default: Indonesian, then English).
| Name | Required | Description | Default |
|---|---|---|---|
| note | No | ||
| claim_id | Yes | ||
| language | No | both | |
| evidence_ids | No | ||
| human_decisions | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full behavioral burden. It usefully discloses that the tool re-runs check_claim so numbers stay consistent, and it explains the language fallback behavior. However, it never states that this is a write/draft-creation operation, whether it overwrites an existing note, whether it requires human_decisions, or what permissions are needed.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two short sentences and front-loads the core action before adding behavioral detail and the language note. The language specification is useful but jammed in as a trailing inline fragment, which slightly hurts scannability.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
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 explained, but this is a multi-parameter drafting operation with no annotations and zero schema description coverage. The description covers purpose, a check_claim rerun, and only one parameter, leaving the roles of claim_id, note, evidence_ids, and human_decisions, plus side effects of drafting, incomplete for correct invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate for five undocumented parameters. It only explains one: the language parameter, adding valuable meanings for 'id', 'en', and 'both' with the fallback default. It says nothing about claim_id, note, evidence_ids, or human_decisions, leaving most parameters unexplained.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource: 'Draft the final decision note for a reviewer.' That clearly distinguishes it from read-oriented siblings like get_claim or list_claims, and the mention of re-running check_claim further separates it from plain validation. It loses a point because it never explicitly says whether this persists a decision versus producing a draft artifact.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is only implied: 'for a reviewer' suggests this is invoked at decision time, and 'Re-runs check_claim' hints at a relationship with that sibling. There is no explicit when-to-use rule, no prerequisites (e.g., must the claim be triaged first), and no guidance on when to use check_claim or update_claim_status instead.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
find_approval_memoA
Find approval memos for an employee, optionally filtered by city and a date they must cover. Only memos with signed=true count as approval.
| Name | Required | Description | Default |
|---|---|---|---|
| city | No | ||
| date | No | ||
| employee_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full behavioral burden. It usefully discloses that only memos with signed=true count as approval, but it does not state read-only behavior, permission requirements, missing-result behavior, or matching rules.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two tight sentences with no wasted wording. The core purpose and the signed=true approval criterion are front-loaded and easy to scan.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description is reasonably complete for a simple lookup tool and does not need to explain return values because an output schema exists. Still, with no annotations and no parameter descriptions, the missing date format and matching semantics leave meaningful gaps for correct invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate. It maps all three parameters conceptually: employee_id identifies the employee, city is an optional filter, and date means the memo must cover that date; however, it gives no date format, city matching rules, or employee ID format.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the specific verb and resource: find approval memos for an employee. It also adds the important signed=true criterion, but it does not explicitly distinguish this tool from approval-related siblings such as request_approval.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by the first sentence: use this to retrieve approval memos for an employee, optionally narrowed by city and date. However, there is no explicit when-to-use/when-not-to-use guidance or comparison to alternative tools.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_calendarA
Get an employee's calendar events between two ISO dates (inclusive).
| Name | Required | Description | Default |
|---|---|---|---|
| date_to | Yes | ||
| date_from | Yes | ||
| employee_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full burden. It does add genuine behavioral detail beyond the schema — ISO date format and an explicit inclusive boundary — which prevents off-by-one range errors. However it is silent on permission requirements, timezone handling, pagination, and what an empty range returns, all of which matter for a calendar read with zero annotation coverage.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single sentence with no filler, front-loading the verb and resource before the range constraint. Every clause (ISO, inclusive) carries information the agent needs.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a three-parameter read tool with an output schema already documenting return shape, the description covers all inputs and the key range semantics. Remaining gaps are permission/prerequisite context and timezone assumptions, which are minor for this complexity level.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate, and it largely does: 'an employee's calendar events' maps to employee_id, 'between two ISO dates' maps to date_from/date_to, and 'exclusive/inclusive' clarifies the boundary semantics that the schema leaves undefined. Only the expected string format (e.g. YYYY-MM-DD) is still left implicit.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Names a specific verb ('Get'), a specific resource ('an employee's calendar events'), and the scope ('between two ISO dates, inclusive'). An agent knows exactly what the tool returns. It does not distinguish itself from any sibling, though no sibling covers calendars, so ambiguity risk is low.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by the resource — an agent needing an employee's schedule would select it — but the description gives no explicit when-to-use condition, no exclusions, and no mention of alternatives among the claim/policy/inbox siblings it might be confused with during a workflow.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_claimA
Get one claim with its trip, items and employee details.
| Name | Required | Description | Default |
|---|---|---|---|
| claim_id | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full disclosure burden. It does usefully reveal that the response is an expanded record (trip, items, employee), but says nothing about permissions, error behavior for missing/inaccessible claims, or response size.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single front-loaded sentence with zero filler; the payload contents are appended efficiently.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
A simple one-parameter read with no output schema, and the description compensates by naming the returned sub-resources. Only the claim_id format and failure behavior are missing, which is minor for this tool shape.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Only one parameter and schema description coverage is 0%, so the schema adds no semantics for claim_id. The description confirms the parameter identifies a claim but gives no format, source, or example beyond that.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Get one claim') and clarifies the payload includes trip, items and employee details. 'One claim' implicitly separates it from the sibling list_claims, but no sibling is named explicitly.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is only implied: the single required claim_id signals a lookup by identifier. The description never says when to prefer this over list_claims or check_claim, nor what happens if the claim_id is unknown.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_outboxA
(Mock) Show sent messages, approval requests and status changes made in this session.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden. It does add real context — the '(Mock)' marker signals demo data and 'in this session' bounds the scope — but it never states the operation is read-only, how results are ordered, or what a message entry contains.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single efficient sentence with the mock and session-scope qualifiers front-loaded and no wasted wording.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a no-argument read tool with no output schema, stating what categories of items appear (messages, approvals, status changes) is close to sufficient. Return shape and ordering are the only gaps.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Zero parameters, so the baseline is 4. Nothing in the schema needs interpretation and the description correctly implies the tool takes no arguments.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Specific verb ('Show') plus a concrete resource set: sent messages, approval requests, and status changes. The 'sent' framing implicitly separates it from search_inbox, though no sibling is named explicitly.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The 'made in this session' clause implies when this is useful (reviewing this session's outbound activity), but there is no explicit when-to-use guidance, no mention of alternatives like search_inbox, and no stated prerequisites.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_policyA
Get the company travel policy (caps, per diem, receipt rules).
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries full burden. 'Get' implies a read operation, and it lists returned categories, but it omits explicit statements about safety, permissions, or caching. Some behavioral context is conveyed, but not enough for a higher score.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single, front-loaded sentence that efficiently conveys the core details without waste. Every part earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a zero-param getter with no output schema, the description sufficiently indicates the return content (caps, per diem, receipt rules). It could mention format (e.g., text vs. structured), but it is largely complete for its simplicity.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters, and the description does not need to explain any. The baseline for zero-param tools is 4, and no additional parameter information is required.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb ('Get') and resource ('company travel policy') with parenthetical detail on contents, making the tool's function clear. It does not explicitly differentiate from siblings, but the distinct resource makes selection straightforward.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by the tool name and resource, but the description provides no explicit when-to-use guidance or alternatives. An agent can infer this is for retrieving policy information, but no conditions or exclusions are stated.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_claimsB
List expense claims with the given status (default: submitted).
| Name | Required | Description | Default |
|---|---|---|---|
| status | No | submitted |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full behavioral burden. It implies a read-only listing operation but does not disclose pagination, ordering, permissions, or whether all claims are returned when status is omitted beyond the stated default.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single compact sentence with the purpose and default front-loaded. There is no wasted text.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
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 explained. Still, the missing allowed status values and lack of sibling routing guidance leave meaningful gaps for a list tool with a filter parameter.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The single status parameter has 0% schema description coverage. The description repeats the default and calls it a status filter, but does not list allowed status values, which is essential for correct invocation.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource: 'List expense claims.' The status filter and default are clear, so an agent can distinguish this from singular retrieval tools like get_claim, though it does not explicitly contrast with siblings such as triage_claims.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by the name and the status filter, and the default behavior is stated. However, there is no explicit guidance on when to use this instead of get_claim, check_claim, or triage_claims.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
request_approvalC
(Mock) Ask the claimant's manager to approve one item. Returns a pending request.
| Name | Required | Description | Default |
|---|---|---|---|
| reason | No | ||
| item_id | Yes | ||
| claim_id | Yes | ||
| manager_id | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden, and it only discloses that the result is a pending request. It doesn't say whether a notification is sent to the manager, whether calling twice creates duplicate requests, whether the request can be rescinded, or what permissions are needed, all of which matter for a mutating approval tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two tight sentences with the mock caveat and action front-loaded, followed by the return value. Nothing is wasted, though the brevity comes at the cost of the parameter detail noted above.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
A four-parameter mutation tool with no annotations, no output schema, and 0% schema coverage demands far more than two sentences. Missing side-effect disclosure, parameter meanings, and sequencing relative to sibling approval tools leave the definition well short of what an agent needs to invoke it reliably.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must explain the four parameters, but it only loosely implies manager_id ('the claimant's manager') and item_id ('one item'). claim_id is never mentioned, and reason is completely unaddressed, leaving half the inputs opaque.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource: asking the claimant's manager to approve one item. It clearly distinguishes this from read-only siblings like get_claim or find_approval_memo, though it doesn't explicitly contrast with the closest sibling draft_decision. The '(Mock)' marker also signals this is a demo-environment operation.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No guidance on when to call this versus draft_decision or find_approval_memo, and no stated preconditions (must a claim exist, must a memo be found first?). The only implied context is that an approval flow exists, which an agent has to infer from the sibling names.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
reset_demoB
Restore all demo state: removes delivered replies, clears outbox, approval requests and status changes.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full behavioral burden. It helpfully discloses what is destroyed (delivered replies, outbox, approval requests, status changes), but it does not say whether the reset is irreversible, what permissions are required, or whether any other demo state is affected.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The definition is a single efficient sentence with the action and affected state front-loaded. Every listed item earns its place by clarifying the reset scope.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a zero-parameter reset tool with no output schema, the description covers the main effects but omits important behavioral context such as irreversibility and required permissions. With no annotations supplied, it should do more to warn an agent before a destructive reset.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters, so there is no parameter semantics to explain; the baseline for zero-parameter tools is 4. The description appropriately does not waste space on nonexistent inputs.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses the specific verb "Restore" with a clear scope, "all demo state," and enumerates the affected artifacts: delivered replies, outbox, approval requests, and status changes. It is easy to distinguish from sibling read/list tools, though it does not explicitly contrast itself with other mutation tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description does not state when to use this reset versus alternatives, nor does it give prerequisites or exclusions. The destructive nature is implied by "removes" and "clears," but there is no explicit guidance about when this should be invoked.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_inboxA
Search an employee's mailbox (mail to or from them). Ranked OR search over subject, body, sender and attachment names; returns the top 5 as summaries (id, subject, snippet, attachments). Pass promising IDs to check_claim; it reports IDs that match no item as no_matching_item.
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | ||
| employee_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
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 ranked OR matching, the fields searched, a hard top-5 cap, and the returned summary fields. It omits permission/scope caveats and what an empty default query returns.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three tight sentences, front-loaded with scope, then behavior, then next step. No filler and every clause carries information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
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 did not need spelling out, yet the description still gives the useful shape of results and the no_matching_item linkage. The main missing piece is the empty/optional query behavior, which matters for correct invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%, so the description must compensate. It defines what the query string matches against (subject, body, sender, attachment names) and the OR/ranking semantics, but says nothing about employee_id format or the fact that query is optional with an empty default — a real gap for a 0%-coverage schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Search an employee's mailbox') and scopes it ('mail to or from them'), which implicitly separates it from get_outbox. It does not name a sibling explicitly, so the differentiation is inferred rather than stated.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives a clear workflow: search here, then pass promising IDs to check_claim. It does not say when to prefer this over get_outbox or what to do when zero results come back, so alternatives are only partly covered.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
send_messageA
(Mock) Email an employee, e.g. to ask for a missing receipt. Goes to the outbox. Scripted replies arrive in that employee's mailbox; check with search_inbox.
| Name | Required | Description | Default |
|---|---|---|---|
| to | Yes | ||
| body | Yes | ||
| subject | No | ||
| claim_id | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations at all, the description carries the full load and does disclose genuinely non-obvious behavior: it is a mock, messages route to the outbox rather than the recipient, and replies are scripted later. It stops short of stating permissions, whether resending duplicates a message, or how claim_id affects delivery, which are gaps for a write tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short sentences, zero waste, and the leading parenthetical "(Mock)" plus the routing statement are front-loaded before the follow-up hint. Every sentence contributes distinct information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a four-parameter write tool with no annotations, no output schema, and 0% schema coverage, the description adequately covers delivery semantics and the reply loop but leaves the meaning of subject and claim_id, error conditions, and return values entirely unaddressed.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must explain all four parameters, and it does not. "Email an employee" maps only loosely to `to`, and `subject` and especially `claim_id` (which implies linking the message to a claim) are never explained anywhere.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource ("Email an employee") and gives a concrete use case (asking for a missing receipt), which tells an agent exactly what the tool does. It also names where the message lands (outbox) and points to search_inbox for the follow-up, so it is distinguishable from sibling read tools without opening a schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides a clear usage context ("e.g. to ask for a missing receipt") and an explicit follow-up path: replies arrive in the employee's mailbox, so check with search_inbox. It lacks an explicit when-not clause or a statement about alternatives such as draft_decision, but the operational guidance is concrete.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
triage_claimsA
Check every submitted claim in a batch (e.g. "2026-W43"). Returns auto-cleared claims, exceptions with their gap codes, and a time-saved estimate. Investigate only the exceptions.
| Name | Required | Description | Default |
|---|---|---|---|
| batch | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full behavioral burden. It usefully describes the return shape (auto-cleared claims, exceptions with gap codes, time-saved estimate) and the follow-up workflow, but it is ambiguous about whether 'auto-cleared' means claims are mutated in the system, and it says nothing about permissions or side effects.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short sentences, all front-loaded with the core action first, then the return value, then the workflow instruction. No sentence is wasted.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a batch tool with no output schema and no annotations, the description covers the essentials: what it operates on, what comes back, and what the agent should do next. The main remaining gap is clarity around whether the operation mutates claim state.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema has 0% description coverage for the single 'batch' parameter, so the description must compensate. It does so partially by providing an example format ('2026-W43'), but it does not explain what the empty/default value means or how batches are identified beyond that one example.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource ('Check every submitted claim in a batch') and distinguishes itself implicitly from single-claim siblings like check_claim and get_claim by operating at batch level. It stops short of naming an alternative tool explicitly, which keeps it from a 5.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
'Investigate only the exceptions' gives a clear follow-up rule after calling the tool, which implies the triage use case. However, it never states when to choose this over check_claim or list_claims, and there are no explicit exclusions or prerequisites.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
update_claim_statusC
(Mock) Set claim status: submitted, on_hold, approved, partially_approved or rejected.
| Name | Required | Description | Default |
|---|---|---|---|
| note | No | ||
| status | Yes | ||
| claim_id | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full behavioral burden. The '(Mock)' prefix is honest about the tool being a demo stub, but nothing is said about whether status transitions are validated, whether the change is reversible, what permissions are needed, or what note does — major gaps for a mutation tool with zero annotation coverage.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single front-loaded sentence with no filler; the enumeration of statuses is the most useful information and it is placed immediately after the verb.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a mutation tool with no annotations, no output schema, and 0% schema description coverage, the description should explain side effects, the role of the note field, and what is returned on success or invalid input. Only the status value set is covered, leaving the definition substantially incomplete.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate, and it partially does by enumerating the five valid status values that the schema does not constrain with an enum. However, claim_id (required) and note (optional, default empty) get no explanation at all, leaving two of three parameters undocumented.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description names a specific verb+resource (set claim status) and enumerates the exact accepted values, so an agent knows precisely what the tool does. It doesn't explicitly differentiate from siblings like check_claim, triage_claims, or draft_decision, which prevents a 5.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no statement of when to use this versus alternatives such as check_claim or triage_claims, and no preconditions (e.g. whether a claim must exist, whether a decision must be drafted first). The agent must infer usage entirely from the tool name.
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.
14 tool updates
v0.1.0- First observed
check_claim - First observed
draft_decision - First observed
find_approval_memo - First observed
get_calendar - First observed
get_claim - First observed
get_outbox - First observed
get_policy - First observed
list_claims - First observed
request_approval - First observed
reset_demo - First observed
search_inbox - First observed
send_message - First observed
triage_claims - First observed
update_claim_status
TDQS
Scored across 14 tools
Each tool targets a distinct resource or action: listing/reading claims, reading policy/mail/calendar/memos, running rules, triaging, drafting, and mutating status. The only mild overlap is check_claim vs triage_claims vs draft_decision (single claim, batch, and note drafting that re-runs rules), but the descriptions clearly delineate when to use each.
All names are snake_case verb_noun compounds, giving a predictable pattern. Verb choice varies a bit (get_/list_/search_/find_/check_), but each verb carries a real distinction rather than being random.
14 tools sits comfortably in the well-scoped range, and every tool maps to a step in the reimbursement review workflow (evidence gathering, rule checking, approval, decision, demo reset). No tool feels redundant or filler.
The surface covers the review lifecycle end to end: policy, evidence (inbox, calendar, memos, outbox), rule checking, triage, drafting, approvals, status updates, and reset. The main gap is claim intake/editing — there is no create_claim or item-update tool, so the agent can only review pre-existing claims.
Maintenance
Related MCP Connectors
Receipt ingestion, Gmail capture, expense analysis, and reviewed reports for AI-agent workflows.
Evidence-readiness MCP server: validate, audit, and score briefs, memos, and evidence packs.
Governed retail, FMCG, and CPG operational tools: runs, cases, approvals, audit-ready execution.
Reconcile business documents, inspect exact differences and evidence, and export reports.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceEnables employee eligibility checks, trip validation, and reimbursement calculations for corporate travel policies.-
- AlicenseAqualityCmaintenanceEnables searching fictional policy documents and assessing expense claims against deterministic rules via MCP, returning structured assessments and citations without an approval tool.2MIT
- AlicenseNot gradedqualityCmaintenanceEnables natural-language-driven purchase-to-pay workflows against mock procurement and payment APIs, including invoice matching, exception handling, human approvals, and idempotent payment scheduling with a tamper-evident audit log.MIT
- AlicenseBqualityBmaintenanceEnables agents to review local travel expenses against fictional policy rules, preview and build immutable XLSX/PDF/DOCX/ZIP claim packages, and track submission, approval, and reimbursement status. Runs entirely offline with SQLite storage, receipt imports, exports, and portable backups.6MIT