Skip to main content
Glama
soizo

cc-switch-websearch

by soizo
README.md
# cc-switch Web Search

> Built-in `web_search` MCP tool for [cc-switch](https://github.com/farion1231/cc-switch)
> (Claude Code / Codex / Gemini CLI provider manager), backed by a
> self-hosted [SearXNG](https://github.com/searxng/searxng) JSON API.

Once installed, cc-switch starts an in-process MCP HTTP server, registers
the entry in `~/.claude/mcp_servers.json`, and Claude Code can call
`mcp__cc-switch-websearch__web_search` for fresh web results — no premium
search API key required.

> **Attribution.** This integration and the bundled installer were
> produced by **Minimax-M3** (a Claude Opus 4.8 based coding agent
> developed by Minimax). Use, modify, and redistribute under the terms of
> the upstream cc-switch MIT licence.

---

## What you get

- A `web_search` MCP tool exposed via in-process HTTP on a random free
  port (`127.0.0.1:<port>/mcp`)
- Automatic registration of the entry in `~/.claude/mcp_servers.json` on
  enable, automatic removal on disable
- A new **Web Search** section under **Settings > Advanced** in
  cc-switch with an enable/disable switch, a SearXNG instance URL input
  + Save, live status (port, running indicator, last query at), and a
  **Test search** box for one-shot queries
- Plain-text natural-language reports (title, URL, snippet per result)
  suitable for Claude to cite directly

---

## Quick start

### 1. Apply

From the repository root:

```bash
python3 install.py apply
```

Or target a specific checkout:

```bash
python3 install.py apply --source ~/Code/cc-switch
```

The installer reads `spec.yaml` and applies it to the target cc-switch
source tree. New files (under `spec/files/`) are copied into place; for
each modification rule the installer finds a tree-sitter anchor in the
user's file (Rust/TSX/TS) or parses the file as JSON/TOML, then makes
the edit in place. Backups of every modified file are written under
`<source>/.spec-backups/<timestamp>/` so `revert` can restore the tree
exactly. Pass `--auto-deps` to also run `pnpm install` and
`cargo check` afterwards.

To revert later:

```bash
python3 install.py revert
```

To check the current state:

```bash
python3 install.py status
```

### 2. Self-host a SearXNG instance

DuckDuckGo's HTML endpoint rejects any Rust HTTP client with a captcha,
and every public SearXNG instance we tested sits behind an Anubis /
Cloudflare challenge — neither works from a headless tool. You need a
**self-hosted** SearXNG. Pick the one-liner for your platform:

<details>
<summary><b>macOS (OrbStack recommended)</b></summary>

```bash
# Install OrbStack if you don't have a Docker runtime yet
brew install --cask orbstack
open /Applications/OrbStack.app   # first launch: ~30 s to bring up the Linux VM

# Persist a permissive settings.yml (disables IP rate-limit, enables JSON)
mkdir -p ~/.config/searxng
cat > ~/.config/searxng/settings.yml <<EOF
use_default_settings: true
server:
  secret_key: "$(openssl rand -hex 32)"
botdetection:
  ip_limit:
    enable: false
  link_token: false
search:
  formats:
    - json
EOF

# Start SearXNG
docker run -d --name searxng --restart unless-stopped \
  -p 8888:8080 \
  -v ~/.config/searxng/settings.yml:/etc/searxng/settings.yml:ro \
  searxng/searxng

# Smoke test
curl -s 'http://127.0.0.1:8888/search?q=rust+language&format=json' | python3 -m json.tool | head
```

</details>

<details>
<summary><b>Windows (PowerShell, Docker Desktop)</b></summary>

```powershell
# Install Docker Desktop if you don't have a Docker runtime yet
winget install Docker.DockerDesktop
# Launch Docker Desktop from the Start menu and wait for the whale icon to settle

# Persist a permissive settings.yml
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.config\searxng" | Out-Null
@"
use_default_settings: true
server:
  secret_key: "$(-join ((1..32) | ForEach-Object { '{0:x2}' -f (Get-Random -Maximum 256) }))"
botdetection:
  ip_limit:
    enable: false
  link_token: false
search:
  formats:
    - json
"@ | Set-Content -Encoding UTF8 "$env:USERPROFILE\.config\searxng\settings.yml"

# Start SearXNG
docker run -d --name searxng --restart unless-stopped `
  -p 8888:8080 `
  -v "$env:USERPROFILE\.config\searxng\settings.yml:/etc/searxng\settings.yml:ro" `
  searxng/searxng

# Smoke test
(Invoke-WebRequest 'http://127.0.0.1:8888/search?q=rust+language&format=json').Content
```

</details>

<details>
<summary><b>Linux (Docker)</b></summary>

```bash
# Install Docker if you don't have it yet (Debian / Ubuntu shown)
sudo apt-get update && sudo apt-get install -y docker.io
sudo usermod -aG docker $USER && newgrp docker
sudo systemctl enable --now docker

# Persist a permissive settings.yml
mkdir -p ~/.config/searxng
cat > ~/.config/searxng/settings.yml <<EOF
use_default_settings: true
server:
  secret_key: "$(openssl rand -hex 32)"
botdetection:
  ip_limit:
    enable: false
  link_token: false
search:
  formats:
    - json
EOF

# Start SearXNG
docker run -d --name searxng --restart unless-stopped \
  -p 8888:8080 \
  -v ~/.config/searxng/settings.yml:/etc/searxng/settings.yml:ro \
  searxng/searxng

# Smoke test
curl -s 'http://127.0.0.1:8888/search?q=rust+language&format=json' | python3 -m json.tool | head
```

For Podman the same command works — just replace `docker` with `podman`
and drop the `sudo` / `usermod` steps.

</details>

### 3. Wire it into cc-switch

1. Build cc-switch from your patched source: `pnpm install && pnpm tauri dev`
2. Open **Settings > Advanced > Web Search**
3. Paste `http://127.0.0.1:8888` into the **SearXNG URL** field and click **Save**
4. Click **Test search** — you should see real results within a few seconds
5. Flip the **Enable Web Search** switch on. cc-switch will:
   - Start the in-process MCP HTTP server
   - Write the `cc-switch-websearch` entry into `~/.claude/mcp_servers.json`
6. Restart Claude Code. The `web_search` tool is now available — try
   *"Search the web for the latest Rust 1.85 release notes and summarise."*

---

## Repository layout

```
.
├── README.md              # this file
├── LICENSE                # our MIT
├── install.py             # apply / revert / status / dev
├── spec.yaml              # the patch — single source of truth
├── spec/files/            # new file contents at their cc-switch paths
│   ├── src/
│   └── src-tauri/
├── requirements.txt       # tree-sitter + grammar deps for install.py
└── upstream/              # unmodified cc-switch clone (gitignored)
```

| Path | Purpose |
| --- | --- |
| `install.py` | Spec-driven patch engine. Loads `spec.yaml`, applies it to a target cc-switch source via tree-sitter (Rust/TSX/TS) or stdlib (JSON/TOML). Apply backs up every modified file under `<source>/.spec-backups/<ts>/`; revert restores from that backup. |
| `spec.yaml` | The single source of truth for the patch. 10 new files (`creates:`) and 15 modifications (`modifies:`) declared as data. Maintained by AI. To change what the patch does, edit this file — `install.py` is generic. |
| `spec/files/` | The 10 new files the patch adds, at their natural cc-switch relative paths. `install.py` copies each into the target tree. |
| `requirements.txt` | Python deps for `install.py`: PyYAML, tree-sitter, tree-sitter-rust, tree-sitter-typescript, tomli_w. Install with `pip install -r requirements.txt`. |
| `upstream/` | Reference clone of unmodified cc-switch. Gitignored; `install.py` bootstraps `test-target/` from here when no real cc-switch source is detected. |

### Why "patch as spec" instead of `.patch` files or post-patch files?

Earlier revisions of this project used two other approaches, both with
real downsides:

- **Static `.patch` files** (e.g. `git format-patch` output). A
  unified diff is fragile: it depends on the exact line numbers and
  surrounding context of the upstream file at the time the diff was
  generated. One whitespace tweak or a single upstream commit can make
  `git apply` fail with a three-way merge conflict.
- **Static post-patch files** (a `patch/` directory of files at their
  final state). Sidesteps the diff-fragility problem but forces the
  patch to one specific upstream version — apply the 3.16.3 snapshot
  to a 3.16.0 checkout and you get code that doesn't compile.

The current design — a **declarative spec applied at install time via
tree-sitter** — trades a touch of complexity for both:

- **Robust to upstream drift.** Each modification is anchored on a
  semantic identifier (`find: { node: mod_item, field: name, equals:
  usage_script }`), not a line number. As long as the anchor still
  exists in the user's file, the patch lands.
- **Fails loudly when it can't.** If an anchor no longer exists in a
  new cc-switch release, `install.py` exits with a clear error naming
  the file + the spec rule. No silent skips, no half-applied state.
- **Easy to update.** To rebase on a new cc-switch release, edit the
  spec's anchors. To add a new feature, append rules. `install.py` is
  generic — the spec is data, the engine is code.

### Files the patch adds / modifies

The spec applies 25 changes — 10 new files and 15 modifications. The
full list lives at the top of `spec.yaml`; in short:

**New (10):**
- `src/components/websearch/{WebSearchPanel,WebSearchStatusBadge}.tsx`
- `src/hooks/useWebSearch.ts`
- `src/lib/api/webSearch.ts`
- `src/types/websearch.ts`
- `src-tauri/src/commands/web_search.rs`
- `src-tauri/src/web_search/{mod,mcp_server,tools,searxng}.rs`

**Modified (15):**
- `src/components/settings/SettingsPage.tsx`
- `src/i18n/locales/{en,ja,zh,zh-TW}.json`
- `src-tauri/Cargo.toml`
- `src-tauri/src/{lib,store}.rs`
- `src-tauri/src/commands/mod.rs`

---

## Why SearXNG (and not DuckDuckGo)?

The original implementation scraped `html.duckduckgo.com` — the same
approach used by `NousResearch/hermes-agent`. In practice every Rust
HTTP client (reqwest, ureq, with or without `http2`, with or without
custom TLS fingerprints) gets back an HTTP 202 captcha page. The only
way we found to make a successful request is to shell out to `curl`,
which is not a sustainable build-time dependency for a Tauri app.

Public SearXNG instances (`baresearch.org`, `disroot.org`,
`search.hbubli.cc`, …) all sit behind Anubis or Cloudflare challenges —
same problem. The integration is therefore designed around a
**user-configured, self-hosted** SearXNG instance. The one-liners in
§2 bring one up locally in under a minute on any platform.

---

## Rebasing on a new cc-switch release

The spec is anchored on identifiers (function names, JSX attribute
values, JSON keys, TOML tables), not line numbers — so most upstream
changes apply cleanly without any edit to `spec.yaml`. When a new
cc-switch release renames or removes one of those anchors, `install.py
apply` will fail with a clear error naming the file and the anchor that
no longer matches. To recover:

```bash
python3 install.py revert        # restore from the last .spec-backups/
cd ~/Code/cc-switch
git fetch upstream
git rebase upstream/main        # or: git pull upstream main
python3 install.py apply         # re-apply; if a rule now fails, edit
                                # spec.yaml to point at the new anchor
pnpm install && pnpm tauri dev
```

The most likely edits after a rebase are:

- A `find: { node: mod_item, field: name, equals: ... }` rule where
  the upstream renamed the mod. Update `equals`.
- A `find: { attribute: { name: value, equals: logConfig } }` rule
  where the upstream renamed the AccordionItem value. Update `equals`.
- A `find: { table: dependencies }` rule where upstream restructured
  Cargo.toml. Update the table path.

If you want to start clean against a new cc-switch release without
trying to rebase an already-patched tree:

```bash
python3 install.py revert        # restore from backup
git pull upstream main           # pull new upstream
python3 install.py apply         # re-apply the spec on top
```

---

## Verifying the SearXNG instance is reachable

The panel's **Test search** button is the fastest check. From a
terminal:

```bash
curl -s 'http://127.0.0.1:8888/search?q=hello&format=json' | python3 -m json.tool | head
```

You should see a JSON object with a `results` array. If you get HTML
or a 403, the instance is up but is missing the
`botdetection.ip_limit: false` / `search.formats: [json]` overrides —
re-check the `settings.yml` in §2.

Container lifecycle:

```bash
docker ps --filter name=searxng      # is it running?
docker logs --tail 50 searxng        # what went wrong?
docker restart searxng               # after editing settings.yml
```

---

## Licence & attribution

- Integration code: MIT, same as upstream cc-switch.
- Installer (`install.py`): MIT.
- Generated by **Minimax-M3** (Minimax).

Issues and PRs welcome — please open them on this repository rather than
on the upstream cc-switch tracker.