Connect over MCP
Configure is one MCP server. The same configure_* tools are at two endpoints. Which one you use depends on who the agent acts for.
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 |
| Connect a person's own headless agent (no browser, no API key) | mcp.configure.dev | The person approves one link. See Connect a headless agent. |
The first row is a person connecting an app they already use. The second and third rows are a developer writing code. Each endpoint rejects the other endpoint's credentials. This is the most common way setup 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. You sign in inside the conversation. When a tool needs you, its result carries a connect_url, which you open once. Repeat this for as many agents as you like. The same profile follows you into each one.
Use a connection token when the client has no OAuth
Claude, ChatGPT, Codex, and any client registered through dynamic client registration run the OAuth flow themselves. They need nothing else. A host without its own OAuth client needs a durable credential instead. Configure offers it in two shapes, because it 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 it after they approve, on the sign-in page that connect_url opens. In the URL form, the token is part of the URL. Treat the whole URL as a password. Anyone who has it has that user's grant.
Configure reads the clientInfo your host sends at initialize to tell which case applies. configure_connect includes this guidance in its result only for clients that need it.
Connect from your own server
Everything below uses api.configure.dev/mcp. The tools and the protocol are the same. Your developer credentials replace the browser sign-in. mcp.configure.dev cannot serve this case, because OAuth needs a person at a browser.
Serve one user: yourself
Examples are a personal assistant, a cron job, or 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 match. 401 means they do not. A key is issued for one agent handle, so CONFIGURE_AGENT must be the handle the key was minted for.
Serve many users: one session each
Your product serves people other than you. Mint a short-lived session for each user on your server, and pass 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 }. It has no connect_url. Only profile.connect() returns one. Do not treat the session's linked as proof the user connected. In the sandbox it is true for a user who never connected, while read() reports correctly.
Each entry in mcp_servers is { type: "url", url, name, authorization_token }. That is a Streamable HTTP endpoint and a bearer, which is all any MCP client needs. The same session works with:
- Anthropic's
mcp_servers - OpenAI's
mcptool on the Responses API - the Vercel AI SDK's
createMCPClient - the MCP TypeScript SDK's
StreamableHTTPClientTransport, with the bearer inrequestInit.headers
The Quickstart shows each one. Always use one session per user. The bearer is the identity.
Pass mcp_servers to the model. If you mint a session and do not pass it, the tools never appear in the model's list, and the model cannot use them.
Why one shared connection cannot work
An MCP connection carries exactly one identity. It comes from the credential, never from tool arguments. No Configure tool accepts a user id as input, and every write is attributed on the server.
If a tool took a user_id argument, the model would choose whose memory to read. One prompt injection in a retrieved email could then expose another user's memory. The credential keeps identity out of the model's reach. Do not share one session across users. The per-user session is what keeps your users' profiles apart.
Serve one person with no browser
This is for a cron job or scheduled agent that acts for one person other than you and cannot run OAuth. It takes three unauthenticated calls. npx -y @configure-ai/mcp login --personal runs them 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>returnsinstallationIdandrefreshTokenafter they approve. Store both.POST /v1/mcp/personal/tokenwith those two values returns a 15-minute access token. CallPOST /v1/mcp/personalwith that token as a bearer, plusX-Configure-MCP-Installation: <installationId>.
The card is an MCP app
configure_connect returns a result a host can render, as well as a link. The rendering is an MCP app, a page Configure serves that the host shows in an iframe. Hosts that support MCP apps show the card inline. It has the heading "Connect your accounts", the Claude, ChatGPT, Gemini, and Grok marks, and Connect and Not now. Other hosts fall back to the connect_url in the result. That is why the result always carries both.
You do not build any of this. Connect opens the same hosted flow as the Quickstart button. With the card, the user taps a button. Without it, the user follows a link.
Match the credential to the host
Each host rejects the other host's credentials:
| 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 comes late, which makes it confusing. mcp.configure.dev completes initialize and lists 9 tools for an API key, or for no credential at all. It refuses only 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 depends on who is signed in, not on your deploy:
- Your API key alone: 9 tools,
configure_connectplus the eight profile tools. You get only what the credential was explicitly granted. - A per-user session: the whole surface, 17 tools:
configure_connect, eight profile tools (profile_read,profile_search,profile_remember,profile_remember_many,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_. The session lists all of them whether or not that user has connected yet. Sheets writes are SDK and REST only. See Connected apps.
The seventeen definitions come to roughly 45,000 characters of JSON. They land in the model's context on every turn. To send fewer, use profile.tools({ connectors, actions }) on the SDK path.
Understand one consequence of the per-user session before you design around it. The session decides which tools are listed. What the user approved decides which calls run. A session lists the full catalog whether or not the user connected each app, or connected at all. A call the user has not authorized comes back as a refusal with the link that fixes it.
So configure_notion_search appears as soon as you mint a session, before the user connects Notion. Calling it before they connect returns tool_not_connected and a connect link. That is the intended path, not an error to avoid.
The full list is on purpose. A model cannot ask the user to connect an app whose tools it has never seen.
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 gives 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. It never means "no data on this user". Show the link verbatim.- A connector call for an unconnected app fails with
tool_not_connected. The error payload carries the link that fixes it. Give the user that link. Never build one yourself. -32009commit_requiredmeans a read obligation is unpaid. Commit the turn, then retry.configure_connectdoes not fix this.
The full failure table is in the Quickstart.