Tool Calling from the Frontend: Let AI Interact with Your UI

Most people think of AI in the browser as a chatbot — you type a question, you get text back. But what if the AI could actually do things in your app? Filter a table, toggle dark mode, navigate to a page, open a modal. Not by generating instructions for the user to follow, but by calling real functions in your frontend.

This is tool calling (or function calling), and it's one of the most powerful patterns in modern AI applications. 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 just 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 key insight is that the AI doesn't execute code — it decides which function to call and with what arguments. Your app stays in full control.


Defining tools

First, let's define what tools our AI can use. Think of this as a 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'],
    },
  },
];

Notice how each tool has a description — this is what the AI reads to understand when to use each function. Good descriptions are critical. 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

Here's where the magic happens. 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. This loop handles it all.


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:

<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 actually 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 expose what you define.
  • Validate inputs — the AI might hallucinate arguments. Always validate tool inputs before executing.
  • Limit scope — don't expose destructive actions (like deleting data) as tools unless you add confirmation steps.
  • Server-side for sensitive ops — 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);
}

Why this matters

Tool calling turns your AI from a text generator into an agent — something that can understand intent and act on it. This pattern is already being 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. And with Vue composables, the architecture stays clean — tools are just functions, the executor is a composable, and the AI loop is a simple while.


TL;DR: Tool calling lets AI interact with your app, not just talk about it. Define tools as functions, let the AI decide when to call them, execute on the frontend, and loop until you get a final response. It's the bridge between chatbot and agent.