Build a Web Research Agent with Mastra
Build a local AI agent that searches the web and cites real sources.
Introduction
30 Second Summary
You ask an AI chatbot what happened in the news this week and it gives you a confident, detailed answer. The problem? Half of it is completely made up. Without access to real information, language models fill the gaps with fabrication.
In this project, you will build a web research agent that searches the internet for real results and synthesizes cited answers. You'll wire a search tool into an AI agent running locally on your machine, so it can look things up instead of guessing.
What You'll Build
You'll ask your agent "What happened in tech news this week?" and get back a summary pulled from real sources, all running on your local machine and tested interactively through Mastra Studio's built-in playground.
By the end of this project, you'll have:
- A Mastra AI agent powered by a local Ollama model that answers current-events questions with real citations instead of hallucinated facts.
- Typed tool definitions using Zod schemas that validate inputs before the agent executes a web search through Tavily.
- An interactive testing setup through Mastra Studio where you can chat with your agent and inspect every tool call it makes.
- Secret Mission: Add a URL content extraction tool so the agent can read full articles for deeper research.
Are there any prerequisites?
This project assumes you have some TypeScript experience and Ollama already installed on your machine. You'll sign up for a free Tavily API key during the project (1,000 credits per month, no credit card required).
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters. You're building a web research agent that demonstrates the fundamental agentic AI pattern: an LLM that reasons about a goal and selects tools to accomplish it.
Set Up Your Mastra Project
To build your web research agent, you need a foundation: a TypeScript project with the right packages installed and configured.
In this step, you will create a new project, install Mastra and its dependencies, and verify the whole setup launches correctly in Mastra Studio.
In this step, get ready to:
- Create a new TypeScript project with all required dependencies installed.
- Configure the environment file for the Tavily API key.
- Build the Mastra project structure and verify Studio launches.
Create the project and install dependencies
Every Mastra project needs a Node.js package with ES module support and strict TypeScript settings.
- In VSCodium, open a terminal by pressing Ctrl+` (backtick key). You can also click Terminal then New Terminal in the menu bar.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir web-research-agent
cd web-research-agent
- Initialize the npm project and set it to use ES modules by running:
npm init -y
npm pkg set type="module"
You should see npm print the contents of your new package.json file in the terminal.
- Install the dev dependencies by running:
npm install -D typescript @types/node mastra@latest
- Install the runtime dependencies by running:
npm install @mastra/core@latest zod ollama-ai-provider-v2 @tavily/core
What packages are these?
- @mastra/core is the Mastra framework for building AI agents.
- zod provides typed schemas that validate your tool inputs and outputs.
- ollama-ai-provider-v2 connects Mastra to your local Ollama models.
- @tavily/core is the SDK for Tavily's web search API.
Install failing?
Make sure you are inside the web-research-agent folder. Run pwd to confirm your current directory ends with /web-research-agent.
If you see permission errors, try prefixing the command with sudo (macOS/Linux) or run your terminal as Administrator (Windows).
Help me troubleshoot my npm install error
- Add the Mastra dev and build scripts by running:
npm pkg set scripts.dev="mastra dev"
npm pkg set scripts.build="mastra build"
npm pkg delete scripts.test
What do these commands do?
These commands add two scripts to your package.json: dev launches Mastra Studio for interactive testing, and build compiles your project for production.
The last command removes the default test script that npm init creates, since you will not need it in this project.
- Verify your scripts are set correctly by running:
npm pkg get scripts
You should see output showing "dev": "mastra dev" and "build": "mastra build".
Now create the TypeScript configuration file that Mastra requires.
- In VSCodium, open the web-research-agent folder by clicking File in the menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the web-research-agent folder, and click Open.
- Create a new file by clicking File then New File.
- Save it immediately as tsconfig.json by pressing Ctrl+S (Windows/Linux) or Cmd+S (macOS) and typing the filename.
- Paste the following configuration into tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"outDir": "dist"
},
"include": ["src/**/*"]
}
Why these TypeScript settings?
Mastra requires ES2022 module output and bundler module resolution. Using CommonJS or node resolution causes import errors at runtime.
The noEmit flag means TypeScript only checks your code for errors. Mastra handles the actual compilation when you build or run the dev server.
- Save tsconfig.json.
You should see tsconfig.json appear in the Explorer sidebar on the left.
Create your environment file
Your agent will call the Tavily web search API, which requires an API key for authentication.
- In VSCodium, create a new file by clicking File then New File.
- Save it as .env in the root of your web-research-agent folder.
- Paste the following into your .env file, replacing the placeholder with your actual Tavily API key:
TAVILY_API_KEY=your-tavily-api-key-here
Where do I find my Tavily API key?
Log in to your Tavily account at app.tavily.com. Your API key is displayed on the dashboard. The free tier includes 1,000 credits per month.
- Save .env.
You should see .env appear in the Explorer sidebar alongside tsconfig.json and package.json.
Can't see the .env file?
Files starting with a dot are hidden by default in some file explorers. In VSCodium's sidebar, the file should appear normally. If you accidentally saved it with a different name, right-click the file in the Explorer sidebar and select Rename.
Help me find my .env file
Create the Mastra project structure and entry point
Mastra expects your agents and tools to live in specific directories under src/mastra/.
- Switch back to your terminal in VSCodium and create the directory structure by running:
mkdir -p src/mastra/agents src/mastra/tools
- Confirm the folders were created by running:
ls src/mastra
You should see agents and tools listed in the output.
Now create the Mastra entry point file that registers your agents with the framework.
- In VSCodium, create a new file and save it as src/mastra/index.ts.
- Paste the following code into src/mastra/index.ts:
import { Mastra } from '@mastra/core'
export const mastra = new Mastra({
agents: {},
})
What does this code do?
This creates a new Mastra instance with an empty agents object. In the next step, you will register your research agent here.
The Mastra constructor is the central registry that connects agents, tools, and the development server together.
- Save src/mastra/index.ts.
✔️ Awesome, I've got everything!
Great. Make sure all your files are saved before continuing.
ⓧ I'd like to double check the full code
Here is what each file should contain at this point:
tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"outDir": "dist"
},
"include": ["src/**/*"]
}
.env:
TAVILY_API_KEY=your-tavily-api-key-here
src/mastra/index.ts:
import { Mastra } from '@mastra/core'
export const mastra = new Mastra({
agents: {},
})
Before you run the dev server, what do you think will appear at http://localhost:4111? Since the agents object is empty, think about whether you will see any agents listed.
- Start Mastra Studio by running:
npm run dev
You should see output indicating the server is running. Open http://localhost:4111 in your browser to see Mastra Studio load with no errors.
Mastra Studio not loading?
If you see a module resolution error, double check that your tsconfig.json has "moduleResolution": "bundler" and your package.json has "type": "module". CommonJS settings will not work with Mastra.
If port 4111 is already in use, check for other processes using that port and stop them before retrying.
Help me troubleshoot Mastra Studio
Your project foundation is solid. Next up, you will build your first agent and discover why it needs tools to answer real questions.
Build a Basic Agent and Hit the Hallucination Wall
Your Mastra project is set up and Studio is running. Now it's time to build your first AI agent and see what it can do on its own.
What happens when you ask an LLM about current events? Without access to the internet, it has no way to know what happened today. It will answer confidently anyway. This step lets you experience that problem firsthand so you understand exactly why agents need tools.
In this step, get ready to:
- Create a research agent powered by Ollama.
- Register the agent and test it in Mastra Studio.
- Observe the hallucination problem when asking about current events.
Create the research agent with Ollama
A Mastra agent combines an LLM with instructions that shape its behaviour. You define the agent in its own file, then register it with the Mastra instance so Studio can find it.
- Create a new file at src/mastra/agents/research-agent.ts by pasting the following code:
import { Agent } from '@mastra/core/agent'
import { ollama } from 'ollama-ai-provider-v2'
export const researchAgent = new Agent({
id: 'research-agent',
name: 'Research Agent',
instructions: `You are a helpful research assistant that answers questions accurately.
When asked about current events or recent information, provide the most up-to-date answer you can.
Always be honest about what you know and don't know.`,
model: ollama('llama3.2'),
})
What does this code do?
This file defines a single agent with four properties:
- id and name identify the agent in Studio and when calling it programmatically.
- instructions is the system prompt that shapes how the agent responds. Right now it just says "be helpful and honest."
- model points to llama3.2 running locally through Ollama. No API key needed.
- Save src/mastra/agents/research-agent.ts.
Seeing a red underline on the import?
If your editor shows an error on the ollama-ai-provider-v2 import, make sure you installed it in Step 1. Run npm list ollama-ai-provider-v2 to confirm it appears in your dependencies.
Help me fix this import error
✔️ Awesome, I've got everything!
Great. Double check you have saved src/mastra/agents/research-agent.ts before moving on.
ⓧ I'd like to double check the full code
import { Agent } from '@mastra/core/agent'
import { ollama } from 'ollama-ai-provider-v2'
export const researchAgent = new Agent({
id: 'research-agent',
name: 'Research Agent',
instructions: `You are a helpful research assistant that answers questions accurately.
When asked about current events or recent information, provide the most up-to-date answer you can.
Always be honest about what you know and don't know.`,
model: ollama('llama3.2'),
})
Register the agent and test in Studio
The agent file exists, but Mastra does not know about it yet. You need to import and register the agent in your Mastra entry point so Studio can display it.
- Open src/mastra/index.ts and replace its contents with the following:
import { Mastra } from '@mastra/core'
import { researchAgent } from './agents/research-agent.ts'
export const mastra = new Mastra({
agents: { researchAgent },
})
What changed here?
You added an import for researchAgent and passed it into the agents object. This registers the agent with Mastra so Studio can find it and you can call it programmatically later.
- Save src/mastra/index.ts.
- Switch back to your terminal and stop the running Mastra Studio process with Ctrl+C.
- Restart Studio by running:
npm run dev
- Open http://localhost:4111 in your browser.
- Select Research Agent from the sidebar on the left.
You should see the chat interface ready for messages. The Research Agent is now loaded and waiting.
Don't see the Research Agent in the sidebar?
Make sure your src/mastra/index.ts imports researchAgent and passes it to the agents object. The key name must match exactly. Also confirm you restarted Studio after saving.
Help me find my agent in Studio
✔️ Awesome, I've got everything!
Great. Confirm you have saved src/mastra/index.ts and can see the Research Agent in Studio.
ⓧ I'd like to double check the full code
import { Mastra } from '@mastra/core'
import { researchAgent } from './agents/research-agent.ts'
export const mastra = new Mastra({
agents: { researchAgent },
})
Now for the real test. Before you send a message, think about this: the llama3.2 model was trained on data up to a fixed point in time. It has no internet access. What do you think will happen when you ask about this week's news?
- Type the following message into the chat and press Enter:
What happened in tech news this week?
Read the response carefully. The agent replies with confidence, listing events, names, and dates. But none of it is real. The model is fabricating information because it has no access to current data.
What is hallucination?
LLMs generate text by predicting the most likely next token. When asked about information outside their training data, they still produce fluent, confident-sounding text. This is called hallucination. The output looks credible but is entirely made up.
This is the core problem that tool calling solves. Instead of guessing, the agent can use a tool to fetch real information before answering.
You have just experienced the hallucination problem firsthand. Your agent sounds helpful but is making things up. In the next step, you will build a web search tool that gives the agent access to real, current information.
Create the Web Search Tool
In Step 2, you saw the hallucination problem firsthand. Your agent confidently fabricated events, names, and dates because it has zero access to current information.
The fix is to give the agent a real capability. Mastra's createTool function lets you define typed, validated tools that an agent can call at runtime. In this step, you will build a web search tool that connects to the Tavily API so the agent can fetch real results from the internet.
In this step, get ready to:
- Build a typed web search tool using Mastra's createTool with Zod schemas.
- Verify the tool compiles with no TypeScript errors.
Build the Tavily search tool with createTool
Mastra requires tools to be defined with createTool(). Each tool declares a typed input schema, a typed output schema, and an execute function that runs when the agent calls it.
- Create a new file at src/mastra/tools/web-search.ts by pasting the following code:
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'
import { tavily } from '@tavily/core'
export const webSearchTool = createTool({
id: 'web-search',
description: 'Search the web for current information on any topic. Use this when you need up-to-date information that you do not already know.',
inputSchema: z.object({
query: z.string().describe('The search query to look up on the web'),
}),
outputSchema: z.object({
results: z.array(z.object({
title: z.string(),
url: z.string(),
content: z.string(),
})),
}),
// Search the web using Tavily and return formatted results
execute: async ({ query }) => {
const tvly = tavily({ apiKey: process.env.TAVILY_API_KEY! })
const response = await tvly.search(query, { maxResults: 5 })
return {
results: response.results.map((r) => ({
title: r.title,
url: r.url,
content: r.content,
})),
}
},
})
What does this code do?
This defines a complete tool the agent can call at runtime:
- id and description tell the LLM what this tool does and when to use it. The agent reads the description to decide whether to call the tool.
- inputSchema uses Zod to validate the agent's input before execution. If the agent passes invalid data, the call fails early instead of crashing inside the function.
- outputSchema declares the shape of data the agent receives back. The agent uses this contract to parse and reason about the results.
- execute is the function that actually runs. It creates a Tavily client, searches the web for up to 5 results, and returns them in the declared output shape.
- Save src/mastra/tools/web-search.ts.
Seeing red squiggles in your editor?
- Make sure the file is saved at src/mastra/tools/web-search.ts (not src/tools/ or the project root).
- Confirm your .env file exists in the project root with TAVILY_API_KEY set.
- Check that @tavily/core and zod are listed in your package.json dependencies. If not, run npm install @tavily/core zod again.
help me fix TypeScript errors in my web-search tool file
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before moving on.
ⓧ I'd like to double check the full code
Here is the complete src/mastra/tools/web-search.ts file:
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'
import { tavily } from '@tavily/core'
export const webSearchTool = createTool({
id: 'web-search',
description: 'Search the web for current information on any topic. Use this when you need up-to-date information that you do not already know.',
inputSchema: z.object({
query: z.string().describe('The search query to look up on the web'),
}),
outputSchema: z.object({
results: z.array(z.object({
title: z.string(),
url: z.string(),
content: z.string(),
})),
}),
execute: async ({ query }) => {
const tvly = tavily({ apiKey: process.env.TAVILY_API_KEY! })
const response = await tvly.search(query, { maxResults: 5 })
return {
results: response.results.map((r) => ({
title: r.title,
url: r.url,
content: r.content,
})),
}
},
})
Verify the code compiles
Before wiring this tool into the agent, confirm everything is properly typed. The TypeScript compiler catches schema mismatches, import errors, and typos before they become runtime failures.
Before you run this, do you think the compiler will pass cleanly or report errors? If it errors, what might be wrong?
- Run the TypeScript compiler in check-only mode:
npx tsc --noEmit
The command prints nothing and returns you to the prompt. That silence means zero type errors. Your tool schemas, imports, and execute function are all properly typed.
Seeing type errors?
- If you see an error about Cannot find module '@mastra/core/tools', confirm you ran npm install @mastra/core@latest in your project root.
- If you see errors related to tavily types, make sure @tavily/core is installed and your tsconfig.json has "moduleResolution": "bundler".
- If the error mentions process.env, confirm @types/node is in your devDependencies.
help me fix tsc --noEmit errors in my Mastra project
Your tool is typed, validated, and ready to go. In the next step, you will wire it into the Research Agent so it can call the tool at runtime and answer questions with real data.
Wire the Tool and See Real Answers
In the last step, you saw the agent confidently make up current events. You also built a working search tool that can pull real results from the web. The missing piece is connecting them.
In this step, you connect the webSearchTool to the agent so it can reason about when to search and use real results to answer questions accurately. The difference is dramatic.
In this step, get ready to:
- Import and wire the web search tool into the agent.
- Update the agent's instructions with a search-and-cite strategy.
- Test in Mastra Studio and see the agent return real, sourced answers.
Add the tool to the agent and update instructions
The agent needs two things to use a tool: a reference to the tool object, and instructions that teach it when and how to use it.
- In VSCodium, open src/mastra/agents/research-agent.ts.
- Replace the entire contents of the file with this updated agent definition:
import { Agent } from '@mastra/core/agent'
import { ollama } from 'ollama-ai-provider-v2'
import { webSearchTool } from '../tools/web-search.ts'
export const researchAgent = new Agent({
id: 'research-agent',
name: 'Research Agent',
instructions: `You are a helpful research assistant that answers questions accurately using web search.
When asked about current events, recent news, or anything you are not certain about:
1. Use the web-search tool to find current information
2. Synthesize the search results into a clear, concise answer
3. Always cite your sources by including the URL where you found the information
When asked about general knowledge you are confident about, you may answer directly without searching.
Format your responses clearly with bullet points for multiple findings.`,
model: ollama('llama3.2'),
tools: { webSearchTool },
})
What changed in this file?
Three things changed from the previous version:
- A new import pulls in webSearchTool from the tools folder you built in Step 3.
- The instructions now teach the agent a clear strategy: search for current events, synthesize results, and always cite URLs.
- The tools: { webSearchTool } property registers the tool so the agent can call it during reasoning.
- Save src/mastra/agents/research-agent.ts.
Seeing a red underline on the import?
Make sure the path is ../tools/web-search.ts with two dots at the start. The agent file is in src/mastra/agents/ and the tool is in src/mastra/tools/, so you need to go up one level.
Also confirm that src/mastra/tools/web-search.ts exists and exports webSearchTool (not a default export).
Help me fix my webSearchTool import in research-agent.ts
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before moving on.
ⓧ I'd like to double check the full code
Here is the complete src/mastra/agents/research-agent.ts file:
import { Agent } from '@mastra/core/agent'
import { ollama } from 'ollama-ai-provider-v2'
import { webSearchTool } from '../tools/web-search.ts'
export const researchAgent = new Agent({
id: 'research-agent',
name: 'Research Agent',
instructions: `You are a helpful research assistant that answers questions accurately using web search.
When asked about current events, recent news, or anything you are not certain about:
1. Use the web-search tool to find current information
2. Synthesize the search results into a clear, concise answer
3. Always cite your sources by including the URL where you found the information
When asked about general knowledge you are confident about, you may answer directly without searching.
Format your responses clearly with bullet points for multiple findings.`,
model: ollama('llama3.2'),
tools: { webSearchTool },
})
Restart Studio and test with the same question
For Mastra Studio to pick up the code change, you need to restart the dev server.
- Switch back to your terminal where npm run dev is running.
- Press Ctrl+C to stop the server.
- Restart Mastra Studio by running:
npm run dev
- Open http://localhost:4111 in your browser.
- Select the Research Agent from the agents list.
Before you type anything, think back to the last step. What do you expect to happen differently this time when you ask about current events?
- Type this question into the chat: What happened in tech news this week?.
This time, the response is completely different. You can see the agent making a tool call to web-search in the Studio response. It receives real search results from Tavily, then synthesizes an answer with actual sources and URLs.
What is happening under the hood?
The agent reads your question and decides it needs current information. It calls the web-search tool with a search query, receives real results from Tavily, and then writes its final answer based on those results.
This is tool calling in action. The LLM reasons about which tool to use and what input to give it. Mastra handles the back-and-forth between the model and the tool automatically.
- Try a follow-up question like What is the latest version of Node.js? to see the agent decide to search again.
- Try a general knowledge question like What is the capital of France? and notice the agent answers directly without calling the tool.
The agent now decides when to search versus when to answer from its own knowledge. That decision-making is the core of agentic reasoning.
Agent not calling the tool?
If the agent answers without calling the search tool, check that the tools: { webSearchTool } property is inside the new Agent({}) config object (not outside it).
Also confirm your updated instructions mention using the web-search tool by name. The model relies on the instructions and the tool description to decide when to call it.
Help me figure out why my agent isn't calling the web-search tool
Your agent now has real-world knowledge on demand. Next up, you will run the agent from a script so it can power any application, not just the Studio playground.
Secret mission
Add a URL Content Extraction Tool
Your agent can search the web, but it only sees short snippets from each result. What if it could read full articles? Add a content extraction tool that lets the agent do multi-step research: search first, then deep-read the most relevant source.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- web-research-agent project directory on your Desktop.
Keep everything running
No action needed. Choose this if you're still actively building or experimenting with new tools and prompts.
- Your project directory stays on your Desktop, ready for you to run npm run dev whenever you want to open Mastra Studio again.
- Your Tavily free account remains active with 1,000 credits per month at no charge.
- The llama3.2 model stays downloaded in Ollama, ready to use in other projects too.
Pause - I'll come back to this later
Shut down running processes to free up system resources. Your files stay on disk.
- If Mastra Studio is still running in your terminal, press Ctrl+C to stop it.
- No other background processes are running. Your project files and Tavily account stay intact for when you return.
Delete - I don't want to use this again
Remove all project files from your machine. This cannot be undone.
- Stop Mastra Studio if it is still running by pressing Ctrl+C in the terminal.
- Remove the project directory by running the command for your operating system.
On macOS or Linux:
cd ~/Desktop
rm -rf web-research-agent
On Windows:
cd ~\Desktop
rmdir /s /q web-research-agent
- Your Tavily free account remains active at no charge. No action is needed there unless you choose to upgrade in the future.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully functional web research agent that searches the internet, synthesizes answers from real sources, and runs entirely on your local machine with Ollama.
You've learned how to:
- Build a Mastra AI agent powered by a local Ollama model, experiencing the hallucination problem firsthand and solving it with tool-calling.
- Create typed tools using createTool with Zod schemas for input/output validation, and wire the Tavily web search API into the agent as a callable tool.
- Test interactively through Mastra Studio's built-in playground and run the agent programmatically from a script for application integration.
- Secret Mission: Added a URL content extraction tool for multi-step research workflows, teaching the agent to search first and then deep-read specific articles.
Ready to quiz yourself?