Tools
Tools allow your agents to interact with external systems, APIs, databases, and perform specific actions. They are one of the most powerful features of VoltAgent, giving your agents the ability to affect the world beyond just generating text.
Creating Basic Tools
VoltAgent provides a createTool
helper function that makes it easy to create type-safe tools with full IntelliSense support. Let's start with a simple example - a calculator tool:
import { createTool } from "@voltagent/core";
import { z } from "zod";
const calculatorTool = createTool({
name: "calculate",
description: "Perform a mathematical calculation",
parameters: z.object({
expression: z.string().describe("The mathematical expression to evaluate, e.g. (2 + 2) * 3"),
}),
execute: async (args) => {
try {
// In production, use a secure math parser instead of eval
const result = eval(args.expression);
return { result };
} catch (error) {
throw new Error(`Invalid expression: ${args.expression}`);
}
},
});
This tool takes a mathematical expression as input and returns the calculated result. Notice how the execute
function automatically infers its parameter types from the Zod schema defined in parameters
. This provides full IntelliSense support in your IDE, helping catch errors during development.
Understanding Tools
A tool in VoltAgent is a function that an agent can call to perform an action or retrieve information. Each tool has:
- Name: A unique identifier for the tool
- Description: Explains what the tool does (the agent uses this to decide when to use the tool)
- Parameters: The inputs the tool requires, defined using Zod schemas
- Execute function: The code that runs when the tool is called
Using Tools with an Agent
To use a tool with an agent, simply include it in the agent's configuration. When you interact with the agent (e.g., using generateText
or streamText
), the LLM will decide based on the prompt and the tool's description whether using the tool is appropriate to fulfill the request.
import { Agent } from "@voltagent/core";
import { VercelAIProvider } from "@voltagent/vercel-ai";
import { openai } from "@ai-sdk/openai";
const agent = new Agent({
name: "Calculator Assistant",
description: "An assistant that can perform calculations",
llm: new VercelAIProvider(),
model: openai("gpt-4o"),
tools: [calculatorTool],
});
// The agent can now use the calculator tool when needed
// const response = await agent.generateText("What is 123 * 456?");
// console.log(response.text);
Using Multiple Tools Together
Agents become more powerful when they can use multiple tools together. Here's an example of an agent that can both check the weather and perform calculations:
import { Agent, createTool } from "@voltagent/core";
import { VercelAIProvider } from "@voltagent/vercel-ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
// Calculator tool
const calculatorTool = createTool({
name: "calculate",
description: "Perform a mathematical calculation",
parameters: z.object({
expression: z.string().describe("The mathematical expression to evaluate"),
}),
execute: async (args) => {
try {
// args is automatically typed as { expression: string }
const result = eval(args.expression);
return { result };
} catch (error) {
throw new Error(`Invalid expression: ${args.expression}`);
}
},
});
// Weather tool
const weatherTool = createTool({
name: "get_weather",
description: "Get the current weather for a location",
parameters: z.object({
location: z.string().describe("The city name"),
}),
execute: async (args) => {
// args is automatically typed as { location: string }
const { location } = args;
// In a real implementation, you would call a weather API
// This is a simplified example
return {
location,
temperature: 22,
conditions: "sunny",
};
},
});
// Create an agent with multiple tools
const multiToolAgent = new Agent({
name: "Multi-Tool Assistant",
description: "An assistant that can check weather and perform calculations",
llm: new VercelAIProvider(),
model: openai("gpt-4o"),
tools: [calculatorTool, weatherTool],
});
// The agent can now use both tools in the same conversation
const response = await multiToolAgent.generateText(
"What's the weather in Paris today? Also, what is 24 * 7?"
);
console.log(response.text);
// Example response:
// "The current weather in Paris is 22°C and sunny. As for your calculation, 24 * 7 = 168."
In this example, the agent can decide which tool to use based on the user's question. When asked both about the weather and a calculation, it will use both tools and combine the results into a single coherent response.
Type Safety with Tools
When you use createTool
, the type of the args
parameter in the execute
function is automatically inferred from the Zod schema you define in parameters
. This provides several benefits:
- IntelliSense support: Your IDE will show autocomplete suggestions for the properties of
args
- Type checking: TypeScript will catch errors if you try to access properties that don't exist
- Refactoring support: When you change the schema, TypeScript will help you update all the places that use it
- Documentation: The types serve as documentation for what data the tool expects
For example, with this tool definition:
const weatherTool = createTool({
name: "get_weather",
description: "Get the current weather for a location",
parameters: z.object({
location: z.string().describe("The city name"),
unit: z.enum(["celsius", "fahrenheit"]).optional().describe("Temperature unit"),
}),
execute: async (args) => {
// args is typed as { location: string; unit?: "celsius" | "fahrenheit" }
const { location, unit = "celsius" } = args;
// ...
},
});
The args
parameter is automatically typed as { location: string; unit?: "celsius" | "fahrenheit" }
, giving you full IDE support without having to manually specify the type.
Tool Hooks
VoltAgent provides lifecycle hooks that let you respond to tool execution events. This allows you to add logging, perform additional actions, or modify behavior when tools are used.
Using hooks
The onToolStart
hook is called just before a tool is executed, and onToolEnd
is called after execution completes. These hooks are particularly useful for:
- Logging tool usage
- Updating UI when tools are running
- Cleaning up resources after tool execution
import { Agent, createHooks } from "@voltagent/core";
import { VercelAIProvider } from "@voltagent/vercel-ai";
import { openai } from "@ai-sdk/openai";
// Define the hooks using createHooks
const hooks = createHooks({
onToolStart(_, tool) {
console.log(`Tool starting: ${tool.name}`);
},
onToolEnd(_, tool, result) {
console.log(`Tool completed: ${tool.name}`);
console.log(`Result: ${JSON.stringify(result)}`);
},
});
// Create an agent with hooks
const agent = new Agent({
name: "Assistant with Tool Hooks",
description: "An assistant that logs tool execution",
llm: new VercelAIProvider(),
model: openai("gpt-4o"),
tools: [calculatorTool],
hooks: hooks,
});
// When the agent uses a tool, the hooks will be triggered
// const response = await agent.generateText("What is 123 * 456?");
Best Practices
Clear Descriptions
Provide clear descriptions for your tools and parameters. The agent relies heavily on these descriptions to understand the tool's purpose, capabilities, and required inputs. Remember that the .describe()
strings in your Zod parameter schemas are crucial information passed directly to the LLM.
Bad Example:
const badTool = createTool({
name: "search",
description: "Searches things", // Vague, doesn't explain what it searches or when to use it
parameters: z.object({
q: z.string(), // Unclear parameter name with no description
n: z.number().optional(), // Unclear what this parameter does
}),
execute: async (args) => {
/* ... */
},
});
Good Example:
const goodTool = createTool({
name: "search_web",
description:
"Searches the web for current information on a topic. Use this when you need to find recent or factual information that may not be in your training data.", // Clear purpose and usage guidance
parameters: z.object({
query: z
.string()
.describe("The search query. Should be specific and focused on what information is needed."),
results_count: z
.number()
.min(1)
.max(10)
.optional()
.describe("Number of results to return. Defaults to 3 if not specified."),
}),
execute: async (args) => {
/* ... */
},
});
Error Handling
Implement proper error handling in your tool's execute function. Return useful error messages that the agent can understand.
execute: async (args) => {
try {
// Tool implementation
return result;
} catch (error) {
throw new Error(`Failed to process request: ${error.message}`);
}
};
Cancellable Tools with AbortSignal
For long-running tools, implement cancellation with AbortSignal. This allows tools to be gracefully cancelled when needed, such as when a user cancels a request or when an operation times out.
Tools receive an options object as their second parameter, which can contain an AbortSignal:
const searchTool = createTool({
name: "search_web",
description: "Search the web for current information",
parameters: z.object({
query: z.string().describe("The search query"),
}),
execute: async (args, options) => {
// Extract the signal from options
const signal = options?.signal;
// Check if already aborted
if (signal?.aborted) {
throw new Error("Search was cancelled before it started");
}
// Example of a fetch operation that respects abort signal
const response = await fetch(`https://api.search.com?q=${args.query}`, {
signal: signal, // Pass the signal to fetch
});
// ... process and return results
return await response.json();
},
});
When calling a tool directly, you can pass the AbortSignal in the options parameter:
// Create an AbortController
const controller = new AbortController();
const signal = controller.signal;
// Set a timeout to abort after 5 seconds
setTimeout(() => controller.abort(), 5000);
try {
// Pass the signal in the options object
const result = await searchTool.execute({ query: "Latest AI developments" }, { signal });
console.log("Results:", result);
} catch (error) {
if (error.name === "AbortError") {
console.log("Search was cancelled by the user");
} else {
console.error("Search failed:", error);
}
}
When using tools with an agent, you can pass the AbortSignal in the options of the agent's generateText or streamText methods:
// Create an AbortController
const controller = new AbortController();
const signal = controller.signal;
// Set a timeout to abort after 30 seconds
setTimeout(() => controller.abort(), 30000);
try {
// Pass the signal to the agent
const response = await agent.generateText("Search for the latest AI developments", {
signal, // The agent will pass this signal to any tools it uses
});
console.log(response.text);
} catch (error) {
if (error.name === "AbortError") {
console.log("Operation was cancelled");
} else {
console.error("Error:", error);
}
}
The AbortSignal mechanism is particularly useful for:
- User-initiated cancellations
- Implementing timeouts for slow operations
- Gracefully stopping batch operations
- Preventing unnecessary work when results are no longer needed
Timeout Handling
For tools that call external APIs, implement timeout handling. While Promise.race
can be used, the AbortController
pattern often integrates more cleanly, especially with fetch
and the agent's own signal propagation.
execute: async (args) => {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Request timed out")), 5000);
});
const resultPromise = fetchDataFromApi(args);
return Promise.race([resultPromise, timeoutPromise]);
};
Alternatively, using AbortSignal with a timeout:
execute: async (args, options) => {
// Create a new AbortController if none provided
const controller = new AbortController();
// Create a timeout that will abort the controller
const timeoutId = setTimeout(() => {
controller.abort("Operation timed out");
}, 5000); // 5-second timeout
try {
// Merge with any parent signal if provided
if (options?.signal) {
options.signal.addEventListener("abort", () => {
controller.abort("Parent operation aborted");
clearTimeout(timeoutId);
});
}
// Use our controller's signal for the API call
const result = await fetchDataFromApi(args, { signal: controller.signal });
clearTimeout(timeoutId); // Clear the timeout if the operation completed successfully
return result;
} catch (error) {
clearTimeout(timeoutId); // Ensure timeout is cleared on error too
throw error;
}
};
Dynamic Tool Registration
You can add tools to an agent after it's been created:
// Create an agent without tools initially
const agent = new Agent({
name: "Dynamic Assistant",
description: "An assistant that can gain new abilities",
llm: new VercelAIProvider(),
model: openai("gpt-4o"),
});
// Later, add tools to the agent
agent.addTools([calculatorTool]);
// Or provide tools for a specific request only
const response = await agent.generateText("Calculate 123 * 456", {
tools: [calculatorTool],
});
MCP (Model Control Protocol) Support
VoltAgent supports the Model Control Protocol (MCP), allowing your agents to seamlessly connect with external model servers, AI systems, and other tools that implement this protocol. This enables you to expand your agent's capabilities without having to write complex integration code.
Using MCP Tools
You can connect to external MCP-compatible servers and use their tools with your agents:
import { Agent, MCPConfiguration } from "@voltagent/core";
import { VercelAIProvider } from "@voltagent/vercel-ai";
import { openai } from "@ai-sdk/openai";
// Set up MCP configuration with multiple servers
const mcpConfig = new MCPConfiguration({
servers: {
// HTTP server configuration
browserTools: {
type: "http",
url: "https://your-mcp-server.example.com/browser",
},
// Local stdio server configuration
localAI: {
type: "stdio",
command: "python",
args: ["local_ai_server.py"],
},
},
});
// Option 1: Get tools grouped by server using getToolsets()
const toolsets = await mcpConfig.getToolsets();
// Access tools for a specific server
const browserToolsOnly = toolsets.browserTools.getTools();
const localAiToolsOnly = toolsets.localAI.getTools();
// Option 2: Get all tools from all servers combined into a single array
const allMcpTools = await mcpConfig.getTools();
// Create an agent using only the browser tools
const agentWithBrowserTools = new Agent({
name: "Browser Agent",
description: "Assistant using only browser tools via MCP",
llm: new VercelAIProvider(),
model: openai("gpt-4o"),
tools: browserToolsOnly, // Use the specific toolset
});
// Create another agent using *all* fetched MCP tools
const agentWithAllMcpTools = new Agent({
name: "MCP-Enhanced Assistant",
description: "Assistant with access to all configured MCP tools",
llm: new VercelAIProvider(),
model: openai("gpt-4o"),
tools: allMcpTools, // Use the combined list
});
// Use the agents (example)
// const response = await agentWithBrowserTools.generateText(
// "Take a screenshot of the current page."
// );
// console.log(response.text);
MCP enables your agents to:
- Connect to browser extensions for web automation
- Access specialized local AI models (image recognition, voice processing, etc.)
- Use API gateways that implement the MCP protocol
- Tap into ecosystems of MCP-compatible tools
For in-depth details on setting up and using the Model Control Protocol with your agents, see the MCP documentation.