closeloop-mcp
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
CloseLoop
MCP-native month-end reconciliation for Indian finance teams: gateway settlements ↔ bank statement ↔ ledger, with deterministic matching you can audit and an AI layer that is only allowed to read.
Results
Held-out (8 seeds × 25,000 payments, unseen noise variants included): 90.5% of bank credits linked to exactly the right settlements (n=1467), 1 wrong link; corrupted UTRs recovered 100.0% (n=352), counting correct suggestions a human still confirms.
Batch credits: 76.2% decomposed correctly (contiguous 100.0%, non-contiguous 50.0%), 23.8% sent to a human as ambiguous; missing bank credits 3 detected / 16 deferred / 0 missed.
Exceptions: macro recall 91.8%, precision 94.1% across 13 types; 1,18,947 records/s; full eval 9.5 s. Details in EVAL.md.
Related MCP server: fincon
Why this exists
[PLACEHOLDER — the author will write this section.]
Architecture
flowchart LR
subgraph SRC["Source data (insert-only, or a read-only mount)"]
P["Payments + refunds"]
S["Gateway settlements + items"]
B["Bank statement"]
L["Ledger export (Tally / Zoho style)"]
end
subgraph ENG["Deterministic engine (source of truth)"]
direction TB
E1["1 · Items: fee + 18% GST on fee"] --> E2a["2a · Exact UTR"] --> E2c["2c · Fuzzy UTR (edit distance)"] --> E2b["2b · Batch: contiguous run → proof-only subset-sum"] --> E3["3 · Bank ↔ ledger + period tie-out"] --> E4["4 · T+n business-day windows"]
end
SRC --> ENG --> ST[("State: runs · matches · exceptions · audit (append-only)")]
AI["Constrained AI layer: narration hints · explanations · draft journals"]
ST -- "read-only evidence" --> AI
AI -- "grounded candidate tokens only" --> E2c
MCP["MCP server (stdio): 9 tools · resource · prompt"] --> ENG
MCP --> ST
C["Claude Desktop / any MCP client"] <--> MCP
H["Human reviewer"] -- "two-step approve / escalate" --> MCP
T[("truth.db (eval only)")] -.-> EV["pnpm eval"]Try it in 60 seconds
npx -y closeloop-mcp check # seeds a demo dataset, reconciles it, prints the summaryUntil the package is on npm, run it from a clone: pnpm install && pnpm build && npx closeloop-mcp check.
Add it to Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json), then restart Claude Desktop:
{
"mcpServers": {
"closeloop": { "command": "npx", "args": ["-y", "closeloop-mcp"] }
}
}From a clone, use absolute paths instead. Claude Desktop doesn't inherit your shell's PATH:
{ "mcpServers": { "closeloop": { "command": "/opt/homebrew/bin/node", "args": ["/absolute/path/to/closeloop/packages/mcp/dist/closeloop-mcp.js"] } } }Then ask:
"What's unreconciled in October and why?"
"Break down the largest unmatched bank credit."
"Break down bank credit bnk_000080."
Or pick the month_end_review prompt. DEMO.md lists three demo cases from the October data (a 15-settlement contiguous payout, a corrupted-UTR fuzzy match and a match conflict), picked by code from an actual run.
Dashboard
Live: https://closeloop-ivory.vercel.app: a read-only snapshot of the October demo run.
apps/dashboard is a read-only static site built from one recorded snapshot of the October demo run (pnpm snapshot). It shows:
the month-end summary;
the exception queue;
exception detail with evidence, rule trace, source-labelled explanations, draft journals and audit history;
the batch decomposition visual;
how it works.
It has no database, no API routes, no LLM calls at runtime and no secrets. Deployment steps for Vercel are in apps/dashboard/README.md.
Design principles
Keep financial truth deterministic; use AI only where data is messy, and only from read-only evidence.
Deterministic engine decides | AI layer may |
every match, exception, exposure class and tie-out | suggest UTR tokens that literally appear in a narration (the scorer still decides) |
fee/GST arithmetic, in integer paise only | explain an exception from a read-only evidence bundle, citing only ids in that bundle |
when a batch decomposition is provably unique | propose a journal entry that must balance to the engine's amount (draft, |
Money is integer paise. It is validated by zod and stored in SQLite
STRICTinteger columns. Rupee strings are formatted for display only.Financial tables are insert-only. Database triggers enforce this, and in customer deployments the source data is attached read-only. No tool, model or human action can edit a payment, settlement, bank line or ledger entry.
Proof before auto-match. A batch credit auto-matches only when the subset is proven unique (meet-in-the-middle, or budgeted branch-and-bound), or when it is a unique contiguous run under a stated payout-model assumption. Everything else goes to a human with ranked candidate sets.
Humans confirm explicitly. Approve and escalate are two-step: a preview with a 5-minute single-use token, then token + reviewer name. Every run, investigation, draft, approval and escalation lands in an append-only audit trail with the actor (system / llm / human).
Untrusted text stays data. Narrations and ledger text reach a model only inside
<untrusted_data>delimiters. The real guards are schema validation, evidence-id checks, a review-policy floor the model can't lower, and the rule that AI output can never create a match.Honest evaluation. Ground truth lives in a separate database the matcher never receives. Held-out data includes noise variants the rules weren't written around. Noise rates are realistic and never inflated. Every published number is generated.
Evaluation
pnpm eval generates 8 independent datasets (seeds fixed up front in config/generator.json). It reconciles the dev and held-out splits of each on isolated in-memory databases, scores them against ground truth, and writes EVAL.md and the Results block above. EVAL.md reports:
match rate by kind
precision/recall per exception type, with n and the per-seed spread
batch results by contiguity and by daily volume
fuzzy recovery by mechanism
missing credits as detected / deferred-to-human / missed
exposure by class
throughput
categorised held-out misses
CI runs a 5k-payment smoke version on every push.
MCP server
Tool | What it does | Annotations |
| Matched counts by kind, exceptions by type, exposure per class (never summed), bank-vs-books tie-out | writes a run |
| Largest first, 20 per page with a cursor, totals for the whole filtered set | read-only |
| Records, rule trace, candidate sets with totals, audit history | read-only |
| Root cause and recommended action from read-only evidence (LLM or deterministic) | writes audit |
| The settlement set behind one bank credit, or the ranked candidate sets | read-only |
| Balanced correcting entry, | writes draft |
| Two-step human approval: preview + token (5 min), then token + reviewer | destructive |
| Two-step human escalation with a note | destructive |
| Append-only audit events, paginated | read-only |
Also: resource closeloop://summary/latest and prompt month_end_review({month?}). Every amount comes as raw amount_paise plus a pre-formatted amount_inr (₹12,33,701.15), and every response starts with a one-line summary, so the model never does arithmetic. On first run with an empty database the server seeds the reproducible demo dataset into ~/.closeloop/closeloop.db (override with CLOSELOOP_DB). Subcommands: closeloop-mcp check (one reconciliation, summary to stderr) and closeloop-mcp seed PATH (write the demo data as a standalone source DB).
MCP Inspector:
npx @modelcontextprotocol/inspector node packages/mcp/dist/closeloop-mcp.js # web UI
npx @modelcontextprotocol/inspector --cli node packages/mcp/dist/closeloop-mcp.js \
--method tools/call --tool-name decompose_bank_credit --tool-arg bank_line_id=bnk_000080LLM setup (optional)
CloseLoop works fully with no API key; every AI step has a deterministic fallback. Any OpenAI-compatible chat-completions endpoint works. Put the settings in .env (git-ignored), or in ~/.closeloop/.env for Claude Desktop:
Provider |
|
|
Groq |
|
|
Google AI Studio |
|
|
OpenRouter |
| any model id, e.g. |
LLM_BASE_URL=https://api.groq.com/openai/v1
LLM_API_KEY=your-key
LLM_MODEL=openai/gpt-oss-120b
LLM_REASONING_EFFORT=low # reasoning models only; keeps answers within the token budgetBudgets are in config/ai.json; narration parsing is sequential and capped per run.
Failure handling:
An HTTP 429 gets one retry, honouring
Retry-Afterand capped at 5 s.A second 429, a timeout, a malformed response or a schema failure falls back to the deterministic path. The reason is recorded on the output and in the audit trail.
JSON mode is requested. If a provider rejects it, CloseLoop retries without it and extracts the JSON from the text.
Deploying inside a customer environment
The server is built to run next to the data, not the other way round.
docker compose up # demo: seeds ./data/source.db, then starts the server
docker compose run --rm -T closeloop # attach an MCP client over stdioRead-only data mounts. Point
CLOSELOOP_SOURCE_DBat the customer's export, mounted:ro. CloseLoop attaches it read-only and keeps its own state (runs, exceptions, audit, drafts) in a separate database on a writable volume (CLOSELOOP_DB). Writes to source data fail at the storage layer, not just in code. The export must be a SQLite file in rollback-journal mode (not WAL) to be readable from a read-only mount.Hardened container. Non-root user (uid 10001), read-only root filesystem with a tmpfs
/tmp, nothing listening on a port. The MCP transport is stdin/stdout.No outbound calls when the LLM is disabled. With no
LLM_*variables the code makes no network requests, and the default compose service runs withnetwork_mode: noneto guarantee it. The LLM is opt-in through environment variables only (docker compose --profile llm up closeloop-llmreads.env).What leaves when the LLM is on. Explanation and drafting prompts contain the evidence records, including narrations. To keep data inside the customer's network, point
LLM_BASE_URLat a self-hosted OpenAI-compatible endpoint (vLLM, Ollama, an internal gateway).Connecting a client to the containerised server:
{ "mcpServers": { "closeloop": { "command": "docker", "args": ["run", "-i", "--rm", "--network", "none", "-v", "/path/to/export:/data:ro", "-v", "closeloop-state:/state", "-e", "CLOSELOOP_SOURCE_DB=/data/source.db", "closeloop-mcp:local"] } } }
Lessons from deployment
Running the AI layer against a real free-tier provider (Groq, openai/gpt-oss-120b) surfaced problems that mocked tests didn't:
Reasoning models spend the token budget on thinking. With a 200-token cap the model used every token reasoning and returned an empty answer. Groq then rejected it under JSON mode as
json_validate_failed(HTTP 400). Fix: sendreasoning_effort: lowwhen configured, raise the budgets, and classifyjson_validate_failedas a malformed answer rather than a bad request. The deterministic fallback kept every output correct in the meantime, which is the point of having one.Free-tier rate limits are per minute, and narration parsing ate them. The tier allowed about 8k tokens a minute (from the response headers). Four parallel narration-parsing calls exhausted it, so the explanations that followed all fell back with
rate_limited. Fix: narration parsing runs sequentially with a per-run call budget (config/ai.json). The one-retry-then-fallback policy means a busy provider degrades output quality, never availability.LLMs will do arithmetic on paise if you let them. An early explanation added up raw paise amounts in its prose. Fix: prompts carry pre-formatted
*_inrstrings with an instruction never to compute, and every MCP response includes pre-computed totals, so the client model never has to add anything either.Model catalogues churn. The model in the first draft of these docs had been retired. Check the provider's live
GET /modelslist before configuring one (that's howopenai/gpt-oss-120bwas chosen); the examples above are only examples.
Known limitations
Synthetic data only. The generator and the matcher were written by the same person. Held-out noise variants the rules weren't written around reduce, but don't remove, that bias.
Narrow scope. Single currency (INR), one Razorpay-style gateway model, one bank account, no multi-entity books.
No real connectors yet: no bank-statement parsers, gateway settlement/payout API, or Tally/Zoho Books sync. Gateway payout reports would resolve most batch ambiguity, which amount-only decomposition can't (see EVAL.md).
The contiguous-batch rule is an assumption. It holds for hold-and-release and scheduled payouts; when it's wrong, the match is wrong. EVAL.md counts how often.
Approximate bank calendar. The holiday list is illustrative and covers 2026 only.
Single node, no authentication. SQLite on one machine. Reviewer names are self-asserted, and confirmation tokens live in memory, so a restart forgets them.
Explanations are checked for structure, not truth. Schema and cited evidence ids are validated; factual accuracy is not.
The dashboard is a static snapshot. Approvals and escalations happen in the MCP agent, not on the site.
Development
pnpm install
pnpm typecheck && pnpm lint && pnpm test
pnpm gen --seed 42 --payments 5000 # demo data → ~/.closeloop/closeloop.db (+ truth.db)
pnpm recon --from 2026-10-01 --to 2026-10-31
pnpm eval # full eval → EVAL.md (+ README Results)
pnpm eval --payments 5000 # CI smoke
pnpm demo:cases # regenerate DEMO.md
pnpm build && pnpm pack:smoke # bundle, then clean-install test of the npm tarballRepo layout: packages/core (generator, matcher, AI layer, audit, eval), packages/mcp (server and npm package), scripts/, config/. The design and every decision are in PLAN.md; project conventions are in CLAUDE.md.
Credits
Design principle of deterministic truth with constrained AI inspired by RazorRecon (https://github.com/Ragh234/RazorRecon). CloseLoop is an independent implementation focused on batch decomposition, fuzzy matching and multi-source ledgers.
This server cannot be deployed
Maintenance
Related MCP Connectors
Read-only finance and operations controls for AI agents with evidence and safe next actions.
Documents in, reconciled double-entry books out. Statements, invoices, receipts, matched and posted.
Governed rebate & revenue close (ASC 606/705/842): explain, ingest, compute, trace to source.
Double entry accounting run by your own AI: books, VAT figures, invoices, month close (UK, DE, AT).
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceAn accounting-ops agent that reconciles payments against open orders, auto-books provably safe payments through a deterministic policy gate, and escalates exceptions to a human queue with audit trails.MIT
- FlicenseBqualityBmaintenanceEnables AI assistants to perform financial reconciliation with a deterministic proof engine: intake files, match transactions, verify proofs, resolve exceptions, and sign off on balanced journals under the user's authority.21-
- AlicenseNot gradedqualityCmaintenanceEnables AI IDEs to perform financial reconciliation through five native tools for matching invoices, detecting bundled payments, and handling bank fee variances.MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to propose structured payment intents, apply deterministic policy, require human approval, and record tamper-evident audit trails without connecting to live payment rails.MIT