Skip to main content
Glama
README.md
# Clio Manage MCP

[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Node β‰₯ 20](https://img.shields.io/badge/node-%E2%89%A520-339933.svg)](https://nodejs.org)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178c6.svg)](https://www.typescriptlang.org)
[![MCP](https://img.shields.io/badge/MCP-1.x-7c3aed.svg)](https://modelcontextprotocol.io)
[![Build](https://github.com/patrickking67/clio-manage-mcp/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/patrickking67/clio-manage-mcp/actions/workflows/build.yml)
[![Status: beta](https://img.shields.io/badge/status-beta-orange.svg)](#roadmap)

> **Run your law practice from a chat box.** A Model Context Protocol server that
> connects **Clio Manage** to Claude as a secure, **per-user remote OAuth connector** β€”
> ask in plain English and your matters, time, billing, calendar, and documents
> answer back. Each user picks an access tier (read-only, read + write, or
> read/write/delete) on a consent page before signing in. Deploy it to Azure with
> `azd up`, run it as a container anywhere, or use it locally over stdio.
>
> *Independent open-source project. Not affiliated with or endorsed by Clio
> (Themis Solutions Inc.).*

<p align="center">
  <a href="#quick-start--azure-remote-connector"><b>πŸš€ Deploy to Azure</b></a> &nbsp;Β·&nbsp;
  <a href="#how-connecting-works">Add to Claude</a> &nbsp;Β·&nbsp;
  <a href="#tool-catalog">41 tools</a> &nbsp;Β·&nbsp;
  <a href="#the-clio-manage-plugin">Plugin</a> &nbsp;Β·&nbsp;
  <a href="#works-with">Works with</a> &nbsp;Β·&nbsp;
  <a href="#security--compliance-posture">Security</a> &nbsp;Β·&nbsp;
  <a href="https://patrickking67.github.io/clio-manage-mcp/">Live site β†—</a>
</p>

**Built with**

![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white)
![Node.js](https://img.shields.io/badge/Node.js-5FA04E?style=for-the-badge&logo=nodedotjs&logoColor=white)
![Microsoft Azure](https://img.shields.io/badge/Microsoft_Azure-0078D4?style=for-the-badge&logo=microsoftazure&logoColor=white)
![Container Apps](https://img.shields.io/badge/Container_Apps-0078D4?style=for-the-badge&logo=microsoftazure&logoColor=white)
![Docker](https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white)
![Bicep](https://img.shields.io/badge/Bicep-519aba?style=for-the-badge&logoColor=white)
![Express](https://img.shields.io/badge/Express-000000?style=for-the-badge&logo=express&logoColor=white)
![Clio](https://img.shields.io/badge/Clio_Manage_v4-2D3B4D?style=for-the-badge)
![OAuth 2.1](https://img.shields.io/badge/OAuth_2.1-EB5424?style=for-the-badge&logo=oauth&logoColor=white)
![AES-256-GCM](https://img.shields.io/badge/AES--256--GCM-1a7f37?style=for-the-badge&logo=keepassxc&logoColor=white)

This server is the boundary between an AI agent (Claude, or any MCP client) and
your firm's Clio Manage instance. It speaks Clio v4 fluently β€” matters, contacts,
time, tasks, notes, calendar, documents, bills β€” and exposes it as a **remote
custom connector**: each attorney adds one URL in Claude, signs in to **their
own** Clio account, and is connected. Tokens are encrypted at rest, every tool
call is audited, and one user's Clio data is never visible to another's session.

### The one-minute version

```text
azd up                                  # 1 Β· deploy to Azure Container Apps (OAuth by default)
#  β†’ in Claude: Settings β†’ Connectors β†’ Add custom connector
#  β†’ paste  https://<your-app>.azurecontainerapps.io/mcp
#  β†’ each attorney signs in to their OWN Clio account   β†’   connected
```

No API keys to hand around and no shared login: Claude does discovery β†’
registration β†’ PKCE, then the user signs in on Clio's own domain. Prefer to stay
local? The same binary runs over stdio for solo and development use.

**What's in the box**

- A **remote OAuth 2.1 custom connector** for Claude: per-user sign-in to Clio,
  Dynamic Client Registration, PKCE, served from Azure Container Apps
- **Interactive scope tiers**: at connect time each user chooses read-only,
  read + write, or read/write/delete on a branded consent page; the session's
  tool list reflects exactly what was granted (`clio:read` / `clio:write` /
  `clio:destructive`)
- 41 tools across 11 Clio domains, plus a generic `clio_api_request` escape hatch
- A composite intake workflow (`clio_open_new_matter`) that chains client +
  matter + opening note + intake task into one agent action
- Per-session, AES-256-GCM encrypted Clio tokens, multi-replica safe on shared storage
- Append-only JSONL audit log designed around ABA Formal Opinion 512

**What's different about this one**

- **Per-user OAuth, not a shared key.** The headline mode (`MCP_AUTH_MODE=oauth`)
  turns this server into an OAuth 2.0 Authorization Server + Protected Resource
  that bridges each Claude user to their own Clio account. No bearer tokens to
  hand out, no shared login. Claude does discovery β†’ registration β†’ PKCE β†’ the
  user signs in on Clio. A simpler shared-token `static` mode is still available.
- **Azure-native.** A single `azd up` provisions Container Apps + ACR + Key
  Vault + Azure Files + Log Analytics, defaults to OAuth mode, and auto-sets
  `PUBLIC_BASE_URL` from the environment domain. Secrets flow from Key Vault to
  the container via managed identity β€” never on disk.
- **End-to-end verified.** `npm run smoke:stdio`, `npm run smoke:http`, and
  `npm run smoke:oauth` drive a real MCP session β€” including the full OAuth
  discovery handshake β€” against the built binary. CI blocks regressions in protocol
  shape, tool registration, auth gating, and resource publication.
- **Multi-region.** US / CA / EU / AU Clio endpoints via one `CLIO_REGION` env.

---

## Contents

1. [What you can ask Claude](#what-you-can-ask-claude)
2. [How connecting works](#how-connecting-works)
3. [Architecture](#architecture)
4. [Quick start β€” Azure (remote connector)](#quick-start--azure-remote-connector)
5. [Quick start β€” local (stdio)](#quick-start--local-stdio)
6. [Tool catalog](#tool-catalog)
7. [Resources](#resources)
8. [Configuration](#configuration)
9. [Security & compliance posture](#security--compliance-posture)
10. [Cost (Azure)](#cost-azure)
11. [FAQ](#faq)
12. [Verification](#verification)
13. [Confirmed Clio API quirks](#confirmed-clio-api-quirks)
14. [The Clio Manage plugin](#the-clio-manage-plugin)
15. [Works with](#works-with)
16. [Optional connectors](#optional-connectors)
17. [Landing page](#landing-page)
18. [Development](#development)
19. [Related work](#related-work)
20. [Roadmap](#roadmap)
21. [License](#license)

---

## What you can ask Claude

Once connected, these are real prompts that route through the connector. The
tool calls happen transparently; the agent picks what to call from the catalog.
In OAuth mode each result reflects **that user's** Clio account.

**Matter lookup**

> "Show me all open matters for Acme Corp."
> "What's the status of matter 2024-0042?"
> "Which matters have been updated since last Monday?"

**Time & billing**

> "How many hours has the team logged on matter 4821 this month?"
> "What's the outstanding balance on matter 4821 and when was the last
> invoice issued?"
> "List all unbilled time entries from Jane in April."

**Intake (composite workflow)**

> "Open a new matter for Jane Smith β€” landlord/tenant, flat fee $2,500.
> Add an opening note summarising the consultation, and create an intake
> task due Friday."

That last one is one `clio_open_new_matter` call that creates the contact,
opens the matter, applies the flat-fee custom rate, attaches the note, and
schedules the task.

**Drafting (writes a note)**

> "Add a note to matter 4821: today's call covered scope and engagement
> letter; client confirmed retainer."

**Calendar & tasks**

> "What do I have on the calendar between April 28 and May 2?"
> "Show my pending tasks across all open matters, grouped by priority."

**Reporting / cleanup**

> "List all bills in `awaiting_payment` state older than 60 days, grouped by
> client."
> "Find every contact created this year that isn't linked to a matter."

The connector retrieves Clio data live on every request. Nothing is cached
or mirrored.

---

## How connecting works

There are two ways to connect, selected by `MCP_AUTH_MODE`.

**OAuth β€” remote custom connector (headline, Azure default).** Each user adds
the connector once and signs in to their own Clio account:

1. In Claude: **Settings β†’ Connectors β†’ Add custom connector**.
2. Paste the connector URL: `${PUBLIC_BASE_URL}/mcp`
   (e.g. `https://ca-cliomanage-prod.<region>.azurecontainerapps.io/mcp`).
3. Claude runs OAuth discovery, registers itself via Dynamic Client
   Registration, and starts a PKCE authorization-code flow.
4. The server shows a **consent page** where the user picks an access tier β€”
   read-only, read + write, or read/write/delete (the last only when the
   server runs with `CLIO_ALLOW_DESTRUCTIVE=true`).
5. The user is redirected to **Clio** to sign in and authorize.
6. Clio returns to the server's `/oauth/clio/callback`, the server bridges the
   Clio tokens into an MCP session scoped to the chosen tier, and Claude lands
   back **connected** β€” with only the tools that tier covers.

> Why tiers live here: Clio Manage's own OAuth has **no** per-request scopes β€”
> an app's access permissions are fixed in Clio's Developer Portal and users
> must accept all of them. Least-privilege at the connector layer is enforced
> by this server's own scopes. Also set the narrowest access permissions that
> work on the Clio Developer Application itself.

No bearer token is pasted anywhere. Each user's Clio tokens are encrypted and
isolated to their own session.

**Static β€” shared bearer token (secondary, single-tenant).** A simpler mode for
solo or single-account setups: one shared bearer token gates `/mcp`, mapped to a
single shared Clio account seeded from a refresh token. See
[Quick start β€” Azure](#quick-start--azure-remote-connector) (optional static
variant) and [docs/oauth-setup.md](docs/oauth-setup.md).

**Local (stdio).** For development and solo use, the binary runs as a local
stdio MCP server and authorizes through the loopback OAuth flow β€” no public URL
needed. See [Quick start β€” local](#quick-start--local-stdio).

---

## Architecture

Azure deployment in OAuth mode (primary). The server is an OAuth 2.0
Authorization Server + Protected Resource that bridges each Claude user to their
own Clio account:

```
                    Azure subscription
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚                                                           β”‚
   β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  HTTPS   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
   β”‚   β”‚  Claude    │─────────►│ Container    │──►│ Clio v4 β”‚  β”‚
   β”‚   β”‚ (each user β”‚  OAuth   β”‚ Apps         β”‚   β”‚  API    β”‚  β”‚
   β”‚   β”‚  signs in) β”‚  + /mcp  β”‚ (stateless)  β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
   β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                β”‚
   β”‚       β–²  per-user Clio sign-in   β”‚                        β”‚
   β”‚       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ (302 via Clio login)   β”‚
   β”‚                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚
   β”‚                  β”‚               β”‚              β”‚         β”‚
   β”‚             β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”  β”‚
   β”‚             β”‚ Key Vaultβ”‚  β”‚ Azure Filesβ”‚ β”‚ App Insightsβ”‚  β”‚
   β”‚             β”‚  (RBAC)  β”‚  β”‚  /state    β”‚ β”‚ + Log Anal. β”‚  β”‚
   β”‚             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
   β”‚                  β–²     (tokens.enc + sessions/ + audit)    β”‚
   β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                               β”‚
   β”‚         β”‚ Managed identity β”‚                               β”‚
   β”‚         β”‚ (KV secrets user β”‚                               β”‚
   β”‚         β”‚  + ACR pull)     β”‚                               β”‚
   β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                               β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

Or as a Mermaid graph (renders inline on GitHub):

```mermaid
flowchart LR
  classDef azure fill:#deebf7,stroke:#08519c,color:#000;
  classDef clio  fill:#fef3c7,stroke:#92400e,color:#000;
  classDef host  fill:#dcfce7,stroke:#166534,color:#000;

  H[Claude<br/>per-user connector]:::host
  subgraph AZ[Azure subscription]
    direction TB
    CA[Container App<br/>OAuth AS + /mcp]:::azure
    KV[Key Vault<br/>RBAC]:::azure
    SF[Azure Files<br/>/state mount]:::azure
    LA[Log Analytics<br/>App Insights]:::azure
    MI[Managed identity]:::azure
    ACR[Azure Container<br/>Registry]:::azure
  end
  CLIO[Clio v4 API<br/>us Β· ca Β· eu Β· au]:::clio

  H -- OAuth discovery + DCR + PKCE --> CA
  H -- user sign-in (302) --> CLIO
  CLIO -- /oauth/clio/callback --> CA
  CA -- REST (per-user token) --> CLIO
  CA --- SF
  CA -. logs/metrics .-> LA
  MI -. pull image .-> ACR
  MI -. read secrets .-> KV
  KV -. inject .-> CA
  ACR -. image .-> CA
```

Resources provisioned by [`infra/main.bicep`](infra/main.bicep):

| Resource                             | Purpose                                                  |
|--------------------------------------|----------------------------------------------------------|
| Log Analytics + Application Insights | Logs, metrics, traces                                    |
| Azure Container Registry (Basic)     | Private image registry, anonymous pull disabled          |
| User-assigned managed identity       | ACR pull + Key Vault Secrets User                        |
| Azure Key Vault (RBAC, soft-delete)  | Stores the Clio app + encryption secrets                 |
| Azure Storage + File Share           | Persistent `/state` mount (tokens.enc, sessions/, audit) |
| Container Apps environment           | Hosts the workload, file share registered                |
| Container App                        | HTTPS ingress, OAuth default, autoscale 1β†’4 by default   |

### HTTP surface

| Endpoint | Purpose |
|---|---|
| `GET /healthz` | Liveness β€” always 200 (`{ status, server, auth_mode, region }`) |
| `GET /readyz` | Readiness β€” 200 in OAuth mode; in static/hybrid, 503 until the shared account is authenticated |
| `POST /mcp` | The MCP endpoint (auth-protected). On 401 it returns `WWW-Authenticate` with `resource_metadata=".../.well-known/oauth-protected-resource/mcp"` |
| `GET\|DELETE /mcp` | 405 (the server is stateless POST-only) |

In `oauth` / `hybrid` mode the server additionally serves the OAuth
Authorization Server + Protected Resource surface:

| Endpoint | Purpose |
|---|---|
| `GET /.well-known/oauth-authorization-server` | Authorization Server metadata (discovery, advertises `clio:read` `clio:write` `clio:destructive` `offline_access` + S256) |
| `GET /.well-known/oauth-protected-resource/mcp` | Protected Resource metadata for `/mcp` (incl. `scopes_supported`) |
| `POST /register` | Dynamic Client Registration |
| `GET /authorize` | Authorization endpoint β€” renders the scope-consent page |
| `POST /oauth/consent` | Consent submission (tier choice) β€” 302s the user to Clio |
| `POST /token` | Token endpoint (PKCE; authorization_code + refresh_token; response carries `scope`) |
| `POST /revoke` | Token revocation |
| `GET /oauth/clio/callback` | Clio's redirect target; completes the bridge |

### Request lifecycle (OAuth mode)

What happens when a connected user invokes a tool β€” session-token check, Clio
token refresh, Clio call, and audit log, end-to-end:

```mermaid
sequenceDiagram
  autonumber
  participant H as Claude<br/>(connected user)
  participant S as Container App<br/>/mcp endpoint
  participant V as OAuth provider<br/>(session verify)
  participant K as Session store<br/>(/state, AES-256-GCM)
  participant C as Clio v4 API
  participant A as Audit log<br/>(/state/audit-*.log)

  H->>+S: POST /mcp Β· Bearer <MCP session token><br/>{ method: tools/call, name, args }
  S->>V: verify access token
  Note right of V: 401 + WWW-Authenticate<br/>resource_metadata on miss
  V->>K: load session β†’ bridged Clio tokens
  alt Clio token expired (or near expiry)
    K->>+C: POST /oauth/token (refresh_token)
    C-->>-K: new access + refresh
    K->>K: re-encrypt session (AES-256-GCM)
  end
  S->>+C: GET/POST /api/v4/... Β· Bearer <Clio access>
  C-->>-S: { data, meta }
  S->>A: append { ts, tool, outcome, duration_ms,<br/>user_id, matter_id, result_count, transport, caller_id }
  S-->>-H: JSON-RPC result
```

---

## Quick start β€” Azure (remote connector)

> Primary path. ~15 minutes the first time. Ends with attorneys adding the
> connector in Claude and signing in to Clio themselves.

### Prerequisites

- Azure subscription with the `Microsoft.App` and `Microsoft.ContainerRegistry`
  providers registered
- `az`, `azd`, and Docker installed locally
- A **Clio Developer Application** (one per deployment). You'll register its
  redirect URI in step 4, after `azd up` tells you the public URL.

### 1. Provision (defaults to OAuth mode)

```bash
az login
azd auth login
azd env new clio-manage-prod
azd env set AZURE_LOCATION eastus2
azd env set CLIO_REGION us           # us | ca | eu | au
azd up                                # builds image, runs Bicep, deploys
```

`azd up` deploys with `MCP_AUTH_MODE=oauth` (the infra default) and **auto-sets
`PUBLIC_BASE_URL`** from the Container Apps environment domain β€” you never set it
by hand.

### 2. Populate Key Vault (3 secrets)

OAuth mode needs exactly three secrets. (No shared bearer token, no bootstrap
refresh token β€” each user authorizes themselves.)

```bash
KV=$(azd env get-values | awk -F= '/AZURE_KEY_VAULT_NAME/{print $2}' | tr -d '"')

az keyvault secret set --vault-name "$KV" --name clio-client-id      --value "<from Clio>"
az keyvault secret set --vault-name "$KV" --name clio-client-secret  --value "<from Clio>"
az keyvault secret set --vault-name "$KV" --name clio-encryption-key --value "$(openssl rand -hex 32)"
```

### 3. Roll a fresh revision so the secrets bind

The first `azd up` starts a revision *before* the secrets exist, so it stays
unhealthy until now. Create a new revision β€” it re-resolves the Key Vault refs:

```bash
APP=$(azd env get-values | awk -F= '/SERVICE_API_NAME/{print $2}' | tr -d '"')
RG=$(azd env get-values | awk -F= '/AZURE_RESOURCE_GROUP/{print $2}' | tr -d '"')
az containerapp update -n "$APP" -g "$RG" --revision-suffix "s$(date +%s)"
```

### 4. Register the redirect URI in Clio, then verify

Grab the public URL and register the connector callback on your Clio Developer
Application (*Settings β†’ Developer Applications*):

```bash
BASE=$(azd env get-values | awk -F= '/SERVICE_API_URI/{print $2}' | tr -d '"')
echo "Register this Redirect URI in Clio: ${BASE}/oauth/clio/callback"

curl -sS "${BASE}/healthz"
# {"status":"ok","server":"clio-manage-mcp","auth_mode":"oauth","region":"us"}
```

### 5. Add the connector in Claude β†’ sign in to Clio

Share the connector URL with each attorney (it's the same for everyone):

```
${PUBLIC_BASE_URL}/mcp      # e.g. https://<your-app>.<region>.azurecontainerapps.io/mcp
```

In Claude: **Settings β†’ Connectors β†’ Add custom connector β†’ paste the URL.**
Claude runs OAuth discovery + dynamic client registration, redirects the user to
**Clio** to sign in and authorize, and returns connected. Each user connects
their own Clio account.

Full walk-through (custom domain, logs, audit export, rotation, troubleshooting,
and the optional shared-account variant): [docs/deployment-azure.md](docs/deployment-azure.md).

### Optional: shared-account (static) variant

For a single-tenant deployment where one shared Clio login is acceptable, deploy
in `static` mode instead and seed a shared bearer token + refresh token:

```bash
azd env set MCP_AUTH_MODE static
azd up
# then also set the two static-mode secrets in Key Vault and restart:
#   clio-http-auth-tokens   (a bearer token your clients present on /mcp)
#   clio-refresh-token      (from examples/bootstrap-refresh-token.mjs)
```

Details: [docs/deployment-azure.md](docs/deployment-azure.md) and
[docs/oauth-setup.md](docs/oauth-setup.md).

---

## Quick start β€” Docker (any host)

Prefer not to use Azure? A prebuilt image is published to GitHub Container
Registry on every push to `main`. Run it on any host that can terminate TLS in
front of it (a VM behind nginx/Caddy/Cloudflare Tunnel, Fly, Render, etc.):

```bash
docker run -d --name clio-manage-mcp -p 8765:8765 \
  -e MCP_AUTH_MODE=oauth \
  -e PUBLIC_BASE_URL=https://mcp.yourfirm.com \
  -e CLIO_CLIENT_ID=... \
  -e CLIO_CLIENT_SECRET=... \
  -e CLIO_ENCRYPTION_KEY="$(openssl rand -hex 32)" \
  -e CLIO_REGION=us \
  -v clio-state:/state \
  ghcr.io/patrickking67/clio-manage-mcp:latest
```

`PUBLIC_BASE_URL` must be the **public HTTPS URL** Claude and Clio actually
reach (the OAuth issuer + redirect must be absolute https). Point your TLS proxy
at port 8765, add `https://mcp.yourfirm.com/mcp` as the connector in Claude, and
register `https://mcp.yourfirm.com/oauth/clio/callback` on the Clio app.

For local development without a public URL, use stdio (below) or
`docker compose up --build` (see [docker-compose.yml](docker-compose.yml)).

> The GHCR image is published by CI. If `docker run` reports `denied`, the
> package is still private β€” make it public under the repo's **Packages**
> settings, or run `docker login ghcr.io` first.

---

## Quick start β€” local (stdio)

> Development, single-user, or seeding a shared-account refresh token for static mode.

```bash
git clone https://github.com/patrickking67/clio-manage-mcp.git
cd clio-manage-mcp
npm install
npm run build
cp .env.example .env
# fill: CLIO_CLIENT_ID, CLIO_CLIENT_SECRET, CLIO_ENCRYPTION_KEY (openssl rand -hex 32)
```

Register `http://127.0.0.1:5678/callback` as a Redirect URI on your Clio
Developer Application (Clio allows several, so the same app can serve both local
stdio and the remote connector). Then wire the server into Claude Desktop /
Claude Code with one of the [examples/](examples/), and run `authenticate with
Clio` in a conversation. The encrypted token blob lives at
`~/.clio-manage-mcp/tokens.enc` and auto-refreshes ahead of expiry. Full guide:
[docs/deployment-local.md](docs/deployment-local.md).

> You can also run the HTTP transport locally in `hybrid` mode for connector
> development by setting `PUBLIC_BASE_URL=http://localhost:8765` β€” see
> [docs/deployment-local.md](docs/deployment-local.md).

---

## Tool catalog

| Domain          | Tools                                                                                   |
|-----------------|-----------------------------------------------------------------------------------------|
| Auth            | `clio_authenticate` Β· `clio_auth_status` Β· `clio_logout` Β· `clio_who_am_i`              |
| Matters         | `clio_list_matters` Β· `clio_get_matter` Β· `clio_create_matter` Β· `clio_update_matter` Β· `clio_delete_matter` Β· `clio_list_matter_contacts` |
| Contacts        | `clio_search_contacts` Β· `clio_get_contact` Β· `clio_create_person_contact` Β· `clio_create_company_contact` Β· `clio_update_contact` Β· `clio_delete_contact` |
| Activities      | `clio_list_activities` Β· `clio_get_activity` Β· `clio_create_time_entry` Β· `clio_create_expense_entry` |
| Tasks           | `clio_list_tasks` Β· `clio_get_task` Β· `clio_create_task` Β· `clio_update_task`           |
| Notes           | `clio_list_notes` Β· `clio_create_note`                                                  |
| Calendar        | `clio_list_calendar_entries` Β· `clio_create_calendar_entry` Β· `clio_list_calendars`     |
| Documents       | `clio_list_documents` Β· `clio_get_document` Β· `clio_get_document_download_url` Β· `clio_list_folders` |
| Bills           | `clio_list_bills` Β· `clio_get_bill` Β· `clio_get_billing_summary`                        |
| Users           | `clio_list_users` Β· `clio_get_user`                                                     |
| Practice areas  | `clio_list_practice_areas`                                                              |
| Workflows       | `clio_open_new_matter` (client + matter + flat-fee + note + task in one call)           |
| Escape hatch    | `clio_api_request` (raw v4 endpoint with `{ data: ... }` wrapping)                      |

In OAuth mode the auth tools (`clio_authenticate`, `clio_logout`) are not used β€”
sign-in happens through Claude's connector flow, not from inside a conversation.
`clio_auth_status` / `clio_who_am_i` still report the current session.

Destructive operations (`clio_delete_*`, `DELETE` via `clio_api_request`) are
disabled unless `CLIO_ALLOW_DESTRUCTIVE=true`.

By scope tier: **27 read**, **12 write**, **2 destructive**.

### How much of the Clio API this covers

Measured against Clio's published OpenAPI document
([`docs.developers.clio.com/openapi.json`](https://docs.developers.clio.com/openapi.json),
`openapi: 3.0.0`, `info.version: v4`), which describes **301 operations across
166 paths in 89 resource groups**.

This server wraps **15 of those groups** as first-class tools β€” roughly 59
operations, chosen as the ones a lawyer touches daily:

| Covered group | Ops | Group | Ops |
|---|---|---|---|
| Matters | 5 | Documents | 7 |
| Contacts | 5 | Folders | 6 |
| Activities | 5 | Tasks | 5 |
| Bills | 5 | Notes | 5 |
| Calendars | 5 | Calendar Entries | 5 |
| Practice Areas | 5 | Users | 3 |
| Clients | 1 | Matter Contacts | 1 |
| Related Contacts | 1 | | |

**The other 74 groups have no dedicated tool** and are reached through
`clio_api_request`, which is bounded by the same scope tiers (GET needs
`clio:read`, POST/PATCH/PUT need `clio:write`, DELETE needs `clio:destructive`
*and* `CLIO_ALLOW_DESTRUCTIVE=true`). The larger ones, if you need them:

Custom Fields Β· Custom Field Sets Β· Custom Actions Β· Communications Β·
Conversations Β· Relationships Β· Reminders Β· Timers Β· Webhooks Β· Reports Β·
Report Presets/Schedules Β· Bank Accounts Β· Bank Transactions Β· Trust Line Items Β·
Trust Requests Β· Clio Payments (Links, Payments) Β· Credit Memos Β· Interest
Charges Β· Line Items Β· Matter Stages Β· Matter Dockets Β· Task Templates Β·
Task Types Β· Text Snippets Β· Document Templates/Versions/Categories Β·
Expense Categories Β· Activity Rates Β· Groups Β· Damages Β· Medical Records/Bills Β·
Grants Β· Utbms Codes/Sets Β· Jurisdictions

That split is a deliberate 80/20, not an oversight: modelling all 301 operations
as tools would blow past any client's tool budget and bury the ones that matter.
If you find yourself reaching for `clio_api_request` repeatedly for the same
endpoint, that's the signal it deserves promotion to a real tool β€” open an issue.

---

## Resources

The server publishes two MCP resources that clients may auto-include at
session start:

| URI                          | What it carries                                                |
|------------------------------|----------------------------------------------------------------|
| `clio://compliance/notice`   | ABA Opinion 512 reminder + audit-logging summary               |
| `clio://auth/status`         | Live JSON view of authentication state and configuration       |

---

## Configuration

Variables and which mode they apply to. In an Azure deployment the OAuth-mode
variables (`MCP_AUTH_MODE`, `PUBLIC_BASE_URL`, transport, ports, state dir) are
set by the Bicep template; you only manage the Key Vault secrets.

| Variable                      | Applies to        | Required        | Default        | Purpose                                                                |
|-------------------------------|-------------------|-----------------|----------------|------------------------------------------------------------------------|
| `CLIO_CLIENT_ID`              | all               | yes             | β€”              | From your Clio Developer Application                                   |
| `CLIO_CLIENT_SECRET`          | all               | yes             | β€”              | From your Clio Developer Application                                   |
| `CLIO_ENCRYPTION_KEY`         | all               | yes             | β€”              | 64-hex (32 bytes). `openssl rand -hex 32`                              |
| `CLIO_REGION`                 | all               | no              | `us`           | `us` / `ca` / `eu` / `au`                                              |
| `MCP_AUTH_MODE`               | http              | no              | `hybrid`†      | `oauth` / `static` / `hybrid`. Azure infra default is `oauth`          |
| `PUBLIC_BASE_URL`             | http (oauth/hybrid) | yes in oauth/hybrid | β€”        | Public HTTPS base URL of this server. Auto-set by Azure Bicep         |
| `MCP_SESSION_TTL_SECONDS`     | http (oauth/hybrid) | no            | `2592000`      | Lifetime of an issued MCP session (30 days). Clio tokens auto-refresh  |
| `CLIO_OAUTH_SCOPES`           | http (oauth/hybrid) | no            | (unset)        | **No-op β€” leave unset.** Clio's authorize endpoint has no `scope` param |
| `CLIO_HTTP_AUTH_TOKENS`       | http (static/hybrid) | static: yes  | β€”              | Comma-separated shared bearer tokens accepted on `/mcp`               |
| `CLIO_BOOTSTRAP_REFRESH_TOKEN`| http (static/hybrid) | no            | β€”              | Seeds the single shared Clio account on first boot                     |
| `CLIO_TRANSPORT`              | all               | no              | `stdio`        | `stdio` or `http`. CLI flags `--stdio` / `--http` override             |
| `CLIO_HTTP_PORT`              | http              | no              | `8765`         | HTTP transport port                                                    |
| `CLIO_HTTP_HOST`              | http              | no              | `0.0.0.0`      | HTTP transport bind                                                    |
| `CLIO_REDIRECT_PORT`          | stdio             | no              | `5678`         | Loopback port for the local OAuth callback                             |
| `CLIO_REDIRECT_HOST`          | stdio             | no              | `127.0.0.1`    | Loopback host for the local OAuth callback                             |
| `CLIO_STATE_DIR`              | all               | no              | `~/.clio-manage-mcp/`† | Holds `tokens.enc`, `sessions/`, audit log. On Azure: the Files mount  |
| `CLIO_API_VERSION`            | all               | no              | (account default) | Pins the Clio API version via the `X-API-VERSION` header               |
| `CLIO_AUDIT_MODE`             | all               | no              | `metadata`     | `none` / `metadata` / `full`                                          |
| `CLIO_ALLOW_DESTRUCTIVE`      | all               | no              | `false`        | Enables DELETE endpoints                                               |
| `CLIO_DEFAULT_PAGE_SIZE`      | all               | no              | `25`           | Records per Clio API page                                              |
| `CLIO_MAX_PAGE_SIZE`          | all               | no              | `200`          | Hard cap on total records returned by a list tool                     |
| `CLIO_MAX_RESULT_CHARS`       | all               | no              | `140000`       | Truncates an oversized tool result below the client's hard limit       |
| `CLIO_DEFAULT_USER_ID`        | all               | no              | β€”              | Default attorney/user id for matter creation                          |
| `LOG_LEVEL`                   | all               | no              | `info`         | `error` / `warn` / `info` / `debug`                                  |

† The server's own default is `hybrid`; the Azure Bicep deploys `oauth`. The
state dir falls back to the legacy `~/.clio-mcp/` when it exists and the new
default does not, so pre-rename installs keep their tokens.

In `oauth` mode the required secrets are just `CLIO_CLIENT_ID`,
`CLIO_CLIENT_SECRET`, and `CLIO_ENCRYPTION_KEY`. `CLIO_HTTP_AUTH_TOKENS` and
`CLIO_BOOTSTRAP_REFRESH_TOKEN` are only consulted in `static`/`hybrid` mode.

### Versions and compatibility

Verified 2026-07-27. Everything here is a moving target β€” re-check before
assuming.

| Thing | Where we are | Notes |
|---|---|---|
| Clio Manage API | v4, account default | Current default is **4.0.13** (promoted 2026-01-06). A Multi-Tax release followed 2026-04-08. Pin with `CLIO_API_VERSION=4.0.13` to insulate against changes Clio ships to the default. |
| Clio access token | 30 days | Refreshed automatically ~60 s before expiry. |
| Clio refresh token | **Does not expire** | A long-lived credential. Always encrypted at rest. |
| Clio rate limit | 50 req/min/token at peak | Higher off-peak, varies by region. The client honours `Retry-After` and retries 429s on any method; 5xx and network errors are retried on GET only. |
| `@modelcontextprotocol/sdk` | **1.29.0** (current stable) | v2 is in beta for the 2026-07-28 spec and splits into `@modelcontextprotocol/client` + `/server`. v1.x keeps receiving fixes for β‰₯6 months after v2 ships. **Staying on v1 for this release.** |
| MCP auth spec | 2025-06-18 via the SDK | Claude accepts 2025-03-26, 2025-06-18, and 2025-11-25. |
| Node | β‰₯20 (built and shipped on 22) | |

**Client limits worth designing around.** Claude.ai and Claude Desktop reject
tool results past **~150,000 characters**; Claude Code truncates at 25,000
tokens (`MAX_MCP_OUTPUT_TOKENS`). Claude.ai's request timeout is **300 s**.
Results are serialised compactly and capped at `CLIO_MAX_RESULT_CHARS` (default
140,000) with an explicit truncation note, so an oversized list degrades into
actionable feedback instead of a silently dropped response β€” but the better fix
is always a narrower `limit` or `fields`.

---

## Security & compliance posture

**At a glance**

| Layer                       | What this server does                                                | What you should still do                                              |
|-----------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------|
| OAuth (per-user)            | OAuth 2.1 + PKCE bridge; each user signs in on Clio's own domain      | Use a single Clio Developer Application per deployment                |
| Token storage               | AES-256-GCM at rest, per session; key in Key Vault (Azure) or env    | Rotate `clio-encryption-key` on offboarding                            |
| `/mcp` auth                 | OAuth session token (oauth) or shared bearer (static), constant-time  | In static mode, rotate `clio-http-auth-tokens` per caller / departure  |
| Audit                       | Append-only JSONL of every tool call (metadata or redacted args)     | Export + retain per firm policy; the server does not rotate            |
| Destructive operations      | Off by default (`CLIO_ALLOW_DESTRUCTIVE=false`)                      | Keep off unless you have a specific reason                             |
| Telemetry                   | None. Only outbound call is to your configured Clio region's API     | Pair with Claude Enterprise / API+ZDR for conversation-side controls   |

**Detail**

- **Per-user OAuth 2.1.** In OAuth mode the server is an OAuth Authorization
  Server + Protected Resource. Claude discovers it, registers via Dynamic Client
  Registration, and runs PKCE. The user logs in directly on Clio's domain; the
  server never sees a Clio password. Each user's Clio tokens are bridged into an
  isolated, encrypted MCP session.
- The encryption key never leaves the host. Tampered ciphertext fails
  decryption β€” `AES-256-GCM` is authenticated encryption, so partial /
  tampered token blobs cannot be silently used.
- The audit log captures: ISO timestamp, tool name, outcome, duration in ms,
  Clio user id, matter id (when applicable), result count, transport
  identifier, and a per-caller fingerprint. In `full` mode it also records
  argument payloads with redaction of known-secret keys.
- The HTTP transport is **stateless POST-only** on `/mcp`. `GET` and `DELETE`
  return 405. An unauthenticated `/mcp` request returns 401 with an RFC 9728
  `WWW-Authenticate` challenge pointing at the protected-resource metadata. The
  static-token check uses constant-time comparison to avoid timing side channels.
- **Multi-replica safe.** Sessions, registered clients, and pending authorizations
  live as encrypted records on the shared `/state` mount, so any replica can
  serve any request given the same encryption key.

Threat model + Azure-specific notes: [docs/security.md](docs/security.md).

---

## Cost (Azure)

For a typical firm at moderate volume (single-digit-thousands of tool
calls/day) running one warm replica:

Verified against Azure retail pricing (East US, pay-as-you-go, 2026-07):

| Component | Idle $/mo | Light use $/mo |
|----------------------------|-----------|-----------|
| Container App (0.5 vCPU, 1 GiB, minReplicas=1, idle rate) | ~$10.20 | ~$10.50–12 |
| Container Apps environment | $0 | $0 |
| Azure Container Registry (Basic, flat daily rate) | ~$5.07 | ~$5.07 |
| Key Vault (standard, per-op) | <$0.05 | <$0.05 |
| Azure Files (billed on used GiB, not the 10 GiB quota) | ~$0.10 | ~$0.30 |
| Log Analytics (first 5 GB/mo free; 30-day retention free) | $0 | $0–1 |
| Application Insights | included with workspace | included |
| **Total** | **~$15–16/mo** | **~$16–18/mo** |

Setting `minReplicas=0` (scale-to-zero) drops the total to **~$5–6/mo** β€” ACR
Basic becomes the floor β€” at the cost of a several-second cold start on the
first request after idle. For a per-user OAuth connector that attorneys hit
throughout the day, one warm replica is the usual choice; for evaluation
deployments, scale-to-zero is the obvious setting (`azd env set MIN_REPLICAS 0`).

---

## FAQ

**How do attorneys connect?**
In OAuth mode (the Azure default), each attorney goes to **Settings β†’ Connectors
β†’ Add custom connector** in Claude, pastes `${PUBLIC_BASE_URL}/mcp`, and signs in
to **their own** Clio account when redirected. No token to copy. See
[How connecting works](#how-connecting-works).

**Does everyone share one Clio login?**
Not in OAuth mode β€” each user authorizes their own Clio account and only sees
their own data. The shared-login model exists only in `static` mode, for
single-tenant setups that opt into it.

**Is this safe for client matter data?**
It's built for it. Clio tokens are encrypted at rest per session, every tool
call is audited, no data is cached or mirrored, and no outbound calls happen
besides Clio. But this server only secures the Clio-to-AI boundary β€” pair it with
**Claude Enterprise** or the **Claude API with Zero Data Retention** so the
conversations themselves get the right handling.

**Does Claude train on what we send through this?**
It depends entirely on the Claude tier you pair this with. Claude Pro/Max
(consumer): Anthropic does not train on chats by default. Claude Team /
Enterprise: explicit no-training contract. Claude API with ZDR: no training,
no retention. This server doesn't change any of that; the tier choice you
make matters far more than anything in this codebase.

**We're on Clio EU / CA / AU. Does it work?**
Yes. Set `CLIO_REGION` to `eu`, `ca`, or `au` and the server routes OAuth +
API + tokens against the matching regional host. Tokens minted in one region
will not authenticate in another, by design.

**How do we revoke a user's access?**
OAuth mode: revoke the connector from the user's side in Claude, or call
`/revoke`; the firm can also revoke the Developer Application in Clio
(*Settings β†’ Developer Applications*), which invalidates everyone. Static mode:
remove the bearer token from `clio-http-auth-tokens` and restart, or delete the
`clio-refresh-token` secret.

**Do we have to use Azure?**
No. Local stdio works completely standalone. Azure Container Apps is the
primary production path because it's the cleanest match for a stateless OAuth
MCP gateway (HTTPS ingress, managed identity, shared file mount, autoscale),
but the Docker image runs on EKS, ECS, Fly, Render, or any other container
host β€” set `MCP_AUTH_MODE`, `PUBLIC_BASE_URL`, and the secrets yourself, and
mount a shared volume at `CLIO_STATE_DIR`.

**Can we use this with hosts besides Claude?**
The OAuth connector targets Claude's custom-connector flow. The underlying
transport is standard MCP (stdio + Streamable HTTP), verified against Claude
Desktop, Claude Code, and the MCP Inspector, and expected to work with any
client implementing those transports.

**What's the trust story for installing this?**
This isn't on npm. Clone, audit, build from source. No telemetry. The only
outbound calls go to your configured Clio region's API.

---

## Verification

Three protocol-level smoke tests plus a unit-test suite drive a real MCP
session against the built binary. They assert on tool count, resource
publication, auth enforcement, the OAuth discovery/registration handshake,
and error shape. All run on every commit ([build.yml](.github/workflows/build.yml)).

```bash
npm run build
npm run smoke:stdio    # raw JSON-RPC over spawned --stdio child
npm run smoke:http     # SDK Client over Streamable HTTP against spawned --http child (static mode)
npm run smoke:oauth    # OAuth discovery: metadata, dynamic client registration, /authorize -> Clio, 401 + WWW-Authenticate
npm test               # unit tests: encrypted session store + Clio OAuth provider
```

A passing stdio run:

```
βœ“ initialize -> clio-manage-mcp 2.2.0
βœ“ tools/list -> 41 tools
βœ“ tool catalog includes expected names
βœ“ resources/list -> 2 resources
βœ“ resources/read clio://auth/status -> authenticated:false
βœ“ tools/call clio_who_am_i (no auth) -> isError:true
ALL CHECKS PASSED βœ“
```

A passing HTTP run (the script pins `MCP_AUTH_MODE=static` so it needs no
public URL):

```
βœ“ /healthz ready
βœ“ /mcp without bearer -> 401
βœ“ /mcp with wrong bearer -> 401
βœ“ Client.connect (initialize round-trip) succeeded
βœ“ tools/list -> 41 tools
βœ“ resources/read clio://auth/status -> authenticated:false
βœ“ GET /mcp -> 405 (server is stateless POST-only)
ALL HTTP CHECKS PASSED βœ“
```

---

## Confirmed Clio API quirks

Empirical findings from Clio v4 that surprised someone. Baked into the
client + tool descriptions so they don't surprise you, and listed here so
the next person doesn't have to re-derive:

- **`billing_method` at the matter root is silently ignored.** To set a flat
  fee, PATCH the matter with `custom_rate: { type: "FlatRate", rates: [...] }`.
  `clio_create_matter`'s `flat_rate_amount` parameter does this for you.
- **`TimeEntry.total = quantity_in_hours Γ— rate`** (NOT `Γ— price`). For
  flat-fee line items use `clio_create_expense_entry` (`total = quantity Γ—
  price`).
- **Activities GET requires explicit `fields`** β€” a bare GET returns only id
  + etag. `description` is write-only; on GET use `note`. `rate` is not a
  valid GET field.
- **Activities list filter is `matter_id` (singular int).** `matter` and
  `matter[id]` are silently ignored β€” you'll get account-wide results back
  with no error.
- **Enum casing is inconsistent across Clio, and it is case-sensitive.** Matter
  status and task status are lowercase (`open`, `complete`) everywhere. Task
  *priority* is lowercase in the list filter (`high`) but capitalized in the
  create/update body (`High`). There is no way to guess this β€” check the spec.
- **Several field names are not what they look like.** Bills use `state` (not
  `status`), `last_sent_at` (not `sent_at`), and `matters` plural (a bill can
  span several). Notes use `author` and `created_at`/`updated_at` (not `user`,
  `date_created`, `date_updated`). Calendar entries use `calendars` plural and
  `calendar_entry_event_type`. Documents use `document_category` and have **no**
  `description` field at all.
- **Activities have no `billed`/`non_billable` query parameters.** A single
  `status` filter covers both axes (`billed`, `unbilled`, `draft`, `billable`,
  `non_billable`, `written_off`). Confusingly, `non_billable` *is* valid in the
  create body β€” filter and body vocabularies differ.
- **Task date filters are `due_at_from` / `due_at_to`**, not
  `due_date_start` / `due_date_end`.
- **Sub-resources are nested, not top-level.** Email addresses and phone numbers
  live at `/contacts/{id}/email_addresses.json`; UTBMS is `/utbms/codes.json`
  and `/utbms/sets.json`; court rules are under `/court_rules/`; Clio Payments
  under `/clio_payments/`; and the timer endpoint is `/timer.json`, singular.

> Every one of the above was verified against Clio's published OpenAPI document
> ([`docs.developers.clio.com/openapi.json`](https://docs.developers.clio.com/openapi.json)),
> and several were live bugs in this repo before that check was run. If you are
> building against Clio, do the same β€” the docs pages do not surface enum casing.
- **Mutating payloads must be wrapped `{ data: ... }`.** The dedicated tools
  do this for you. `clio_api_request` wraps when you pass `data:`; pass
  `body:` to send something verbatim.
- **Address `name` is enum-validated** β€” exactly `Work`, `Home`, `Billing`,
  or `Other`. The tools coerce invalid names to `Work`.
- **DELETE on bills is soft-delete (void).** The bill moves to `void` state
  rather than disappearing.
- **Region cross-talk fails.** A token minted at `app.clio.com` will not
  authenticate against `eu.app.clio.com`. Pick one and stick with it. The same
  applies to the OAuth bridge: the redirect and token exchange must both target
  the configured region.
- **`/oauth/authorize` has no `scope` parameter.** The full accepted set is
  `client_id`, `response_type`, `redirect_uri`, `state`, `redirect_on_decline`.
  App permissions are fixed per-application in the Developer Portal and every
  user accepts all of them, which is exactly why this server implements its own
  scope tiers. `CLIO_OAUTH_SCOPES` therefore does nothing β€” leave it unset.
- **`redirect_on_decline` defaults to `false`, which strands users.** Without
  it, a user who clicks Decline stops on a Clio page and the calling application
  never finds out. This server always sends `true`. (Clio's docs promise the
  redirect but do not specify which error parameters come back on it β€” the
  server handles both the named-error and bare-redirect cases.)
- **Refresh tokens never expire; access tokens last 30 days.** Convenient, but
  it means a leaked refresh token is valid until someone revokes the app in
  Clio. Treat it as the crown jewel β€” it is why every stored copy is encrypted.
- **Rate-limit headers are on every response.** `X-RateLimit-Limit`,
  `X-RateLimit-Remaining`, and `X-RateLimit-Reset` (50/min/token at peak, higher
  off-peak, varies by region). The client currently reacts to a 429 with
  `Retry-After` rather than pre-emptively backing off β€” see the roadmap.

---

## The Clio Manage plugin

The MCP server gives Claude **capability** β€” 41 tools against the Clio v4 API.
The plugin in [`plugin/`](plugin/) gives it **judgment**: which tool to reach
for, how to chain them into real workflows, when to stop and ask a human, and
what ABA Op 512 requires along the way.

```bash
claude /plugin marketplace add patrickking67/clio-manage-mcp
claude /plugin install clio-manage@clio
```

Then `/clio:doctor` to verify the connection.

**8 commands** β€” complete workflows, not single tool calls:

| | | | |
|---|---|---|---|
| `/clio:doctor` | `/clio:daily` | `/clio:intake` | `/clio:log-time` |
| `/clio:conflicts` | `/clio:wip` | `/clio:ar` | `/clio:trust-recon` |

**10 skills** that activate on their own β€” setup, search, best practices,
matter intake, time entry, billing, contacts, calendar, document automation,
trust accounting.

**3 agents** β€” `clio-intake-agent` (autonomous intake, gated writes),
`clio-data-analyst` (read-only reporting), `clio-conflicts-checker` (exhaustive
conflict search with name-variant expansion, read-only).

**3 hooks** that make the safety posture real rather than advisory:

| Event | Behavior |
|---|---|
| `SessionStart` | Reports region, sign-in state, key validity, and whether deletes are enabled β€” so "why isn't Clio working" is answered before it's asked |
| `PreToolUse` on `clio_delete_*` | Asks you to confirm the specific record id. Client files are firm records with retention obligations |
| `PostToolUse` on writes | Local `client-writes.jsonl` trail. **Metadata only** β€” never argument values, because that file isn't encrypted |

Full plugin docs: [`plugin/README.md`](plugin/README.md).

---

## Works with

One server, one set of guidance, every major agent surface.

| Surface | How it connects | What it gets |
|---|---|---|
| **Claude.ai / Desktop** | Remote OAuth connector, or the `.mcpb` bundle | 41 tools, 2 resources, per-user Clio sign-in |
| **Claude Code** | Plugin + MCP, or `.mcp.json` | Everything above, plus commands, skills, agents, hooks |
| **Claude Cowork** | Portable skills in `.agents/skills/` | The 10 Clio workflow skills |
| **OpenAI Codex** | Reads `AGENTS.md` + `.agents/skills/` natively | Repo instructions and the same workflow skills |
| **Cursor** | Reads `AGENTS.md` directly, plus `.cursor/rules/` | Repo instructions and glob-scoped source guardrails |
| **GitHub Copilot** | `.github/copilot-instructions.md` | Repo instructions and the non-negotiables |
| **Microsoft Foundry / Copilot Studio** | Remote MCP endpoint | See [docs/foundry.md](docs/foundry.md) |
| **Any MCP client** | Streamable HTTP or stdio | The full tool surface |

The guidance has **one canonical source** β€” [`AGENTS.md`](AGENTS.md). Vendor
files are thin adapters that point at it, and the Clio skills are authored once
in `plugin/skills/` and mirrored to `.agents/skills/` by `npm run sync:ai`.
`npm run check:ai` fails CI if any of it drifts: a missing adapter, a version
mismatch across the six artifacts that carry it, a stale skills mirror, a
tracked user-local file, or a documented command that no longer exists.

---

## Optional connectors

The remote Clio connector is the one your firm consumes from Claude. Other
connectors are recommended for specific skills but never required:

| Tier | Connector | What it adds |
|---|---|---|
| Recommended | Microsoft 365 | Outlook, Calendar, SharePoint, Word, Teams |
| Recommended | Google Workspace | Gmail, Calendar, Drive, Docs |
| Useful | DocuSign | eSignature for engagement letters |
| Useful | Stripe | Payments + Clio Payments reconciliation |
| Useful | Slack / Teams | Internal firm comms |
| Ops | Sentry / App Insights | Monitor the MCP server in production |

Setup steps for each: [`docs/connectors.md`](docs/connectors.md).

---

## Landing page

A static GitHub Pages site lives at [`docs/`](docs/) and deploys via
[`.github/workflows/pages.yml`](.github/workflows/pages.yml). Enable
**Settings β†’ Pages β†’ Source: GitHub Actions** on the repo and the next push
to `main` publishes it. Public URL:

> https://patrickking67.github.io/clio-manage-mcp/

The site is built with Tailwind CSS via CDN (no build step) and renders
hero, feature, tool-catalog, install, plugin, connectors, security, and FAQ
sections from a single `docs/index.html`.

---

## Development

```bash
npm install
npm run dev:stdio        # tsx watch, stdio mode
npm run dev:http         # tsx watch, http mode
npm run lint             # tsc --noEmit
npm run build            # tsc + chmod +x
npm run smoke:stdio      # protocol smoke test (stdio)
npm run smoke:http       # protocol smoke test (http, static mode)
npm run smoke:oauth      # protocol smoke test (http, OAuth discovery)
npm test                 # unit tests (session store, OAuth provider, scopes, client, result cap)
npm run pack:mcpb        # build the production-only .mcpb bundle
npm run smoke:mcpb       # unpack that bundle and run it standalone
npm run inspector        # MCP Inspector against the built binary
```

The MCP Inspector is the fastest way to iterate on tool schemas against a
real Clio account. Source map and declaration files ship with the build so
debuggers and IDEs work out of the box.

### Building the MCPB bundle

`npm run pack:mcpb` is the only supported way to produce `clio-manage-mcp.mcpb`.
It never packs the working tree β€” it stages exactly the runtime inputs
(`build/`, `manifest.json`, `package.json`, lockfile, README, LICENSE, icon),
runs `npm ci --omit=dev` in that staging tree, and packs the result. Packing the
repo in place would sweep in `typescript`, `tsx`, `esbuild`, and `@types/*`,
which is how the bundle previously reached 17.8 MB. It is now **4.2 MB**.

The install is deliberately hermetic: `npm_config_*` variables exported by the
parent `npm run` are stripped and an empty user config is passed, so the bundle
depends on nothing but `package-lock.json`. (Without this, a developer whose
`~/.npmrc` sets `allow-scripts` cannot pack at all β€” npm 12 rejects that key
during a project-scoped install.)

`npm run smoke:mcpb` is the check that matters. `smoke:stdio` runs the repo's
own `build/` against the repo's full `node_modules`, so it cannot notice a
runtime dependency that `--omit=dev` pruned. `smoke:mcpb` unpacks the archive to
a temp directory and then **launches it the way Claude Desktop does** β€” reading
`manifest.server.mcp_config`, substituting `${__dirname}` and
`${user_config.*}`, and spawning from an unrelated working directory so the
bundle has to stand on its own absolute paths and bundled `node_modules`. It
asserts none of the declared devDependencies are present, that the manifest's
entry point resolves to a real file, and that the `user_config` β†’ env mapping
actually delivers `CLIO_CLIENT_ID` / `CLIO_CLIENT_SECRET` /
`CLIO_ENCRYPTION_KEY` β€” a broken mapping means a fresh install cannot boot,
and testing an invented command would never reveal it. Then it drives real
JSON-RPC. Both run in CI on every push, and the bundle is uploaded as a build
artifact.

Project layout:

```
src/
β”œβ”€β”€ index.ts              entry point β€” picks transport from --stdio/--http
β”œβ”€β”€ config.ts             env loading + region routing + auth-mode resolution
β”œβ”€β”€ server.ts             McpServer factory
β”œβ”€β”€ audit.ts              JSONL audit log with secret redaction
β”œβ”€β”€ resources.ts          clio:// MCP resources
β”œβ”€β”€ auth/                 Clio OAuth flow, OAuth AS provider, encrypted session store
β”œβ”€β”€ clio/                 HTTP client (auth refresh, retry, pagination)
β”œβ”€β”€ transports/           stdio + Streamable HTTP (OAuth/static/hybrid)
β”œβ”€β”€ tools/                tool modules, 41 tools
└── util/                 stderr logger, error types
infra/main.bicep          Container Apps + ACR + Key Vault + Files (OAuth default)
Dockerfile                multi-stage build, distroless-style runtime
scripts/smoke-*.mjs       end-to-end MCP protocol tests (stdio, http, oauth, mcpb)
scripts/pack-mcpb.mjs     reproducible production-only MCPB build
docs/                     deployment-local, deployment-azure, oauth, security, connectors
examples/                 client configs + bootstrap-refresh-token.mjs
```

---

## Related work

Two prior open-source Clio MCP implementations informed this one, and both
are worth reading if you're evaluating options:

- **[oktopeak/clio-mcp](https://github.com/oktopeak/clio-mcp)** β€” TypeScript,
  local-stdio focused, ~15 read-mostly tools. Strong on the law-firm-IT
  install ergonomics; ships an npm package with a clean 6-step setup.
- **[lawyered0/clio-mcp](https://github.com/Lawyered0/clio-mcp)** β€” Python /
  FastMCP, with a deeply documented set of Clio API quirks (flat-fee
  `custom_rate` setup, activity field aliases, region routing). Most of the
  quirks section in this README originates from that prior empirical work.

This implementation contributes: a **per-user remote OAuth 2.1 connector** for
Claude (Authorization Server + Protected Resource bridging to Clio),
**Azure-native deployment** with Bicep + azd, **broader tool surface** (41 vs.
~15), **stateless Streamable HTTP transport** with encrypted per-session token
storage, **end-to-end protocol smoke tests** in CI, and a **composite intake
workflow** that chains the most common matter-opening sequence into one agent
action.

---

## Roadmap

Shipped in 2.3.0: `redirect_on_decline` so a declined Clio sign-in returns
cleanly instead of stranding the user, a result-size cap below Claude's
~150k-character limit, per-replica audit files (SMB appends are not atomic),
a `..` guard on the raw-API escape hatch, and a **reproducible production-only
MCPB build** (`npm run pack:mcpb`) that is verified by unpacking and running it
(`npm run smoke:mcpb`) β€” 17.8 MB β†’ 4.2 MB.

Shipped in 2.2.0: interactive scope tiers with a consent page, MCPB packaging
for Claude Desktop, ETag/If-Match on update tools, `X-API-VERSION` pinning,
truncation-aware list results, GET-only retry (no write duplication), and a
Microsoft Foundry / Copilot Studio guide ([docs/foundry.md](docs/foundry.md)).

Next:

- Honour Clio's `X-RateLimit-Remaining` / `X-RateLimit-Reset` headers to back
  off *before* a 429 rather than reacting to one.
- Sweep stale DCR client registrations β€” `sweep()` covers pending records and
  sessions, but client registrations have no expiry and accumulate.
- Index sessions by refresh-token hash; today a refresh grant scans and decrypts
  every session file, which is a network round trip each over Azure Files.
- Migrate to MCP SDK v2 once it ships stable (2026-07-28 spec).

- Typed **trust accounting** tools (`/bank_accounts`, `/bank_transactions`,
  `/trust_line_items`) β€” today the highest-stakes domain runs through the raw
  escape hatch.
- **Communications** (`/communications.json`), **custom fields** write support,
  and **webhook subscriptions** for live matter / task / bill events.
- OS-keychain integration for the encryption key (macOS Keychain, Linux
  secret-service, Windows Credential Manager) so the key isn't on disk.
- Private Endpoint + Front Door / API Management options in Bicep
  (see [docs/foundry.md](docs/foundry.md) for the APIM AI Gateway pattern).
- CIMD (Client ID Metadata Documents) alongside DCR once client support
  stabilizes, to avoid client-registration buildup on busy deployments.

---

## License

MIT β€” see [LICENSE](LICENSE).

TDQS

A3.6/5.0

Scored across 41 tools

Disambiguation4/5

Tools are generally well-differentiated with clear names and descriptions. However, there is minor potential for confusion between generic contact creation tools (create_company_contact vs. create_person_contact) and the high-level open_new_matter tool that wraps multiple steps, but descriptions clarify distinct use cases.

Naming Consistency5/5

All tools follow a strict 'clio_<verb>_<noun>' pattern (e.g., clio_create_matter, clio_list_bills, clio_get_contact). The verb is always imperative and the noun is singular. This consistent naming makes the tool set predictable and easy to navigate.

Tool Count4/5

At 41 tools, the server is comprehensive but not excessively large for a full-featured legal practice management API. Each tool covers a distinct CRUD operation or domain entity. The inclusion of a generic api_request tool reduces the need for more dedicated tools, keeping the count reasonable.

Completeness4/5

The tool set covers most major entities (matters, contacts, tasks, time/expenses, bills, documents, folders, calendars, notes, users) with create, read, list, update, delete where appropriate. Missing operations like document upload are partly mitigated by the download URL tool and the generic api_request. Minor gaps exist (e.g., trust requests handled only via api_request), but overall coverage is strong.