Connected apps and actions
When a user connects an app on Configure's page, the tools for it appear on their MCP session with no work from you. This page is the map: what exists, what changes the outside world, and how to narrow it.
The tools
On a user's MCP session, sixteen tools: configure_connect, seven profile tools, and these eight for connected apps. They are all listed for every session, connected or not, and a call the user has not authorized fails closed with the link that fixes it.
| Reads | Changes external state |
|---|---|
configure_gmail_search | configure_email_send |
configure_calendar_get | configure_calendar_create_event |
configure_drive_search | |
configure_notion_search | |
configure_sheets_search, configure_sheets_read |
Through the SDK and REST only, not on the MCP session: configure_email_search (Gmail and Outlook together) and the Sheets writes, configure_sheets_values_update, configure_sheets_values_append, configure_sheets_create_spreadsheet, and configure_sheets_add_sheet.
On the SDK path profile.tools() returns 14: it leaves out configure_profile_commit and configure_project_share, which the MCP session carries. The rest are always there: configure_profile_read, configure_profile_search, configure_profile_remember, configure_profile_forget, configure_profile_import, and configure_connect.
Scope what your agent may do
On the Quickstart path the model holds these tools through the MCP session (step 6) and calls them itself; the scope is the session's, and a tool the user has not authorized refuses with the link that fixes it.
You can also run the calls yourself, on a handle that carries the user's own token (from Sign in with Configure). Then you choose what the model sees:
ts
import { Configure } from "configure";
const configure = new Configure({ apiKey: process.env.CONFIGURE_API_KEY, agent: process.env.CONFIGURE_AGENT });
const profile = configure.profile({ token: session.token }); // the user's token, not your externalId
const tools = profile.tools({
connectors: ["gmail", "calendar"], // reads to expose
actions: ["calendar.create_event"], // state-changing tools to expose; [] for none
});
// Hand `tools` to the model (Anthropic shape; toOpenAIFunctions(tools) for OpenAI),
// then run whatever it calls:
const result = await profile.executeTool(call);A tool that is not in the set is refused with ACCESS_DENIED before any request leaves your server. On a profile keyed by externalId, executeTool serves the profile tools only and answers connector calls with authorization_required and a link; connected apps are reached through the session.
What a refusal carries
How a refusal reaches you depends on which handle the profile was made with, and the difference matters because one is a value and the other is thrown.
On a profile keyed by token, a connector the user has not connected, or whose access expired, throws a ConfigureError with code: "TOOL_NOT_CONNECTED" and suggestedAction: "reconnect". There is no connect_url on that error. Mint one with profile.connect({ app }) and put that in your reply. On a message channel do not send that link: a text keeps it forever and a minted link expires in the thread, so use auth.createMessageSignInUrl({ reason: "reconnect" }) instead, as Message agents explains.
On a profile keyed by externalId, the same call returns rather than throws, and the value is the connect payload itself: { status: "authorization_required", connect_url, app, connected: false, instructions }. Use connect_url verbatim.
A missing scope is a different failure and fails with permission_denied, so read the code before you tell the model anything.
You can narrow the session's catalog too, and on a busy chat you should. Sixteen tool definitions is real prompt weight, some of them the model has no business reaching in your product, and configure_profile_read in particular tells the model to call it first on every turn, which fights the context you already injected. Filter tools by name after listTools() and hand the model only what your product uses.
Read the cause before you tell the model anything. Not every failure is a missing connection: a quota, a rate limit, an expired token, and a genuinely unconnected app all surface as a failed tool call, and some frameworks flatten the cause to something like "An error occurred" on the way through. Handed that, a model guesses the most common explanation and tells a connected user to connect again. One integration showed a reader that prompt four times while their account had been connected the whole time, because the real error was a monthly read quota. Keep the original error, branch on it, and only say "connect" when the payload says so. Read the cause off the error object rather than stringifying it: a rejected MCP call is an object, so String(error) gives you "[object Object]" and every pattern you match against it fails at once. A revoked or expired token behaves the same way, so reconnecting is one click for the user and no code for you.
Actions and approval
Two things gate a state-changing call, and neither is a per-call prompt from Configure: the user must have connected the app, and the user must have approved your agent. Both fail closed on Configure's side.
Whether to confirm each send or each event with the user is your call and your UI. The components bundle ships a card for it, <configure-tool-approval>, which shows the tool and its parameters with Approve and Deny, times out to Deny, and emits configure:tool-approve, configure:tool-deny, or configure:tool-always-allow with the action id. It executes nothing; your handler runs the tool only on approve. Tool annotations also mark configure_email_send as destructive so MCP clients that confirm destructive calls do so on their own.