Ask an AI assistant a question about your practice and you hit the same wall right away: it doesn’t know a thing about your practice.
“Draft a status letter for the Whitfield matter” gets you a beautifully written letter about nothing, because the model has never heard of Whitfield. So you paste. The client’s name, where the case stands, the last three filings, the deadline that’s keeping you up. Then you do it all again tomorrow, because tomorrow’s session has never heard of Whitfield either.
That loop is the main reason AI feels like a toy in a law office instead of a tool. It’s also a confidentiality decision you’re making dozens of times a day without really noticing you’re making it.
The fix is obvious in principle: let the assistant read the practice management system directly. Here’s what that took, and why it took more than it should have.
What should have existed
There’s an open standard for this. The Model Context Protocol is a published specification for hooking AI applications up to outside systems. The documentation’s own comparison is a USB-C port for AI. Instead of every assistant building a custom integration with every data source, the data source exposes one MCP server, and any MCP client can talk to it. Claude supports it. So does ChatGPT, and VS Code, and a growing list of others. Build the connector once, use it everywhere.
Here’s what it looks like in a law office, and it’s not what most lawyers picture:
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ Claude │◄──────►│ Clio connector │◄──────►│ Clio API │
│ (the client)│ MCP │ (runs on YOUR │ HTTPS │ app.clio.com│
│ │ │ machine) │ OAuth │ /api/v4 │
└──────────────┘ └──────────────────┘ └──────────────┘
People commonly get this picture wrong in three places.
It isn’t a data upload. Nothing ships your database anywhere. The connector sits in the middle. When you ask about a matter, the assistant calls one tool, the connector makes one API request, and one answer comes back.
It isn’t running loose. The connector offers a fixed menu. If there’s no “create a time entry” tool, no amount of prompting gets you a time entry. The boundary is code. A prompt can’t talk its way past it.
It runs where you put it. A connector on your own machine using your own logged-in session is a whole different risk from a cloud service holding your credentials. Both get called “connectors.” They’re not the same animal.
So you’ve got an open standard, broad client support, and a sensible design. You’d expect your practice management vendor to publish one.
As of this writing, Clio doesn’t. There’s no official, first-party MCP server. What’s out there is a handful of open-source ones written by other firms and small vendors, and they’re all over the map on coverage and quality.
So I wrote it
As it happens, it isn’t an MCP server. My integration is older than my interest in the protocol, and it’s a plain Python client that Claude Code runs. Three files. Roughly, the smaller the file, the more it matters.
keychain.py — twenty lines
Two functions wrapped around the macOS security command: read a secret, write a secret. Every credential lives in the operating system keychain and nowhere else. No .env file, no config constant, nothing in version control.
This is the most important file in the project, and there’s nothing clever in it. A Clio refresh token is a permanent key to every client file the firm has, and it doesn’t expire on its own. Let it land in a file inside a synced folder and now it lives on a vendor’s servers and on every device that folder touches. Twenty lines is the difference between that and not that.
clio_auth.py — a hundred and twenty lines, run once
The OAuth authorization-code flow. It opens a browser, you approve the application against your own Clio account, it catches the redirect on a local listener, trades the code for tokens, and writes them to the keychain. Then you never run it again.
Two details worth stealing. It generates a random state value before sending you to Clio and verifies the callback returns the same value. That’s the standard defense against somebody else’s authorization response getting fed to your listener. And it stores three things: the refresh token, the current access token, and the access token’s expiry, so nothing downstream has to make a pointless round trip to find out whether its token is still good.
clio_client.py — the client itself
Its docstring is one sentence, and that sentence is the whole design: a thin Clio v4 REST client with an endpoint allowlist.
The allowlist is the lock
If another lawyer takes one thing from this piece, I want it to be this.
The client doesn’t expose Clio’s API. It exposes a hand-written list of the operations this practice actually needs, about twenty named methods with typed arguments. Search matters. Get a matter. Search contacts. List calendar entries. List tasks. List time entries. A few writes. Each one is there because someone needed it for a specific job.
Every other endpoint in Clio’s API isn’t reachable. It isn’t discouraged, and it isn’t blocked by a line in a prompt. There’s simply no method to call. If a model decided at three in the morning that deleting a matter was the right move, there’d be nothing to invoke.
That’s a different kind of guarantee from telling an assistant to be careful. A prompt is a request. An unimplemented endpoint is a wall.
The catch is where the discipline gets tested: adding a method is a deliberate act. The minute you write a generic call_any_clio_endpoint(path, method, body) helper because it’d be handy, you’ve torn down the allowlist and put up a suggestion in its place.
The plumbing that keeps it usable
Three small behaviors, each one there because life without it was maddening.
Token refresh is automatic and invisible. Access tokens last about an hour. Instead of checking expiry everywhere, every request carries the current token; if Clio answers 401 on the first try, the client refreshes and retries once. One retry, no loop. A 401 that won’t go away is a real problem and you should hear about it.
Rate limiting is handled by waiting. On a 429, read the Retry-After header and sleep that long. That’s the whole strategy, and it’s never needed to be more. When someone asks you to slow down, slow down.
Pagination follows the cursor. Clio returns pages with a meta.paging.next URL. The client yields from a generator and follows the cursor, so the calling code asks for “matters matching X, limit 25” and gets 25 without ever knowing pages exist.
Field selection is a confidentiality control
This one gets its own heading because people usually describe it wrong. Clio’s API takes a fields parameter that says exactly what comes back, including nested values like client{id,name}. Every method in my client declares a narrow default.
People call this a performance feature. In a law office it’s a minimum-necessary control. Whatever you request lands in the assistant’s context and becomes part of a conversation. Ask for everything because listing fields was tedious, and everything about that matter is now sitting in that context, including fields you had no use for. A narrow field list per operation makes holding back the default and widening it a choice.
Four things the documentation doesn’t lead with
These cost me time. Maybe they’ll save you some.
You can’t create a bill through the API. POST /bills returns a 404. You can post time entries programmatically, but generating the actual invoice happens in the user interface. Any workflow that assumes it can run from captured time straight through to an issued invoice doesn’t exist. Plan for a human step. A bill is an assertion to a client, so that’s probably where a human belongs anyway.
Task creation requires an assignee. Leave it off and the call fails in a way that doesn’t obviously point at the assignee.
Time is stored in seconds. The quantity field on a time entry is seconds, not hours. Divide by 3,600. This is how you get a report saying you worked four thousand hours last Tuesday, and once you’ve seen that you never forget it.
Rounding happens on Clio’s side, upward, to the tenth of an hour. Stage entries on tenth-of-an-hour multiples before you approve them, or the number you approved and the number your client gets billed won’t match.
Why this shouldn’t have been my job
Everything above took a weekend, most of it OAuth. I’m reasonably technical and I enjoyed it. It still shouldn’t have been necessary, and not just because it ate my weekend.
Every firm is writing the same four hundred lines. Right now, in some number of law offices, somebody’s figuring out that quantity is in seconds. Somebody else is finding out that POST /bills 404s. Each of them is solving a problem with exactly one right answer, an answer the vendor already knows and could publish once.
The vendor is the only one in a position to do it right. A first-party connector could do things no firm-built one can. Real scoped permissions, so a connector could get read access to matters and calendar and nothing else, enforced on the server instead of by my own restraint in picking which methods to write. Per-user tokens instead of a firm-wide key. An audit log on the vendor’s side showing what an assistant actually touched. When a client asks what an AI system did with their file, that log is the difference between an answer and a shrug. And a kill switch that works even if the machine holding the token is gone.
I built an allowlist because it was the best control available to me. Server-side scopes would beat it every time, and I know it.
The gap is getting filled, and that’s what worries me. With no official connector, community ones have sprung up. Firms are installing them. Understand exactly what that means: you are handing someone else’s code an OAuth token to your entire client file.
If you’re going to do that, read the code. Don’t skim it. There are four questions, and if the source doesn’t answer them, the answer is no: does it ever send anything to a host other than Clio; where does it store the refresh token; does it log request or response bodies to disk, and where; can it write, and under what conditions. Minnesota Rule of Professional Conduct 1.6(c) requires “reasonable efforts to prevent the inadvertent or unauthorized disclosure of, or unauthorized access to, information relating to the representation of a client,” and “I installed a package with good reviews” isn’t obviously a reasonable effort when the package holds the keys to every client file you have.
The people writing these aren’t the problem. They’re filling a real gap, and some of them are careful. The problem is that a vendor’s silence has dumped a security decision on every individual firm, and the firm is the party in the chain least equipped to make it.
And it costs more than security. The whole argument of this section is that legal services get cheaper when lawyers can carry more work well. The solo who could most use that leverage is exactly the person least able to burn a weekend on OAuth plumbing. Every hour any of us spends rebuilding the same connector is overhead that ends up in somebody’s bill, or in a case somebody couldn’t afford to take. A vendor serving tens of thousands of firms is the one place this work gets done once instead of ten thousand times. That’s what platforms are for.
The API limits point the same way. You can’t create a bill. Practice management vendors built their APIs for the integrations they saw coming, like document management and accounting sync, and nobody designed for an assistant that needs to read broadly and write narrowly. As history, that’s forgivable. As a roadmap, it’s a lot less so.
Where that leaves you
Build your own if you can. It’s a weekend, you control the surface area, no third party holds your token, and it runs from cron and launchd as well as from a chat window. That last part matters, because most of my firm’s Clio traffic is scheduled automation, not conversation.
If you adopt one, read the source. All four questions. If you can’t answer them, don’t run it.
And don’t sweat the protocol. Scripts and MCP aren’t rival security models. They’re rival interfaces to the same one. An MCP server for Clio would be a wrapper around the same allowlist. The wall is the set of operations that exist; the protocol is how you knock on it. The real question is never “MCP or scripts,” it’s “have you decided what operations exist, and are the credentials somewhere sane.” If the answer is no, MCP won’t save you. It’ll just make an oversized surface easier to find.
For what it’s worth: if I were starting today and mostly wanted to chat with it, I’d write the MCP server, because discovery and typed arguments really are better in a chat. If scheduled jobs were the main use, I’d write exactly what I have.
Where I’m still relying on habit
A few of my allowlisted methods do write: create a matter, a contact, a task, a time entry. Each one exists for a specific workflow a person drives, and the rule is that nothing writes to Clio without a person approving that specific write. Time entries get staged as a table for review. Matters get created through an intake flow that reads the details back before posting. No scheduled job writes to Clio.
I’d love to tell you the code enforces that. It doesn’t. Workflow discipline enforces it, along with the fact that no automated process happens to call a write method. If I built it again, I’d put the write methods in a module the scheduled jobs don’t import, so the boundary was a fact about the code and not a fact about my habits. That’s where it stands, and it’s the next thing I’m fixing.
Which is the same argument I’m making about Clio, just smaller. A control that depends on someone remembering isn’t a control. It’s an intention.
Starting from zero
- Register an application in the developer portal. Client ID, client secret, redirect URI pointing at your own machine.
- Write the keychain wrapper first. Twenty lines. Decide where the token lives before you have one.
- Run the OAuth flow once, with
stateverification. Store refresh token, access token, expiry. - Write three read methods, not twenty. Search matters, get a matter, list calendar entries. Live with them for a week; you’ll find out which two you actually needed.
- Add writes last, and only for a workflow with a human in it.
- Keep the allowlist an allowlist. The generic passthrough helper is the moment you gave up.
Sources
- Model Context Protocol, Introduction and specification
- Clio developer documentation (API v4, OAuth 2.0, rate limits)
- Minn. R. Prof. Conduct 1.6
- ABA Standing Committee on Ethics and Professional Responsibility, Formal Opinion 512 (July 29, 2024)
General commentary on legal technology and practice management. Not legal advice, not ethics advice, and not an endorsement of any product. Formal Opinion 512 is advisory and not binding in any jurisdiction. No client information, matter number, or account identifier appears in this article; example matters are invented.