# Connect client tools (/en/docs/developers/agent/embed/client-tools)



Let the agent read page state on demand or perform a page action. Declare a tool on the agent and implement a matching callback in your Kit page. Kit returns the result to the session so the agent can continue.

## Prepare the integration [#prepare]

You need a working Kit page, a service agent's public ID (`agent_…`), and an API Key on your backend. This example reads a lesson selection without a video, database, or extra dependencies. Your website must provide an authenticated `POST /api/praxis-token` route returning `{ token }`, as described in [Embed tokens and users](/en/docs/developers/agent/embed/tokens). This is your route, not a Praxis endpoint.

Use a client tool for on-demand reads. Use [page context](/en/docs/developers/agent/embed/context#page-context) for small facts needed with every message.

## Declare the tool [#declare-tool]

Save this as `register-tool.ts`, replace `agent_example`, and run `bun register-tool.ts` on a server with `PRAXIS_API_KEY` set. It reads the agent, then updates `tools` through the public API. Other tools are preserved, and the current `version` guards against concurrent changes.

```ts
// register-tool.ts — run on the server with: bun register-tool.ts
const key = process.env.PRAXIS_API_KEY;
if (!key) throw new Error("Set PRAXIS_API_KEY on the server");
const url = "https://api-develop.deepraxis.com/agent/v1/agents/agent_example";
const headers = { Authorization: `Bearer ${key}`, "Content-Type": "application/json" };
const currentResponse = await fetch(url, { headers });
if (!currentResponse.ok) throw new Error(`Read failed: ${currentResponse.status}`);
const current = await currentResponse.json();
const tool = {
  type: "custom",
  name: "read_selected_lesson",
  description: "Read the selected lesson when the user asks which lesson is open.",
  input_schema: { type: "object", properties: {}, additionalProperties: false },
};
const tools = [...(current.tools ?? []).filter(
  (item: { type: string; name?: string }) =>
    !(item.type === "custom" && item.name === tool.name),
), tool];
const response = await fetch(url, {
  method: "POST", headers,
  body: JSON.stringify({ version: current.version, tools }),
});
if (!response.ok) throw new Error(`Update failed: ${response.status}`);
const saved = await response.json();
console.log({ id: saved.id, version: saved.version });
```

**Expected result:** the terminal prints the agent ID and new version. Issue a token for that version and start a new session. Tokens pinned to an older version and existing sessions do not automatically adopt the new definition. If the embed policy pins a version, update it in the Dashboard or specify the new `version` during issuance.

Tool names must match `^[a-zA-Z0-9_-]{1,64}$` and must not duplicate custom or built-in tool names. `input_schema.type` must be `object`. Describe when the agent should call the tool. A browser function alone does not register a tool with the agent.

## Handle the call in your page [#handle-call]

Serve this HTML from your website. It uses the Develop Kit and API; the embed policy must allow the page origin.

```html
<!doctype html>
<html lang="en">
<meta charset="utf-8" />
<title>Lesson assistant</title>
<label>Current lesson
  <select id="lesson">
    <option value="Lesson 3: Delivery rules">Lesson 3: Delivery rules</option>
    <option value="Lesson 4: Returns">Lesson 4: Returns</option>
  </select>
</label>
<output id="tool-status" aria-live="polite"></output>
<praxis-agent style="display:block;height:600px"></praxis-agent>
<script type="module">
  import "https://kit-develop.praxisusercontent.com/v1/praxis-kit.js";
  await customElements.whenDefined("praxis-agent");
  const chat = document.querySelector("praxis-agent");
  const lesson = document.querySelector("#lesson");
  chat.onClientTool = async ({ name }) => {
    if (name !== "read_selected_lesson") throw new Error(`Unknown tool: ${name}`);
    return { title: lesson.value };
  };
  chat.addEventListener("praxis.tool.change", event => {
    const { name, status, isError } = event.detail;
    document.querySelector("#tool-status").textContent =
      `${name}: ${status}${isError ? " (error)" : ""}`;
  });
  chat.addEventListener("praxis.log", event => console.warn(event.detail));
  chat.setOptions({
    api: {
      url: "https://api-develop.deepraxis.com",
      getToken: async () => {
        const response = await fetch("/api/praxis-token", {
          method: "POST", credentials: "same-origin",
        });
        if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
        const { token } = await response.json();
        if (typeof token !== "string" || !token) throw new Error("Missing token");
        return token;
      },
    },
    agent: { name: "Lesson assistant" },
  });
</script>
</html>
```

Assign `onClientTool` as a JavaScript property on the element. It is neither an HTML attribute nor a `setOptions` option. The callback receives `{ id, name, input }`: call ID, tool name, and input. Validate the input in your handler and throw for unknown tool names.

Strings are returned unchanged. Other serializable values become JSON text; `undefined` becomes an empty string. Exceptions, rejected promises, and serialization failures become error results. Do not include credentials or another user's data.

## Verify calls and recovery [#verify]

Ask the agent to use the tool to identify the open lesson. &#x2A;*Expected result:** the page reports `start`, then `end`, and the agent answers using the returned lesson. Change the selection and ask again. An `end` event means the server accepted the result, not that the entire response finished.

| Symptom                                   | Action                                                                                                     |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| No call                                   | Check the new session's agent version and the tool description                                             |
| `not available on this page`              | Assign `onClientTool` before a call arrives                                                                |
| Handler failure                           | Fix input validation or page logic; the agent receives an error result                                     |
| Result submission failure in `praxis.log` | The handler is not automatically rerun. Check connectivity and session state before repeating side effects |

The server waits at most 120 seconds, then records an error result and continues. Kit claims call IDs and checks historical results to avoid duplicate execution. This is not a transaction guarantee across browser tabs. Backend authorization and idempotency are required for tools that change business data.

## Authorization and next steps [#next]

Client `custom` tools bypass the platform's tool confirmation cards. Implement any page-action confirmation in your own UI. Enforce data permissions on the backend, without trusting browser values or model-supplied identity.

[Tool authorization](/en/docs/developers/agent/embed/tool-authorization) · [Component reference](/en/docs/developers/agent/embed/component-reference) · [Update agent API](/en/docs/api/native/agent/updateAgent).
