Tool Calling from the Frontend
How to let AI agents interact with your UI through function calling
Most people think of AI in the browser as a chatbot: you type a question, you get text back. But the AI can also do things in your app, like filter a table, toggle dark mode, navigate to a page or open a modal, by calling real functions in your frontend.
This is tool calling (or function calling). Let's build it with Vue.
What is tool calling?
When you send a message to an LLM like Claude, you can also send a list of "tools", functions the model can choose to call. Instead of responding with text, the model can say: "I want to call the function filterProducts with the argument { category: 'electronics' }."
Your app then executes that function and sends the result back to the model, which can use it to formulate a final answer.
It looks like this:
User: "Show me only the electronics products"
AI thinks: I should call filterProducts({ category: "electronics" })
App: *executes the function, filters the table*
AI responds: "Done! I've filtered the products to show only electronics."
The AI doesn't execute any code. It decides which function to call and with what arguments, and your app stays in control of what runs.
Defining tools
First, define the tools the AI can use. This is the contract between your app and the model:
// tools/definitions.ts
export const tools = [
{
name: 'filter_products',
description:
'Filter the product list by category, price range, or search query',
input_schema: {
type: 'object' as const,
properties: {
category: {
type: 'string',
description: 'Product category to filter by',
},
maxPrice: {
type: 'number',
description: 'Maximum price filter',
},
query: {
type: 'string',
description: 'Free text search query',
},
},
},
},
{
name: 'toggle_theme',
description: 'Switch between light and dark mode',
input_schema: {
type: 'object' as const,
properties: {
theme: {
type: 'string',
enum: ['light', 'dark'],
description: 'The theme to switch to',
},
},
required: ['theme'],
},
},
{
name: 'navigate_to',
description: 'Navigate to a page in the application',
input_schema: {
type: 'object' as const,
properties: {
path: {
type: 'string',
description:
'The route path to navigate to (e.g., "/about", "/commits")',
},
},
required: ['path'],
},
},
];
Each tool has a description, which is what the AI reads to understand when to use the function. Good descriptions matter a lot here, so be specific about what the tool does and when it should be used.
The tool executor
Now we need a composable that maps tool names to actual functions in our app:
// composables/useToolExecutor.ts
export function useToolExecutor() {
const router = useRouter();
const { toggleTheme } = useTheme();
const filters = ref({
category: '',
maxPrice: Infinity,
query: '',
});
const handlers: Record<string, (input: any) => any> = {
filter_products(input) {
if (input.category) filters.value.category = input.category;
if (input.maxPrice) filters.value.maxPrice = input.maxPrice;
if (input.query) filters.value.query = input.query;
return { success: true, applied: input };
},
toggle_theme(input) {
toggleTheme(input.theme);
return { success: true, theme: input.theme };
},
navigate_to(input) {
router.push(input.path);
return { success: true, navigated: input.path };
},
};
function execute(toolName: string, toolInput: any) {
const handler = handlers[toolName];
if (!handler) return { error: `Unknown tool: ${toolName}` };
return handler(toolInput);
}
return { execute, filters };
}
This is intentionally simple. Each handler is a plain function that does something in the app and returns a result. The AI will receive that result and use it in its response.
The AI loop
We send a message to the AI along with our tool definitions. If the AI decides to use a tool, we execute it and send the result back, and we keep going until the AI gives a final text response.
// composables/useAgentChat.ts
import { ref } from 'vue';
import { tools } from '~/tools/definitions';
export function useAgentChat(executor: ReturnType<typeof useToolExecutor>) {
const messages = ref<Array<{ role: string; content: any }>>([]);
const isThinking = ref(false);
async function send(userMessage: string) {
messages.value.push({ role: 'user', content: userMessage });
isThinking.value = true;
try {
let response = await callAI(messages.value);
// Tool use loop: keep going until we get a text response
while (response.stop_reason === 'tool_use') {
const toolBlocks = response.content.filter(
(b: any) => b.type === 'tool_use',
);
// Add assistant's response (with tool_use blocks)
messages.value.push({ role: 'assistant', content: response.content });
// Execute each tool and collect results
const toolResults = toolBlocks.map((block: any) => ({
type: 'tool_result',
tool_use_id: block.id,
content: JSON.stringify(executor.execute(block.name, block.input)),
}));
messages.value.push({ role: 'user', content: toolResults });
// Call AI again with the tool results
response = await callAI(messages.value);
}
// Final text response
const text = response.content
.filter((b: any) => b.type === 'text')
.map((b: any) => b.text)
.join('');
messages.value.push({ role: 'assistant', content: text });
} finally {
isThinking.value = false;
}
}
return { messages, isThinking, send };
}
async function callAI(messages: any[]) {
const res = await fetch('/api/ai/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages, tools }),
});
return res.json();
}
The while (response.stop_reason === 'tool_use') loop is the core of this pattern. The AI might call one tool, or five in a row, before giving a text answer, and the loop handles both.
The server endpoint
The backend is just a thin proxy to keep your API key safe:
// server/api/ai/chat.post.ts
export default defineEventHandler(async (event) => {
const { messages, tools } = await readBody(event);
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.ANTHROPIC_API_KEY!,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: 'claude-sonnet-4-5-20250514',
max_tokens: 1024,
system:
'You are a helpful assistant embedded in a web application. Use the available tools to help the user interact with the app. Always confirm what you did after using a tool.',
messages,
tools,
}),
});
return response.json();
});
Putting it all together
Here's the component that ties everything together:
<template>
<div class="agent">
<div class="messages">
<div
v-for="(msg, i) in visibleMessages"
:key="i"
:class="['message', msg.role]"
>
{{ msg.content }}
</div>
<div v-if="isThinking" class="message assistant thinking">
Thinking...
</div>
</div>
<form @submit.prevent="handleSend">
<input v-model="input" placeholder="Try: 'Switch to dark mode'" />
<button type="submit" :disabled="isThinking">Send</button>
</form>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useToolExecutor } from '~/composables/useToolExecutor';
import { useAgentChat } from '~/composables/useAgentChat';
const executor = useToolExecutor();
const { messages, isThinking, send } = useAgentChat(executor);
const input = ref('');
const visibleMessages = computed(() =>
messages.value.filter((m) => typeof m.content === 'string'),
);
async function handleSend() {
const text = input.value.trim();
if (!text) return;
input.value = '';
await send(text);
}
</script>
Now the user can type things like "Switch to dark mode", "Show me only products under 50 euros", or "Take me to the about page", and the AI will do it.
Security: keeping control
Tool calling gives the AI power over your UI, so you need guardrails:
- Whitelist tools explicitly. Never let the AI call arbitrary functions, only the ones you define.
- Validate inputs. The AI might hallucinate arguments, so check tool inputs before executing.
- Limit scope. Don't expose destructive actions (like deleting data) as tools unless you add confirmation steps.
- Keep sensitive operations on the server. If a tool needs to modify a database or call an external API, do it on the server, not the client.
function execute(toolName: string, toolInput: any) {
const handler = handlers[toolName];
if (!handler) return { error: `Unknown tool: ${toolName}` };
// Validate specific inputs
if (toolName === 'navigate_to' && !toolInput.path?.startsWith('/')) {
return { error: 'Invalid path: must start with /' };
}
return handler(toolInput);
}
Where this is used
Tool calling turns your AI from a text generator into an agent, something that can understand intent and act on it. The pattern is already used in production apps:
- Customer support bots that can look up orders, apply discounts, or escalate tickets
- Dashboard assistants that filter data, generate reports, or export CSVs
- IDE copilots that can run commands, open files, and navigate code
The frontend is the natural place for UI-related tools. With Vue composables the architecture stays small: tools are functions, the executor is a composable, and the AI loop is a while.
TL;DR: Tool calling lets AI act in your app as well as talk about it. Define tools as functions, let the AI decide when to call them, execute them on the frontend, and loop until you get a final response.