Ledgerly CRM MCP
Implementation fact Synthetic data: Ledgerly
An MCP server that gives sellers and their AI agents governed access to a revenue system: territory-scoped reads, PII masked by role, and changes an agent can propose but never apply.
- Status
- Write-up ready, code not public yet
- Relevant to
- AI and agents, GTM Systems
Results · Synthetic data: Ledgerly
- Synthetic benchmarkSynthetic data: Ledgerly. Measured against a hidden answer key in that data. It shows the method works there, not a production result.for the duplicate rule against the hidden answer key: 142 pairs, 100% recallSynthetic benchmark
- Implementation factA fact about what was built and tested, not a business outcome.prompt-injection red team that runs in CI with a policy-as-code checkImplementation fact
- Implementation factA fact about what was built and tested, not a business outcome.in the evidence export, with the audit chain verifiedImplementation fact
- Implementation factA fact about what was built and tested, not a business outcome.passing, plus a clean typecheckImplementation fact
At a glance
- Question
- How can an AI agent work in a CRM without holding the full permissions of an API key?
- Data
- A local SQLite copy of Ledgerly’s synthetic CRM data, plus a hand-built fixture where every expected answer is known.
- How it was tested
- Tests for each guarantee, a prompt-injection red team that runs in CI, and tamper tests on the audit log.
- Result
- 104 tests pass with a clean typecheck, and the evidence export reports 6 controls PASS with the audit chain verified.
- What it doesn’t show
- HTTP auth uses a shared-secret token rather than OAuth, and it runs on SQLite rather than Salesforce.
The problem
Giving an agent a CRM API key gives it the key's full permissions: booked revenue, comp-relevant ownership and customer PII. The questions that matter are about the failure case: blast radius, permissions, what happens when an automated change is wrong, and evidence.
What I built, in detail
- Tools over stdio for local Claude Code use, or over Streamable HTTP with signed, short-lived tokens. Reads are scoped to the caller’s territory, and an out-of-scope account looks the same as a missing one.
- Changes are proposals with a computed blast radius and risk level. A different person approves, high-risk changes need RevOps, and agents never approve.
- Approval applies in one transaction that checks every row still holds the values the request was based on. Anything stored can be rolled back.
- Every call goes into an append-only, hash-chained audit log that exports as change-management evidence.
How I checked it
- Tests run against a hand-built fixture where every expected answer is known.
- The real data caught what the fixture could not: a name-matching duplicate rule scored 0 for 53 against the answer key, so I removed it.
- A demo drives separate server processes over real stdio, because tests that call functions directly don’t prove the MCP wiring works.
- An adversarial pass (red team, policy check, tamper tests) was written to break the guarantees rather than confirm them.
Limitations
- HTTP auth is a shared-secret signed token, not OAuth. Production would use the MCP authorization spec with an identity provider.
- It runs on a local SQLite copy, not Salesforce, and the Slack-style notifier never posts.
Where it came from
My real estate CRM already runs an MCP server where reads are open, writes are proposals I confirm, and nothing can send. This hardens that pattern for a sales team. Read the AI CRM case study.
Stack
TypeScript, MCP SDK (stdio and Streamable HTTP), zod, node:sqlite, node:crypto, DuckDB, Vitest, GitHub Actions