local-web-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@local-web-mcpFetch this article using my local connection: https://www.cnbc.com/2024/09/example"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
local-web-mcp
Version 0.3.0 · AGPL-3.0
This tool is developed with support from AI, but it has been evaluated by a human before upload.
An MCP server that gives Claude a fallback web fetcher running on your own machine. When the built-in fetcher is blocked, this one tries from your IP, your connection, and optionally your logged-in session.
One tool: fetch_url_locally.
The problem it solves
Hosted fetchers run from datacentre IP ranges, which a lot of sites refuse outright. A local stdio MCP server does not: the client spawns the process on your machine, so requests originate from your ordinary connection.
Verified working on sites the hosted fetcher could not read, including CNBC, Politiken and Bloomberg.
Related MCP server: auth-fetch-mcp
What it does not change
Worth stating plainly, because the framing invites overclaiming.
Model inference still runs on Anthropic's servers.
Fetched page text is still sent to the model as tool output. Anything read through an authenticated session goes with it.
It changes who talks to the website, not where the model runs.
The hard boundary
Can the page be read without executing JavaScript?
If yes, this handles it. If no, nothing at the header, TLS or cookie layer will get there, and no amount of User-Agent tuning helps. Sites using DataDome, Cloudflare Turnstile, PerimeterX and similar serve a challenge that must be executed to pass. For those, use a browser-based tool or open the page yourself.
This is a boundary to know, not a bug to fix.
Verdicts
Every response is labelled. The dangerous case is not an obvious 403, it is a 200 that is not the page: a bot interstitial, a consent wall, an empty client-rendered shell. Those get summarised as though they were the article.
Verdict | Meaning | What to do |
| Real content | Use it |
| Bot protection detected | Browser required, stop |
| Refused outright (403), usually bot detection | Browser required, stop |
| Client-rendered shell, no content without JS | Browser required, stop |
| 401, login wall, or subscription wall | Cookies may help |
| Any other non-2xx status (404, 429, 5xx) | Retry later for 429/5xx only |
| Under 200 characters extracted | Failed read, not an empty page |
Anything other than ok attaches an explicit warning telling the model not to
treat the body as content.
Body evidence outranks the status code. A publisher that serves a paywalled
article as HTTP 403 is reported as login_required, not blocked, because
the useful advice there is to export cookies rather than to reach for a
browser.
Detection matches on vendor infrastructure rather than wording: challenge
pages are localised, so captcha-delivery.com is a reliable signal where
"verifying your device" is not. Vendor cookies in response headers count as
evidence when paired with a refusal status.
The tool
fetch_url_locally(url, max_chars=20000, use_session=true)
Parameter | Type | Default | Purpose |
| string | required | The page to fetch. |
| integer |
| Cap on returned text, clamped to 200000. Truncation is reported in the header |
| boolean |
| Send cookies. Pass |
The reply is a short header followed by the extracted text:
URL: https://example.com/article
Status: 200
Verdict: ok
Title: The headline
Session: authenticated (the user's cookies were sent)
The article text...Session: appears only when cookies were actually loaded and scoped to that
host. A verdict other than ok adds a WARNING: line telling the model not to
treat the body as content.
Failures come back as an error naming the cause, for example
[blocked_host] Host '192.168.1.5' resolves to a private or internal address.
Codes: invalid_url, blocked_scheme, blocked_host, dns_failure, timeout,
too_many_redirects, upstream_error, invalid_argument.
Requirements
Python 3.11+ (uses
asyncio.timeout). With uv you do not need to install it yourself; uv fetches a suitable interpreterClaude Desktop or Claude Code. Stdio servers do not work in the browser or the mobile app, which require a publicly reachable HTTPS endpoint.
Install
With uv, which fetches a suitable Python and the dependencies itself:
uv --directory /absolute/path/to/local-web-mcp run local-web-mcpRun that once in a terminal before registering the server. The first run resolves and builds the environment, which takes long enough that a client launching it cold may give up and report the server as failed even though the configuration is correct.
It prints Starting local-web and then waits for a client on stdin, which is
what a working server looks like. Once you see that line, the environment is
built: press Ctrl+C and carry on.
Or with a plain virtual environment:
python3 -m venv .venv
.venv/bin/pip install -r requirements.txtRegister with Claude Desktop
Add to claude_desktop_config.json (macOS:
~/Library/Application Support/Claude/, Linux: ~/.config/Claude/):
{
"mcpServers": {
"local-web": {
"command": "uv",
"args": [
"--directory",
"/absolute/path/to/local-web-mcp",
"run",
"local-web-mcp"
]
}
}
}If uv is not on the PATH that the client sees, use its absolute path as
command. The virtual-environment equivalent is:
{
"mcpServers": {
"local-web": {
"command": "/absolute/path/to/.venv/bin/python",
"args": ["/absolute/path/to/local_web_mcp.py"]
}
}
}Claude Code:
claude mcp add local-web -- uv --directory /absolute/path/to/local-web-mcp run local-web-mcpConfiguration
All optional. Every setting has a working default.
Variable | Default | Purpose |
| unset | Path to a cookie jar |
| unset | Domains the jar may be used for. Set this whenever you set a cookie file |
|
| Allow private, loopback and link-local targets |
| empty | Hostnames exempt from the IP check |
| Chrome UA | Sent on every request |
|
| Language preference. Sites use it to pick a language and sometimes a regional edition |
|
| Seconds |
|
| Seconds |
|
| Hard ceiling per call |
|
| Response body cap |
|
| Redirect hops, shared with meta refreshes |
The default Accept-Language asks for Danish first. That is a deliberate
default rather than a neutral one, so set LOCALWEB_ACCEPT_LANGUAGE if you
want English or anything else:
"env": { "LOCALWEB_ACCEPT_LANGUAGE": "en-GB,en;q=0.9" }Session cookies
Point LOCALWEB_COOKIE_FILE at an export from a browser extension. The loader
accepts Netscape cookies.txt with any header or none, and JSON exports from the
common extensions. See cookies.txt.example.
Always set LOCALWEB_COOKIE_DOMAINS. Without it the whole jar is live and
any fetch can carry an unrelated session. With it, every cookie outside those
domains is dropped at load time, before the jar reaches a client. Matching covers
subdomains (example.com covers www.example.com) but not lookalikes
(evil-example.com).
Three things to keep in mind:
Content read through your session is sent to the model as tool output.
A cookie file is a credential file.
chmod 600, and it is gitignored here.Cookies expire. A previously working source returning
login_requiredis usually a stale export rather than lost access.
Pass use_session=false to check whether a page is genuinely public.
Reaching a local network
Private addresses are blocked by default, and deliberately so. The model chooses
URLs partly from text it has just read, so a fetched page can attempt to steer it
at 192.168.1.1. The blocklist means a prompt injection in page content cannot
turn this into a network scanner.
Prefer a narrow allowlist over the blanket switch:
"env": { "LOCALWEB_ALLOWLIST": "nas.local,nas" }Security
Scheme restricted to http and https; URLs carrying credentials rejected
Hostname resolved and every returned address validated before the request
IPv6-mapped IPv4 (
::ffff:10.0.0.1) unwrapped before checkingCloud metadata endpoints blocked unconditionally: the hostname is resolved before both the allowlist and
ALLOW_PRIVATE, so neither can open themRedirects and meta refreshes followed manually, revalidated at each hop
Cookie counts logged, never names, values or domains
Known limitations
No JavaScript. The hard boundary above.
DNS rebinding. The hostname is resolved for validation, then resolved again on connect. Closing that gap needs the connection pinned to the validated IP. Worth doing before enabling
ALLOW_PRIVATEon an untrusted network.Challenge detection is signature-based and will drift as vendors change their markup. Run
inspect_response.pyif a verdict looks wrong.HTML-to-text extraction is dependency-free and crude.
trafilaturawould be markedly better for serious article extraction.
Files
File | Purpose |
| The server |
| Tolerant cookie loader. Required, imported by the server |
| Diagnostic: dumps what the fetcher actually receives |
| Verdicts, target validation, cookie loading and scoping |
| Blocked targets and one live fetch, over stdio |
| Annotated template for a cookie jar |
| Package metadata and the |
| Pinned dependency versions for reproducible installs |
| Runtime dependencies for the plain-venv route |
| Excludes the venv, caches, and any cookie file |
| Full AGPL-3.0 text |
Diagnosing a wrong verdict
uv run python inspect_response.py https://example.com/articlePrints the status, the interesting headers, the start of the body, and then runs the server's own detection against it. Always check this before changing detection logic. A browser and this fetcher are frequently served entirely different responses, so what you see on screen is not evidence about what the tool received.
Tests
uv run python unit_test.py
uv run python smoke_test.pyunit_test.py is offline and exits non-zero on failure, so it works as a
pre-commit gate. smoke_test.py drives the server over stdio and makes one live
request, so it needs a network connection. Run both from the repository root.
On the plain-venv route, substitute .venv/bin/python for uv run python.
Licence
Copyright (C) 2026 David Lindholm.
GNU Affero General Public License v3.0 or later. See LICENSE.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Available Tools
1 toolfetch_url_locallyFetch a URL from the user's machineARead-only
Fetch a web page over the user's own internet connection and return its readable text.
USE THIS WHEN the built-in web fetch has already been tried and: it returned an error, 403, or 429; it returned a Cloudflare or captcha interstitial; it returned a cookie/consent wall or a near-empty page; the source is geo-restricted; the source sits behind the user's institutional access or paid subscription; or the URL is on the user's local network.
DO NOT use it as the first attempt for ordinary public pages -- the built-in fetcher is faster and does not use the user's bandwidth.
The response carries a verdict line. If the verdict is not 'ok', the body is an interstitial rather than the article and must not be summarised as though it were the content.
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | ||
| max_chars | No | ||
| use_session | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Clearly discloses that the response includes a verdict line that must be checked, and warns that a non-'ok' verdict means the body is an interstitial and must not be summarized as content. This is valuable behavioral context beyond the readOnlyHint annotation, and it directly prevents a likely misuse.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is appropriately sized for the complexity, with the core purpose front-loaded and usage conditions clearly structured. It earns its length by giving concrete error cases and a safety caveat; it is not overly verbose, though it could be slightly tightened.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity and the fact that there is an output schema, the description is complete for an agent to call it correctly: it knows when to use it, what to expect in the response, and how to avoid misinterpreting interstitials. No critical operational detail is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate; it does not mention individual parameters like max_chars or use_session, but the core parameter 'url' is fully implied ('Fetch a web page...'). The behavioral verdict warning adds context that indirectly helps the agent interpret the output, but explicit parameter-level guidance is still missing.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific action ('Fetch a web page over the user's own internet connection') and an explicit resource ('user's own... URL'), and it differentiates itself by emphasizing the local-network/user-machine execution path versus the built-in fetcher. It is unambiguous about what the tool does.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides extensive when-to-use criteria: use only after built-in fetch fails with errors, interstitials, geo-restrictions, etc., and explicitly says DO NOT use as first attempt for ordinary pages. This is exactly the kind of conditional routing guidance an agent needs.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
1 tool update
v0.3.0- First observed
fetch_url_locally
TDQS
Scored across 1 tool
There is only one tool, so there is no possibility of confusing it with another tool. Its purpose is clearly described and scoped.
The single tool name follows a clear, descriptive snake_case convention. With only one tool, there are no inconsistent patterns to evaluate.
A single tool is borderline for a server, but it may be justified for a narrowly-focused fallback fetch utility. It feels thin compared to broader MCP servers, but not absurdly so.
The tool completely covers its stated purpose of fetching pages locally and returning readable text. Minor gaps exist around request customization or response metadata, but no core workflow dead ends are apparent.
Maintenance
Related MCP Connectors
Reliable web access for AI agents: smart HTTP, rotating proxies, and full-browser rendering.
Web search, browser automation, scraping, crawling and CAPTCHA solving for AI agents.
Undetectable cloud browser sessions for AI agents and scrapers. Navigate, extract, click, captcha.
Reliable web fetching for AI agents with retry, circuit breaker, caching, and anti-bot bypass
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceFetches content from authenticated web pages by driving your signed-in Chrome/Edge browser via DevTools Protocol, automatically handling login redirects and reusing sessions across domains.8MIT
- AlicenseAqualityAmaintenanceEnables AI assistants to access content from authenticated web pages by opening a real browser for manual login and session capture. It saves browser profiles locally so users only need to log in once per service for future automated access.447 npm36MIT
- AlicenseAqualityDmaintenanceEnables AI agents to control the user's Chrome or Firefox browser, leveraging existing sessions for tasks requiring authentication and user handoff.1844 npm17MIT
- FlicenseNot gradedqualityDmaintenanceUnlimited, session-authenticated web search and fetch for AI tools using your own browser. Supports authenticated/paywalled pages without API keys.-