Connect over MCP
Configure is one MCP server. The same configure_* tools are reachable at two endpoints, and which one you use depends on who the agent is acting for. That is the only question you have to answer.
Choose how your agent connects
| You want to | Endpoint | Credential |
|---|---|---|
| Give your own Claude, ChatGPT, Cursor or Codex your Configure memory | mcp.configure.dev | OAuth. The host runs it. Nothing to hold. |
| Give your product's users their own memory inside your agent | api.configure.dev/mcp | A per-user session your server mints with mcpSession() |
| Run one agent for yourself, headless, with no browser | api.configure.dev/mcp | X-API-Key plus X-Agent, your own credentials |
The first row is a person connecting an app they already use. The second and third are a developer writing code. A credential from one row is rejected by the other endpoint, and that is the single most common way this goes wrong.
Connect an app you already use
You are a Configure user and you want Claude or ChatGPT to know you. There is nothing to build.
text
https://mcp.configure.dev| Client | How |
|---|---|
| Claude Code | claude mcp add --transport http configure https://mcp.configure.dev |
| Codex | npx -y @configure-ai/mcp login --personal --client codex |
| Claude, ChatGPT, Cursor, anything that does OAuth | Add https://mcp.configure.dev as a remote MCP server in its connector settings. |
The configure_* tools appear in the client. Sign-in happens inside the conversation: when a tool needs you, its result carries a connect_url you open once. There is no step three, and you can repeat this for as many agents as you like: the same profile follows you into every one of them.
Use a connection token when the client has no OAuth
Claude, ChatGPT, Codex, and anything registered through dynamic client registration run the OAuth flow themselves and need nothing else. A host without an OAuth client of its own needs a durable credential instead, and Configure offers it in two shapes because we cannot detect which one your client's settings screen supports:
| Your client's settings screen has | Use |
|---|---|
| A field for request headers | https://mcp.configure.dev with Authorization: Bearer <token> |
| Only a server URL box | https://mcp.configure.dev/u/<token> |
The token is a long-lived scoped cfg_agt_ credential the user mints from the sign-in page that connect_url opens, after they approve. In the URL form the token is the URL, so treat the whole string as a password: anyone holding it holds that user's grant.
Configure works out which of these applies from the clientInfo your host sends at initialize, and configure_connect puts the guidance in its result only for clients that need it.
Connect from your own server
Everything below uses api.configure.dev/mcp. Same tools, same protocol; your developer credentials are the key instead of a browser sign-in. mcp.configure.dev cannot serve this case, because OAuth needs a person at a browser and your server does not have one.
Serve one user: yourself
A personal assistant, a cron job, a headless agent on someone's platform. There is one user, and it is you. Send your own credentials and call the server directly:
bash
curl -X POST https://api.configure.dev/mcp \
-H "X-API-Key: $CONFIGURE_API_KEY" \
-H "X-Agent: $CONFIGURE_AGENT" \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'200 means the key and the agent name agree. 401 means they do not: a key is issued for one agent handle, so CONFIGURE_AGENT has to be the handle it was minted for.
Serve many users: one session each
Your product serves people who are not you. Mint a short-lived session per user, server-side, and hand the result to the model:
ts
// A session is minted for any user you name. Whether they have connected comes
// from read().profile.linked, and only connect() mints a link.
const session = await profile.mcpSession();
const response = await anthropic.beta.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
betas: ["mcp-client-2025-11-20"],
system: systemPrompt,
messages: turnMessages,
mcp_servers: session.mcp_servers,
tools: session.mcp_servers.map((s) => ({ type: "mcp_toolset", mcp_server_name: s.name })),
});The session returns { ok, linked, continued, session, mcp_url, mcp_servers, message, livemode }. There is no connect_url on it; that comes from profile.connect() and nowhere else. Do not treat its linked as proof the user connected: in the sandbox it is true for a user who never has, while read() correctly reports otherwise.
Each entry in mcp_servers is { type: "url", url, name, authorization_token }: a Streamable HTTP endpoint and a bearer. That is all any MCP client needs, so the same session works with Anthropic's mcp_servers, OpenAI's mcp tool on the Responses API, the Vercel AI SDK's createMCPClient, and the MCP TypeScript SDK's StreamableHTTPClientTransport with the bearer in requestInit.headers. The Quickstart has each one written out. One session per user, always: the bearer is the identity.
Minting a session and not handing mcp_servers to the model is not wiring; the tools never appear in its list, and the model cannot use what it cannot see.
Why one shared connection cannot work
An MCP connection carries exactly one identity, and that identity comes from the credential, never from tool arguments. No Configure tool accepts a user id as input, and every write is attributed server-side.
That is deliberate. If a tool took a user_id argument, the model would be choosing whose memory to read, and one prompt injection in a retrieved email becomes a cross-user breach. Identity has to arrive somewhere the model cannot reach, which is the credential. So a session per user is not overhead you can optimize away; it is the thing that keeps your users' profiles apart.
Serve one person with no browser
A cron job or a scheduled agent that acts for one person, not for you, and cannot run OAuth. Three unauthenticated calls, which npx -y @configure-ai/mcp login --personal runs for you:
POST /v1/mcp/personal/sessionswith{ "client": "<your name>" }returns asessionIdand abrowserUrl. The person opens the URL once and approves.GET /v1/mcp/personal/sessions/<id>returnsinstallationIdandrefreshTokenwhen they have. Store both.POST /v1/mcp/personal/tokenwith those two returns a 15-minute access token. CallPOST /v1/mcp/personalwith it as a bearer plusX-Configure-MCP-Installation: <installationId>.
The card is an MCP app
configure_connect returns a result a host can render, not only a link. That rendering is an MCP app: a page Configure serves that the host shows in an iframe, which is the same card the Quickstart mounts on the SDK path. Hosts that support MCP apps show the card with its Connect button inline; hosts that do not fall back to the connect_url in the result, which is why the result always carries both.
Nothing about that is yours to build. The consent wording and the permission list are inside Configure's origin either way, and the only difference between a host that renders the card and one that does not is whether the user taps a button or follows a link.
Match the credential to the host
A credential that works on one host is rejected by the other:
| Host | Accepts | Rejects |
|---|---|---|
mcp.configure.dev | OAuth grants, and cfg_agt_ connection tokens | API keys, minted session tokens |
api.configure.dev/mcp | X-API-Key plus X-Agent, minted session tokens | OAuth access tokens |
The failure is late, which is what makes it confusing: mcp.configure.dev will complete initialize and list 8 tools for an API key, or for no credential at all, and only refuse when a tool is called. This has broken production once. Check the host before you check the credential.
Read the tool list correctly
The tool list is a function of who is signed in, not of your deploy:
- Your API key alone: 8 tools,
configure_connectplus the seven profile tools. Only what the credential was explicitly granted. - A per-user session: the whole surface, 16 tools:
configure_connect, seven profile tools (profile_read,profile_search,profile_remember,profile_forget,profile_commit,profile_import,project_share), six reads (gmail_search,calendar_get,drive_search,notion_search,sheets_search,sheets_read), and two actions (email_send,calendar_create_event), all prefixedconfigure_, whether or not that user has connected yet. Sheets writes are SDK and REST only; see Connected apps.
Those definitions are not small: the sixteen come to roughly 45,000 characters of JSON, and they land in the model's context on every turn. profile.tools({ connectors, actions }) on the SDK path is how you send fewer.
The second one has a consequence worth understanding before you design around it. Advertising is keyed on the session; authority stays keyed on what the user approved. A session is offered the full catalog whether or not the user has connected each app, or connected at all, and a call the user has not authorized comes back as a refusal carrying the link that fixes it.
So configure_notion_search appears the moment you mint a session, not the moment the user connects Notion. Calling it before they connect returns tool_not_connected and a connect link, which is the intended path rather than an error to avoid.
This is deliberate. Advertising only what the credential is already granted looks safer and is not: the model cannot ask for a capability it was never told exists, so the user connects Gmail and the surface never says so. A smaller list is not a safer list, it is a blinder one.
Two things follow for your code:
- A tool count is not an auth check. Tools being present says nothing about what will execute.
- Most clients cache the tool list at connect time. A user who links after connecting needs a fresh connection before the fuller list appears.
Handle refusals
A Configure refusal is not a dead end; it hands the model the exact next step:
authorization_requiredarrives as a successful tool result with aconnect_urlandexpires_at. It means the user is not signed in, never "no data on this user". Show the link verbatim.- A connector call for an unconnected app fails with
tool_not_connectedand the error payload carries the link that fixes it. Hand the user that link; never construct one. -32009commit_requiredmeans a read obligation is unpaid: commit the turn, then retry.configure_connectis not the fix.
The full failure table lives in the Quickstart.