Skip to content

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 whole integration is one loop: build the model's tool list as [...yourTools, ...profile.tools()], call the model, and for each tool call it makes, route the call to profile.executeTool(call) when call.name starts with configure_ and to your own handler otherwise. Feed the results back and repeat. The rest of this page is that same loop written out for Anthropic and for OpenAI.

The token used below comes from the server-side Configure OAuth 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 openai
ts
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_KEY

Two 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 with toOpenAIFunctions() (exported from configure).
  • 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 six default tools:

  • configure_profile_read
  • configure_profile_search
  • configure_profile_remember
  • configure_profile_forget
  • configure_profile_import
  • configure_connect

configure_profile_forget ships in the default set so the agent can remove a memory when the user says to forget it; configure_connect ships so the agent can mint a sign-in or connect-an-app link when the user is not connected yet. configure_profile_commit is the one profile tool held back (server-side write-back); enable it with advanced: { commit: true }.

Everything else is opt-in, and the option you pass decides exactly which tools appear:

Enable withTools 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.utilitySearchconfigure_web_search, configure_url_fetch
advanced.filesconfigure_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.

Personalization infrastructure for agents