clio-mcp
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-mcpcreate a new matter for client John Smith about the trademark dispute"
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-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.
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).
Tools (10)
Tool | Purpose |
| Auth check — confirm credentials work |
| Create entity client (Inc., LLC, etc.) |
| Create individual client |
| Create matter, optional flat-fee setup via |
| Add a billable line item (add-on expense, not the primary flat fee) |
| Search by name and/or email |
| Search by display_number, query, or client_id |
| Cleanup test data |
| Cleanup test data |
| 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 |
| ✅ Verified |
MCP Inspector | stdio |
| ✅ 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 defaultThis 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_applicationsCA:
https://ca.app.clio.com/settings/developer_applicationsEU/UK:
https://eu.app.clio.com/settings/developer_applicationsAU:
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/callbackApprove. 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 |
|
CA |
|
EU/UK |
|
AU |
|
billing_method at the matter root is silently ignored — use custom_rate instead
Every 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 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 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.
This server cannot be installed
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/lawyered0/clio-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server