MCP <> Connecting Your Clients' AI Assistants
Consent inside the embedded CoPilot
How to host the AI authorization screen on your own site, with the Ask Clio CoPilot running as an iframe. Your customer approves the connection without leaving your page and without being sent to a Clio-hosted screen.
What you are building
When one of your customers connects an AI tool (Claude, ChatGPT) to their account, that tool starts an OAuth authorization. Clio needs a page to show the customer what is being requested so they can approve or deny it.
That page is yours. Clio redirects the browser to a route on your site; you already know who is signed in, so nobody has to authenticate again. You render the CoPilot in an iframe, it shows the consent card, and it hands the result back to the AI tool.
Before you start
Everything here is configured in the Ask Clio dashboard, under MCP Settings — available to anyone with a Firm Admin or Firm Employee role. Three fields matter:
Consent page URL — set this to your consent route. While it is empty, Clio sends your customers to a hosted fallback page that only your staff can complete. Include the trailing slash before any query string.
Allow client self-service — must be on, or the CoPilot tells your customers that connecting isn't available.
MCP enabled — the firm-level switch. Turning it off later revokes every live connection, not just new ones.
The same page shows your firm's connector URL, which is what your customers paste into their AI tool.
No tokens in that field
Consent page URL holds a plain route and nothing else — https://yourapp.com/ai-consent. It is a single static value for your whole firm, so a session token cannot go in it: tokens are per-customer and expire. Nor does client_id belong there.
Both are resolved inside your route, per request, at step 3 and step 4 below. That is where the flow becomes customer-specific — not in this setting. You never generate or send a consent link yourself; Clio sends each customer to this one route and your route works out who they are.
The flow
Clio → your site
Clio redirects the browser to your route
A top-level navigation to your
consent_page_urlwith one parameter appended:Nobody generates that id by hand — not you, and not the AI tool. Clio creates the authorization record and appends its id on the way out, so it arrives on your route automatically.
What is not automatic is getting it into the iframe. It lands on your page's URL; the embedded CoPilot reads its own frame URL, which is a different thing. Forwarding it inward is step 4, and it is the step most integrations miss.
Your backend
Identify the customer and mint a session token
Authenticate them however you normally do. Then create an Ask Clio SDK session server-side — the same call you already make to embed the chat widget — and keep the returned token for the next step.
Mint it per request
Session tokens are per-customer and expire. Never hard-code one into
consent_page_urlor a static template; generate a fresh token each time this route is served.Your page
Embed the CoPilot, carrying the request id
The iframe URL needs three parameters: your
client_id, thestsession token from step 3, and theauthorization_request_idfrom step 2. Whenever that third parameter is present, the CoPilot opens on the consent card instead of the chat.If you use the Ask Clio embed script rather than a raw iframe, append the same parameter to the URL you pass it and make sure the panel is open — a consent card behind a collapsed launcher looks like a dead end:
Do not sandbox the frame
Step 6 requires the frame to navigate the top-level page. If you must set
sandbox, it has to includeallow-top-navigation-by-user-activationalongsideallow-scripts,allow-same-originandallow-forms— or you must implement the message listener in step 6 instead.Back to the AI tool
The browser returns to the AI tool
Clio replies with a redirect URL carrying the authorization code. This has to be a top-level navigation: pointing the iframe at
claude.aiwould try to load Claude inside your widget, and it refuses to be framed.The CoPilot navigates the top window for you, so an ordinary unsandboxed iframe needs no code here. If your page blocks top-level navigation, listen for the message instead and perform the redirect yourself:
Always follow the redirect
Send the customer to
redirectToeven when it carries anerror=parameter. A denial is a valid OAuth outcome and the AI tool knows how to present it. Checkevent.originbefore acting on any message.
Constraints worth designing around
The request expires. An authorization request is valid for about ten minutes and can be used once. If your route bounces the customer through a login, they may return to an expired request — the CoPilot says so and asks them to start again from their AI tool.
One consent URL per firm.
consent_page_urlis a single value, so it must point at a route that works for every one of your customers rather than at anything user-specific.Account details must be linked first. If your tools need values like a clearing account id and the customer's session does not carry them, the CoPilot refuses to connect rather than creating a connection that authenticates and then fails on every request.
Allow clipboard access. Without
allow="clipboard-write"on the frame, copy buttons fall back to a legacy method that works but is less reliable.
If something goes wrong
The chat loads instead of the consent card
authorization_request_id never reached the iframe. Check the frame's src, not your page's URL.
Nothing visible; launcher stays collapsed
The panel was not opened. Call the SDK's open method when the parameter is present.
“This authorization request isn't valid”
The id is unknown or belongs to another firm — usually a truncated or re-used parameter.
“This request has expired or was already used”
Past the ten-minute window, or the customer double-submitted. They start again from the AI tool.
Approve succeeds, then a blank or refused panel
Top-level navigation was blocked. Implement the CLIO_CONSENT_COMPLETE listener.
“Connecting isn't available here yet”
Client self-service is off for your firm. Ask your Clio contact to enable it.
“Sign in through your firm to connect”
No st parameter, or the session expired. Mint a fresh token when serving the route.
The simpler alternative
If rendering consent inside your page is not a requirement, you can skip the embed entirely. Have your route mint the session token and then redirect top-level to the CoPilot:
The consent card fills the page, and the return trip needs no cooperation from you at all. Worth noting that either way the customer ends up at the AI tool when they approve — the only difference is whether the card renders inside your site for those few seconds.
Let your clients use their own AI assistant — Claude, ChatGPT, Perplexity, or any MCP-compatible client — to ask about their account in plain language. The assistant reaches your tools through Ask Clio and answers with live data.
You stay in control of which tools are reachable and whether any of them can act rather than only read. Your clients never leave your brand, and their credentials never reach the AI provider.
How It Works
Your firm already exposes tools to Ask Clio. This publishes a subset of them at a connector URL your clients paste into their AI client:
Find yours under Configuration → MCP, alongside the switch that turns the connector on. It carries your firm's name rather than ours, which is what lets your clients recognise it as yours; there is a second address that works too. If you do not see that section yet, your Ask Clio contact can give you the URL and flip the switch for you.
Available in the sandbox environment today. The connector runs on dev-mcp.askclio.ai, and the production host in the URLs on this page is not serving yet. Build and test against the sandbox; talk to your Ask Clio contact before pointing clients at it, and they will tell you when the production host is live.
Every URL on this page uses the production host. Swap mcp.askclio.ai for dev-mcp.askclio.ai while you are building.
Your knowledge base comes along with the tools, with nothing to configure. Whatever you have published — documents, articles, URLs — is reachable through the connector the same way your own chat already answers from it, and stays current as you add to it.
Two things decide whether the assistant ever looks at a document. Titles reach the model without it searching, so
2025 fee scheduleearns a look anddocument final v3does not. Summaries are what it reads to decide whether to open one, before it has seen a word of the body. Neither is cosmetic here in the way it is on a shelf.
Choose a connection method
Three methods, and you can use any of them without the others. They all produce the same connection underneath, so Chat History, archiving and your audit trail look identical regardless.
What your client does
Pastes a key you show them
Pastes your connector URL, clicks approve
Connects from a panel in your product
What you build
One backend call
A consent page plus one backend endpoint
A panel, and no backend at all
Key handling
Shown once, copy-pasted
Never shown to anyone
Never shown to anyone
Best for
Getting started quickly
Production client experience
Clients already using your assistant
Most firms end up on OAuth, because a key someone has to copy is a key someone will paste into the wrong window.
The next four steps cover OAuth with a consent page you host yourself. That is the arrangement that keeps your clients' identity entirely on your side — Ask Clio never sees their browser. Connecting from inside your app comes after them, and composes with either of the other two rather than replacing them.
Step 1 — Point us at your consent page
Under Configuration → MCP, set your consent page URL:
Leave it blank and your clients land on an Ask Clio-hosted page, which only your own staff can complete.
While you are there, copy your connector URL. Your clients need it.
Step 2 — Build the consent page
When a client adds your connector, their AI assistant sends their browser to:
Your page authenticates them the way every other page of yours does. They are already signed in to your application, so you already know who they are.
Then fetch what they are being asked to approve:
No credentials needed — the id is itself proof of possession.
Render client_name and the tools list. Use display_name, not name — alpaca_list_open_positions_for_account is not something anyone can meaningfully consent to. Show access too, so read-only and read-write look different.
If expired or consumed is true, the request is spent. Tell your client to start again from their AI client. These live ten minutes by design.
Step 3 — Approve from your backend
When your client clicks approve, your page calls your own backend, which calls us:
client_id and client_secret are the Client Identity and Client Signing Key your firm already uses for the chat SDK. If you are creating session tokens today, you have them.
Your
client_secretmust never reach a browser. It mints connections for any of your users, so a page holding it hands that power to whoever opens the tab. This call belongs in your backend.
user.id is your own identifier for that person — the same value you already send when creating a session token for the chat SDK. We key their connection on it, so the same id always resolves to the same connection.
Step 4 — Send them back
The response gives you a URL:
Redirect the browser there. Their AI client finishes the handshake on its own, and the connector is live.
Your connector's own address
Your connector answers on two addresses, and they are the same server:
The second exists so your clients see your firm's name rather than ours when they add the connector, which is what "become your own app" comes down to in practice. There is nothing to configure and nothing to migrate. Both are live for you at the same time, and you choose which one you hand out.
You can change your mind later at no cost. A token issued on one address is accepted on the other, so a client already connected on the first URL keeps working: no reconnecting, no approving anything again, no window in which their assistant stops answering.
One thing does not move on its own, and it is worth telling clients. Whoever already pasted the old URL into their assistant keeps seeing that URL in their connector list, because that text lives in their client and we cannot rewrite it from here. Only connections made after the switch show the new address.
If you inspect discovery you will see the authorization server stays on mcp.askclio.ai under both addresses. That is deliberate rather than an oversight: your subdomain is the protected resource and ours is the authorization server, which is exactly the split RFC 9728 exists for. It means one place issues and revokes tokens for every firm, and your clients never see the difference.
Connecting from inside your app
Everything above assumes your client is somewhere else — in Claude, or on a page you sent them to. If they are already in your product with your embedded assistant open, none of that is necessary. They hold a session token you issued, we can verify it, and that is enough to let them set up their own connection without leaving the screen.
Your clients can approve a consent from here without you turning anything on. The Allow client self-service switch under Configuration → MCP governs one thing: whether they may generate and rotate their own access keys. It ships off, because handing your customers their own credentials is not something switching your connector on should imply, and if you want to stop them connecting altogether that is the master MCP switch rather than this one.
These endpoints live on the Ask Clio API host, not on the connector host the rest of this page uses — api.askclio.ai in production, dev-dashboard.askclio.ai in the sandbox. Every one of them authenticates with the session token your assistant already has:
GET /api/sdk/mcp/status/
Everything the panel renders, in one call
POST /api/sdk/mcp/personal-keys/
Their key, rotating on each call
GET /api/sdk/mcp/connections/
What they have connected
POST /api/sdk/mcp/connections/<id>/revoke/
Disconnect one
POST /api/sdk/mcp/consent/<id>/approve/
Decide a consent in place
Two things this panel should not draw. A key that has been revoked is left out of status and connections here, unlike on the dashboard, which keeps them for audit; disconnecting revokes a connection's keys along with it, so after a disconnect expect no keys back and offer to generate one rather than showing the old value. And an OAuth connection has no key of its own, so access_keys comes back empty on those: show the connection, not an access key.
Do not ask them for their account details on this path. Step 2 exists because a hosted page has no idea who is standing in front of it. Here we do: the account context comes from their session, and anything you send in the request body is ignored rather than trusted — the caller is the client themselves, and honouring it would let one of them bind a connection to somebody else's account.
status returns required_context alongside context_ready, so your panel can tell whether anything is still missing instead of rendering a field nobody needs to fill in.
Connections made this way are read-only. Every other gate asks your firm before granting write, and on this path there would be nobody to ask.
To decide OAuth consents in-app as well, point your consent page URL at a route inside your product rather than at a standalone page. We append authorization_request_id to whatever URL you set, exactly as in Step 2, and your route posts the decision to the endpoint above with the session token instead of going through your backend.
Dynamic Inputs, under a different name
runtime_context is the same thing as Dynamic Inputs in the chat SDK, where the field is called tools_input. Same keys, same values, same purpose: injected into tool calls server-side, never shown to the model.
If you already pass tools_input when creating a session token, pass those same key-value pairs here. On the in-app path you do not pass them at all — they are already on the session.
This is the one that costs a day. If we do not have the account context, the connection still succeeds. It authenticates, it lists every tool, and then every single call fails — the unresolved field goes to your clearing house as a literal
{{account_id}}and they answer about the account rather than about the missing context. It reads like a broken integration on their side. It is not.
Read required_context from Step 2 and use its key values verbatim.
The context key is not necessarily the parameter name your tools show. A firm whose tools take an input called
account_idmay still key its context onalpaca_account_id. Send whatrequired_contextsays, not what looks right.
An empty required_context means your tools need nothing, and you can omit runtime_context.
Handling errors
404 on the GET
Unknown request id
Ask them to start again from their AI client
410 on approve
Already used, or expired
Same — single-use, ten-minute life
200 with error= in redirect_to
Rejected upstream
Redirect anyway; their AI client shows the error
400 {"firm": ...}
Your MCP connector is switched off
Turn it back on under Configuration → MCP
400 {"access": ...}
Read-write requested without it being enabled
Enable it, or request read-only
401 on an in-app call
No session token, or the session expired
Issue a fresh session token
429 on a tool call
The connection passed its per-minute limit
Back off and retry; ask us to raise the limit if it is routinely hit
Approve answers 200 whenever it reached a decision, even if the grant failed upstream. Check redirect_to for an error= parameter rather than relying on the status code.
Listing your connector as your own app
Everything above gets your clients connected by pasting a URL. Listing is the other route: your firm appears in Claude's connector directory and ChatGPT's app directory under your own name, and your clients find you there instead of being handed a link.
You submit, not us. The connector is yours, the API behind it is yours, and both reviews ask the owner to prove it. What we do is make sure nothing on our side stops you passing.
What we already provide
Your connector is served from your own subdomain, https://your-slug.mcp.askclio.ai/mcp, which matters more than it looks: OpenAI verifies domain ownership by asking that exact domain to hand back a token, so a shared address could only ever have verified one firm.
Every tool we publish carries a readable title and a flag saying whether it only reads or can change something. Anthropic's portal groups tools by exactly that and flags any that are missing it, so this is the difference between reaching review and being stopped before it.
Authentication is OAuth 2.1 with dynamic client registration, which is what both directories expect, and the connection is HTTPS throughout.
Claude
Submit from the directory submission portal inside your own Claude organisation's settings. You need a Team or Enterprise organisation, since the settings do not exist on individual plans, and by default only Owners can submit.
The portal connects to your live connector and syncs your tools, so have it switched on and working before you start. Then it asks for the listing itself: a name up to 100 characters, a tagline up to 55, a description up to 2,000, one to five categories, your documentation URL, your privacy policy URL, a support contact, an icon, and the URL slug for your listing page. The slug is permanent once published.
Two things reject submissions more than anything else. A missing or incomplete privacy policy is an immediate no, and it has to cover what you collect, how it is used and stored, who else sees it, how long you keep it, and how to reach you. And a reviewer has to be able to use your connector end to end, which means a test account with realistic data and instructions detailed enough to follow without asking you anything.
You will also confirm who owns the API behind the connector. Wrapping someone else's API without their permission does not pass.
ChatGPT
Submit from the plugin portal, choosing the option for a plugin with an MCP server, and give it your production connector URL.
The publisher needs a verified individual or business identity on the OpenAI platform, and whoever submits needs Apps Management set to Write on their organisation role.
Domain verification is the step that involves us. OpenAI issues you a token and expects the domain serving your MCP server to return it, and only it, from /.well-known/openai-apps-challenge. Paste that token into Configuration → MCP and we serve it from your subdomain as plain text, nothing else in the body. Nobody else's firm can answer with it, and the shared address deliberately does not answer at all.
You also need demo credentials that work without two-factor or an email confirmation a reviewer cannot receive, and test cases: five showing your connector doing what it claims, and three showing it refusing or asking for clarification when it should.
Gemini
There is no directory to submit to. Google's consumer connectors are arranged by partnership, and the CLI gallery is neither vetted nor durable. What does work today is Gemini Enterprise, where your customer connects your server themselves from their own team settings. That path asks for Streamable HTTP and a certificate from a publicly trusted authority, both of which your connector already has.
Before you go live
Turning the connector off is not a pause button
Switching your MCP connector off revokes every live connection along with its tokens and keys, and switching it back on restores none of them. Every connected client has to reconnect from scratch.
That is deliberate — "disable" has to mean disable — but it means the switch is not something to toggle while investigating.
Past about ten tools, the client loads them on demand
Claude and ChatGPT both stop putting every tool definition in the model's context once a connector publishes more than roughly ten, and load them on demand instead. A tool that has not been loaded is not in the model's context at all — its description included. We publish around thirty, so assume your connector is always in this mode.
Two things follow from that. A catalogue written into a tool's description never reaches the model, which is why your knowledge base is listed in the connector's own instructions instead — those are sent when the client connects and always arrive. And a tool the model has not loaded is one it has to decide to load from its name and a short description, so name the subject in the first few words rather than opening with what the tool does to it.
Name your connector without spaces
The AI client, not Ask Clio, decides how a tool is addressed, and it builds that name from what your connector is called. A connector named Ask Clio produced ask_clio: where the client expected something else, and a smaller model failed three times in a row over it while a larger one worked it out. It costs nothing to avoid.
One conversation is one chat
Your clients' AI conversations arrive in Chat History split the way they happened, with a new chat starting after about half an hour of silence rather than everything landing in one endless thread.
Your nightly archiving email follows the same boundary, so a client who has three separate conversations in a day produces three emails rather than one covering all of them.
Every tool call a client makes lands there, and that includes searches of your knowledge base: what was asked, and what came back. So a review of what your clients have been doing shows the questions answered out of your own documents alongside the account lookups, rather than only the latter.
Tool changes and connected clients
Enable or disable a tool, or change its access level, and your connector serves the new list immediately. AI clients cache what they were given, though, so a client already connected may keep showing the old set until it reconnects or the person starts a new conversation.
One thing never changes on an existing connection: its read-only versus read-write level, which is fixed when the connection was granted. Moving a client from read-only to read-write requires them to reconnect.
Restricting which AI clients can connect
Under Configuration → MCP → Client Access Controls, "Allow unrecognized AI clients" is on by default: any client that completes the flow can connect. Turn it off and only the ones you list may start a connection.
The list you pick from is the set we recognise — Claude, ChatGPT, Perplexity, Cursor, Windsurf, Cline, VS Code, and scripts and SDKs. You can also type anything else, matched against whatever the client calls itself, so an internal agent of your own can be approved by name. If the client you want is not on the list, reach out to support and we will add it.
Be clear-eyed about what this buys you. A client's name is self-reported, so the setting keeps unexpected integrations out rather than determined ones. The IP allowlist beside it is the control that actually enforces. Use the name list for tidiness and the IP list for security.
Treat the request id as single-use
The authorization_request_id travels in the URL. If your client forwards that link, whoever opens it approves using their own session on your site, granting their own account. Not a data leak, but a confusing outcome — do not put the id anywhere it will be logged or shared.
Consent inside the embedded CoPilot
How to host the AI authorization screen on your own site, with the Ask Clio CoPilot running as an iframe. Your customer approves the connection without leaving your page and without being sent to a Clio-hosted screen.
What you are building
When one of your customers connects an AI tool (Claude, ChatGPT) to their account, that tool starts an OAuth authorization. Clio needs a page to show the customer what is being requested so they can approve or deny it.
That page is yours. Clio redirects the browser to a route on your site; you already know who is signed in, so nobody has to authenticate again. You render the CoPilot in an iframe, it shows the consent card, and it hands the result back to the AI tool.
Before you start
Everything here is configured in the Ask Clio dashboard, under MCP Settings — available to anyone with a Firm Admin or Firm Employee role. Three fields matter:
Consent page URL — set this to your consent route. While it is empty, Clio sends your customers to a hosted fallback page that only your staff can complete. Include the trailing slash before any query string.
Allow client self-service — must be on, or the CoPilot tells your customers that connecting isn't available.
MCP enabled — the firm-level switch. Turning it off later revokes every live connection, not just new ones.
The same page shows your firm's connector URL, which is what your customers paste into their AI tool.
No tokens in that field
Consent page URL holds a plain route and nothing else — https://yourapp.com/ai-consent. It is a single static value for your whole firm, so a session token cannot go in it: tokens are per-customer and expire. Nor does client_id belong there.
Both are resolved inside your route, per request, at step 3 and step 4 below. That is where the flow becomes customer-specific — not in this setting. You never generate or send a consent link yourself; Clio sends each customer to this one route and your route works out who they are.
The flow
Clio → your site
Clio redirects the browser to your route
A top-level navigation to your
consent_page_urlwith one parameter appended:Nobody generates that id by hand — not you, and not the AI tool. Clio creates the authorization record and appends its id on the way out, so it arrives on your route automatically.
What is not automatic is getting it into the iframe. It lands on your page's URL; the embedded CoPilot reads its own frame URL, which is a different thing. Forwarding it inward is step 4, and it is the step most integrations miss.
Your backend
Identify the customer and mint a session token
Authenticate them however you normally do. Then create an Ask Clio SDK session server-side — the same call you already make to embed the chat widget — and keep the returned token for the next step.
Mint it per request
Session tokens are per-customer and expire. Never hard-code one into
consent_page_urlor a static template; generate a fresh token each time this route is served.Your page
Embed the CoPilot, carrying the request id
The iframe URL needs three parameters: your
client_id, thestsession token from step 3, and theauthorization_request_idfrom step 2. Whenever that third parameter is present, the CoPilot opens on the consent card instead of the chat.If you use the Ask Clio embed script rather than a raw iframe, append the same parameter to the URL you pass it and make sure the panel is open — a consent card behind a collapsed launcher looks like a dead end:
Do not sandbox the frame
Step 6 requires the frame to navigate the top-level page. If you must set
sandbox, it has to includeallow-top-navigation-by-user-activationalongsideallow-scripts,allow-same-originandallow-forms— or you must implement the message listener in step 6 instead.Back to the AI tool
The browser returns to the AI tool
Clio replies with a redirect URL carrying the authorization code. This has to be a top-level navigation: pointing the iframe at
claude.aiwould try to load Claude inside your widget, and it refuses to be framed.The CoPilot navigates the top window for you, so an ordinary unsandboxed iframe needs no code here. If your page blocks top-level navigation, listen for the message instead and perform the redirect yourself:
Always follow the redirect
Send the customer to
redirectToeven when it carries anerror=parameter. A denial is a valid OAuth outcome and the AI tool knows how to present it. Checkevent.originbefore acting on any message.
Constraints worth designing around
The request expires. An authorization request is valid for about ten minutes and can be used once. If your route bounces the customer through a login, they may return to an expired request — the CoPilot says so and asks them to start again from their AI tool.
One consent URL per firm.
consent_page_urlis a single value, so it must point at a route that works for every one of your customers rather than at anything user-specific.Account details must be linked first. If your tools need values like a clearing account id and the customer's session does not carry them, the CoPilot refuses to connect rather than creating a connection that authenticates and then fails on every request.
Allow clipboard access. Without
allow="clipboard-write"on the frame, copy buttons fall back to a legacy method that works but is less reliable.
If something goes wrong
The chat loads instead of the consent card
authorization_request_id never reached the iframe. Check the frame's src, not your page's URL.
Nothing visible; launcher stays collapsed
The panel was not opened. Call the SDK's open method when the parameter is present.
“This authorization request isn't valid”
The id is unknown or belongs to another firm — usually a truncated or re-used parameter.
“This request has expired or was already used”
Past the ten-minute window, or the customer double-submitted. They start again from the AI tool.
Approve succeeds, then a blank or refused panel
Top-level navigation was blocked. Implement the CLIO_CONSENT_COMPLETE listener.
“Connecting isn't available here yet”
Client self-service is off for your firm. Ask your Clio contact to enable it.
“Sign in through your firm to connect”
No st parameter, or the session expired. Mint a fresh token when serving the route.
The simpler alternative
If rendering consent inside your page is not a requirement, you can skip the embed entirely. Have your route mint the session token and then redirect top-level to the CoPilot:
The consent card fills the page, and the return trip needs no cooperation from you at all. Worth noting that either way the customer ends up at the AI tool when they approve — the only difference is whether the card renders inside your site for those few seconds.
Last updated