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 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 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 four default profile tools:

  • configure_profile_read
  • configure_profile_search
  • configure_profile_remember
  • configure_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 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