Skip to main content
Glama

Clio Manage MCP

License: MIT Node ≥ 20 TypeScript MCP Build Status: beta

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

TypeScript Node.js Microsoft Azure Container Apps Docker Bicep Express Clio OAuth 2.1 AES-256-GCM

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   →   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

  2. How connecting works

  3. Architecture

  4. Quick start — Azure (remote connector)

  5. Quick start — local (stdio)

  6. Tool catalog

  7. Resources

  8. Configuration

  9. Security & compliance posture

  10. Cost (Azure)

  11. FAQ

  12. Verification

  13. Confirmed Clio API quirks

  14. The Clio Manage plugin

  15. Works with

  16. Optional connectors

  17. Landing page

  18. Development

  19. Related work

  20. Roadmap

  21. License


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_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 (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 .-> CA

Resources 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 /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:

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)

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.)

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/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.

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: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).

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.

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 hybrid mode for connector development by setting PUBLIC_BASE_URL=http://localhost:8765 — see 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, 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.


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 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), 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/ 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@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 agentsclio-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.


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

Any MCP client

Streamable HTTP or stdio

The full tool surface

The guidance has one canonical sourceAGENTS.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:

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

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

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_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).

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 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.

Install Server
A
license - permissive license
A
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

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