Skip to content

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 toEndpointCredential
Give your own Claude, ChatGPT, Cursor or Codex your Configure memorymcp.configure.devOAuth. The host runs it. Nothing to hold.
Give your product's users their own memory inside your agentapi.configure.dev/mcpA per-user session your server mints with mcpSession()
Run one agent for yourself, headless, with no browserapi.configure.dev/mcpX-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
ClientHow
Claude Codeclaude mcp add --transport http configure https://mcp.configure.dev
Codexnpx -y @configure-ai/mcp login --personal --client codex
Claude, ChatGPT, Cursor, anything that does OAuthAdd 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 hasUse
A field for request headershttps://mcp.configure.dev with Authorization: Bearer <token>
Only a server URL boxhttps://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:

  1. POST /v1/mcp/personal/sessions with { "client": "<your name>" } returns a sessionId and a browserUrl. The person opens the URL once and approves.
  2. GET /v1/mcp/personal/sessions/<id> returns installationId and refreshToken when they have. Store both.
  3. POST /v1/mcp/personal/token with those two returns a 15-minute access token. Call POST /v1/mcp/personal with it as a bearer plus X-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:

HostAcceptsRejects
mcp.configure.devOAuth grants, and cfg_agt_ connection tokensAPI keys, minted session tokens
api.configure.dev/mcpX-API-Key plus X-Agent, minted session tokensOAuth 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_connect plus 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 prefixed configure_, 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_required arrives as a successful tool result with a connect_url and expires_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_connected and the error payload carries the link that fixes it. Hand the user that link; never construct one.
  • -32009 commit_required means a read obligation is unpaid: commit the turn, then retry. configure_connect is not the fix.

The full failure table lives in the Quickstart.

Personalization infrastructure for agents