We Gave a Local LLM Live Web Access. Here’s How A developer demonstrated connecting a local qwen3 model running in Ollama to Zenrows MCP, first through the open-source ollmcp bridge with no code and then via a short Python agent acting as its own MCP client. In test runs, the local agent read a Cloudflare-fronted Indeed search and cleared an anti-bot challenge page through Zenrows MCP, while a plain requests.get() to the same page returned a 403. The setup relies on Ollama's hosted web_search and web_fetch APIs, since Ollama has no MCP client of its own, with Zenrows handling fetching, JavaScript rendering and proxy rotation. This article was originally published on the Zenrows blog https://www.zenrows.com/blog/ollama-agents-live-web-access-zenrows-mcp . We gave a local LLM live web access, and this post shows how. You'll connect a qwen3 model in Ollama to Zenrows MCP https://www.zenrows.com/mcp , first with no code through the ollmcp bridge and then with a short Python agent that is its own MCP client. You'll need Ollama, a tool-capable model, Node.js, and Python 3.11 or later. A Zenrows API key is optional. Ollama publishes web search and web fetch as hosted APIs that you call with an Ollama API key, and it has no MCP client of its own. In our test runs, the local qwen3 agent read a Cloudflare-fronted Indeed search and cleared an anti-bot challenge page through Zenrows MCP. A plain requests.get to the same challenge page returned a 403. The code is on GitHub https://github.com/ZenRows/ollama-agent-web-access-zenrows-mcp . You ask a question, Ollama picks a tool, Zenrows makes the requests, and the target site returns the pages. The model never contacts the site. The agent calls the extract tool, Zenrows fetches the page through Fetch with protected access, JavaScript rendering, and proxy rotation in one API call, and the model gets the page as structured JSON fields. Zenrows MCP has two transports. The remote server runs over Streamable HTTP and authenticates with your API key as a Bearer token. The local server runs as a STDIO subprocess through the @zenrows/mcp npm package with your key in an environment variable. This tutorial uses the local one, since it runs next to Ollama on your machine. These tools matter for this setup. scrape returns a page as Markdown by default, with HTML, PDF, and screenshot output also available. extract returns structured JSON, which matters for local models with small context windows. browser tools open a cloud browser session for tasks that span more than one request. crawl tools collect the URLs behind one start page. batch tools submit a list of URLs as one background job. brew install ollama brew services start ollama ollama pull qwen3 ollama show qwen3:latest Ollama registers the model as qwen3:latest . The ollama show output lists tools under Capabilities, which confirms the model can call the Zenrows tools. Model architecture qwen3 parameters 8.2B context length 40960 embedding length 4096 quantization Q4 K M Capabilities completion tools thinking python3 -m venv .venv source .venv/bin/activate pip install python-dotenv requests Without a key, the local server creates a Free account with 5,000 credits a month on its first tool call. To use your own account, create one https://app.zenrows.com/register and save the key in .env . ZENROWS API KEY=your key here ollmcp https://pypi.org/project/ollmcp/ is an open-source Python bridge that connects MCP servers to a local Ollama model over STDIO, SSE, and Streamable HTTP. It also installs the ollama and mcp packages the agent imports later. This tutorial was tested with ollmcp 0.35.0 on Python 3.14. pip install ollmcp Create zenrows-mcp.json . { "mcpServers": { "zenrows": { "command": "sh", "args": "-c", "set -a && . ./.env && set +a && exec npx -y @zenrows/mcp" } } } The sh -c command sources .env into the environment before starting npx , so the JSON holds no secret and can be committed. Add .env to .gitignore , and run ollmcp from this directory because the .env path is relative. ollmcp --servers-json zenrows-mcp.json --model qwen3:latest On October 21, 2026, the bridge connected and listed the Zenrows tools output trimmed . 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 │ ╰───────────────────────────────────────────────╯ 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. The script starts the Zenrows MCP server with npx , so it doesn't need ollmcp or zenrows-mcp.json to run. Create agent.py . The pinned versions are in the repo's requirements.txt https://github.com/ZenRows/ollama-agent-web-access-zenrows-mcp/blob/main/requirements.txt . 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 Three details matter on a local model. OPTIONS sets the context window. Ollama uses num ctx , Ollama truncated that prompt and the model answered from part of the listings. ALLOWED ARGS filters the model's arguments and blocks anything the model invents, so only the URL reaches the server. Zenrows then turns on JavaScript rendering and premium proxies only when the page needs them. data object next to a few status flags. The script hands the model only that The test target is the Scrapingcourse antibot challenge page https://www.scrapingcourse.com/antibot-challenge , which reports its own verdict. 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. For comparison, without zenrows.py sends a plain request to the same page. python import requests r = requests.get "https://www.scrapingcourse.com/antibot-challenge" print r.status code, len r.text python3 without zenrows.py 403 5517 The request was blocked with a 403. Confirm what sits in front of Indeed. curl -sI https://www.indeed.com | grep -iE "^ server|date :" date: Wed, 21 Oct 2026 08:15:02 GMT server: cloudflare Create cloudflare site.py with the same code as agent.py and change the QUESTION line. QUESTION = "List the company names and job titles from https://www.indeed.com/jobs?q=data+analyst&l=Austin%2C+TX." 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 The extraction returned ok: true with 16 job listings and no challenge response, and the model listed every company and job title on the page. Here is how three MCP servers compared, 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 in the 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 | One MCP configuration gave a local Ollama agent access to live pages, including Cloudflare-protected ones, while Zenrows handled access, rendering, and proxies for every request. For a task spanning several pages, ask in plain English and the agent can use the crawl and batch tools. agents.zenrows.com .