{"slug": "we-gave-a-local-llm-live-web-access-heres-how", "title": "We Gave a Local LLM Live Web Access. Here’s How", "summary": "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.", "body_md": "This article was originally published on the [Zenrows blog](https://www.zenrows.com/blog/ollama-agents-live-web-access-zenrows-mcp).\n\nWe 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.\n\nOllama 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).\n\nYou 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.\n\nZenrows 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.\n\nThese tools matter for this setup.\n\n`scrape` returns a page as Markdown by default, with HTML, PDF, and screenshot output also available.\n`extract` returns structured JSON, which matters for local models with small context windows.\n`browser_*` tools open a cloud browser session for tasks that span more than one request.\n`crawl_*` tools collect the URLs behind one start page.\n`batch_*` tools submit a list of URLs as one background job.\n\n```\nbrew install ollama\nbrew services start ollama\nollama pull qwen3\nollama show qwen3:latest\n```\n\nOllama registers the model as `qwen3:latest`. The `ollama show` output lists `tools` under Capabilities, which confirms the model can call the Zenrows tools.\n\n```\n  Model\n    architecture        qwen3\n    parameters          8.2B\n    context length      40960\n    embedding length    4096\n    quantization        Q4_K_M\n\n  Capabilities\n    completion\n    tools\n    thinking\npython3 -m venv .venv\nsource .venv/bin/activate\npip install python-dotenv requests\n```\n\nWithout 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`.\n\n```\nZENROWS_API_KEY=your_key_here\n```\n\n[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.\n\n```\npip install ollmcp\n```\n\nCreate `zenrows-mcp.json`.\n\n```\n{\n  \"mcpServers\": {\n    \"zenrows\": {\n      \"command\": \"sh\",\n      \"args\": [\"-c\", \"set -a && . ./.env && set +a && exec npx -y @zenrows/mcp\"]\n    }\n  }\n}\n```\n\nThe `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.\n\n```\nollmcp --servers-json zenrows-mcp.json --model qwen3:latest\n```\n\nOn October 21, 2026, the bridge connected and listed the Zenrows tools (output trimmed).\n\n```\nFound server in config: zenrows\nConnecting to server: zenrows\nSuccessfully connected to zenrows with 62 tool(s) and 3 prompt(s) and 1 resource(s)/template(s)\n╭──────────────────────────── 🔧 Available Tools ────────────────────────────╮\n│ ✓ zenrows.scrape                         ✓ zenrows.extract                 │\n│ ✓ zenrows.batch_create                   ✓ zenrows.batch_status            │\n...\n│ ✓ zenrows.browser_switch_tab             ✓ zenrows.browser_batch           │\n╰─────────────────────────── 62/62 tools enabled ────────────────────────────╯\n╭───────────────────────────────────────────────╮\n│ Current model: qwen3:latest (ollama)          │\n│ Capabilities: 🔧 Tools │ 💭 Thinking (medium) │\n╰───────────────────────────────────────────────╯\n```\n\nEvery 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.\n\nThe 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).\n\n``` python\nfrom dotenv import load_dotenv\nload_dotenv()\n\nimport asyncio\nimport json\nimport os\nimport ollama\nfrom mcp import ClientSession, StdioServerParameters\nfrom mcp.client.stdio import stdio_client\n\nSERVER = StdioServerParameters(\n    command=\"npx\",\n    args=[\"-y\", \"@zenrows/mcp\"],\n    env=dict(os.environ),\n)\nMODEL = \"qwen3:latest\"\n# give the model enough context to read the whole tool result\nOPTIONS = {\"num_ctx\": 16384}\nQUESTION = \"Extract the page heading from https://www.scrapingcourse.com/antibot-challenge.\"\nALLOWED_ARGS = {\"url\"}\n\nasync def main():\n    async with stdio_client(SERVER) as (read, write):\n        async with ClientSession(read, write) as session:\n            await session.initialize()\n\n            listed = await session.list_tools()\n            tools = [\n                {\"type\": \"function\", \"function\": {\n                    \"name\": t.name,\n                    \"description\": t.description,\n                    \"parameters\": t.input_schema,\n                }}\n                for t in listed.tools if t.name == \"extract\"\n            ]\n\n            messages = [{\"role\": \"user\", \"content\": QUESTION}]\n            reply = ollama.chat(model=MODEL, messages=messages, tools=tools, options=OPTIONS)\n            messages.append(reply[\"message\"])\n\n            for call in reply[\"message\"].get(\"tool_calls\") or []:\n                args = {k: v for k, v in call.function.arguments.items() if k in ALLOWED_ARGS}\n                print(\"calling\", call.function.name, args)\n                result = await session.call_tool(call.function.name, args)\n                text = result.content[0].text\n                print(\"result:\", text[:300])\n                # pass the model only the parsed fields from the response\n                data = json.loads(text).get(\"data\")\n                messages.append({\"role\": \"tool\", \"content\": json.dumps(data)})\n\n            print(ollama.chat(model=MODEL, messages=messages, tools=tools, options=OPTIONS)[\"message\"][\"content\"])\n\nasyncio.run(main())\n```\n\nThree details matter on a local model.\n\n`OPTIONS` sets the context window. Ollama uses `num_ctx`, Ollama truncated that prompt and the model answered from part of the listings.\n`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.\n`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.\n\n```\npython3 agent.py\nUsing existing API key from env (secrets dir: /Users/user/.zenrows)\nZenrows MCP server running on stdio (secrets dir: /Users/user/.zenrows)\ncalling extract {'url': 'https://www.scrapingcourse.com/antibot-challenge'}\nresult: {\"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\"}}}\nThe page heading (h1) from the URL is:\n\n**Antibot Challenge**\n\nThis is the title extracted from the webpage, which also happens to be the name of the antibot challenge page.\n```\n\nFor comparison, `without_zenrows.py` sends a plain request to the same page.\n\n``` python\nimport requests\n\nr = requests.get(\"https://www.scrapingcourse.com/antibot-challenge\")\nprint(r.status_code, len(r.text))\npython3 without_zenrows.py\n403 5517\n```\n\nThe request was blocked with a 403.\n\nConfirm what sits in front of Indeed.\n\n```\ncurl -sI https://www.indeed.com | grep -iE \"^(server|date):\"\ndate: Wed, 21 Oct 2026 08:15:02 GMT\nserver: cloudflare\n```\n\nCreate `cloudflare_site.py` with the same code as `agent.py` and change the `QUESTION` line.\n\n```\nQUESTION = \"List the company names and job titles from https://www.indeed.com/jobs?q=data+analyst&l=Austin%2C+TX.\"\npython3 cloudflare_site.py\nUsing existing API key from env (secrets dir: /Users/user/.zenrows)\nZenrows MCP server running on stdio (secrets dir: /Users/user/.zenrows)\ncalling extract {'url': 'https://www.indeed.com/jobs?q=data+analyst&l=Austin%2C+TX'}\nresult: {\"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\nHere are the company names and job titles from the Indeed search results in Austin, TX:\n\n1. **Paywall Bypass** - Data Analyst  \n2. **Visa U.S.A. Inc.** - Data & Insights Analyst  \n3. **Texas Department of Transportation** - Data Analyst IV  \n4. **Austin Community College** - Analyst, Institutional Research (Hourly)  \n5. **Austin Community College** - Analyst, Human Resources Data  \n6. **University of Texas at Austin** - Data Analyst, College of Liberal Arts  \n7. **Steck Systems** - Data Analyst  \n8. **Apple** - Business Insights Analyst, Apple Store Online Operations  \n9. **Google** - Business Data Analyst II, Google Global Infrastructure, Strategy and Operations  \n10. **Google** - Business Data Analyst II, Google Global Infrastructure, Strategy and Operations  \n11. **University of Texas at Austin** - Senior Data Analyst  \n12. **The University of St. Augustine for Health...** - Institutional Research Analyst  \n13. **Ericsson** - Data Analyst Strategy and Operations  \n14. **Texas Department of Transportation** - Business Analyst II  \n15. **SHI International Corporation** - Associate Analytics Engineer - Data & Visualization  \n16. **Health & Human Services Comm** - Data Analyst V  \n\nLet me know if you'd like additional details like locations or salary information!\n```\n\nThe extraction returned `ok: true` with 16 job listings and no challenge response, and the model listed every company and job title on the page.\n\nHere is how three MCP servers compared, as documented on October 21, 2026.\n\n|  | Zenrows MCP | Firecrawl MCP | Playwright MCP | \n|---|---|---|---|\n| How it runs | Cloud or local | Cloud or local | On your browser | \n| 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 | \n| 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 | \n| 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` | \n\nOne 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.\n\n`agents.zenrows.com`.", "url": "https://wpnews.pro/news/we-gave-a-local-llm-live-web-access-heres-how", "canonical_source": "https://dev.to/zenrows/we-gave-a-local-llm-live-web-access-heres-how-1a88", "published_at": "2026-11-05 06:47:47+00:00", "updated_at": "2026-11-05 06:49:27.166283+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "ai-tools", "large-language-models", "ai-crawlers"], "entities": ["Ollama", "Zenrows", "qwen3", "ollmcp", "Cloudflare", "Indeed", "GitHub"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/we-gave-a-local-llm-live-web-access-heres-how", "markdown": "https://wpnews.pro/news/we-gave-a-local-llm-live-web-access-heres-how.md", "text": "https://wpnews.pro/news/we-gave-a-local-llm-live-web-access-heres-how.txt", "jsonld": "https://wpnews.pro/news/we-gave-a-local-llm-live-web-access-heres-how.jsonld"}}