Skip to main content
Glama
keyless-web-search-mcp

keyless-web-search-mcp

by dollarser

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: ddgs-mcp

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 tests

Quick 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.js

Install 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-self

Use --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({ query, count?, maxSources?, engines? })

Param

Default

Meaning

query

—

Search query, any language

count

8

Max merged results (1–20)

maxSources

2

Max usable engines per search (hard cap 2; fallback attempts are bounded)

engines

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

url

—

Absolute http(s) URL (anything else is rejected with a diagnostic)

maxChars

20000

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

bing

www.bing.com/search

✅ ~10 organic blocks on both lines. Depending on the line, www serves the SERP directly or 302s to the cn.bing.com twin (the engine follows it); cn uses direct result URLs instead of /ck/a trackers — both shapes parse

360

www.so.com/s

✅ clean 200; organic blocks carry the real URL in data-mdurl

google

www.google.com/search

Reachable (200) but this client/IP is not trusted for plain-HTML SERPs: the body is a no-JS enablejs meta-refresh wall with zero organic results. Detected and skipped with a diagnostic; from a cleaner IP it would join the merge (see the bypass attempts below)

baidu

www.baidu.com/s

✅ on the Shanghai and HK lines: the risk control gates script-shaped requests (UA-only 302s to the wappass.baidu.com image captcha regardless of cookies) but accepts the full document-navigation header signature (Accept + Referer + sec-fetch-* + Upgrade-Insecure-Requests) — stable across repeated runs, with or without cookies. The engine sends that signature; lines that gate harder (the earlier mainland line) still hit the detected-and-skipped captcha diagnostic. Parser reads the block's mu attribute (direct URL); non-organic cards (recommend lists, hot boards, related search) are skipped like ad slots elsewhere

naver

search.naver.com/search.naver

✅ on the HK line (200, ~10 fds-web-doc-root organic blocks, stable across runs; answers Chinese queries with Chinese results). Title anchor carries the direct URL; the Korean a11y label "새 창 열림" is stripped from titles and snippets. Korean-first index that also covers global technical sites; untested from mainland lines (would degrade to the standard unreachable diagnostic)

yandex

yandex.com/search

✅ 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 Organic blocks, direct URLs in OrganicTitle anchors). The engine warms the session in-process (30-min TTL, one re-warm retry on a challenged answer). Caveat: from datacenter IPs Yandex runs a rolling rate state — a burst of searches flips the IP back into captcha mode for a few minutes, which is why it sits sixth in the pool (consulted only when the engines ahead of it fail)

duckduckgo

html.duckduckgo.com/html

❌ 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:

  1. JS challenge (computational) — decoded, it computes a proof value and sets an SG_SS cookie (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 resulting SG_SS cookie was accepted once — Google answered 200 and issued the NID/AEC trust cookies it otherwise never sends to this IP.

  2. Session-pattern layer (behavioral) — subsequent plain-HTTP follow-ups, including an exact replica of the script's own reload flow (emsg=SG_REL + matching sei, jar-merged cookies, browser headers), escalate to the google.com/sorry 429 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_fetch

The 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 tools
web_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.

Inputs: url (required), maxChars.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.3/5.0

    Scored across 2 tools

    Disambiguation5/5

    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.

    Naming Consistency5/5

    Both tool names follow the same verb_noun pattern: web_search and web_fetch. The naming is clear, consistent, and predictable.

    Tool Count4/5

    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.

    Completeness5/5

    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

    ActivityStale
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      C
      maintenance
      Enables 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.
      2
      77 PyPI
      10
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      Enables keyless multi-engine web, news, image, and video metasearch plus full-page markdown extraction for AI agents over MCP, with resilient fallback across DuckDuckGo, Bing, Brave, Google, and other backends.
      6
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      Enables any MCP client to run keyless multi-engine web searches with RRF fusion, fetch pages and documents as Markdown, and full-text search a local SQLite index. It keeps queries and cached content on the user's machine, degrading gracefully across engine tiers when providers are blocked or rate-limited.
      8
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables LLMs and coding assistants to run web searches by concurrently aggregating multiple commercial search engines per scenario, merging and deduplicating URL-normalized results with per-call fallback. When no API keys or all commercial engines fail, it automatically degrades to key-free DuckDuckGo and Bing channels so search never goes offline.
      110 npm
      MIT