Agent SDK
Client tools
Define safe client-side tools so the agent can update your app UI, refresh data, navigate, and coordinate product workflows.
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:
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:
const agent = useAppAgent({
apiKey: "emcy_sk_xxxx",
agentId: "ag_xxxxx",
clientTools,
});Example#
This example exposes three client tools:
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:
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:
requestApprovalrequestInput
You do not need to define those yourself.
Instead, render:
agent.approvals.pendingagent.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
