keyless-web-search-mcp
A keyless MCP stdio server that exposes web search (and optionally web fetch) tools over MCP without requiring API keys.
web_search: Searches the web using a self-operated pool of engines (Bing, 360, Baidu, Google, Naver, Yandex, DuckDuckGo) with no API key.
Query-language routing: When no explicit engine list is given, Chinese/Korean/Japanese/Russian queries get region-appropriate default priorities.
Bounded multi-engine fallback: Keeps at most two usable engines per search, back-fills failed/challenged/irrelevant engines within an attempt budget, and reports engine notes.
Result quality: Relevance-ranking with source-quality adjustment, canonical URL deduplication, and per-engine link cleanup (tracker unwrapping, direct URLs).
Configurable output:
count(1–20),maxSources(1–2), optional explicitenginespriority list.web_fetch: Opt-in (
ENABLE_WEB_FETCH=1) anonymous HTTP(S) reader that returns readable text, with SSRF protections (blocks private/reserved/local targets), redirect validation, bounded output (maxChars), and status header.Zero build step: Standalone stdio MCP server; install with npm and run with
node index.js.
Provides web search through Baidu as one of the keyless search engines, including parsing of organic results and relevance ranking merged with other engines.
Provides web search through DuckDuckGo as a keyless search engine, used as a last-resort engine where network access permits.
Provides web search through Google as a keyless search engine, parsing and unwrapping Google result links when the engine is reachable and returns organic results.
Provides web search through Naver as a keyless search engine, including parsing of direct result URLs and relevance-ranked merging with other engines.
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., "@keyless-web-search-mcpSearch the web for MCP server tutorials"
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.
keyless-web-search-mcp
Standalone MCP stdio server: the keyless web_search tool over a self-operated engine pool (Bing, 360, Baidu, Google, Naver, Yandex, DuckDuckGo) and — opt-in via ENABLE_WEB_FETCH=1 — web_fetch (anonymous HTTP(S) page reader). Each search keeps at most two usable engines, routes default priorities by query language, back-fills failed or irrelevant engines within a bounded attempt budget, relevance-ranks results, merges them, and dedupes by canonical URL. Self-contained directory, zero build step, not part of the harness package system — move it anywhere.
Why this exists
The harness's built-in web_search (DeepSeek provider) needs DEEPSEEK_API_KEY and sends every query to the DeepSeek cloud. This server is the local-model-friendly alternative: no keys, no vendor search API, works on mainland-China and HK networks (Bing/360/Baidu from mainland lines, Bing/360/Naver/Yandex from the tested HK line; DuckDuckGo is not reachable from any of them).
web_fetch exists for the same reason on the read side: it was built when the harness web profile mounted a search provider but no fetch provider, so the built-in web_fetch tool failed every call with WEB_PROVIDER_UNAVAILABLE. That gap is now filled by the harness, so web_fetch registers only when ENABLE_WEB_FETCH=1 is set in the server's env — the default mount is search-only, and a session that already has a harness fetch tool does not see two readers.
Versioning. The version in package.json / SERVER_VERSION is bumped only by the project owner when cutting a release; working-tree changes never bump it.
Related MCP server: WET - Web Extended Toolkit
Run
npm install --cache ./.npm-cache # deps: @modelcontextprotocol/sdk, zod
node index.js # speaks MCP over stdio (web_search only)
ENABLE_WEB_FETCH=1 node index.js # also register the web_fetch reader
npm test # run deterministic ranking/fallback testsQuick probe without a client:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0.0.1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
| node index.jsInstall in Claude Code
Install it for the current user so every Claude Code project can use it. Resolve both paths when registering the server; this avoids depending on the shell's working directory or PATH later:
claude mcp add --transport stdio --scope user web-search-self -- "$(command -v node)" "/absolute/path/to/keyless-web-search-mcp/index.js"
claude mcp get web-search-selfUse --scope local instead to keep it private to the current project, or --scope project to write a shareable .mcp.json. Claude Code stores user/local registrations in ~/.claude.json; MCP server definitions do not belong in settings.json.
After registration, claude mcp list should report web-search-self as connected. The tool appears as mcp__web-search-self__web_search (and mcp__web-search-self__web_fetch too, when the registration's env sets ENABLE_WEB_FETCH=1).
Tools
web_search
web_search({ query, count?, maxSources?, engines? })
Param | Default | Meaning |
| — | Search query, any language |
|
| Max merged results (1–20) |
|
| Max usable engines per search (hard cap 2; fallback attempts are bounded) |
| query-language dependent | Optional explicit pool in priority order; when omitted, Chinese/Korean/Japanese/Russian queries use region-appropriate priorities |
Two-source policy. The pool is routed by query language when engines is omitted: Chinese starts with 360/Baidu/Bing; English starts with Bing/Google/Naver/360 so the four-attempt budget retains working fallbacks on the tested mainland/HK networks. An explicit engines list remains authoritative. Candidates run in bounded parallel rounds; an engine consumes a source slot only after it returns parseable results that pass conservative lexical relevance filtering. Failed, challenged, empty, or irrelevant engines are back-filled up to the attempt and collector budgets, and diagnostics are shown in an "Engine note" line. Results are ranked by query overlap with a small source-quality adjustment: obvious repost/content-farm signals are penalized, while official/docs/education/GitHub signals are favored without hard-blocking ordinary sites. Merging remains round-robin interleaved (engine A's 1st, engine B's 1st, A's 2nd, …), and canonical URL dedupe removes fragments, common tracking parameters, and safe www. differences. Resilience: an engine that throws a bot-challenge diagnostic enters a 30-minute challenge memory and is skipped — without consuming an attempt — on subsequent searches; if the relevance filter discards every engine's results, the search returns the parsing engines' raw top results with an explicit note instead of failing empty; long queries relax the relevance threshold proportionally (the 0.3 bar holds for up to 4 terms), and boolean words like or no longer count as query terms.
Result: numbered list of title [engine] / real destination URL / snippet. Link cleanup per engine: Bing click-tracker links decode locally (base64 u parameter); 360 reads the data-mdurl attribute; Baidu reads the block's mu attribute (direct URL, with one best-effort redirect GET only for legacy link?url= wrappers); Naver and Yandex titles carry the direct URL in the anchor; Google /url?q= wrappers unwrap; DuckDuckGo /l/?uddg= trackers unwrap. Per-engine probe budgets: 10 s (Bing, Baidu), 8 s (360, Naver, Yandex), 5 s (Google, DuckDuckGo) — all run in parallel, so the round is only as slow as its slowest member.
web_fetch (opt-in: ENABLE_WEB_FETCH=1)
web_fetch({ url, maxChars? })
Param | Default | Meaning |
| — | Absolute |
|
| Max content characters returned (1000–100000) |
Anonymous public-web read, no credentials: browser User-Agent, at most five redirects, 20 s wall clock covering headers and body, response body cut at exactly 5 MB. Every initial and redirect target is checked after DNS resolution; loopback, private, link-local, carrier-grade NAT, reserved, multicast, and local hostnames are rejected to prevent SSRF into the machine or cloud metadata services. HTML is stripped to visible text (script/style/noscript/svg/head/iframe/canvas/form dropped — an unclosed script/style/noscript/iframe cuts the document there, matching HTML5 raw-text parsing — block boundaries become newlines, the <title> is carried in the header), and up to 30 unique in-page links are appended (relative refs resolved against the final URL, fragments and non-http schemes skipped, low-signal search/social hosts dropped) so the model can follow up without re-searching. text/* and XML media pass through entity-decoded using their declared charset; JSON, JavaScript, and form bodies carry no entity layer and pass through byte-exact; binary media types are rejected. A status-header note flags large pages that yield little visible text (likely JavaScript-rendered). Output is a status header — status, final url (after redirects), content-type, truncation — followed by the content. Non-2xx answers return the body head plus a note (403/429 read as bot-check or paywall), never a fabricated page.
Engines and their status
Verified 2026-07 from three lines (an earlier mainland line, a Shanghai Telecom line, and a Hong Kong Zenlayer datacenter line):
Engine | Endpoint | Status from these networks |
|
| ✅ ~10 organic blocks on both lines. Depending on the line, |
|
| ✅ clean 200; organic blocks carry the real URL in |
|
| Reachable (200) but this client/IP is not trusted for plain-HTML SERPs: the body is a no-JS |
|
| ✅ on the Shanghai and HK lines: the risk control gates script-shaped requests (UA-only 302s to the |
|
| ✅ on the HK line (200, ~10 |
|
| ✅ on the HK line: same navigation-signature lesson as Baidu — script-shaped requests get the SmartCaptcha "not a robot" checkbox, but the full document-navigation header set plus the homepage's cookie session (yandexuid et al.) is served the plain HTML SERP (~50 |
|
| ❌ TCP-unreachable on all three tested lines (including HK); last-resort engine for networks where it works |
Deliberately not in the pool: Sogou — 302s to sogou.com/antispider/, the same IP-wall class Baidu covers. Also scanned and rejected from the HK line: Mojeek (serves a captcha page), Ecosia (403 "Ecosia Firewall"), MetaGer (redirects to a non-result page), and Yahoo/Brave/Qwant/Startpage/goo.ne.jp (TCP-unreachable from both mainland and HK lines).
Google wall: what was tried
The 200 response from this IP is not a block but a JS challenge interstitial (~90 KB of obfuscated/encrypted JavaScript; the no-JS path is a meta-refresh into a dead end). The wall turns out to be two layers:
JS challenge (computational) — decoded, it computes a proof value and sets an
SG_SScookie (5-minute expiry), then reloads. This layer is solvable outside a browser: running the page's scripts in plain Node.js with a light DOM shim (cookie jar,navigator,Image,document) completed the computation, and the resultingSG_SScookie was accepted once — Google answered 200 and issued theNID/AECtrust cookies it otherwise never sends to this IP.Session-pattern layer (behavioral) — subsequent plain-HTTP follow-ups, including an exact replica of the script's own reload flow (
emsg=SG_REL+ matchingsei, jar-merged cookies, browser headers), escalate to thegoogle.com/sorry429 anomaly wall. This layer judges the whole session (TLS/HTTP2 fingerprint, request pacing, method mix), which Node's OpenSSL/undici stack does not match.
Client-side levers that were tested and did not change layer 1's answer: the gbv=1 parameter, cookie warm-up, full browser fingerprint headers, the retry/enablejs flow, the /m entry, text-browser/feature-phone/IE6/old-Android UAs, a spoofed Googlebot UA (Google validates it), CONSENT cookies, POST submission (405), alternate TLDs, and the "click here" emsg=SG_REL link without the cookie. Google-proxy alternatives were also unreachable or walled from this network: Startpage and Qwant time out, Mojeek serves a captcha page, Ecosia 403s.
Conclusion: layer 1 is Node-solvable (demonstrated); layer 2 is not, from a non-browser network stack. The only reliable paths to Google from this machine are a clean-IP proxy or a real browser context (headless Chromium); both sit in the same IP/traffic-pattern layer that a scraper cannot honestly out-argue, and repeated probing risks extending the IP's anomaly window — so this server does not attempt them. Google stays a back-fill engine that activates on networks where it will serve a plain-HTML SERP.
Mount into DeepSeek Harness
Add to your cordis.yml (the MCP bridge hot-reloads this entry):
- id: mcp-search
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: search
transport: stdio
command: node
args: ['/absolute/path/to/keyless-web-search-mcp/index.js']
# env: { ENABLE_WEB_FETCH: '1' } # opt-in: also register web_fetchThe model then sees the tool as mcp__search__web_search; with the commented env line enabled it also sees mcp__search__web_fetch.
Limitations
Search quality is heuristic: lexical relevance handles obvious off-topic results and Chinese n-grams; internal search-engine pages are excluded; content-farm/repost signals only adjust ordering, not a universal trust judgment. A relevant result is not proof that its claims are correct — fetch and cross-check important facts.
Scraping, not an API: layout changes can break a parser; the failure is loud ("no parseable organic results"), never fabricated. The Google and DuckDuckGo parsers are written from documented SERP structure and are not live-verifiable from this network — tune them the first time their engines actually answer.
Rate control: both public engines tolerate casual use; hammering draws bot challenges.
Query leaves the machine for the consulted engines (that is the price of keyless search); with the two-source cap, at most two of them see a query per search.
Fetch is a keyless public-page reader, not a browser or intranet client: JS-rendered content is invisible to it (same class of limit as the search parsers); pages that 403/429 anonymous clients are reported, not bypassed. Local/private/reserved network targets are deliberately blocked, including redirect destinations.
Available Tools
2 toolsweb_fetchWeb Fetch (keyless anonymous HTTP(S) reader)A
Fetch one HTTP(S) URL anonymously and return the page as readable text: HTML is stripped to visible text with block structure preserved; text/markdown/JSON/XML pass through decoded. No credentials: browser User-Agent, up to five validated redirects, bounded output. Local, private, link-local, and reserved network targets are blocked, including after DNS resolution and redirects. The result is a status header (status code, final URL, content-type, truncation) followed by the content. Use it to read full page content for a URL, e.g. one found by web_search.
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | Absolute http(s) URL to fetch. | |
| maxChars | No | Maximum content characters to return (default 20000, hard cap 100000). |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries full responsibility. It discloses anonymous access, no credentials, browser User-Agent, redirect limits, target blocking, and truncation behavior, which is strong transparency for a network tool.
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?
Three purposeful sentences: main behavior, safety boundaries, and output/usage context. No filler, no redundant schema echoing, and key constraints are front-loaded.
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?
With no output schema or annotations, the description still explains output format, status header components, redirects, target restrictions, and content conversion. This is an unusually complete one-paragraph contract for a fetch tool.
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?
The schema already clearly documents both parameters at 100% coverage, with maxChars including a default and limits. The description affirms bounded output but does not add substantial meaning about parameter formats beyond the schema.
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 verb, resource, and output: fetches one HTTP(S) URL and returns readable text. It also references web_search, making the distinction between search and fetch explicit without ambiguity.
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?
Description explicitly says to use it to read page content for a URL and gives a web_search output as an example, naming the sibling tool. It does not explicitly state when not to use it, so it stops short of a full when/when-not map.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
web_searchWeb Search (self pool: Bing / 360 / Baidu / Google / Naver / Yandex / DuckDuckGo)A
Search the web without any API key from a self-operated pool: Bing, 360, Baidu, Google, Naver, Yandex, and DuckDuckGo. Each search keeps at most two usable engines — the pool is routed by query language when no explicit pool is given, failed or irrelevant engines are back-filled within a bounded attempt budget, and results are relevance-ranked, quality-adjusted, and deduplicated by canonical URL. Each result includes a title, the real destination URL, a snippet, and its engine. Engines unreachable from the local network, or walled by captcha/JS/bot checks, are skipped and reported in an engine note; the remaining engines still answer.
| Name | Required | Description | Default |
|---|---|---|---|
| count | No | Maximum merged results to return (default 8). | |
| query | Yes | Search query in any language. | |
| engines | No | Optional explicit engine pool in priority order; omitted uses query-language routing. | |
| maxSources | No | Maximum usable engines per search (default 2, hard cap 2; fallback attempts are bounded). |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description thoroughly discloses behavioral details: no API key needed, engine pool self-operated, at most two usable engines, back-fill on failures, relevance/quality ranking, deduplication, skipping unreachable or walled engines, and reporting an engine note. This exceeds the baseline and compensates for the absence of annotations. No contradiction with annotations since none are provided.
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 concise and front-loaded with the primary purpose (search the web without API key) and then provides relevant behavioral details in a structured manner. Each sentence adds value; it is not overly verbose. Slight redundancy with the engines list in the title but overall well-structured.
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?
The description covers key aspects: purpose, engine selection, failure handling, result format, and edge cases (captcha/JS/bot walls). No output schema exists, but the description mentions what each result includes (title, URL, snippet, engine), which partially compensates. The absence of annotations is mitigated by the detailed description. Minor gaps remain about the return structure of the full response, but the information is sufficient for an agent to invoke the tool correctly.
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?
The input schema already has complete (100% coverage) descriptions for all four parameters: count, query, engines, and maxSources. The description adds contextual insight on routing by query language and back-fill behavior but does not add substantial new parameter semantics beyond what the schema provides. Per the guideline, high schema coverage means baseline 3 is appropriate.
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?
The description clearly states the tool performs a web search using a self-operated engine pool, listing the supported engines and the result characteristics (title, URL, snippet, engine). It does not explicitly contrast with the sibling tool web_fetch, but the purpose is distinct and clear.
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?
The description explains how the engine pool is selected (query-language routing when no explicit pool), how failures are handled (back-filling, bounded attempts), and how results are ranked and deduplicated. It does not explicitly state 'when to use' versus 'when not to use' but provides contextual routing guidance. The sibling tool web_fetch is not referenced, so no alternative routing is mentioned.
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.
2 tool updates
v1.1.0- First observed
web_fetch - First observed
web_search
TDQS
Scored across 2 tools
The two tools are completely distinct: web_search performs web searches across multiple engines, while web_fetch retrieves the content of a specific URL. There is no overlap or ambiguity between them.
Both tool names follow the same verb_noun pattern: web_search and web_fetch. The naming is clear, consistent, and predictable.
The server has only two tools, which is slightly below the typical well-scoped range of 3-15. However, each tool is essential and covers the core functionality of keyless web search and fetching, so the count is reasonable for the narrow scope.
The tool surface fully covers the stated purpose of keyless web search: web_search finds relevant results and web_fetch retrieves full page content. There are no obvious missing operations for this domain.
Maintenance
Related MCP Connectors
LLM-ready web search + instant answers + URL-to-clean-text fetch for agents and RAG.
Web search for AI agents — one tool across 6 engines, routed to the cheapest + cached.
Multi-engine search for AI agents. Trust scoring, local corpus, MCP-native. Self-hostable, BYOK.
Search the web, images, videos, news, and local businesses with robust filters, freshness controls…
Related MCP Servers
- AlicenseAqualityBmaintenanceEnables web search without API keys using DuckDuckGo and Bing search engines, and retrieves webpage content. Supports multiple search engines simultaneously with privacy protection and asynchronous processing.29MIT
- AlicenseAqualityAmaintenanceWeb search (embedded SearXNG), content extraction, and library docs indexing with hybrid search. No API keys required.617Apache 2.0
- FlicenseNot gradedqualityDmaintenanceProvides free web search, content fetching, image search, and deep research via SearXNG, no API keys required.-
- AlicenseNot gradedqualityCmaintenanceEnables web search using Bing and DuckDuckGo combined, and fetching readable content from web pages, with no API key required.92MIT