Handling Tool Calls
If your runtime already speaks MCP, you do not need this loop. Remote clients paste https://mcp.configure.dev, and code connects to https://mcp.configure.dev/mcp; the configure_* tools appear natively. Use this guide when you own the model loop.
profile.tools() returns model-callable Configure functions and profile.executeTool() dispatches them. Keep your existing tool router: forward only Configure-prefixed (configure_*) calls to Configure, and run your own tools as you do today.
The token used below comes from the server-side Continue with Configure exchange or the inline Link fallback (configure.profile({ externalId }) for an unlinked user instead). The model never sees that token; it only sees tool schemas and tool results.
Install the packages and construct the client (see Installation for where CONFIGURE_API_KEY and CONFIGURE_AGENT come from; set OPENAI_API_KEY for the OpenAI client):
bash
npm install configure openaits
import { Configure, toOpenAIFunctions } from "configure";
import OpenAI from "openai";
const configure = new Configure({
apiKey: process.env.CONFIGURE_API_KEY,
agent: process.env.CONFIGURE_AGENT,
});
const openai = new OpenAI(); // reads OPENAI_API_KEYTwo SDK details matter when you wire this into an existing loop:
profile.tools()returns Anthropic-native schemas:{ name, description, input_schema }. Pass them straight to the Anthropic SDK. For OpenAI, wrap them withtoOpenAIFunctions()(exported fromconfigure).profile.executeTool()accepts either{ name, arguments }(OpenAI-style) or{ name, input }(Anthropic-style). Pass the tool call through in whichever shape your provider produced.
When the model calls configure_profile_read, you have proof that the model-to-Configure tool path works. It shows up in your logs as a normal tool call.
Anthropic
ts
import Anthropic from "@anthropic-ai/sdk";
import { Configure, ConfigureError } from "configure";
const anthropic = new Anthropic();
const configure = new Configure({
apiKey: process.env.CONFIGURE_API_KEY,
agent: process.env.CONFIGURE_AGENT,
});
async function chat({ token, messages, system }) {
const profile = configure.profile({ token });
// profile.tools() is already Anthropic-shaped; spread it next to your own tools.
const tools = [...yourTools, ...profile.tools({ connectors: ["gmail"] })];
while (true) {
const response = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system,
messages,
tools,
});
const toolUses = response.content.filter((block) => block.type === "tool_use");
if (!toolUses.length) {
return response.content.find((block) => block.type === "text")?.text ?? "";
}
messages.push({ role: "assistant", content: response.content });
const toolResults = [];
for (const call of toolUses) {
const result = call.name.startsWith("configure_")
? await executeConfigureTool(profile, { name: call.name, input: call.input })
: await executeYourTool(call);
toolResults.push({
type: "tool_result",
tool_use_id: call.id,
content: JSON.stringify(result),
});
}
messages.push({ role: "user", content: toolResults });
}
}
async function executeConfigureTool(profile, toolCall) {
try {
return await profile.executeTool(toolCall);
} catch (error) {
if (error instanceof ConfigureError) {
return {
error: "configure_tool_failed",
code: error.code,
message: error.message,
suggestedAction: error.suggestedAction,
requestId: error.requestId,
};
}
return { error: "configure_tool_failed", message: "Configure tool failed" };
}
}OpenAI
ts
import OpenAI from "openai";
import { Configure, ConfigureError, toOpenAIFunctions } from "configure";
const openai = new OpenAI();
const configure = new Configure({
apiKey: process.env.CONFIGURE_API_KEY,
agent: process.env.CONFIGURE_AGENT,
});
async function chat({ token, messages }) {
const profile = configure.profile({ token });
// Your own tools are already OpenAI-shaped; convert only the Configure tools.
const tools = [
...yourTools,
...toOpenAIFunctions(profile.tools({ connectors: ["gmail"] })),
];
while (true) {
const completion = await openai.chat.completions.create({
model: "gpt-4o",
messages,
tools,
tool_choice: "auto",
});
const message = completion.choices[0].message;
if (!message.tool_calls?.length) {
return message.content ?? "";
}
messages.push(message);
for (const call of message.tool_calls) {
const args = JSON.parse(call.function.arguments || "{}");
const result = call.function.name.startsWith("configure_")
? await executeConfigureTool(profile, { name: call.function.name, arguments: args })
: await executeYourTool(call);
messages.push({
role: "tool",
tool_call_id: call.id,
content: JSON.stringify(result),
});
}
}
}
async function executeConfigureTool(profile, toolCall) {
try {
return await profile.executeTool(toolCall);
} catch (error) {
if (error instanceof ConfigureError) {
return {
error: "configure_tool_failed",
code: error.code,
message: error.message,
suggestedAction: error.suggestedAction,
requestId: error.requestId,
};
}
return { error: "configure_tool_failed", message: "Configure tool failed" };
}
}Verify without a model
To confirm the tool path is wired (in a smoke test, or when no provider key is set), call the read tool directly. This is the same dispatch the model uses, with no provider involved:
ts
const profile = configure.profile({ token });
const result = await profile.executeTool({
name: "configure_profile_read",
arguments: { sections: ["identity", "summary", "preferences", "imports"] },
});A non-error result confirms your keys, agent handle, token (or externalId), and the tool path are correct.
Framework Adapters
If your framework already drives the loop and exposes an executeTool/onToolCall callback (for example the Vercel AI SDK), you do not need the loops above. Pass a thin adapter that forwards Configure-prefixed calls:
ts
executeTool: (toolCall) => {
if (!toolCall.name.startsWith("configure_")) return executeYourTool(toolCall);
return executeConfigureTool(profile, toolCall);
},This is a framework-specific shortcut. Most providers need an explicit loop like the ones above. In message agents, use the same structured failure to send your hosted sign-in, reconnect, permissions, or approval surface before continuing.
Tool Set
With no options, profile.tools() returns the four default profile tools:
configure_profile_readconfigure_profile_searchconfigure_profile_rememberconfigure_profile_forget
configure_profile_forget ships in the default set so the agent can remove a memory when the user says to forget it.
Everything else is opt-in, and the option you pass decides exactly which tools appear:
| Enable with | Tools returned |
|---|---|
connectors: ["gmail"] | configure_gmail_search |
connectors: ["outlook"] | configure_email_search |
connectors: ["calendar"] | configure_calendar_get |
connectors: ["drive"] | configure_drive_search |
connectors: ["notion"] | configure_notion_search |
connectors: ["sheets"] | configure_sheets_search, configure_sheets_read |
actions: ["email.send"] | configure_email_send |
actions: ["calendar.create_event"] | configure_calendar_create_event |
actions: ["sheets.values_update"] | configure_sheets_values_update |
actions: ["sheets.values_append"] | configure_sheets_values_append |
actions: ["sheets.create_spreadsheet"] | configure_sheets_create_spreadsheet |
actions: ["sheets.add_sheet"] | configure_sheets_add_sheet |
advanced.utilitySearch | configure_web_search, configure_url_fetch |
advanced.files | configure_file_read, configure_file_list, configure_file_search, configure_file_write, configure_file_delete |
Hosted UI helpers are separate optional runtime surfaces. Tool visibility means hosted/app capability, not user authorization: expose supported connector/action tools when the product surface requested them, then let profile.executeTool() enforce the enabled set, linked state, connector state, permissions, scopes, approval state, clear user intent, and runtime policy. A call to configure_email_send fails unless profile.tools({ actions: ["email.send"] }) was used for that profile object.
profile.commit() is server-side write-back called after the model turn. It is not part of the default model tool set.