Skip to content

All projects

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

  • for the duplicate rule against the hidden answer key: 142 pairs, 100% recallSynthetic benchmark
  • prompt-injection red team that runs in CI with a policy-as-code checkImplementation fact
  • in the evidence export, with the audit chain verifiedImplementation fact
  • 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