Clio Manage MCP
The Clio Manage MCP server connects AI assistants (like Claude) to Clio Manage, enabling law firms to manage matters, clients, billing, calendar, documents, and more through natural language.
Authentication & Session Management
Check auth status, identify the current user, authenticate via OAuth or static token, and logout.
Matter Management
List, get, create, update, and soft-delete matters; filter by status, client, practice area, attorney, or date; list matter contacts.
Contact Management
Search, get, create (person or company), update, and delete contacts; manage emails, phones, and addresses.
Time & Expense Tracking
List and get time/expense entries; log billable or non-billable hours; record expenses or flat-fee line items on a matter.
Task Management
List, get, create, and update tasks; filter by matter, status, priority, due date, or assignee; mark tasks complete.
Notes
List notes on matters/contacts and create notes with subject and detail (plain text or HTML).
Calendar
List available calendars, list calendar entries within a date range, and create calendar events optionally linked to a matter.
Document Management
List documents and folders, get document metadata, and retrieve short-lived signed download URLs.
Billing
List bills (filtered by matter, client, state, or date), get bill details, and retrieve aggregated billing summaries (billed, paid, outstanding balance).
Users & Firm Configuration
List and get firm users; list configured practice areas.
Composite Intake Workflow (clio_open_new_matter)
Single-call intake: creates or reuses a client contact, opens a matter, optionally sets a flat fee, adds an opening note, and creates an intake task — all at once.
Raw API Escape Hatch (clio_api_request)
Issue arbitrary requests to any Clio v4 API endpoint not covered by dedicated tools (e.g., custom fields, trust accounts, webhooks, communications).
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Clio Manage MCPwhat are my billable hours this week?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Clio Manage MCP
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.).
Built with
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
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 → connectedNo 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_requestescape hatchA composite intake workflow (
clio_open_new_matter) that chains client + matter + opening note + intake task into one agent actionPer-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-tokenstaticmode is still available.Azure-native. A single
azd upprovisions Container Apps + ACR + Key Vault + Azure Files + Log Analytics, defaults to OAuth mode, and auto-setsPUBLIC_BASE_URLfrom 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, andnpm run smoke:oauthdrive 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_REGIONenv.
Contents
Related MCP server: filevine-mcp
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_paymentstate 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:
In Claude: Settings → Connectors → Add custom connector.
Paste the connector URL:
${PUBLIC_BASE_URL}/mcp(e.g.https://ca-cliomanage-prod.<region>.azurecontainerapps.io/mcp).Claude runs OAuth discovery, registers itself via Dynamic Client Registration, and starts a PKCE authorization-code flow.
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).The user is redirected to Clio to sign in and authorize.
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 (optional static
variant) and 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.
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):
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 .-> CAResources provisioned by 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 |
Container Apps environment | Hosts the workload, file share registered |
Container App | HTTPS ingress, OAuth default, autoscale 1→4 by default |
HTTP surface
Endpoint | Purpose |
| Liveness — always 200 ( |
| Readiness — 200 in OAuth mode; in static/hybrid, 503 until the shared account is authenticated |
| The MCP endpoint (auth-protected). On 401 it returns |
| 405 (the server is stateless POST-only) |
In oauth / hybrid mode the server additionally serves the OAuth
Authorization Server + Protected Resource surface:
Endpoint | Purpose |
| Authorization Server metadata (discovery, advertises |
| Protected Resource metadata for |
| Dynamic Client Registration |
| Authorization endpoint — renders the scope-consent page |
| Consent submission (tier choice) — 302s the user to Clio |
| Token endpoint (PKCE; authorization_code + refresh_token; response carries |
| Token revocation |
| 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:
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 resultQuick 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.AppandMicrosoft.ContainerRegistryproviders registeredaz,azd, and Docker installed locallyA Clio Developer Application (one per deployment). You'll register its redirect URI in step 4, after
azd uptells you the public URL.
1. Provision (defaults to OAuth mode)
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, deploysazd 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.)
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:
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):
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/mcpIn 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.
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:
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 and 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.):
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:latestPUBLIC_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).
The GHCR image is published by CI. If
docker runreportsdenied, the package is still private — make it public under the repo's Packages settings, or rundocker login ghcr.iofirst.
Quick start — local (stdio)
Development, single-user, or seeding a shared-account refresh token for static mode.
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/, 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.
You can also run the HTTP transport locally in
hybridmode for connector development by settingPUBLIC_BASE_URL=http://localhost:8765— see docs/deployment-local.md.
Tool catalog
Domain | Tools |
Auth |
|
Matters |
|
Contacts |
|
Activities |
|
Tasks |
|
Notes |
|
Calendar |
|
Documents |
|
Bills |
|
Users |
|
Practice areas |
|
Workflows |
|
Escape hatch |
|
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,
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 |
| ABA Opinion 512 reminder + audit-logging summary |
| 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 |
| all | yes | — | From your Clio Developer Application |
| all | yes | — | From your Clio Developer Application |
| all | yes | — | 64-hex (32 bytes). |
| all | no |
|
|
| http | no |
|
|
| http (oauth/hybrid) | yes in oauth/hybrid | — | Public HTTPS base URL of this server. Auto-set by Azure Bicep |
| http (oauth/hybrid) | no |
| Lifetime of an issued MCP session (30 days). Clio tokens auto-refresh |
| http (oauth/hybrid) | no | (unset) | No-op — leave unset. Clio's authorize endpoint has no |
| http (static/hybrid) | static: yes | — | Comma-separated shared bearer tokens accepted on |
| http (static/hybrid) | no | — | Seeds the single shared Clio account on first boot |
| all | no |
|
|
| http | no |
| HTTP transport port |
| http | no |
| HTTP transport bind |
| stdio | no |
| Loopback port for the local OAuth callback |
| stdio | no |
| Loopback host for the local OAuth callback |
| all | no |
| Holds |
| all | no | (account default) | Pins the Clio API version via the |
| all | no |
|
|
| all | no |
| Enables DELETE endpoints |
| all | no |
| Records per Clio API page |
| all | no |
| Hard cap on total records returned by a list tool |
| all | no |
| Truncates an oversized tool result below the client's hard limit |
| all | no | — | Default attorney/user id for matter creation |
| all | no |
|
|
† 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 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 |
| 1.29.0 (current stable) | v2 is in beta for the 2026-07-28 spec and splits into |
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 |
| OAuth session token (oauth) or shared bearer (static), constant-time | In static mode, rotate |
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 ( | 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-GCMis 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
fullmode it also records argument payloads with redaction of known-secret keys.The HTTP transport is stateless POST-only on
/mcp.GETandDELETEreturn 405. An unauthenticated/mcprequest returns 401 with an RFC 9728WWW-Authenticatechallenge 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
/statemount, so any replica can serve any request given the same encryption key.
Threat model + Azure-specific notes: 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.
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).
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 providerA 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_methodat the matter root is silently ignored. To set a flat fee, PATCH the matter withcustom_rate: { type: "FlatRate", rates: [...] }.clio_create_matter'sflat_rate_amountparameter does this for you.TimeEntry.total = quantity_in_hours × rate(NOT× price). For flat-fee line items useclio_create_expense_entry(total = quantity × price).Activities GET requires explicit
fields— a bare GET returns only idetag.
descriptionis write-only; on GET usenote.rateis not a valid GET field.
Activities list filter is
matter_id(singular int).matterandmatter[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(notstatus),last_sent_at(notsent_at), andmattersplural (a bill can span several). Notes useauthorandcreated_at/updated_at(notuser,date_created,date_updated). Calendar entries usecalendarsplural andcalendar_entry_event_type. Documents usedocument_categoryand have nodescriptionfield at all.Activities have no
billed/non_billablequery parameters. A singlestatusfilter covers both axes (billed,unbilled,draft,billable,non_billable,written_off). Confusingly,non_billableis valid in the create body — filter and body vocabularies differ.Task date filters are
due_at_from/due_at_to, notdue_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.jsonand/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), 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_requestwraps when you passdata:; passbody:to send something verbatim.Address
nameis enum-validated — exactlyWork,Home,Billing, orOther. The tools coerce invalid names toWork.DELETE on bills is soft-delete (void). The bill moves to
voidstate rather than disappearing.Region cross-talk fails. A token minted at
app.clio.comwill not authenticate againsteu.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/authorizehas noscopeparameter. The full accepted set isclient_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_SCOPEStherefore does nothing — leave it unset.redirect_on_declinedefaults tofalse, 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 sendstrue. (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, andX-RateLimit-Reset(50/min/token at peak, higher off-peak, varies by region). The client currently reacts to a 429 withRetry-Afterrather 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/ 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.
claude /plugin marketplace add patrickking67/clio-manage-mcp
claude /plugin install clio-manage@clioThen /clio:doctor to verify the connection.
8 commands — complete workflows, not single tool calls:
|
|
|
|
|
|
|
|
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 |
| Reports region, sign-in state, key validity, and whether deletes are enabled — so "why isn't Clio working" is answered before it's asked |
| Asks you to confirm the specific record id. Client files are firm records with retention obligations |
| Local |
Full plugin docs: 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 | 41 tools, 2 resources, per-user Clio sign-in |
Claude Code | Plugin + MCP, or | Everything above, plus commands, skills, agents, hooks |
Claude Cowork | Portable skills in | The 10 Clio workflow skills |
OpenAI Codex | Reads | Repo instructions and the same workflow skills |
Cursor | Reads | Repo instructions and glob-scoped source guardrails |
GitHub Copilot |
| Repo instructions and the non-negotiables |
Microsoft Foundry / Copilot Studio | Remote MCP endpoint | See docs/foundry.md |
Any MCP client | Streamable HTTP or stdio | The full tool surface |
The guidance has one canonical source — 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.
Landing page
A static GitHub Pages site lives at docs/ and deploys via
.github/workflows/pages.yml. Enable
Settings → Pages → Source: GitHub Actions on the repo and the next push
to main publishes it. Public URL:
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
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 binaryThe 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.mjsRelated work
Two prior open-source Clio MCP implementations informed this one, and both are worth reading if you're evaluating options:
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 — Python / FastMCP, with a deeply documented set of Clio API quirks (flat-fee
custom_ratesetup, 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).
Next:
Honour Clio's
X-RateLimit-Remaining/X-RateLimit-Resetheaders 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 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.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/patrickking67/clio-manage-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server