Skip to main content
Glama
euuuuuuan

crm-mcp-agent

by euuuuuuan

crm-mcp-agent

The SQL work of a CRM/lifecycle marketer, done by an agent over MCP — behind a governance kernel that decides how far the agent may go, how it is stopped, and what gets written down.

License Python Dependencies Tests Red team Data

English · 한국어 · 日本語 · 简体中文 · 繁體中文 · Español · Português (Brasil) · Deutsch · Français · Русский · Italiano · Bahasa Indonesia · Türkçe

Quickstart · Architecture · Design decisions · Governance kernel · Credits

IMPORTANT

Apache-2.0 covers source code only. Data files, fixtures, and any other non-code content are governed by the records inCREDITS.md, which take precedence for those files. Content not listed there is unrecorded, not free to reuse.

WARNING

All data in this repository is 100% synthetic, at every commit. Every row is produced by a seeded generator: names are combinatorial from small fixed token lists, phone numbers all use the unusable 010-0000-XXXX exchange, and every email address is on the example.com domain reserved for documentation by RFC 2606. There is no real customer, campaign, or company data here, and no code path that talks to anything other than the local SQLite file the generator writes.

What this is

This is a prototype for one narrow question: can an agent, talking to a small purpose-built MCP server, do the ad-hoc segment and campaign lookups a CRM marketer normally does by hand-writing SQL against a customer data platform — and can the write side be made safe enough to expose at all? It ships a deterministic synthetic CDP, five read-only MCP tools with a hard non-PII boundary, and a pure-standard-library governance kernel that every write-side call must pass through. It is a prototype, not a product, and explicitly not an integration with any marketing-automation vendor: there is no vendor SDK, account, or credential anywhere in it, and execute_campaign is a simulator that appends a receipt to a local ledger — it will not call an outward API at any milestone. What is real here is the mechanism: the tiering, the fingerprint-bound approvals, the kill switch, the caps derived from the audit log, and 45 adversarial tests whose job is to break all of it.

Related MCP server: atomic-crm-mcp

Highlights

  • 108 tests in 0.772 s, measured on this snapshot: 53 unit tests plus 55 adversarial and scenario tests — 33 PII red-team attacks, 12 governance red-team attacks, and 10 end-to-end governance scenarios.

  • Exactly one third-party dependency, and it is pinned to a major version: mcp>=1,<2. Everything else — generator, governance kernel, guards, agent loop, tests — is standard library. Verified against mcp 1.28.1 on Python 3.14.6.

  • Determinism is proven, not asserted. The publication gate generates the synthetic CDP twice with the same seed and compares SHA-256 digests; they are byte-identical. Generation never reads the wall clock — the 180-day window is anchored to a fixed calendar date.

  • The synthetic CDP, measured at seed 42: 5,000 customers, 99,922 events, 40 campaigns, 1,225 daily campaign-result rows, 12 rule-based segments, and 8,078 segment memberships across 7 tables.

  • Five read-only MCP tools (segment_query, campaign_history, campaign_performance, audience_overlap, budget_status) and three write-side tools (propose_campaign, execute_campaign, read_audit_log), each mapped to one of 4 authority tiers.

  • A non-PII guard with three independent legs so one bug is not a leak: deny-by-default allowlists (PII columns appear in no allowlist), a recursive output-side scrubber that fails the call closed on any phone/email/RRN-shaped string, and no column-injection surface at all — field and operator names are only ever dict keys into hardcoded SQL fragments, and only values are bound parameters.

  • A governance kernel in 8 stdlib modules with rails checked cheapest-and-most-absolute first: kill switch → circuit breaker → rate caps → approval broker → append-only decision log. Fail-closed throughout: unreadable kill state reads as engaged, garbled breaker state reads as tripped.

  • Grants that cannot be replayed. An approval is bound to SHA-256(tool + "\n" + canonical_json(args)), consumed by a single atomic UPDATE so racing consumers cannot both win, and expires on a TTL (600 s default, 3,600 s ceiling). Caps for the irreversible tier are deliberately tight: 3 per day, 10 per week.

  • Caps are re-derived from the audit log on every check, so the counter cannot drift out of sync with the record — the log is the counter.

  • 19 numbered design decisions in docs/DECISIONS.md, including the ones that argue against the obvious choice.

Use it

Two surfaces, and one owner console:

Surface

What it is

Authority

MCP server

A FastMCP stdio server. Point any MCP client at it. Opens SQLite with mode=ro, so even a bug that tried to write is refused by the database itself.

Read-only (T0)

Write tools

propose_campaign (draft, self-approved but audited and capped), execute_campaign (simulated, requires a fresh human grant per call), read_audit_log.

T1 / T3, kernel-gated

gov.cli

The human's side of the kernel: 10 subcommands including status, grant, deny, trip, clear, kill, phase, audit, report, pending.

Owner only

An agent loop (python3 -m agent.loop) runs one governed cycle: analyse with read-only tools → propose a draft → request the gated execute → after a human grants, execute (simulated) → report. Honest label: the default planner is a deterministic scripted planner, not an LLM. The property being demonstrated is not planning quality — it is that whatever plans, the write path stays behind the kernel. --auto-grant collapses the human gate for tests and demos and says so loudly; --planner ollama is an opt-in local-model variant that validates the model's choice against the same closed sets and falls back to scripted.

Governance state (approval database, decision log, ledgers, breaker and kill-switch files) lives outside the repository under a git-ignored directory, relocatable with CRM_MCP_GOV_DIR. The synthetic database path is set with CRM_MCP_DB_PATH; see .env.example. There are no secrets to configure, because there is no paid API and no network call at runtime.

Quickstart

Prerequisites: Python 3.11 or newer (measured on 3.14.6) and the mcp package pinned below major version 2mcp>=1,<2. The pin is a real prerequisite, not decoration: mcp is pre-1.0-style in its API churn, and an unpinned install can pick up a future 2.x whose FastMCP surface no longer matches server/mcp_server.py. Install it exactly as pinned.

git clone <your-fork-url> crm-mcp-agent
cd crm-mcp-agent
python3 -m venv .venv
.venv/bin/pip install "mcp>=1,<2"                       # the pin is required
python3 data/synth_cdp.py --seed 42 --out data/cdp.db   # generator is stdlib-only
.venv/bin/python -W error::ResourceWarning -m unittest discover   # 108 tests
.venv/bin/python -m server.mcp_server                   # stdio MCP server
.venv/bin/python -m gov.cli status                      # kill / breaker / caps / pending

The database is git-ignored and must be regenerated after a clone; the same seed always reproduces it. Note that the generator needs no virtualenv at all — only the MCP server does.

Architecture

flowchart TB
  subgraph DATA["Synthetic data — stdlib only"]
    GEN["data/synth_cdp.py — seeded RNG, fixed 180-day window"] -->|writes| DB[("data/cdp.db — SQLite, git-ignored")]
  end
  subgraph READ["Read path (T0)"]
    DB -->|"mode=ro"| SRV["server/mcp_server.py — FastMCP, stdio"]
    GRD["server/guards/pii_guard.py"] -.->|"allowlists + output scrubber + no injection surface"| SRV
    SRV --> CLI2["MCP client / agent"]
  end
  subgraph GOVK["Governance kernel — gov/, 8 stdlib modules"]
    K["kernel.guard(tool, args)"]
    KILL["kill switch — file present = deny all"] --> K
    BRK["circuit breaker — garbled reads as tripped"] --> K
    CAP["rate caps — re-derived from the audit log"] --> K
    BRO["approval broker — fingerprint-bound, single-use, TTL"] --> K
    K -->|"allow / approval_required / deny"| W["server/tools_write.py"]
    K -->|every non-T0 decision| LOG[("decision-log.jsonl — append-only")]
    OWN([human owner via gov.cli]) -->|grant / deny / trip / kill / phase| BRO
    W -->|simulated receipts and draft ledgers| CLI2
  end
  CLI2 --> K
  LOG --> CAP

Load-bearing properties:

  • Tiering is the whole design. T0 read-only is auto-allowed and unaudited; T1 draft is self-approved but audited and capped; T2 (reversible, reserved) requires an owner-promoted phase or else escalates onto the T3 path; T3 irreversible/outward requires a fresh human grant per call. Nothing can promote itself.

  • Ordering is a safety property. The rails are checked cheapest and most absolute first, and the cap check runs before the approval broker — so spamming a capped tool cannot flood a human with approval prompts. A denied policy call never creates a pending approval, which means a hard block cannot be approved into existence.

  • The log is the counter. Caps are recomputed from the append-only decision log on every check rather than kept in a separate tally, which removes an entire class of desynchronisation bug at the cost of a re-read.

  • The guard has two code paths on purpose. An allowlist alone fails if a new field is added carelessly; a scrubber alone fails if the regex misses a shape. Both run, and the red-team suite includes positive controls that prove the scrubber actually fires rather than silently passing.

  • mode=ro is defence in depth, not the mechanism. The primary guarantee is that no write tool touches the CDP at all; the read-only connection is there so a bug cannot become a mutation.

Project layout

data/schema.sql        7 tables: customers, events, campaigns, campaign_results,
                       segments, segment_membership, customer_features
data/synth_cdp.py      deterministic generator: seeded RNG, fixed calendar anchor
server/mcp_server.py   FastMCP stdio server, 5 read-only tools, mode=ro connection
server/tools_write.py  3 write-side tools, every call routed through the kernel
server/guards/pii_guard.py   allowlists + recursive output scrubber + injection refusal
gov/policy.py          tool to tier map, risk by tier, T2 promotion phase
gov/kernel.py          guard(tool, args) — the one decision point
gov/broker.py          fingerprint-bound, single-use, TTL'd approval grants
gov/caps.py            per-tool day/week caps, counted from the audit log
gov/breaker.py         circuit breaker; unreadable state means tripped
gov/audit.py           append-only decision log
gov/cli.py             the owner console: 10 subcommands
agent/loop.py          one governed cycle; scripted planner by default
tests/                 6 files, 53 unit tests
eval/test_pii_redteam.py       33 PII attacks incl. positive controls
eval/test_gov_redteam.py       12 governance attacks, all must be refused
eval/scenarios/                10 end-to-end governance scenarios
docs/DECISIONS.md      19 numbered decisions with their trade-offs

How this was built

The adversarial suite is the deliverable, not the appendix. Of 108 tests, 45 exist to break this system rather than to confirm it: 33 try to get personal data out of a read-only tool, and 12 try to defeat the governance rails. The governance attacks are the interesting half — grant replay, cross-proposal and cross-tool fingerprint tampering, extra-argument tampering, TTL-expiry abuse, cap outspend, propose-spam, kill-switch bypass while holding a valid grant, breaker garbling, unknown-tool probes, approval-id guessing, and PII laundering through the agent-loop report path. Every one must be refused for the suite to pass. Writing the attack first and the rail second is what makes the rail credible.

Positive controls, because a silent no-op passes every negative test. The PII red-team includes tests that assert the scrubber fires on known-bad input. Without them, a guard accidentally disabled would still make the whole suite green — a failure mode that is invisible precisely when it matters most.

Ground truth independent of the guard. The final sweep calls all five read-only tools with benign arguments and substring-checks their JSON output against the actual names, phones, and emails in the underlying database. That check does not use the regex scrubber, so it cannot be fooled by a scrubber bug — it compares against reality.

Determinism verified by re-execution, not by claim. The generator's promise is that the same seed reproduces the same dataset anywhere, any day. The publication gate proves it by generating twice and comparing digests. One of the design decisions exists specifically to protect that: generation never touches the wall clock, and the entire event window is anchored to a fixed date, so a test written in January cannot pass differently in July.

AI-augmented in bounded lanes, with a deterministic arbiter. The implementation was produced by an agent relay — one agent building or auditing a lane, a second cross-checking the behaviour and the evidence, the author adjudicating. The arbiter was never a model's opinion: it was unittest discover and its exit code. This matters more than usual for a governance kernel, because a plausible-sounding rail that does not actually hold is worse than no rail. Several decisions in docs/DECISIONS.md exist because a first design sounded safe and the red-team disagreed — the cap-before-broker ordering and the never-approvable-deny rule both came from attacks, not from planning.

Stated deviations instead of quiet ones. docs/DECISIONS.md contains a section on deviations from the modelled daemon contract, written down rather than smoothed over, and separate entries on why --auto-grant exists and why it is dangerous, why T2 exists with no tool yet, and why the default planner is scripted rather than an LLM. Honest labels are cheaper to write than to retrofit.

A fail-closed publication gate. This public snapshot is produced by an out-of-repo sanitizing pipeline that never writes to the private source. It archives a ref, applies exclusions, overlays publication-only documentation, then refuses to commit unless every gate is green: secret scanning, credential-shaped-file detection, absolute-home-path and email-shape checks, a fixed-string employer/client wordlist expanded over delimiter variants, exclusion verification, a syntax build, the full 108-test suite, and a claims gate that re-runs the command behind each number here — including regenerating the synthetic database to confirm the row counts and the digest equality.

Development

.venv/bin/python -W error::ResourceWarning -m unittest discover      # 108 tests
.venv/bin/python -m unittest discover -v                             # same, verbose
python3 -m compileall -q agent data eval gov server                   # syntax gate
python3 data/synth_cdp.py --seed 42 --out data/cdp.db                 # regenerate data
python3 -m gov.cli status                                            # rails snapshot
python3 -m gov.cli report 7                                          # digest the log
python3 -m agent.loop                                                # one governed cycle

-W error::ResourceWarning is deliberate: an unclosed SQLite connection becomes a test failure rather than a warning nobody reads.

When adding a write-side tool: give it a tier in gov/policy.py first, then add the red-team attack that tries to bypass its gate, then implement it. When changing a rail, add the attack that the change is supposed to defeat.

Localization

The English README is canonical; 12 translations live under docs/i18n/ and are listed in docs/i18n/README-INDEX.md. Keep commands, paths, numbers, links, and measured claims synchronized across all of them — the claims gate will not let this file disagree with the code, and a translation must not disagree with this file. Note that the PII guard's regex shapes are Korean-specific (phone format, resident-registration-number shape), which is a property of the domain rather than of the documentation language.

Credits

See CREDITS.md for the four-tier register covering the synthetic dataset and every other non-code file, the fail-closed rule for unlisted content, and what a fork must remove. Its terms take precedence over the code licence for those files. The single third-party runtime dependency retains its own upstream licence wherever it is installed; requirements.txt is the authoritative record of it.

License

Source code is licensed under the Apache License 2.0; modification notices are recorded in NOTICE. Data files and other non-code content are not licensed by Apache-2.0 and remain governed by CREDITS.md.

A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    -
    quality
    F
    maintenance
    MCP server for Atomic CRM that enables agents to read and write CRM data via SQL queries, with OAuth authentication and Row Level Security enforced.
    Last updated
    4
  • F
    license
    -
    quality
    C
    maintenance
    A governed MCP server for integrating AI agents with customer data, featuring role-based access control, field redaction, and human-in-the-loop approval for secure support operations.
    Last updated
    1
  • A
    license
    -
    quality
    A
    maintenance
    An MCP server that enforces agent persona permissions by acting as a gateway, scoping tool access and denying unauthorized capabilities.
    Last updated
    28
    6
    MIT

View all related MCP servers

Related MCP Connectors

  • Autopilot MCP server for GEO analyses, reports, content, audits, memories and agents.

  • An MCP Server that provides identity verification and anti-fraud tools for AI agents via deepidv.

  • An MCP server giving access to Grafana dashboards, data and more.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/euuuuuuan/crm-mcp-agent-public'

If you have feedback or need assistance with the MCP directory API, please join our Discord server