---
title: "How to Give Ollama Agents Live Web Access With Zenrows MCP"
description: "A local qwen3 model on Ollama reads a live Indeed job search and clears an anti-bot challenge page through one MCP config. Setup, the agent code, and the model's real answers."
---
# How to Give Ollama Agents Live Web Access With Zenrows MCP

> A local qwen3 model on Ollama reads a live Indeed job search and clears an anti-bot challenge page through one MCP config. Setup, the agent code, and the model's real answers.

Source: https://www.zenrows.com/blog/ollama-agents-live-web-access-zenrows-mcp

In this tutorial, a local Ollama agent reads a live Indeed job search and clears an anti-bot challenge page, all through [Zenrows MCP](/mcp). A plain HTTP request to the same challenge page returned a 403. Ollama publishes its `web_search` and `web_fetch` as hosted APIs for [local models](https://ollama.com/blog/web-search), called with an Ollama API key. For a local Ollama agent that needs to read protected pages and [access the live web](/glossary/ai-agents-and-the-web/how-to-give-an-agent-live-web-access), Zenrows MCP can cover the fetch layer. The agent calls the `extract` tool, Zenrows fetches the page through Fetch, which handles protected access, JavaScript rendering, and proxy rotation in a single API call, and the model gets the page as structured JSON fields. When you want the full page as Markdown instead, the same server has a `scrape` tool.

This is particularly important when retrieving content from protected sites, as they require more than just an HTTP request. [Bot detection systems](/glossary/web-data-access/how-do-websites-detect-scrapers) employ various techniques to evaluate the request's IP reputation, browser behavior, and other signals before exposing the content. A `requests.get()` call can't reliably get around any of these. Likewise, a Playwright browser, while it solves the rendering problem, still leaves you exposed to the rest of the bot detection system.

This guide is for models running on your own machine. If your agent runs in Claude, Cursor, or another hosted client, the [Claude and Cursor setup guide](/blog/pull-web-data-into-claude-cursor-and-any-ai-agent-with-zenrows-mcp) covers that path. Here, you'll see the **Zenrows MCP and how to use it for your Ollama agent's fetch layer**.

## TL;DR

- Ollama's `web_search` and `web_fetch` are hosted REST APIs at [ollama.com](https://ollama.com/blog/web-search) that you call with an Ollama API key.
- Ollama has no [MCP](/glossary/ai-agents-and-the-web/what-is-mcp) client of its own. You connect it to the local Zenrows MCP server (`npx -y @zenrows/mcp`) either through the `ollmcp` bridge, with no code, or through a short Python agent that is its own MCP client.
- On the tested targets, a local `qwen3` agent read a Cloudflare-fronted Indeed search and listed the companies and job titles, and it cleared an anti-bot challenge page, where a plain `requests.get()` to the challenge page returned a 403.
- The agent passes the model only the parsed fields from `extract` and raises Ollama's context window, so the model reads the whole job search instead of a truncated part of it.
- All code is on [GitHub](https://github.com/ZenRows/ollama-agent-web-access-zenrows-mcp).

## Why the Zenrows MCP?

A local model can decide which page it needs, but it does not fetch that page itself. Something has to make the request, and on a protected site that request has to get past the bot detection. **The Zenrows MCP has two transports**. One is **the remote MCP server**, which runs over streamable HTTP and can be authenticated using your Zenrows API key as a Bearer token. **The other is local** and runs as a STDIO (Standard Input/Output) subprocess through the `@zenrows/mcp` npm package with your key passed in an environment variable. For this piece, you will use the local one, since it runs next to Ollama on your machine. In the test runs below, **Zenrows MCP retrieved content from both protected targets through a local Ollama workflow**.

Firecrawl and Playwright both publish an MCP server as well. Here is how the three compare, as documented on October 21, 2026:

| | Zenrows MCP | Firecrawl MCP | Playwright MCP |
| --- | --- | --- | --- |
| How it runs | Cloud or local | Cloud or local | On your browser |
| Auth | OAuth sign-in or API key as a Bearer token, or `ZENROWS_API_KEY` environment variable | OAuth sign-in or API key as a Bearer token, or `FIRECRAWL_API_KEY` environment variable | None and runs locally |
| Protected access | One integration combines fast scraping and full browser automation, with anti-bot bypass on every tool | [Their MCP documentation](https://docs.firecrawl.dev/mcp-server) doesn't mention anti-bot handling or proxy rotation | Runs from your own IP with no proxy rotation |
| Transport | Hosted Streamable HTTP, or local STDIO via the command documented in [Zenrows MCP documentation](https://docs.zenrows.com/mcp/overview) | Hosted Streamable HTTP at `mcp.firecrawl.dev`, or local via `npx -y firecrawl-mcp` | Local STDIO via `npx @playwright/mcp` |

The **differences among these three MCPs are in their ability to handle protected systems**. Playwright MCP drives a browser from your own IP. Firecrawl's MCP is great for scraping and LLM-based extraction, and its MCP documentation does not cover proxy or bot-protection handling. For a wider comparison, this article covers various [MCP servers for web scraping](/blog/best-scraping-mcp-servers), and [Zenrows MCP for AI agents](/blog/zenrows-mcp) explains what the server gives any MCP client.

On Zenrows' end, these exposed tools matter for this setup.

- `scrape` retrieves a page through Zenrows Fetch and returns Markdown by default. It also supports other output formats, such as HTML, PDF, and screenshots.
- `extract` returns structured JSON instead of a page body. This is important for local models because their context windows are small.
- Use the `browser_*` tools to open a cloud browser session when a task spans more than one request. Within the `browser_*` tools is `browser_navigate` that starts a session and returns a `session_id`, and `browser_close` that ends and frees up resources after a session.
- The `crawl_*` tools run a [Crawl](/products/crawl) from one start URL and collect the URLs it finds behind that page, with each page's HTML when you ask for it. `crawl_create` starts the crawl, and `crawl_wait` returns its status and the first URLs once it finishes.
- The `batch_*` tools submit a whole list of URLs as one [Batch](/products/batch) job that runs in the background, so the agent doesn't wait on each page in turn. `batch_create` submits the job, `batch_wait` returns when the run ends, and `batch_results` pages through the results.

## What You Will Build

This step-by-step guide **walks you through building a local web research agent that runs on Ollama and reads live web pages, including those behind protected sites, through Zenrows MCP**. The agent will take a question, call the `extract` tool, and return output based on the retrieved content. This runs one retrieval round, which is what this question needs.

A task spanning several pages would loop until the model stops requesting tools. The same server covers that too: its crawl tools start from a listing page and collect the pages linked from it, and its batch tools submit a whole list of URLs as one job and return the results when it finishes. You ask for either in plain English, the same way you ask for one page.

By the end, you will have a local Ollama model connected through an MCP bridge for chatting with no code, a verified Zenrows server configuration, and a Python agent that extracts content from a protected page and a live job search.

## Set Up Zenrows MCP With Ollama

### Prerequisites

Before you start building, you need the following:

- Ollama installed and running. [Download](https://ollama.com/download/mac) it or install and start it with Homebrew using the script below.

```bash
brew install ollama
brew services start ollama
```

- Ollama running locally with a [tool-capable model](https://ollama.com/search?c=tools) like qwen 3. Ollama registers this as `qwen3:latest` which is the tag the script uses.

```bash
ollama pull qwen3
ollama show qwen3:latest
```

This is the output:

```text
user@Mac ollama_zenrows % ollama pull qwen3
pulling manifest
pulling a3de86cd1c13: 100% ▕██████████████████▏ 5.2 GB/5.2 GB  2.4 MB/s      0s
verifying sha256 digest
writing manifest
success
```

The `ollama show` output lists `tools` under Capabilities, which confirms the model can call the Zenrows tools:

```text
user@Mac ollama_zenrows % ollama show qwen3:latest
  Model
    architecture        qwen3
    parameters          8.2B
    context length      40960
    embedding length    4096
    quantization        Q4_K_M

  Capabilities
    completion
    tools
    thinking
```

- A Zenrows API key is optional for the local MCP server. On its first tool call without a key, the server creates a Free account for you with [5,000 credits per month](https://docs.zenrows.com/mcp/overview). To use your own account, [create a free Zenrows account](https://app.zenrows.com/register) and copy the API key from your dashboard.
- Node.js and Python 3.11 or later. Node runs the MCP server through `npx`, and the bridge is a Python package. Create a venv in your project directory.

```bash
python3 -m venv .venv
source .venv/bin/activate
```

Then install the dependency and Python's requests library.

```bash
pip install python-dotenv requests
```

Now that you have Ollama installed, your virtual environment activated, and dependencies set up, let's set up Zenrows MCP with Ollama.

### 1. Install the Bridge

**Ollama has no MCP client of its own, so you need a bridge** to chat with MCP tools from the terminal. This tutorial uses [ollmcp](https://pypi.org/project/ollmcp/). It is an open-source Python client bridge for Ollama that connects the [MCP server](/blog/mcp-server) to a local Ollama model over STDIO, SSE, and Streamable HTTP. It also installs the `ollama` and `mcp` Python packages that the agent later in this guide imports. This tutorial was tested with ollmcp 0.35.0 on Python 3.14.

```bash
pip install ollmcp
```

### 2. Configure the Zenrows MCP Server

Create `zenrows-mcp.json` in your project directory. This is a variant of the config block from the [official Zenrows MCP server docs](https://docs.zenrows.com/mcp/overview#local-mcp-server) that keeps the key in `.env` instead of the JSON file. Create `.env` with `ZENROWS_API_KEY=your_key_here` and keep it out of version control.

```json
{
  "mcpServers": {
    "zenrows": {
      "command": "sh",
      "args": ["-c", "set -a && . ./.env && set +a && exec npx -y @zenrows/mcp"]
    }
  }
}
```

`@zenrows/mcp` will be automatically downloaded on the first run of the `npx -y` flag. Save your key in `.env` in the same directory:

```bash
ZENROWS_API_KEY=your_key_here
```

The `sh -c` command sources `.env` into the environment before starting `npx`, so the MCP server reads `ZENROWS_API_KEY` from there. The JSON holds no secret and can be committed. However, your `.env` cannot, so add it to `.gitignore`. The `.env` path is relative, so run `ollmcp` from this directory.

### 3. Verify the Connection and Chat With No Code

Start the bridge and point it at the config and model.

```bash
ollmcp --servers-json zenrows-mcp.json --model qwen3:latest
```

The bridge starts the MCP server and lists the tools it exposes. A successful initialization should list the Zenrows server and the expected tools, including `scrape` and `extract`. On 21 October 2026, the bridge connected and listed the Zenrows tools, all enabled by default (output trimmed):

```text
Found server in config: zenrows
Connecting to server: zenrows
Successfully connected to zenrows with 62 tool(s) and 3 prompt(s) and 1 resource(s)/template(s)
╭──────────────────────────── 🔧 Available Tools ────────────────────────────╮
│ ✓ zenrows.scrape                         ✓ zenrows.extract                 │
│ ✓ zenrows.batch_create                   ✓ zenrows.batch_status            │
...
│ ✓ zenrows.browser_switch_tab             ✓ zenrows.browser_batch           │
╰─────────────────────────── 62/62 tools enabled ────────────────────────────╯
╭───────────────────────────────────────────────╮
│ Current model: qwen3:latest (ollama)          │
│ Capabilities: 🔧 Tools │ 💭 Thinking (medium) │
╰───────────────────────────────────────────────╯
```

From there, you can chat with `qwen3` in the terminal and ask for a live page in plain English. Every enabled tool's schema goes into the model's context, so type `/tools` and keep `scrape` and `extract` switched on for a small local model. `/human-in-the-loop` asks you to approve each tool call before it runs.

That is the no-code route. To put the same tools inside your own program, the next section builds the agent in Python. All code is on [GitHub](https://github.com/ZenRows/ollama-agent-web-access-zenrows-mcp).

## Building a Web-Aware Ollama Agent With Zenrows MCP

The agent loop has four layers. **You ask a question, Ollama picks a tool, Zenrows makes the requests, and the target site returns the pages**. The model itself never contacts the site. The script is its own MCP client: it starts the Zenrows MCP server with `npx`, so it does not need `ollmcp` or `zenrows-mcp.json` to run. The model sees every `extract` field in the tool schema, and `ALLOWED_ARGS` keeps the call down to the URL, keeping credentials out of the model context. It only sees the content returned by the `extract` tool.

![How the local agent reads a live page: the question goes to qwen3 on Ollama, the model picks the extract tool, the Zenrows MCP server fetches the page, and only the parsed data goes back to the model](/blog/_img/ollama-agent-zenrows-mcp-loop.png)

In this tutorial, you will make use of the scraping course bot-protected [challenge page](https://www.scrapingcourse.com/antibot-challenge). This page also reports its own verdict in the output, so you can tell if it was successful. To begin, create an `agent.py` file within the project directory and run it. The script was tested on Python 3.14 with the pinned versions in the [GitHub repository's requirements.txt](https://github.com/ZenRows/ollama-agent-web-access-zenrows-mcp/blob/main/requirements.txt), against `qwen3:latest`.

```python
from dotenv import load_dotenv
load_dotenv()

import asyncio
import json
import os
import ollama
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

SERVER = StdioServerParameters(
    command="npx",
    args=["-y", "@zenrows/mcp"],
    env=dict(os.environ),
)
MODEL = "qwen3:latest"
# give the model enough context to read the whole tool result
OPTIONS = {"num_ctx": 16384}
QUESTION = "Extract the page heading from https://www.scrapingcourse.com/antibot-challenge."
ALLOWED_ARGS = {"url"}

async def main():
    async with stdio_client(SERVER) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            listed = await session.list_tools()
            tools = [
                {"type": "function", "function": {
                    "name": t.name,
                    "description": t.description,
                    "parameters": t.input_schema,
                }}
                for t in listed.tools if t.name == "extract"
            ]

            messages = [{"role": "user", "content": QUESTION}]
            reply = ollama.chat(model=MODEL, messages=messages, tools=tools, options=OPTIONS)
            messages.append(reply["message"])

            for call in reply["message"].get("tool_calls") or []:
                args = {k: v for k, v in call.function.arguments.items() if k in ALLOWED_ARGS}
                print("calling", call.function.name, args)
                result = await session.call_tool(call.function.name, args)
                text = result.content[0].text
                print("result:", text[:300])
                # pass the model only the parsed fields from the response
                data = json.loads(text).get("data")
                messages.append({"role": "tool", "content": json.dumps(data)})

            print(ollama.chat(model=MODEL, messages=messages, tools=tools, options=OPTIONS)["message"]["content"])

asyncio.run(main())
```

`OPTIONS` sets the model's context window. Ollama uses a [4,096-token context window by default](https://docs.ollama.com/faq), and the Indeed prompt later in this guide came to about 6,900 tokens. Without `num_ctx`, Ollama truncated that prompt and the model answered from part of the listings. With it, the model read all of them.

The `ALLOWED_ARGS` filters the model's arguments and blocks anything the model invents before it reaches the server. The agent passes only the URL, and Zenrows decides how to fetch it, turning on JavaScript rendering and premium proxies only when the page needs them. The `extract` response wraps the parsed fields in a `data` object next to a few status flags, and the script hands the model that `data` object alone, which keeps the tool message small enough for a local model's context window.

Below is the output of the script. It returns structured JSON and confirms success.

```text
(.venv) user@Mac ollama_zenrows % python3 agent.py
Using existing API key from env (secrets dir: /Users/user/.zenrows)
Zenrows MCP server running on stdio (secrets dir: /Users/user/.zenrows)
calling extract {'url': 'https://www.scrapingcourse.com/antibot-challenge'}
result: {"ok":true,"mode":"auto","empty":false,"data":{"challenge_result":{"message":"You bypassed the Antibot challenge! :D","status":"bypassed"},"page":{"challenge_type":"antibot","title":"Antibot Challenge"}}}
The page heading (h1) from the URL is:

**Antibot Challenge**

This is the title extracted from the webpage, which also happens to be the name of the antibot challenge page.
(.venv) user@Mac ollama_zenrows %
```

Ollama returns a `tool_call` and the script sends the result back as a `tool` message. The model sees only the returned page content. Let's try it with the same target without Zenrows in the loop.

```python
import requests

r = requests.get("https://www.scrapingcourse.com/antibot-challenge")
print(r.status_code, len(r.text))
```

This run, on 21 October 2026, resulted in a [403 block](/blog/403-web-scraping). An agent that tries to retrieve content from that site using only an HTTP request would encounter a challenge page.

```text
(.venv) user@Mac ollama_zenrows % python3 without_zenrows.py
403 5517
```

![What reaches the agent from a protected page: a plain request gets a 403 challenge page, while the extract tool through Zenrows MCP returns the page's fields as JSON](/blog/_img/ollama-agent-protected-page-twin.png)

## The Agent Read a Live Indeed Search Through Zenrows MCP

Your agent was able to get content from the demo site. Let's see how it will perform against a Cloudflare-protected site like [indeed.com](https://www.indeed.com). You can run the script below to confirm that it is actually a [Cloudflare-protected](/blog/bypass-cloudflare) site.

```bash
curl -sI https://www.indeed.com | grep -iE "^(server|date):"
```

On 21 October 2026, the command above returned the output below.

```text
(.venv) user@Mac ollama_zenrows % curl -sI https://www.indeed.com | grep -iE "^(server|date):"
date: Wed, 21 Oct 2026 08:15:02 GMT
server: cloudflare
```

Back to testing the agent on indeed.com, create `cloudflare_site.py` with the same Python code from `agent.py` and change the `QUESTION` line to the one below. Once done, run the script:

```python
QUESTION = "List the company names and job titles from https://www.indeed.com/jobs?q=data+analyst&l=Austin%2C+TX."
```

In this test run on 21 October 2026, the extraction returned `ok: true` with 16 job listings from the search and no challenge response, and the model listed every company and job title on the page.

```text
(.venv) user@Mac ollama_zenrows % python3 cloudflare_site.py
Using existing API key from env (secrets dir: /Users/user/.zenrows)
Zenrows MCP server running on stdio (secrets dir: /Users/user/.zenrows)
calling extract {'url': 'https://www.indeed.com/jobs?q=data+analyst&l=Austin%2C+TX'}
result: {"ok":true,"mode":"auto","empty":false,"data":{"job_listings":[{"benefits":null,"company_logo_url":null,"company_name":"Paywall Bypass","detail_url":"/rc/clk?jk=1d9d5ad6081ee5e0&bb=0Jmy1L_Rym9Qr_CYZLvqO-iSA0EMSX-Z6TVgVMeYzV9VdW2jJoxc32S2GChQtdu5Qm9Zo3b5B6zKmWui0wMrKaTa7wukcUUmE83DPPtvU-K-2B_k_liBF0N
Here are the company names and job titles from the Indeed search results in Austin, TX:

1. **Paywall Bypass** - Data Analyst  
2. **Visa U.S.A. Inc.** - Data & Insights Analyst  
3. **Texas Department of Transportation** - Data Analyst IV  
4. **Austin Community College** - Analyst, Institutional Research (Hourly)  
5. **Austin Community College** - Analyst, Human Resources Data  
6. **University of Texas at Austin** - Data Analyst, College of Liberal Arts  
7. **Steck Systems** - Data Analyst  
8. **Apple** - Business Insights Analyst, Apple Store Online Operations  
9. **Google** - Business Data Analyst II, Google Global Infrastructure, Strategy and Operations  
10. **Google** - Business Data Analyst II, Google Global Infrastructure, Strategy and Operations  
11. **University of Texas at Austin** - Senior Data Analyst  
12. **The University of St. Augustine for Health...** - Institutional Research Analyst  
13. **Ericsson** - Data Analyst Strategy and Operations  
14. **Texas Department of Transportation** - Business Analyst II  
15. **SHI International Corporation** - Associate Analytics Engineer - Data & Visualization  
16. **Health & Human Services Comm** - Data Analyst V  

Let me know if you'd like additional details like locations or salary information!
(.venv) user@Mac ollama_zenrows %
```

## What Your Agent Can Now Do

This tutorial walked you through building a local Ollama agent that reads live web pages, including Cloudflare-protected ones, using a single MCP configuration. This setup gives you a reproducible integration pattern for testing protected targets. Zenrows MCP provides the web retrieval interface, while Zenrows handles access, rendering, and proxies for every request. You can also use the same server for other hosted agents. If you want to run it on [Claude, Cursor, or other cloud clients](/blog/pull-web-data-into-claude-cursor-and-any-ai-agent-with-zenrows-mcp), Zenrows MCP provides the fetch interface. For scripts and CI jobs, the [Zenrows CLI](/cli) runs the same requests from your terminal, and an agent with no Zenrows account can [pay per request](/blog/ai-agents-can-pay-for-zenrows) over x402 or MPP at `agents.zenrows.com`.

[Start building free](https://app.zenrows.com/register) with 5,000 credits every month.

## FAQ

### What Tools Do AI Engineers Use to Give Local Ollama Agents Reliable Web Access?

Ollama has `web_search` and `web_fetch` endpoints that a model can call as tools, hosted at ollama.com and called with an Ollama API key. For protected sites, Zenrows MCP connects a local Ollama agent to Zenrows retrieval tools through the local STDIO transport, started with `npx -y @zenrows/mcp`. Zenrows MCP covers proxy rotation, JavaScript rendering, and anti-bot bypass on every tool.

### Does Ollama Support MCP?

Ollama runs models that can call tools, but it does not include an MCP client of its own. To use MCP servers with a local model, run a bridge such as [ollmcp](https://pypi.org/project/ollmcp/), or write a short client with the `mcp` and `ollama` Python packages, as this guide does. Either way, the Zenrows MCP server gives the model live web access through the same local config.

### Which Ollama Models Support Tool Calling?

Pick a model from Ollama's [tools category](https://ollama.com/search?c=tools), such as `qwen3`, and confirm it with `ollama show <model>`: a tool-capable model lists `tools` under Capabilities. Tool calling is what lets the model ask Zenrows MCP for a page, so a model without it can't use the server.

### Does Zenrows Have an MCP Server?

Yes. The [Zenrows MCP server](https://github.com/ZenRows/zenrows-mcp) works with any MCP client and has two transports. The local server runs as a STDIO subprocess through the `@zenrows/mcp` npm package, with the API key supplied through an environment variable or created for you on the first call. The remote option uses Streamable HTTP with OAuth sign-in or Bearer authentication. Both expose scraping, structured extraction, crawl and batch jobs, and browser tools. To learn more, read about [giving your AI agent real web access](/blog/zenrows-mcp) with Zenrows MCP.

### What Web Scraping API Has the Best MCP Integration?

For a local Ollama workflow, Zenrows MCP did the whole job in this guide: a local model read an anti-bot challenge page and a live Indeed job search through one MCP config. The same connection also gives the model `scrape` for page content in Markdown and other formats, `extract` for structured JSON, crawl jobs that discover the pages behind a start URL, batch jobs for many URLs at once, and `browser_*` tools for browser sessions with persistent cookies and tabs.

### How Do You Reliably Connect an AI Agent to Live Websites?

Give the agent a fetch tool through an [MCP server](/glossary/ai-agents-and-the-web/what-is-a-web-scraping-mcp-server) rather than letting it make HTTP requests directly. The model calls the tool, the server retrieves the page, and the model receives clean content. Zenrows MCP exposes `scrape` and `extract` for single pages, Crawl and Batch for many pages, and Browser Sessions for workflows that require browser interaction, with access and rendering handled by Zenrows on every request. This is important because on protected sites, a direct request returns a challenge page or a 403 error.

### Why Does My Ollama Agent Answer From Only Part of a Page?

The tool result is larger than the model's context window, so Ollama truncates the prompt. Ollama's default context window is 4,096 tokens, and the Indeed search in this guide came to about 6,900 tokens. Pass `options={"num_ctx": 16384}` to `ollama.chat()`, or set `OLLAMA_CONTEXT_LENGTH` for the Ollama server, and send the model the `data` object from Zenrows `extract` rather than the full page, so the whole result fits.
