Connector repair
Connector tokens die. A user revokes access in their Google account, a grant expires, a provider drops a scope. When that happens, Configure failures describe their own fix: the response names the app, why it broke, and a minted link that lands the user on exactly that connector.
You do not build these links. A repair link is minted server-side, scoped to one user, one agent, and one app, and it expires.
The repair payload
Every connector failure that Configure can classify carries a suggested_action object:
json
{
"type": "reconnect",
"app": "gmail",
"reason": "token_expired",
"url": "https://mcp.configure.dev/connect/mcc_-5hLA7QQbsBqL34xoyp58WKI",
"expires_at": "2026-08-11T05:05:47.997Z"
}| Field | Meaning |
|---|---|
type | reconnect (a browser trip fixes it) or retry_later (nothing the user can fix) |
app | gmail, outlook, calendar, drive, notion, or sheets |
reason | token_expired, permission_revoked, provider_scope_missing, not_connected, rate_limited, or provider_down |
url | reconnect only: the minted link. Show it to the user verbatim. |
expires_at | reconnect only: when that link stops working |
retry_later never carries a link, deliberately. Asking a user to reconnect a healthy account because a provider was rate limiting you teaches them to distrust the prompt.
Failures Configure cannot classify with confidence carry no suggested_action at all. Treat its absence as "no repair I can stand behind", not as "connected".
Over REST
The object rides at the top level of the error envelope, next to request_id:
json
{
"error": {
"type": "tool_error",
"code": "tool_not_connected",
"message": "gmail access expired. The user must reconnect gmail before this operation.",
"param": "tool",
"retryable": false,
"suggested_action": "reconnect",
"doc_url": "https://docs.configure.dev/errors/tool_not_connected"
},
"request_id": "req_148ae84c-f5ec-4235-9d2f-e0fd2afea901",
"suggested_action": {
"type": "reconnect",
"app": "gmail",
"reason": "token_expired",
"url": "https://mcp.configure.dev/connect/mcc_-5hLA7QQbsBqL34xoyp58WKI",
"expires_at": "2026-08-11T05:05:47.997Z"
}
}Two fields share the name, at two levels, and they are not the same thing:
error.suggested_actionis a legacy string:reconnect,connect_tool, orretry. Old clients branch on it.- the top-level
suggested_actionis the structured object above.
A dead grant and a connector that was never connected are both HTTP 400 tool_not_connected with retryable: false, because both are fixed by one browser trip. The copy and the legacy string differ, and that difference matters in your UI:
| Situation | error.suggested_action | What to render |
|---|---|---|
| Never connected | connect_tool | "Connect Gmail" |
| Grant died (expired, revoked, scope pulled) | reconnect | "Reconnect Gmail" |
| Rate limit or provider outage | retry | Nothing user-facing; back off |
Telling a user to connect an account they already connected sends them hunting for a button that is not there. Branch on the structured reason when you want the sharper sentence ("Google revoked access" reads better than "expired"), and on type when you only need to decide between a link and a retry.
ts
const res = await fetch("https://api.configure.dev/v1/connectors/gmail/messages/search", {
method: "POST",
headers: {
"X-API-Key": process.env.CONFIGURE_API_KEY!,
"X-Agent": process.env.CONFIGURE_AGENT!,
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query: "in:inbox", max_results: 5 }),
});
if (!res.ok) {
const body = await res.json();
const repair = body.suggested_action;
if (repair?.type === "reconnect") return showReconnectPrompt(repair.app, repair.url);
if (repair?.type === "retry_later") return backOff();
throw new Error(body.error?.message);
}Over MCP
The same object rides in error.data.suggested_action on the JSON-RPC error, and the prose the model reads already contains the link, so an agent that ignores the structure still recovers:
json
{
"jsonrpc": "2.0",
"id": 4,
"error": {
"code": -32000,
"message": "Gmail access expired. Ask the user to reconnect Gmail. Give the user this link to connect it: https://mcp.configure.dev/connect/mcc_...",
"data": {
"code": "tool_not_connected",
"suggested_action": { "type": "reconnect", "app": "gmail", "reason": "token_expired", "url": "https://mcp.configure.dev/connect/mcc_..." },
"connect_url": "https://mcp.configure.dev/connect/mcc_...",
"next_tool_call": { "name": "configure_connect", "arguments": { "app": "gmail" } }
}
}
}next_tool_call is the recovery spelled out: call configure_connect with that app to mint a fresh link if the one you were handed has expired. Never construct a connect URL yourself.
SDK support today
The TypeScript SDK parses the legacy string only. ConfigureError.suggestedAction is "reconnect" (a string); the structured object is not exposed on the error yet, so raw REST and MCP consumers are the ones who get app, reason, and url. If you need the link from TypeScript, read it off the response body yourself as in the example above, or mint one with profile.connect({ app: "gmail" }).
ts
import { ConfigureError } from "configure";
try {
await profile.executeTool(call);
} catch (error) {
if (error instanceof ConfigureError && error.suggestedAction === "reconnect") {
const { connect_url } = await profile.connect({ app: "gmail" });
return showReconnectPrompt("gmail", connect_url);
}
throw error;
}Test it before a user finds it
Test mode can kill a connector on demand:
bash
curl -X POST https://api.configure.dev/v1/sandbox/simulate \
-H "X-API-Key: $CONFIGURE_API_KEY" -H "X-Agent: $CONFIGURE_AGENT" \
-H 'Content-Type: application/json' \
-d '{"scenario":"gmail_token_expired"}'Gmail calls on that sandbox then fail exactly like a dead connected account, so you can assert your reconnect prompt renders. POST /v1/sandbox/reset puts it back.