Architecture
Audience: Engineers and technical leads evaluating or integrating with the Case Automation MCP Server.
What you will accomplish: Understand the system's component layout, data flow, and design invariants so you can reason about where to extend, configure, or debug.
Prerequisites: Familiarity with Python async, MCP protocol basics, and ports-and-adapters architecture.
Estimated time: ~15 minutes.
What is the Case Automation MCP Server?
The server is a Python application that sits between AI agents (like Claude) and your firm's operational systems — case management, CRM, email, and document storage. It translates agent intent into typed, audited, reversible operations, and blocks any external or irreversible action behind a human approval gate.
Think of it as a safety layer: the AI can read, draft, and compute freely, but it cannot send, file, or route without a human saying yes.
System diagram
The diagram below shows the major components and data flow. A table equivalent follows for accessibility.
┌───────────────────────────────────────────────┐
MCP client │ MCP SERVER (Python) │
(Claude / agent) ──┤ FastMCP: tools · resources · prompts │
│ │ │
Webhooks (3rd- │ ▼ │
party events) ──► │ ┌──────────────┐ ┌───────────────────────┐ │
FastAPI sidecar │ │ Workflow │ │ Core services │ │
│ │ Orchestrator │◄─►│ - Document generation│ │
Scheduler ──────► │ │ (state mach.)│ │ - Deadline engine │ │
(Celery beat) │ └──────┬───────┘ │ - Verification / QC │ │
│ │ │ - Notification │ │
│ ▼ │ - Extraction (OCR/LLM)│ │
│ ┌──────────────┐ └───────────────────────┘ │
│ │ Connector │ │
│ │ layer (ports)│ Case · CRM · Email · Docs │
│ └──────┬───────┘ │
└─────────┼─────────────────────────────────────┘
▼
┌──────────┬───────────┬───────────┬──────────────┐
│ Case mgmt│ CRM │ Email │ Document │ (external APIs)
│ (Clio…) │(Salesforce│ (Graph / │ store │
│ │ /HubSpot)│ Gmail) │ (NetDocs/SP) │
└──────────┴───────────┴───────────┴──────────────┘
Persistence: Postgres (state, audit, jobs)
Redis (queue / locks / idempotency)
Secret store (vault/KMS)| Component | Role | Receives from | Sends to |
|---|---|---|---|
| MCP server (FastMCP) | Publishes tools, resources, prompts to AI agents | Agent requests via stdio/SSE | Workflow orchestrator |
| Workflow orchestrator | Runs durable state machines with human gates | MCP server, sidecar, scheduler | Core services, connector layer |
| Core services | Reusable engines: document gen, deadlines, QC, extraction, notification | Orchestrator step handlers | Connector layer |
| Connector layer | Ports-and-adapters: normalised domain model ↔ vendor APIs | Core services | External vendor APIs |
| Domain pack | Supplies all practice-specific config/policy (terminology, case types, deadline rules, document classes, confidentiality policy, RBAC roles, PII additions), selected and validated at startup | Startup config (CAM_DOMAIN_PACK) | Workflows, core services, observability, RBAC |
| FastAPI sidecar | Receives webhooks + serves approval UI | 3rd-party webhook events | Orchestrator |
| Scheduler (Celery beat) | Fires time-based triggers (reminders, sweeps) | Clock | Orchestrator |
| PostgreSQL | State, audit log, job persistence | All internal components | — |
| Redis | Queue, distributed locks, idempotency store | All internal components | — |
| Secret store (Vault/KMS) | Encryption keys, OAuth tokens at rest | Config at startup | — |
Component responsibilities
MCP server (cam.mcp_server)
Publishes the tool/resource/prompt surface to the agent. Stateless request handlers that call into the core. Every tool is thin: validate → call core → audit → return.
Sidecar (cam.sidecar)
Receives webhooks and fires time-based triggers; both enqueue workflow runs through the same orchestrator. Provides:
POST /webhooks/{connector}— verified, deduplicated, normalised, enqueuedGET/POST /approvals/{token}— human gate resolution via web UI or email action
Workflow orchestrator (cam.core.orchestrator)
Runs multi-step workflows as durable state machines, not scripts. A process survives restarts, pauses at a human gate for hours/days, and resumes cleanly.
Key properties:
- Persistence: each run = a row + step states in Postgres; inputs/outputs stored for audit & resume
- Gates: a
GATE:*step parks the run inawaiting_approval; an approval resumes it - Idempotency: each step's external effect keyed on
(run_id, step) - Retry/compensation: transient errors retry with backoff; fatal errors park the run for human attention (never silent-fail)
Connector layer (cam.connectors)
Ports-and-adapters architecture. A Connector Protocol per category defines the port; each vendor is an adapter. Workflows depend on the port, never the adapter.
Current v1 candidates:
| Category | Recommended v1 | Auth | Webhook format |
|---|---|---|---|
| Case management | Clio | OAuth 2.0 PKCE | X-Clio-Signature: sha256=… |
| CRM | Lawmatics | OAuth 2.0 | Lawmatics HMAC header |
| Microsoft Graph (365) | OAuth 2.0 delegated | Azure Event Grid subscription | |
| Document store | SharePoint / OneDrive | OAuth 2.0 (same token) | — |
Domain pack (cam.packs)
The seam that makes the engine domain-agnostic. A DomainPack bundles everything practice-specific — terminology, case types, deadline rule sets, document classes, QC packet kinds, the confidentiality (restriction) policy, RBAC roles, and PII pattern additions. Exactly one pack is selected per deployment via CAM_DOMAIN_PACK and validated at startup; an incomplete or safety-weakening pack causes a refuse-to-serve. Immigration ships as the reference pack (packs/immigration) and reproduces the original behaviour 1:1; a second pack (packs/consulting) proves the seam generalises.
Two guarantees are engine-owned and a pack can only tighten, never weaken: the confidentiality gate (no restricted document reaches an external recipient — fail-closed, never warns) and the PII redaction floor (a pack may add patterns, never remove the baseline). See Domain Packs.
Core services (cam.core.services)
Reusable engines shared by all workflows:
- Document generation (
document_gen) — template → DOCX/PDF pipeline with gap placeholders - Deadline engine (
deadline) — business-day computation, reminders, escalation, dead-man's-switch - QC verification (
qc) — composable check registry (7 built-in checks, any-fail-blocks) - Extraction (
extraction) — PDF text + OCR + LLM structuring pipeline - Notification — email drafting and routing (internal, not direct send)
Persistence layer (cam.persistence)
The persistence layer uses the Repository pattern to decouple domain logic from ORM concerns:
class Repository[T](Protocol):
async def get(self, id: str) -> T: ...
async def list(self, filter: FilterSpec) -> list[T]: ...
async def add(self, entity: T) -> T: ...
async def update(self, entity: T) -> T: ...
async def delete(self, id: str) -> None: ...Concrete implementations: ContactRepository, MatterRepository, DocumentRepository, DeadlineRepository, CommunicationRepository, TaskRepository, AuditRepository (append-only — no update or delete).
The UnitOfWork context manager ensures that a business action and its audit write commit atomically in the same database transaction. If the audit write fails, the entire transaction rolls back — enforcing the write-before-complete invariant.
Feature flags
All workflow feature flags are set via the single CAM_FEATURE_FLAGS environment variable as a JSON blob. Flags default to off — a workflow cannot be triggered until its flag is explicitly enabled. This prevents accidental activation of workflows that are still in development or awaiting firm sign-off.
| Key | Default | Enables |
|---|---|---|
intake | false | Client intake workflow |
status_update | false | Status update email workflow |
document_gen | false | Document generation pipeline |
document_routing | false | Document routing pipeline |
qc | false | QC verification service |
CAM_FEATURE_FLAGS='{"intake":true,"status_update":false,"document_gen":false,"document_routing":false,"qc":false}'Design principles
- Vendor-agnostic and domain-agnostic core. Workflows speak a normalised domain model; vendors live only behind adapters, and everything practice-specific lives in a swappable domain pack. The engine knows nothing about immigration — or any single domain.
- Safe by default. External/irreversible actions are drafted, not done, until a human gate clears them.
- Idempotent everything. Every state-changing action carries an idempotency key; retries can never double-send.
- Audit is not optional. Every action writes an immutable audit record before it is considered complete.
- Two faces, one core. Same core library exposed both interactively (MCP server) and autonomously (scheduler + webhook sidecar).
- Documentation is part of "done". Tool schemas, connector contracts, and workflow specs are checked in and CI-verified.
Because workflows depend only on Protocol interfaces (ports), replacing Clio with MyCase means implementing one adapter file. No workflow code changes. This is the key extensibility mechanism — see Connector Layer for the full contract.
Domain model (normalisation layer)
Every workflow and connector speaks the same Pydantic v2 models:
class Contact(BaseModel):
id: str; source: str
name: str; email: EmailStr | None
phone: str | None; role: str | None
external_ids: dict[str, str]
class Matter(BaseModel):
id: str; source: str
reference: str; title: str
status: str; practice_area: str | None
client: Contact; responsible: str | None
opened_at: datetime; key_dates: list["Deadline"]
external_ids: dict[str, str]
class Document(BaseModel):
id: str; matter_id: str
name: str; mime_type: str; uri: str
classification: str | None
version: int; privileged: bool
checksum: str; created_at: datetime
class Deadline(BaseModel):
id: str; matter_id: str
name: str; due_at: datetime
rule_id: str | None
status: Literal["pending","reminded","done","missed"]
escalation_level: int
class Communication(BaseModel):
id: str; matter_id: str | None
direction: Literal["in","out"]
channel: str; subject: str | None
body: str; status: Literal["draft","pending_approval","sent"]
participants: list[Contact]
class ACL(BaseModel):
matter_id: str; user_id: str
permission: Literal["read","write","none"]; external: bool
class Task(BaseModel):
id: str; matter_id: str
title: str; assignee: str | None
due_at: datetime | None; status: strMapping to/from vendor payloads is the connector's job; the core never sees a Clio JSON blob. These types carry no domain semantics — practice_area, case types, document classes, and the confidentiality flag (Document.restricted, with privileged kept as a backward-compatible alias) are populated from the active domain pack, not hard-coded.
Technology stack
| Concern | Choice |
|---|---|
| Language | Python 3.12+ |
| MCP | mcp SDK / FastMCP |
| HTTP / webhooks | FastAPI + Uvicorn |
| Models / validation | Pydantic v2 |
| HTTP client | httpx + tenacity |
| DB / migrations | PostgreSQL + SQLAlchemy 2 + Alembic |
| Queue / scheduling | Redis + Celery |
| Document generation | docxtpl / python-docx + Jinja2, WeasyPrint or LibreOffice headless |
| Extraction | pdfplumber / PyMuPDF + Tesseract OCR + LLM structuring |
| Observability | structlog + OpenTelemetry + Prometheus |
| Tests | pytest + respx/vcr + Hypothesis |
| Packaging | uv / poetry, ruff, mypy |
| Feature flags | CAM_FEATURE_FLAGS JSON blob (default: all off) |
Connectors read tokens from the secret store at runtime. OAuth refresh tokens are stored encrypted in Postgres. Never run env-dumping commands.
Repository layout
case_automation_mcp/
├── pyproject.toml
├── src/cam/
│ ├── mcp_server/ # FastMCP tools, resources, prompts
│ ├── sidecar/ # FastAPI webhooks + scheduler/worker
│ ├── core/
│ │ ├── domain/ # Pydantic domain model
│ │ ├── workflows/ # state-machine workflow defs
│ │ ├── services/ # docs, deadlines, qc, extraction, notify
│ │ ├── audit/ # audit log
│ │ └── orchestrator/ # run engine, gates, idempotency
│ ├── packs/ # domain packs: base contract + immigration (ref) + consulting
│ ├── connectors/
│ │ ├── ports.py # ports (Protocols) + ConnectorError taxonomy
│ │ ├── case_clio/ # + CONNECTOR.md, contract tests
│ │ ├── crm_*/ email_*/ docs_*/
│ ├── persistence/ # SQLAlchemy models, Alembic migrations
│ │ ├── repositories/ # Repository[T] protocol + concrete repos
│ │ └── uow.py # UnitOfWork (action + audit in one transaction)
│ └── config/ # settings, secret loading, feature flags
├── tests/ # 424 unit + PBT tests
└── docs/ # workflow specs, ADRs, this siteVerification
After reading this page, you should be able to:
- Explain why workflows never import a vendor adapter directly
- Describe what happens when a workflow step encounters a transient error vs a fatal error
- Identify which component enforces the "audit before complete" invariant
- Name the two entry points that trigger workflow runs (MCP tool + webhook/scheduler)
Common mistakes
| Mistake | Consequence | Correct approach |
|---|---|---|
| Importing a vendor adapter in workflow code | Locked to one vendor; swap requires touching N files | Import the Protocol port; inject adapter at startup |
| Skipping idempotency keys on write steps | Double-send or double-file on retry | Always provide idem_key on write tools |
| Treating the orchestrator as a script runner | No crash recovery; no gate enforcement | Use the @workflow decorator and state machine pattern |
| Putting credentials in env-dump commands | Secrets leak in logs and CI artifacts | Use the secret store; never print loaded keys |
Last updated: 2026-06-25
Next steps
- Quickstart — Get the server running locally
- Capabilities: Domain Packs — How the engine is configured for a practice
- Capabilities: MCP Surface — Explore the tool catalog
- Capabilities: Workflow Engine — Understand state machines and gates