loading…
Search for a command to run...
loading…
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, matte
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.
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.
Supports flat-fee billing in one call — clio_create_matter(..., flat_rate_amount=X) chains POST + PATCH so Clio flips billing_method to "flat" and auto-creates the billable line item. This uses Clio's custom_rate association (top-level billing_method is silently ignored on POST/PATCH — see docs/flat-fee-workaround.md and the Confirmed Clio API quirks section below).
| 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 flat-fee setup via flat_rate_amount |
clio_create_flat_fee_activity |
Add a billable line item (add-on expense, not the primary flat fee) |
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 |
# 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)
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.
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.
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 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.
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.
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).
Sign in to Clio and go to Settings → Developer Applications. Direct URL by region:
https://app.clio.com/settings/developer_applicationshttps://ca.app.clio.com/settings/developer_applicationshttps://eu.app.clio.com/settings/developer_applicationshttps://au.app.clio.com/settings/developer_applicationsClick New Application. Fill in:
Clio MCP)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)contacts, matters, activities, users. Add bills, calendar, documents if you'll extend.Save. You get back:
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.
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.
.envCLIO_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).
redirect_uri must match EXACTLY between the app config, the /oauth/authorize URL, and the /oauth/token POST — including trailing slash, port, and protocol. Mismatches return generic 400 invalid_grant.invalid_grant on Step 3, the code probably expired (10 min limit) or was already used.ca.app.clio.com, use that host throughout. Mixing US and non-US endpoints in the dance returns 400 invalid_grant or 401 later.bills to 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.
These were discovered empirically against the live API. Trust them, don't re-derive:
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 at the matter root is silently ignored — use custom_rate insteadEvery value sent for a top-level billing_method field ("flat", "Flat", "FLAT", "FlatRate", "flat_fee", "contingency", integers, etc.) saves as "hourly". PATCH on the field after creation also returns 200 but doesn't change it. The flat-fee setter is not billing_method — it's a nested custom_rate association: PATCH /matters/{id}.json with {"data": {"custom_rate": {"type": "FlatRate", "rates": [{"user": {"id": <attorney>}, "rate": <amount>}]}}}. After that PATCH, GET returns billing_method: "flat" and Clio auto-creates a billable flat_rate: true TimeEntry for the amount. clio_create_matter exposes this via the flat_rate_amount parameter. See docs/flat-fee-workaround.md.
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.
description and note for the line-item text. GET only accepts note in ?fields=... — querying description returns 400 InvalidFields.rate is NOT a valid GET field on activities (returns 400). Use price × quantity.matter_id filter footgun on /activities.jsonList 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.
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.
name is enum-validatedMust be exactly "Work", "Home", "Billing", or "Other". The natural-sounding "Business" returns 422. The _normalize_address helper auto-coerces invalid/missing names to "Work".
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.
Use "type": "Company" for entities (Inc., LLC, Ltd., numbered companies) and "type": "Person" for individual humans. Both POST to /contacts.json.
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.
Some accounts get ~1 hour TTL, others get 30 days (expires_in: 2592000). The token manager honors whatever the server returns.
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-ishDELETE 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.
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 billingYou 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.
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.
PRs welcome, especially for:
If you discover Clio API behavior that contradicts what's documented here, please open an issue with a reproduction.
MIT. See LICENSE.
Built from frustration with the official documentation. The flat-fee setup in particular took several hours of empirical testing and a back-and-forth with Clio support to land on custom_rate as the correct mechanism — written up in docs/flat-fee-workaround.md so the next person doesn't have to.
By @BitGrateful.
Добавь это в claude_desktop_config.json и перезапусти Claude Desktop.
{
"mcpServers": {
"clio-mcp": {
"command": "npx",
"args": []
}
}
}