Innkeeper
An MCP server that automates night audit for small hotels by reconciling three ledgers (PMS, card processor, OTA) using AI, adjudicating mismatches with evidence, and cryptographically signing the books.
README
<div align="center"> <img src="docs/icon.svg" alt="Innkeeper" width="144" height="144" /> <h1>🛎️ Innkeeper</h1> <p><em>The night audit finally sleeps — an autopilot night auditor that reconciles three ledgers, adjudicates every mismatch with cited evidence, and signs the books.</em></p> <img src="docs/readme-hero.svg" alt="Innkeeper — the night audit finally sleeps" width="100%" />
<br/><br/>
<br/>
An autopilot night auditor for small hotels. Every night at 2 AM it reconciles the property-management ledger against the card processor's settlements and the OTA's PDF statement, adjudicates every mismatch with cited evidence and competing hypotheses, auto-clears the confident ones inside an expected-loss policy gate, and queues only the material discrepancies for the owner's coffee — then signs the night's books cryptographically.
A husband-and-wife inn runs 14 rooms; every night one of them stays up past midnight squinting at three statements that disagree by $6.67 — because the last time they let it slide, it was $2,300 by month-end.
Innkeeper is a CLI + MCP servers, not a hosted web app — but the audit engine is deployed live on Alibaba Function Compute (
/health·/verify·/run, offline, zero keys — see ☁️ Deployed). Everything above also runs locally, offline, with zero API keys; seeDEMO.mdfor the exact copy-paste script.
⚠️ MOCK SYSTEMS — read this first
The three source systems (PMS, card processor, OTA) are mocks, shipped in this repo as three MCP-compatible servers (
mcp/) that read from a committed, deterministically seeded 14-room month. This is the honest path to realism: the data is a coherent month with planted, ground-truth-labeled discrepancy archetypes and real reportlab-rendered OTA PDFs.Which transport runs, precisely: the offline demo and every test run on
FakeQwen— a deterministic stand-in that computes each verdict from the mismatch's own arithmetic and memos (the same signalsqwen3.7-maxreasons over) and parses the committed statement sidecar; it makes no VL call and never reads the ground-truth labels. The realqwen3-vl-plustwo-pass read of the 8-pt PDF andqwen3.7-maxadjudication run only under--livewith aDASHSCOPE_API_KEY— a real, key-gated path you can run yourself (see the Run the real Qwen path (--live) section below); the reportlab PDFs exist so that path has a genuine document to read.No live PMS/processor/OTA is contacted, and Innkeeper is decision support with signed evidence, not an accounting system of record. The graded core runs offline with zero API keys.
🚀 Quickstart (offline, no keys)
python3.12 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
innkeeper seed --nights 30 # deterministic month + rendered OTA statement PDFs
innkeeper run --night 2026-07-04 # fetch → extract → match → adjudicate → gate → signed close
innkeeper replay --night 2026-07-04 # re-derive byte-identical, zero keys (invariant I4)
innkeeper verify-chain # recompute every root, check every signature
innkeeper bench # 277/281 auto-cleared · 0 false clears · accuracy 0.9964
pytest -q # 404 passed
The full demo script is in DEMO.md.
☁️ Deployed on Alibaba Function Compute
Innkeeper is deployed live on Alibaba Function Compute (FC 3.0, managed
python3.10 runtime — no container, no ACR) at
https://innkeeper-temfmzpqug.ap-southeast-1.fcapp.run. Every endpoint runs
offline on the committed fixtures/ + ledger/ — zero keys, no network, no
vision model — so a judge can verify the signed books straight from a browser:
| Endpoint | What it does |
|---|---|
GET /health |
liveness |
GET /verify |
re-verifies the 30 signed closes in the cloud — roots, Ed25519 signatures, evidence sha256 bindings, a byte-identical replay of 2026-07-04, and a one-byte tamper caught (I2 / I3 / I4) |
GET /run?night=2026-07-04 |
reproduces one deterministic FakeQwen night audit — 39 txns, 12 mismatches, 11 auto-cleared, 1 queued, Merkle root d175694c… |
The live qwen3-vl-plus / qwen3.7-max path is wired and verified with a real
DashScope smoke call, and stays key-gated behind --live (next section); the
deployed endpoints are the offline-deterministic engine, byte-for-byte
replayable. Full request/response transcript:
docs/proof/DEPLOY_PROOF.md. The 02:00 timer
trigger is configured in infra/fc/s.yaml; a captured
console recording of the cron firing on its own is the one remaining artifact.
🔑 Run the real Qwen path (--live)
The offline demo above proves the whole pipeline with zero keys. To exercise
the actual Qwen Cloud models on the real reportlab-rendered statement — the one
step this project can't fake — add your key and the live extra:
pip install -e ".[live]" # openai + pypdfium2 rasterizer
export DASHSCOPE_API_KEY=sk-… # dashscope.console.aliyun.com/apiKey
innkeeper run --night 2026-07-04 --live # SAME pipeline, real models
Under --live the extractor is LiveQwen: it rasterizes the committed 8-pt
OTA PDF and calls qwen3-vl-plus twice (temperatures 0.0 / 0.4) — agreement
becomes confidence, disagreement escalates (I5) — and the mismatch residue is
adjudicated by qwen3.7-max + thinking returning typed JSON the gate
computes over. The evidence citations are bound to the on-disk sha256 hashes
either way, so a live verdict can never cite a hash that doesn't resolve. This
is the single key-gated path; nothing else needs a network.
🏗️ The pipeline
flowchart LR
TMR["Function Compute<br/>(deployed · 02:00 timer configured)"] --> RUN["audit run"]
RUN -->|"MCP tools"| M["3 mock servers — PMS · processor · OTA-PDF"]
M --> EX["extract: qwen3-vl-plus two-pass + bbox (I5)"]
RUN --> MT["deterministic 3-tier matcher<br/>clears ~77% — zero model calls"]
EX --> MT
MT -->|"mismatch residue"| ADJ["adjudicate: qwen3.7-max + thinking → typed Verdict"]
ADJ <--> T{{"Qwen transport"}}
T --> FQ["FakeQwen — default, no key"]
T --> LQ["LiveQwen — qwen3.7-max · qwen3-vl-plus"]
ADJ --> G["E-loss gate: conf ≥ .85 ∧ ≤ $50 ∧ ≠ true_error"]
G --> CL["signed Merkle night-close → prev_root chain (I1–I4)"]
<sub>As built = the three source systems are in-repo mocks (disclosed above); everything is green on FakeQwen, keyless. Live qwen3-vl-plus / qwen3.7-max sit behind DASHSCOPE_API_KEY; the app is deployed live on Function Compute (managed python3.10 — /health · /verify · /run, offline) and the 02:00 timer trigger is configured in infra/fc/s.yaml. Plain-text view below.</sub>
fetch (3× MCP) → extract (qwen3-vl-plus, two-pass + bbox) → deterministic match
→ adjudicate the residue (qwen3.7-max + thinking) → E[loss] policy gate
→ signed Merkle night-close → chain
A deterministic three-tier matcher (ref_exact → fuzzy-ref → amount+date) clears
~77% of transactions with zero model calls. Only the mismatch residue reaches
the language model. Every verdict is a typed Verdict:
a classification, evidence citations from ≥2 systems (each bound by sha256),
competing hypotheses, and a confidence — so the gate is math over typed fields,
not vibes.
The policy gate is expected-loss math, not a button:
auto_clear ⟺ confidence ≥ 0.85 ∧ materiality ≤ $50 ∧ class ≠ true_error
generalised: E[loss] = amount × (1 − confidence) ≤ τ
Two hard constraints can only ever queue: a true_error classification (I1)
and a two-pass extraction disagreement (I5).
🔒 Invariants (tested, not promised)
| Invariant | Test | |
|---|---|---|
| I1 | Zero false auto-clears on the planted true errors across all 30 nights — the load-bearing one | test_invariants.py::test_I1_* |
| I2 | Every verdict cites ≥2 systems with resolvable sha256 hashes | test_I2_* (parametrised × 30 nights) |
| I3 | The signed close chain verifies; a one-byte tamper (verdict / evidence / root) fails | test_I3_* |
| I4 | replay --night N reproduces identical verdicts, root, and signature |
test_I4_* (× 30 nights) |
| I5 | A two-pass extraction disagreement always escalates, never averages | test_I5_* |
📈 Benchmark
Seeded month, 30 nights, 1199 transactions, FakeQwen (deterministic, offline).
Regenerate with python scripts/bench.py → docs/BENCH.md.
| metric | value | target |
|---|---|---|
| classification accuracy | 0.9964 | ≥ 0.92 |
| HITL action accuracy | 1.0000 | — |
| false auto-clears on true errors | 0 | 0 (invariant) |
| auto-cleared / mismatches | 277/281 (98.58%) | — |
| queue precision / recall | 1.00 / 1.00 | — |
| residue fraction (LLM-touched) | 23.44% | small |
| runtime (30 nights, offline) | 0.16s | < 5 min/night |
| modelled cost / night | $0.0327 | ~$0.15 |
The single class disagreement is the page-broken row correctly declining to
classify (unknown) while still queueing — the right call under I5. Across the
whole τ-sweep the false-clear column stays 0: the true-error and
escalation constraints hard-queue at every threshold, so the risk knob trades
automation against review load without ever touching the safety floor.
🧩 Why only Qwen Cloud
| Qwen surface | What it does here | Without it |
|---|---|---|
qwen3-vl-plus |
reads the 8-pt OTA statement table, bbox-cited per figure | a document-AI vendor bill + losing the evidence-highlight |
| two-pass VL agreement | agreement = confidence, disagreement = escalation (I5) | single-pass misreads corrupt money decisions silently |
qwen3.7-max + thinking |
ranks competing hypotheses over evidence | plus-tier collapses to the first plausible story |
| structured output | the E[loss] gate has typed fields to compute over | free-text verdicts can't be gated, benched, or replayed |
| function calling / 3× MCP | typed, logged tool calls = the replayable decision log | bespoke connectors; forfeits the rubric's named example |
Remove Qwen Cloud and a 14-room inn needs a document-AI vendor, a frontier LLM, and a tool-orchestration framework — three bills it will never pay, and no single-vendor trail to sign into the books.
🗂️ Layout
src/innkeeper_audit/ amounts · config · schemas · crypto · matcher · policy
pipeline · report · verify · benchmark · cli
qwen/ fake (offline) · live (DashScope) · base
mcp/ server (JSON-RPC) · tools · pms/processor/ota
mcp/ pms_server.py · processor_server.py · ota_server.py (stdio)
scripts/ bench.py · verify_offline.py · check_submission_readiness.py
infra/fc/ s.yaml · wsgi.py (deployed HTTP handler) · audit_handler.py · PROOF.md
tests/ 404 tests
docs/ BENCH.md · friction-log.md · proof/DEPLOY_PROOF.md
✅ Testing & CI
5-stage pipeline: Quality → Security → Build → Offline Proof → Deploy Gate.
# ── Code Quality ────────────────────────────
ruff check . # lint
mypy src # type check (advisory on qwen/live.py)
pytest --cov=innkeeper_audit --cov-report=term # 404 tests, 100% coverage
# ── Offline Proof (the invariants, live) ────
innkeeper seed --nights 30
innkeeper run --night 2026-07-04
innkeeper replay --night 2026-07-04 # I4: byte-identical re-derivation
innkeeper verify-chain # I3: every root + signature checked
python scripts/verify_offline.py # socket-guarded tamper proof
innkeeper bench # I1: 0 false auto-clears, always
# ── Security ────────────────────────────────
pip-audit # dependency vulnerability scan
| Layer | Tool | Status |
|---|---|---|
| Code Quality | ruff + mypy | ✅ |
| Unit Testing | pytest, 404 tests, 100% coverage | ✅ |
| Invariant Testing | I1–I5, parametrised × 30 nights | ✅ |
| Security (SAST) | CodeQL (python) |
✅ |
| Security (SCA) | Dependabot (pip) + pip-audit |
✅ |
| Secret Scanning | TruffleHog | ✅ |
| Build Verification | sdist/wheel + CLI entrypoint smoke test | ✅ |
CI runs on every push/PR to main — see .github/workflows/ci.yml.
📋 Status — honest
- ✅ Offline core is complete and green: seed, matcher, adjudication, gate,
crypto, MCP, CLI,
bench,verify_offline, 404 passing tests. - 🔑 Live Qwen path is wired but behind
DASHSCOPE_API_KEY(--live):qwen3-vl-plustwo-pass extraction andqwen3.7-maxadjudication via the DashScope OpenAI-compatible endpoint. Not exercised in the offline suite by design; the deterministicFakeQwencomputes verdicts from the same signals. - ☁️ Deployed live on Alibaba Function Compute (managed
python3.10) —/health·/verify·/runrun offline on the committed ledger (/verifyre-verifies the signed-close chain in the cloud;/runreproduces a night audit). Transcript indocs/proof/DEPLOY_PROOF.md. The02:00timer trigger is configured (infra/fc/s.yaml); a captured console recording of the cron firing on its own is the one remaining step. - ○
report.htmlis optional (single-page render viainnkeeper report --night N --html); the primary report is Markdown. No Next.js UI shipped — the CLI + signed ledger are the product surface.
Built solo in a five-project sprint. The night auditor finally sleeps.
🤝 Contributing & Security
Issue/PR templates, Code of Conduct, and a security policy live under
.github/ — see CONTRIBUTING.md
and SECURITY.md.
📄 License
MIT © 2026 Edy Cu
🏷️ Versioning
This project uses Semantic Versioning with fully automated version management driven by Conventional Commits — the version is never edited by hand.
| Commit type | Bump | Example |
|---|---|---|
fix: … |
patch | 1.0.0 → 1.0.1 |
feat: … |
minor | 1.0.0 → 1.1.0 |
feat!: … or BREAKING CHANGE: footer |
major | 1.0.0 → 2.0.0 |
python-semantic-release keeps the version in sync
across pyproject.toml and src/innkeeper_audit/__init__.py.
- In CI/CD: Stage 6 of the pipeline (
.github/workflows/ci.yml) runs on every push tomain, computes the next version from the commits since the last tag, then commits + tags it automatically. - Locally:
pip install -e ".[release]" semantic-release version # compute + apply the next version and tag
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。