Now domain-agnostic — configure any practice with Domain Packs (immigration is the reference pack)

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)
ComponentRoleReceives fromSends to
MCP server (FastMCP)Publishes tools, resources, prompts to AI agentsAgent requests via stdio/SSEWorkflow orchestrator
Workflow orchestratorRuns durable state machines with human gatesMCP server, sidecar, schedulerCore services, connector layer
Core servicesReusable engines: document gen, deadlines, QC, extraction, notificationOrchestrator step handlersConnector layer
Connector layerPorts-and-adapters: normalised domain model ↔ vendor APIsCore servicesExternal vendor APIs
Domain packSupplies all practice-specific config/policy (terminology, case types, deadline rules, document classes, confidentiality policy, RBAC roles, PII additions), selected and validated at startupStartup config (CAM_DOMAIN_PACK)Workflows, core services, observability, RBAC
FastAPI sidecarReceives webhooks + serves approval UI3rd-party webhook eventsOrchestrator
Scheduler (Celery beat)Fires time-based triggers (reminders, sweeps)ClockOrchestrator
PostgreSQLState, audit log, job persistenceAll internal components
RedisQueue, distributed locks, idempotency storeAll internal components
Secret store (Vault/KMS)Encryption keys, OAuth tokens at restConfig 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, enqueued
  • GET/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 in awaiting_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:

CategoryRecommended v1AuthWebhook format
Case managementClioOAuth 2.0 PKCEX-Clio-Signature: sha256=…
CRMLawmaticsOAuth 2.0Lawmatics HMAC header
EmailMicrosoft Graph (365)OAuth 2.0 delegatedAzure Event Grid subscription
Document storeSharePoint / OneDriveOAuth 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.

KeyDefaultEnables
intakefalseClient intake workflow
status_updatefalseStatus update email workflow
document_genfalseDocument generation pipeline
document_routingfalseDocument routing pipeline
qcfalseQC verification service
CAM_FEATURE_FLAGS='{"intake":true,"status_update":false,"document_gen":false,"document_routing":false,"qc":false}'

Design principles

  1. 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.
  2. Safe by default. External/irreversible actions are drafted, not done, until a human gate clears them.
  3. Idempotent everything. Every state-changing action carries an idempotency key; retries can never double-send.
  4. Audit is not optional. Every action writes an immutable audit record before it is considered complete.
  5. Two faces, one core. Same core library exposed both interactively (MCP server) and autonomously (scheduler + webhook sidecar).
  6. Documentation is part of "done". Tool schemas, connector contracts, and workflow specs are checked in and CI-verified.
Advanced: swapping vendors

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: str

Mapping to/from vendor payloads is the connector's job; the core never sees a Clio JSON blob. These types carry no domain semanticspractice_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

ConcernChoice
LanguagePython 3.12+
MCPmcp SDK / FastMCP
HTTP / webhooksFastAPI + Uvicorn
Models / validationPydantic v2
HTTP clienthttpx + tenacity
DB / migrationsPostgreSQL + SQLAlchemy 2 + Alembic
Queue / schedulingRedis + Celery
Document generationdocxtpl / python-docx + Jinja2, WeasyPrint or LibreOffice headless
Extractionpdfplumber / PyMuPDF + Tesseract OCR + LLM structuring
Observabilitystructlog + OpenTelemetry + Prometheus
Testspytest + respx/vcr + Hypothesis
Packaginguv / poetry, ruff, mypy
Feature flagsCAM_FEATURE_FLAGS JSON blob (default: all off)
No credentials in source

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 site

Verification

After reading this page, you should be able to:

  1. Explain why workflows never import a vendor adapter directly
  2. Describe what happens when a workflow step encounters a transient error vs a fatal error
  3. Identify which component enforces the "audit before complete" invariant
  4. Name the two entry points that trigger workflow runs (MCP tool + webhook/scheduler)

Common mistakes

MistakeConsequenceCorrect approach
Importing a vendor adapter in workflow codeLocked to one vendor; swap requires touching N filesImport the Protocol port; inject adapter at startup
Skipping idempotency keys on write stepsDouble-send or double-file on retryAlways provide idem_key on write tools
Treating the orchestrator as a script runnerNo crash recovery; no gate enforcementUse the @workflow decorator and state machine pattern
Putting credentials in env-dump commandsSecrets leak in logs and CI artifactsUse the secret store; never print loaded keys

Last updated: 2026-06-25

Next steps