Skip to main content
Glama
DE-BARY-LLC

debary-mcp-compliance

Official
by DE-BARY-LLC
README.md
# debary-mcp-compliance

[![tests](https://github.com/DE-BARY-LLC/debary-mcp-compliance/actions/workflows/ci.yml/badge.svg)](https://github.com/DE-BARY-LLC/debary-mcp-compliance/actions/workflows/ci.yml)

An MCP server that gives a language model access to a fair-housing and advertising
rule engine for US real-estate listing text — **without giving it the decision.**

The engine is deterministic: 81 phrase rules, 35 required elements, six states, no
model anywhere in the decision path. This server carries the question in and the
verdict out. The model on the other end receives every finding, the statute behind
it, and the wording the rule base proposes instead. What it does not receive is
permission to disagree.

> That is the reason this is an MCP server and not a REST endpoint with a nice
> description. A model needs a tool it cannot argue with, and MCP is the protocol
> that puts that tool inside its reach without putting it inside its judgement.

Built by [DE BARY LLC](https://debary.us). The engine it wraps runs at
[gate.debary.us](https://gate.debary.us).

---

## Try it

No configuration, no rule base, no network:

```bash
pip install -e .
python demo.py
```

The demo starts the server as a subprocess, speaks MCP to it, and walks through a
listing that fails, the statute behind each finding, a repair, and the re-check that
catches what the repair still missed.

⚠️ With nothing configured the server runs on a **sample rule base** — six rules and
four required elements, written for this repository so it can be run and tested by
anyone. It labels itself: every result carries `backend: "sample"` and
`rules_version: "sample-1"`. It is illustrative and is **not** a compliance tool.
Point `GATE_APP_DIR` or `GATE_URL` at the real base to get real answers.

---

## What it exposes

MCP has three primitives. This server uses all three, and each one is here because
it is the right shape for what it carries.

### Tools — things the model asks the engine to do

| Tool | Purpose |
|---|---|
| `check_listing` | Run listing copy through the engine. Returns findings, missing required elements, and a `blocking` verdict to branch on. |
| `explain_rule` | The full rule behind a finding, including the statute. So a model cites the authority instead of recalling one. |
| `suggest_rewrite` | Character offsets of every problem span plus the approved replacement wording. **Deliberately does not rewrite** — composing text is the model's job, verifying it is the engine's. |
| `get_receipt` | A stored check: what was examined, against which rule version, and the SHA-256 of the exact text. |

### Resources — material the model can read directly

| URI | Contents |
|---|---|
| `gate://rules` | The rule base index: every rule and required element by id, severity and category |
| `gate://rules/{rule_id}` | One rule in full, with authority and source document |
| `gate://elements/{state}` | Disclosure elements an advertisement must carry in that state |
| `gate://receipt/{receipt_id}` | A past check with its anchoring block |

### Prompts — the workflows, so each client does not reinvent them

| Prompt | What it enforces |
|---|---|
| `review_listing` | check → cite → rewrite → **check the rewrite again**. The last step is the one that gets skipped when a model improvises. |
| `explain_verdict` | Turn a receipt into something a licensed agent can act on, including what the engine could *not* verify mechanically. |

---

## Design notes

**The verdict is not negotiable.** `blocking` is true when any high-severity rule
fires or any required element is missing. The tool description says so, the server
instructions say so, and the `review_listing` prompt tells the model to report a
failed rewrite plainly rather than presenting it as fixed.

**`blocking: false` is not a clearance.** It means no rule fired. `elements_manual`
lists what the engine cannot check mechanically — those still need a person. A tool
that quietly reports success for what it did not examine is worse than no tool.

**Every finding carries its authority.** A finding without a citation is an opinion,
and a rule engine that has opinions is a language model with extra steps. This is
enforced in the test suite, not just intended.

**Three backends, one interface.** `LocalBackend` imports the engine and evaluates
in-process — no network, and the listing text never leaves the machine.
`RemoteBackend` calls a running gate over HTTP. `SampleBackend` carries a small rule
base of its own so the server runs on a fresh clone. All three return the same shape,
so the MCP layer never learns which one answered.

**The fallback announces itself.** A sample that silently impersonates the real thing
would be a trap. This one puts `sample` in every result and `sample-1` in the version
field, and there is a test that fails if either label goes missing.

**Two transports.** `stdio` for a desktop client, `streamable-http` for the
container. `sse` is still accepted for older clients; it is deprecated in the
protocol and should not be chosen for anything new.

**Requires the 2.x SDK.** The Python SDK renamed `FastMCP` to `MCPServer` in 2.0 and
moved it from `mcp.server.fastmcp` to `mcp.server.mcpserver`; result fields went from
`structuredContent` to `structured_content`. This server targets 2.x only. Supporting
both majors behind a shim was considered and rejected — it would add a compatibility
layer to a project whose whole argument is that the decision path stays simple.

---

## Running it

### Against a local rule base

```bash
pip install -e .
GATE_APP_DIR=/path/to/compliance-gate/app debary-mcp-compliance
```

### Against a running gate

```bash
GATE_URL=https://gate.debary.us GATE_TOKEN=… debary-mcp-compliance
```

`GATE_APP_DIR` wins if both are set: evaluating in-process is faster and keeps the
listing text off the wire.

### In Docker

```bash
docker compose up -d
```

The container listens on `:8765/mcp` (streamable HTTP) and is published on host
port `8766`, bound to loopback. Put a tunnel in front of it rather than opening a
port. The rule base is mounted read-only — the container can evaluate rules, not
change them.

### Claude Desktop

```json
{
  "mcpServers": {
    "debary-compliance": {
      "command": "debary-mcp-compliance",
      "env": { "GATE_APP_DIR": "/path/to/compliance-gate/app" }
    }
  }
}
```

### Claude Code

```bash
claude mcp add debary-compliance \
  --env GATE_APP_DIR=/path/to/compliance-gate/app \
  -- debary-mcp-compliance
```

---

## Configuration

| Variable | Meaning |
|---|---|
| `GATE_APP_DIR` | Directory holding `engine.py` and `rules.json`. Selects the local backend. |
| `GATE_URL` | Base URL of a running gate. Selects the remote backend. |
| *(neither set)* | Falls back to the built-in sample rule base. |
| `GATE_TOKEN` | Bearer token for that gate, if its API is closed. |
| `MCP_TRANSPORT` | `stdio` (default), `streamable-http`, or `sse`. |
| `MCP_HOST` / `MCP_PORT` | Bind address for the HTTP transports. Default `0.0.0.0:8765`. |

---

## Tests

```bash
pip install -e ".[dev]"
pytest -q                                            # 21 protocol tests, no setup
GATE_APP_DIR=/path/to/compliance-gate/app pytest -q  # + 3 production tests
```

The tests do not import the tool functions and call them. They launch the server as
a subprocess, complete the handshake over stdio, and drive it as a client — because a
tool that works in-process and fails over the wire is a tool that does not work.

What they check, beyond the happy path:

- every tool declares an output schema, so callers branch on fields rather than prose
- every finding carries an authority **and** a replacement wording
- findings never overlap, so nothing is double-counted and no rewrite is corrupted
- rewrite offsets actually index the text they claim to match
- the digest is of the exact text submitted, not a normalised copy
- an unknown rule id is an error, not an empty answer
- an element the engine cannot verify comes back `manual`, never as a pass
- the sample backend labels itself as one

The three production tests skip unless `GATE_APP_DIR` is set. They assert the rule
version and the counts this project states in public, and that verdicts differ by
state — claims no sample can stand in for.

---

## Scope and limits

⚠️ **Not legal advice.** The rules were compiled from public statutes, agency
guidance and industry policy; each carries its source and the date it was read.
Compliance is decided by regulators and courts, not by this server.

⚠️ **Six states:** TX, CA, FL, AZ, NY, NC. A check run against the wrong state
silently applies the wrong required elements — the `state` argument is not cosmetic.
The server does not keep its own copy of this list: it asks the engine, because a
hand-maintained copy drifted once and made two states unreachable.

⚠️ **The sample rule base is not the product.** Six rules against public statutes,
written so this repository can be run by anyone. The base behind `gate.debary.us` is
larger, is compiled from sourced documents with read dates, and is maintained
separately.

⚠️ **Advisory findings are state-scoped.** A phrase that is binding in one state may
come back as `advisory` in another. That is the engine being precise, not uncertain.

---

Apache-2.0 · DE BARY LLC, Austin TX

TDQS

A4.4/5.0

Scored across 4 tools

Disambiguation5/5

Each tool occupies a distinct role: running a compliance check, explaining a rule, suggesting rewrite spans, and retrieving an audit receipt. There is no meaningful overlap or ambiguity between them.

Naming Consistency5/5

All four tools follow the same snake_case verb_noun pattern: check_listing, explain_rule, suggest_rewrite, get_receipt. The naming is predictable and makes the action and target of each tool immediately clear.

Tool Count5/5

Four tools is a well-scoped size for a compliance-assistance server. Each tool supports a necessary step in the workflow without adding redundant or decorative surface area.

Completeness5/5

The tool set covers the full workflow: check a listing, understand why it was flagged, obtain safe replacement wording, and retrieve verifiable proof of a prior check. The deliberate absence of auto-rewriting is handled cleanly through the check-again instruction.

Maintenance

ActivitySlowing
ResponsivenessNo issues