Agent SDK

Client tools

Define safe client-side tools so the agent can update your app UI, refresh data, navigate, and coordinate product workflows.

7 sections

clientTools lets the agent run app-owned functions inside the host environment.

This is how an agent goes beyond chat and starts interacting with your UI directly.

Use client tools for things like:

  • refreshing the page's data after an MCP mutation
  • prefilling a form
  • navigating to a different view
  • selecting a record in the current screen
  • opening a local modal or drawer
  • highlighting records the agent is about to touch
  • rendering local charts or insight panels
  • asking for approval before a destructive plan

The prop shape#

The App Agent config accepts a clientTools map:

TS
type ClientToolDefinition = {
  description: string;
  parameters: Record<
    string,
    {
      type: string;
      description: string;
      required?: boolean;
      enum?: string[];
    }
  >;
  execute: (params: Record<string, unknown>) => Promise<unknown>;
};
 
type ClientToolsMap = Record<string, ClientToolDefinition>;

Use it with useAppAgent:

TSX
const agent = useAppAgent({
  apiKey: "emcy_sk_xxxx",
  agentId: "ag_xxxxx",
  clientTools,
});

Example#

This example exposes three client tools:

TSX
import type { ClientToolsMap } from "@emcy/agent-sdk/app";
 
const clientTools: ClientToolsMap = {
  openInvoice: {
    description: "Open the invoice detail view for a specific invoice.",
    parameters: {
      invoiceId: {
        type: "string",
        description: "Invoice id to open.",
        required: true,
      },
    },
    execute: async ({ invoiceId }) => {
      const id = String(invoiceId ?? "").trim();
      if (!id) {
        return { success: false, error: "invoiceId required" };
      }
 
      router.push(`/invoices/${id}`);
      return { success: true, invoiceId: id };
    },
  },
  refreshInvoices: {
    description: "Refresh the current invoice list.",
    parameters: {},
    execute: async () => {
      await refetchInvoices();
      return { success: true };
    },
  },
  highlightInvoiceRows: {
    description: "Briefly highlight invoice rows in the current table.",
    parameters: {
      ids: {
        type: "array",
        description: "Invoice ids to highlight.",
        required: true,
      },
    },
    execute: async ({ ids }) => {
      highlightRows(Array.isArray(ids) ? ids.map(String) : []);
      return { success: true };
    },
  },
};

That gives the agent an important split of responsibilities:

  • MCP tools do the real server-side work
  • client tools keep the visible product UI in sync

When to use client tools vs MCP tools#

Use MCP tools when the action should happen on the server side or against a real backend API.

Use client tools when the action should happen in the current app surface.

Good split:

  • MCP tool: update_invoice_status

  • client tool: refreshInvoices

  • MCP tool: get_invoice

  • client tool: openInvoice

Use prompt and context to teach the handoff#

The SDK exposes your clientTools, but you should still teach the agent when to use them.

Two good places:

  • the agent System Prompt
  • the per-turn appContext

Example:

TSX
useAppAgent({
  appContext={{
    hostRefreshInstruction:
      "After any successful invoice mutation, call refreshInvoices before you answer so the host page reflects the latest data.",
  }}
  clientTools,
});

That is a strong pattern for embedded agents: server truth comes from MCP, but visible UI completion comes from clientTools.

Built-in interaction actions#

The App Agent layer also reserves two built-in client tools:

  • requestApproval
  • requestInput

You do not need to define those yourself.

Instead, render:

  • agent.approvals.pending
  • agent.requests.pending

and resolve them through:

  • agent.approvals.resolve(...)
  • agent.requests.submit(...)
  • agent.requests.cancel(...)

Best practices#

  • keep client tool names explicit and verb-first
  • describe what the tool changes in the UI
  • return structured results from execute
  • expose only actions that are safe for the current page
  • keep the set small so the agent is not choosing from unnecessary browser actions
  • use client tools to close the loop after MCP mutations
  • keep destructive work on the server side, not in client tools