clio-mcp
An MCP server for Clio Manage legal practice management software that enables Claude and other MCP clients to read and write Clio data including contacts, matters, and activities directly from chat.
README
clio-mcp
A Model Context Protocol server for Clio Manage, the practice management software for law firms.
Lets Claude (or any MCP client) read and write your Clio data — contacts, matters, activities — directly from chat. Built and tested against Clio's v4 REST API.
Includes a documented workaround for Clio's silent rejection of billing_method on matter creation — see docs/flat-fee-workaround.md and the Confirmed Clio API quirks section below.
Tools (10)
| Tool | Purpose |
|---|---|
clio_who_am_i |
Auth check — confirm credentials work |
clio_create_company_contact |
Create entity client (Inc., LLC, etc.) |
clio_create_person_contact |
Create individual client |
clio_create_matter |
Create matter, optional default attorney |
clio_create_flat_fee_activity |
Add a flat-fee billable line item — the workaround |
clio_find_contact |
Search by name and/or email |
clio_find_matter |
Search by display_number, query, or client_id |
clio_delete_matter |
Cleanup test data |
clio_delete_contact |
Cleanup test data |
clio_api_request |
Generic v4 API escape hatch |
Quick start
# 1. Clone and install
git clone https://github.com/Lawyered0/clio-mcp.git
cd clio-mcp
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# 2. Get OAuth credentials from Clio (one-time, ~5 min)
# See "Getting Clio OAuth credentials" below for the full dance.
# You'll end up with: CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN.
# 3. Configure
cp .env.example .env
# edit .env with your credentials and (optional) default attorney id
# 4. Test it works (Ctrl+C to exit)
python clio_mcp_server.py --stdio
# 5. Wire it into your MCP client (verified paths below)
# - Claude Desktop: build a DXT (see docs/claude-desktop-dxt.md) ← recommended
# - Claude Code CLI: edit ~/.claude/settings.json (see below)
# - MCP Inspector: mcp dev clio_mcp_server.py (for development)
Verified clients
This server has been used in production against the following MCP clients:
| Client | Transport | How to wire | Status |
|---|---|---|---|
| Claude Desktop (Code tab and regular chat) | stdio | DXT extension — see docs/claude-desktop-dxt.md | ✅ Verified |
| Claude Code CLI | stdio | ~/.claude/settings.json — see below |
✅ Verified |
| MCP Inspector | stdio | mcp dev clio_mcp_server.py |
✅ Verified |
| Cowork / claude.ai web / other URL-based hosts | HTTPS | Would need a public tunnel + auth on the server | ⚠️ Not pursued — see HTTP mode notes |
TL;DR: if you're on Mac, install via DXT into Claude Desktop. If you're already a Claude Code CLI user, the JSON config is two lines. Either path takes ~5 minutes once you have OAuth credentials.
HTTP mode (not recommended yet)
The server can also run as an HTTP service:
python clio_mcp_server.py # binds 127.0.0.1:8765/mcp by default
This was built for use with URL-based connectors (Cowork, claude.ai web, etc.) but those hosts typically require HTTPS, often reject 127.0.0.1 URLs, and may have additional security policies. Making this safe for production use means: terminating TLS (e.g. Caddy with tls internal), exposing it via a tunnel (e.g. Cloudflare Tunnel), and adding header-based auth inside the server. None of that is implemented or verified. PRs welcome.
Claude Code CLI config
Add to ~/.claude/settings.json:
{
"mcpServers": {
"clio": {
"command": "/absolute/path/to/clio-mcp/.venv/bin/python",
"args": [
"/absolute/path/to/clio-mcp/clio_mcp_server.py",
"--stdio"
]
}
}
}
Restart your Claude Code session, then try clio_who_am_i to verify.
Claude Desktop config (DXT)
Claude Desktop uses DXT extensions. See docs/claude-desktop-dxt.md for a working manifest template and install instructions. Note: Claude Desktop requires a full app restart (Cmd+Q + relaunch) before a newly-installed DXT appears in any session's tool registry.
.env
CLIO_CLIENT_ID=<from Clio developer portal>
CLIO_CLIENT_SECRET=<from Clio developer portal>
CLIO_REFRESH_TOKEN=<from initial OAuth dance — see docs/oauth-setup.md>
# Optional: defaults to US (app.clio.com). For other regions:
# CA: https://ca.app.clio.com/api/v4/ + https://ca.app.clio.com/oauth/token
# EU: https://eu.app.clio.com/api/v4/ + https://eu.app.clio.com/oauth/token
# AU: https://au.app.clio.com/api/v4/ + https://au.app.clio.com/oauth/token
CLIO_BASE_URL=https://app.clio.com/api/v4/
CLIO_TOKEN_URL=https://app.clio.com/oauth/token
# Optional: if set, used as the default responsible_attorney + originating_attorney
# on clio_create_matter when the caller doesn't pass attorney_id explicitly.
CLIO_DEFAULT_ATTORNEY_ID=
chmod 600 .env for hygiene.
Getting Clio OAuth credentials (one-time)
This is the trickiest part of setup. Clio uses OAuth 2.0 authorization-code flow — you do this dance once to mint a refresh token, then the server handles access-token refreshes automatically. The refresh token is long-lived, so you should only ever do this once per OAuth app (unless the token gets revoked).
Step 1 — Create a developer application in Clio
Sign in to Clio and go to Settings → Developer Applications. Direct URL by region:
- US:
https://app.clio.com/settings/developer_applications - CA:
https://ca.app.clio.com/settings/developer_applications - EU/UK:
https://eu.app.clio.com/settings/developer_applications - AU:
https://au.app.clio.com/settings/developer_applications
Click New Application. Fill in:
- Name: anything (e.g.
Clio MCP) - Redirect URI:
http://localhost:8765/callback(Clio just needs the code to land somewhere — the page will fail to load when you're redirected there, that's expected and fine) - Scope: check every scope you might want to use — adding scopes later requires re-running this whole dance. At minimum:
contacts,matters,activities,users. Addbills,calendar,documentsif you'll extend.
Save. You get back:
- Client ID (visible in the app list anytime)
- Client Secret (shown once on creation — copy it now, you cannot retrieve it later)
Step 2 — Get an authorization code
Visit this URL in your browser, substituting YOUR_CLIENT_ID and your region's host:
https://app.clio.com/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost:8765/callback
Approve. Your browser will redirect to http://localhost:8765/callback?code=XXXXXXXX... and show a "connection refused" error page. Ignore the error — copy the code=XXXXXXXX value out of the browser's address bar.
The code is single-use and expires in ~10 minutes. Move quickly to Step 3.
Step 3 — Exchange the code for a refresh token
curl -X POST https://app.clio.com/oauth/token \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "grant_type=authorization_code" \
-d "code=THE_CODE_FROM_STEP_2" \
-d "redirect_uri=http://localhost:8765/callback"
(Substitute your region's host if not US.)
Response:
{
"access_token": "<short-lived; ignore>",
"token_type": "bearer",
"expires_in": 2592000,
"refresh_token": "<this is the one you want>"
}
Copy the refresh_token — that's what goes into .env as CLIO_REFRESH_TOKEN. The access_token you can discard; the server mints fresh ones on demand.
Step 4 — Drop into .env
CLIO_CLIENT_ID=<from Step 1>
CLIO_CLIENT_SECRET=<from Step 1>
CLIO_REFRESH_TOKEN=<from Step 3>
Run clio_who_am_i via your MCP client. If it returns 200 with your user record — you're done forever. If 401 — re-run from Step 2 (the code expired, or the redirect URI didn't match exactly).
Common gotchas
redirect_urimust match EXACTLY between the app config, the/oauth/authorizeURL, and the/oauth/tokenPOST — including trailing slash, port, and protocol. Mismatches return generic400 invalid_grant.- Don't reuse the code — it's single-use. If you get
invalid_granton Step 3, the code probably expired (10 min limit) or was already used. - Region matters — if your Clio account is on
ca.app.clio.com, use that host throughout. Mixing US and non-US endpoints in the dance returns400 invalid_grantor401later. - Scope changes require re-doing the dance. If you add
billsto the app's scopes later, you need a new auth code → new refresh token. Existing tokens don't auto-acquire new scopes.
For re-authorization (if your refresh token ever gets revoked) and additional troubleshooting, see docs/oauth-setup.md.
Confirmed Clio API quirks
These were discovered empirically against the live API. Trust them, don't re-derive:
Region routing
Clio runs on regional hosts. Mixing region endpoints in a single request/response cycle returns 401 invalid_token — a token minted at one region won't authenticate against another region's API. Pick one host and stick with it for both OAuth and API calls.
| Region | Host |
|---|---|
| US | app.clio.com |
| CA | ca.app.clio.com |
| EU/UK | eu.app.clio.com |
| AU | au.app.clio.com |
billing_method is silently ignored on POST /matters.json
This is the big one. Every value sent for billing_method ("flat", "Flat", "FLAT", "FlatRate", "flat_fee", "contingency", integers, etc.) results in billing_method: "hourly" on subsequent GET. PATCH after creation also returns 200 but doesn't change the value. ~16 companion field guesses (flat_rate_amount, flat_fee_amount, rate, matter_rate, billing_preference, etc.) all silently ignored too. The Clio web UI uses a private endpoint not exposed in the public REST API.
Workaround: leave matters as "hourly" and add a flat-amount Activity. See docs/flat-fee-workaround.md and use clio_create_flat_fee_activity.
TimeEntry total math
TimeEntry.total = quantity_in_hours × rate, not × price. So a TimeEntry with quantity_in_hours: 0 always totals $0 regardless of price. For flat-fee line items, use ExpenseEntry (total = quantity × price) — qty=1, price=N → total=N.
Activity field-name aliases
- POST accepts both
descriptionandnotefor the line-item text. GET only acceptsnotein?fields=...— queryingdescriptionreturns 400 InvalidFields. rateis NOT a valid GET field on activities (returns 400). Useprice×quantity.
matter_id filter footgun on /activities.json
List filter param is matter_id (singular int). matter or matter[id] are silently ignored and return account-wide activities — typo here returns wrong results without an error.
Default GET on activities returns minimal fields
A bare GET /activities/{id}.json returns only id and etag. You must explicitly pass ?fields=id,type,date,note,total,price,quantity,non_billable,... to get anything useful.
Address name is enum-validated
Must be exactly "Work", "Home", "Billing", or "Other". The natural-sounding "Business" returns 422. The _normalize_address helper auto-coerces invalid/missing names to "Work".
Mutating payloads must be wrapped
All POST/PATCH bodies must be {"data": {...}}. Sending the payload at root fails. The named tools handle this; the generic clio_api_request does NOT add the wrapper for you.
Contact type discriminator
Use "type": "Company" for entities (Inc., LLC, Ltd., numbered companies) and "type": "Person" for individual humans. Both POST to /contacts.json.
Refresh tokens may or may not rotate
Depends on the OAuth app config. The token manager handles both cases — if the refresh response includes a new refresh_token, it persists to .clio_tokens.json; if not, the .env value stays canonical. You don't need to reason about this in normal use.
Access token TTLs vary
Some accounts get ~1 hour TTL, others get 30 days (expires_in: 2592000). The token manager honors whatever the server returns.
Clio silently accepts unknown POST fields
Sending {"data": {"client": {"id": X}, "description": "...", "zzz_bogus": 1}} to /matters.json returns 201 with the unknown field dropped. Validation errors are NOT a reliable way to discover valid field names — you have to read the docs (or read this README).
/contacts.json and /matters.json deletes are idempotent-ish
DELETE returns 204 on success. DELETE on an already-deleted resource returns 404. DELETE on a contact with open bills returns 409 with a specific error code; on a contact that's a client of an open matter returns 422.
Bills are soft-deleted
DELETE on /bills/{id}.json returns 204 immediately, but the bill is moved to "void" state rather than purged from records. Probably an accounting/audit-trail design choice. Voided bills eventually drop off list endpoints.
practice_area_id doesn't drive billing
You might think setting practice_area_id to a "small claims" area would make the matter flat-fee. It doesn't. Practice areas are pure metadata; they don't affect any billing fields.
Architecture
The server is a single Python file (clio_mcp_server.py) using FastMCP from the official Anthropic MCP SDK. It runs on demand (stdio mode for Claude Code / Claude Desktop / MCP Inspector) or as a long-lived HTTP service (for URL-based connectors).
The token manager (ClioTokenManager) handles OAuth refresh transparently — every API call checks the cached access token, refreshes if expired (with a 60s safety buffer), and persists rotated refresh tokens to .clio_tokens.json (chmod 600).
All tools return a uniform {"status_code": int, "body": <parsed JSON>} shape so error payloads from Clio's validator come through verbatim — useful when something doesn't work as expected.
Contributing
PRs welcome, especially for:
- Additional tools (bills, trust requests, calendar entries, document uploads, etc.)
- Confirmed quirks I haven't documented
- Region/locale support
- Tests (the empirical findings would benefit from a recorded-cassette test suite)
If you discover Clio API behavior that contradicts what's documented here, please open an issue with a reproduction.
License
MIT. See LICENSE.
Acknowledgements
Built from frustration with the official documentation. The flat-fee workaround in particular took several hours of empirical testing to uncover — written up in docs/flat-fee-workaround.md so the next person doesn't have to.
By @BitGrateful.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.