Client Tools

Build client-side tools that run in the browser

Create tools that run in the browser with access to DOM, state, and UI.


Why Frontend Tools?

  • Access browser APIs - DOM, localStorage, navigation
  • Use React state - Read/write app state directly
  • Show rich UI - Render components as results
  • Real-time updates - Stream results as they happen

Basic Tool

import { useTools } from '@yourgpt/copilot-sdk/react';
import { z } from 'zod';

function MyTools() {
  useTools({
    search_products: {
      description: 'Search the product catalog',
      parameters: z.object({
        query: z.string().describe('Search query'),
        limit: z.number().optional().default(10),
      }),
      handler: async ({ query, limit }) => {
        const results = await fetch(`/api/search?q=${query}&limit=${limit}`);
        return results.json();
      },
    },
  });

  return null;
}

Tool Structure

FieldRequiredDescription
descriptionYesWhat the tool does (AI reads this)
parametersYesZod schema for inputs
handlerYesAsync function that runs
toModelOutputNoBeta: synchronous mapping of the raw result to native model content
requiresApprovalNoAsk user before running
hiddenNoHide tool from chat UI (still executes)

Multiple Tools

useTools({
  get_weather: {
    description: 'Get weather for a city',
    parameters: z.object({ city: z.string() }),
    handler: async ({ city }) => fetchWeather(city),
  },

  add_to_cart: {
    description: 'Add product to cart',
    parameters: z.object({
      productId: z.string(),
      quantity: z.number().default(1),
    }),
    handler: async ({ productId, quantity }) => {
      await cart.add(productId, quantity);
      return { success: true };
    },
  },

  navigate: {
    description: 'Go to a page in the app',
    parameters: z.object({ path: z.string() }),
    handler: async ({ path }) => {
      router.push(path);
      return { navigated: path };
    },
  },
});

With Approval

Require user confirmation before running:

useTools({
  delete_account: {
    description: 'Permanently delete user account',
    parameters: z.object({}),
    requiresApproval: true,
    handler: async () => {
      await deleteAccount();
      return { deleted: true };
    },
  },
});

Use requiresApproval: true for destructive or sensitive actions.


Hidden Tools

Hide tool execution from the chat UI while still executing normally:

useTools({
  navigate: {
    description: 'Navigate to a page in the app',
    parameters: z.object({ path: z.string() }),
    hidden: true, // Won't show in chat UI
    handler: async ({ path }) => {
      router.push(path);
      return { navigated: path };
    },
  },

  track_event: {
    description: 'Track analytics event',
    parameters: z.object({
      event: z.string(),
      properties: z.record(z.any()).optional(),
    }),
    hidden: true, // Silent background operation
    handler: async ({ event, properties }) => {
      analytics.track(event, properties);
      return { tracked: true };
    },
  },
});

Hidden tools are useful for navigation, analytics, or background operations that shouldn't clutter the chat UI.


Deferred Loading

Mark tools with deferLoading: true to exclude them from every request. They are injected automatically only when the user's message semantically matches the tool — reducing token cost for large tool sets.

useTool({
  name: "run_sql_query",
  description: "Execute a SQL query against the database",
  deferLoading: true,
  searchKeywords: ["sql", "query", "database", "table"],
  inputSchema: z.object({ query: z.string() }),
  handler: async ({ query }) => db.execute(query),
});

Add searchKeywords alongside description for more accurate relevance matching.


Fallback Renderer

<CopilotChat> resolves a renderer for each tool call using this priority chain:

PrioritySourceDescription
1toolRenderers[toolName]Per-tool renderer on <CopilotChat>
2tool.renderRender function on the tool definition
3mcpToolRendererCatch-all for MCP tools
4fallbackToolRendererCatch-all for any unmatched tool
5Built-in defaultGeneric execution card
<CopilotChat
  toolRenderers={{
    get_weather: ({ args, result }) => <WeatherCard {...result} />,
  }}
  fallbackToolRenderer={({ toolName, args, result }) => (
    <pre>{JSON.stringify(result, null, 2)}</pre>
  )}
/>

AI Response Control

Control how the AI responds after tool execution:

handler: async ({ query }) => {
  const results = await search(query);

  return {
    data: results,
    _aiResponseMode: 'brief',      // 'verbose' | 'brief' | 'silent'
    _aiContext: `Found ${results.length} results`,
    _aiContent: 'Here are the results:',
  };
}
ModeBehavior
verboseAI explains results in detail
briefAI gives short summary
silentNo AI response, just show tool result

Error Handling

handler: async ({ query }) => {
  try {
    const results = await search(query);
    return { success: true, data: results };
  } catch (error) {
    return {
      success: false,
      error: error.message,
    };
  }
}

The success flag

The success field is read by the AI to decide how to continue reasoning after a tool call. Returning success: false tells the AI the tool did not accomplish its goal — it can then retry with different parameters, try an alternative tool, or explain the failure to the user.

// ✅ Good — AI understands what went wrong and can adapt
return {
  success: false,
  error: 'No results found for that date range. Try a wider range.',
};

// ❌ Avoid — throwing an exception stops the agentic loop entirely
throw new Error('No results found');

Always include a descriptive error string alongside success: false. The AI uses this message to reason about its next step, so vague errors like "Unknown error" reduce its ability to recover gracefully.


Best Practices

  1. Clear descriptions - AI uses this to decide when to call the tool
  2. Validate with Zod - Type safety and clear parameter docs
  3. Return structured data - AI understands JSON better than strings
  4. Handle errors - Return error info instead of throwing
  5. Use approval for destructive actions - Delete, purchase, send

Returning images

The beta supports native image tool results on OpenAI Responses and Anthropic. Keep the raw application result and supply toModelOutput, or return the supported _aiContent envelope for Runtime/Copilot tools. See image tool results for exact shapes, execution semantics, media limits, and persistence requirements.


Next Steps

On this page