Department Web-Search MCP Gateway
A self-hosted web-search gateway that reuses a single logged-in browser session to handle intranet/SSO logins once, providing web_search and read_webpage tools for any MCP client.
README
Department Web-Search MCP Gateway
A self-hosted web-search service the whole department can share. It reuses a
single logged-in browser session (a shared service account), so intranet /
SSO / consent-wall logins are handled once — every client just calls a
web_search tool, no per-user login or API key.
Any MCP client connects to one URL:
- Chatbox (≥1.14)
- OpenCode — local, on a shared server, or via vscode-remote
- Claude Code (and other coding agents that speak MCP)
It is the T1 “centralized search gateway” from the research notes: one internal machine + one shared Chrome profile + one HTTP MCP endpoint.
How it works
Chatbox / OpenCode(local|server|vscode-remote) / Claude Code
│ remote MCP (Streamable HTTP, /mcp) — same URL for everyone
▼
┌──────────────────────────────────────────────┐
│ Gateway (this service, Node + Express) │
│ • Bearer token (optional) + Host validation │
│ • MCP tools: web_search / read_webpage │
└──────────────────────────────────────────────┘
│ connectOverCDP / launchPersistentContext
▼
┌──────────────────────────────────────────────┐
│ Chrome (persistent profile, shared account) │ ← logged in ONCE via `npm run login`
│ • per-request new tab (isolation) │
│ • concurrency cap + timeouts │
└──────────────────────────────────────────────┘
│ optional fallback
▼
SearXNG (if SEARXNG_URL set) — public-search fallback when browser returns nothing
createMcpHandler serves both 2025-era and 2026-era MCP clients on the same
/mcp endpoint, so client transport compatibility is not a concern.
Headless Linux servers + a Windows PC for login
Servers have no GUI, but a human can log in on a Windows PC. Pick a mode in
.env (BROWSER_MODE) — the code is identical, only config differs.
⚠️ Do NOT copy a Windows Chrome profile directory to Linux. Chromium encrypts cookies with OS-bound keys (DPAPI on Windows, keyring/”peanuts” on Linux), so a copied profile loses the login silently. Use one of the cross-OS-safe modes below.
Mode C — BROWSER_MODE=cdp (recommended): Linux gateway attaches to the Windows browser
- Windows PC (stays on): log in once with the shared account, then keep
Chrome running with a local-only debug port:
chrome --remote-debugging-port=9222 --remote-debugging-address=127.0.0.1 ^ --user-data-dir=C:\dept-search-profile - Carry that port safely to the Linux server with an SSH reverse tunnel
(run on the Windows PC; Win10/11 ships OpenSSH):
ssh -R 9222:127.0.0.1:9222 linuxuser@gateway.server - Linux server:
.env→BROWSER_MODE=cdp,CDP_ENDPOINT=http://127.0.0.1:9222(local on the server, tunneled back to the Windows browser). Thennpm start. - Login stays live (cookies refresh as the browser is used); no profile copy; the unauthenticated CDP port is never on the network. Downside: Windows PC off → searches fail until it’s back (use Mode B if that’s unacceptable).
Mode B — BROWSER_MODE=storagestate: snapshot, Linux self-sufficient
- Windows PC:
npm run login(headed), log in, press Enter → writesauth.json(OS-agnostic JSON of cookies + localStorage). - Copy
auth.jsonto the Linux server, setBROWSER_MODE=storagestate,STORAGE_STATE_FILE=./auth.json, runnpm start. Linux runs its own headless browser loading the snapshot — no tunnel, survives the Windows PC being off. - Trade-off: a frozen snapshot — re-export when the SSO cookie expires; carries only cookies + localStorage (not IndexedDB/client certs) — fine for most SSO.
Mode A — BROWSER_MODE=persistent: Windows PC runs everything
- If a spare Windows PC can be the always-on service host:
npm run loginthere (seeds the profile), thennpm startwithBROWSER_MODE=persistent. - Linux servers are pure clients pointing at
http://<windows-pc>:8787/mcp. - Simplest of all — no tunnel, no snapshot ceremony.
Client onboarding is identical in every mode: clients point at the gateway’s MCP URL; the gateway talks to whichever browser mode is configured.
Bring-up runbook — Mode C (Linux gateway + Windows browser)
The confirmed setup: a Linux server runs the gateway; an always-on
Windows PC runs a real Chrome (logged in once) and an SSH reverse tunnel.
No browser is downloaded to the Linux server (playwright-core only).
Windows PC (once, then leave running) — see windows/README.md
windows\start-browser.ps1→ dedicated Chrome on127.0.0.1:9222, profileC:\dept-search-profile. Sign in with the shared account (SSO/2FA). Keep it open.$env:GATEWAY_SSH = "linuxuser@gateway.server"; windows\start-tunnel.ps1→ maintainsssh -R 9222:127.0.0.1:9222 gateway, auto-reconnects.- Make both Scheduled Tasks (At startup / On logon, run whether logged on or not) so the PC is a self-healing browser appliance.
Linux gateway server (this machine)
cd dept-web-search-gateway
cp .env.example .env
# edit .env:
# BROWSER_MODE=cdp (default)
# CDP_ENDPOINT=http://127.0.0.1:9222 (the tunneled port, local on this server)
# HOST=0.0.0.0
# ALLOWED_HOSTS=search.internal,localhost # hostnames clients will use
# GATEWAY_TOKEN=... (optional; else rely on network ACL)
npm install # lean — playwright-core, no Chromium download
npm run build # typecheck
npm start # dev (tsx); or `npm run build && npm run start:prod`
curl http://127.0.0.1:8787/health # {"ok":true,...}
Point clients at http://<this-server>:8787/mcp (see Client onboarding).
Sanity-check the tunnel
On the Linux server:
curl -s http://127.0.0.1:9222/json/version # Chrome's JSON → tunnel + Chrome are up
Empty / connection refused → the Windows Chrome or the reverse tunnel isn’t
running yet; web_search will fail until it is.
Setup (one-time)
cd dept-web-search-gateway
npm install # also runs `playwright install chromium`
cp .env.example .env # then edit .env (see knobs below)
1) Seed the shared login (the crux)
Run once on a machine with a display (or under xvfb-run -a):
npm run login
# or, for an internal portal:
LOGIN_START_URL=https://wiki.internal npm run login
A real Chrome window opens. Sign in with the shared service account
(SSO / 2FA), confirm you’re logged in to the search engine / portal, then close
the window. The session is persisted to BROWSER_PROFILE_DIR (default ./.profile)
and reused by the headless gateway from now on.
Servers are headless? Do the
npm run loginstep on the Windows PC, then choose Mode B (copyauth.jsonto Linux) or Mode C (SSH-tunnel CDP to Linux) as described in “Headless Linux servers + a Windows PC for login” above. Renewal when the SSO session expires: Mode A/B → re-runnpm run login(and re-copyauth.jsonfor B); Mode C → just re-login in the Windows Chrome.
2) Run the gateway
npm start # dev (tsx)
# or production:
npm run build && npm run start:prod
You should see:
[server] MCP gateway on http://0.0.0.0:8787/mcp (engine=bing)
[server] profile=./.profile
Client onboarding (give these to your colleagues)
Replace search.internal / 8787 with your gateway host/port. Everyone uses
the same URL.
Chatbox (≥1.14)
Settings → MCP → Add Server → choose Remote / URL:
- URL:
http://search.internal:8787/mcp - (if
GATEWAY_TOKENis set) add a headerAuthorization: Bearer <TOKEN>where the client supports it; otherwise protect with network ACL.
One-click deep link (put on your intranet page):
chatbox://mcp/install?server=<base64 of {"name":"websearch","url":"http://search.internal:8787/mcp"}>
OpenCode — all three flavors
Add to opencode.json (project) or ~/.config/opencode/opencode.json (global):
{
"mcp": {
"websearch": {
"type": "remote",
"url": "http://search.internal:8787/mcp",
"enabled": true
}
}
}
- Local opencode: same snippet, host =
127.0.0.1or the gateway host. - Server opencode: the process runs on the server → point at the gateway’s internal URL directly (the server must reach it over the internal network).
- vscode-remote opencode: the process runs on the remote host → point at the gateway’s internal URL (reachable from that host). No tunneling needed because the gateway is on the internal network.
- Verify:
opencode mcp list.
Claude Code
claude mcp add --transport http websearch http://search.internal:8787/mcp
# with a token:
claude mcp add --transport http --header "Authorization: Bearer <TOKEN>" \
websearch http://search.internal:8787/mcp
Cline / Cursor / others
If they support remote MCP, point at the same URL. If they only do stdio, run a tiny local shim that calls the HTTP gateway (a 20-line wrapper) — not included here, but trivial to add.
Tools exposed
| Tool | Args | Returns |
|---|---|---|
web_search |
query (str, required), engine (bing|google|duck|custom, optional) |
list of {title, url, snippet} as text + JSON |
read_webpage |
url (str, required) |
# title + main text (≤20k chars), login/SSO handled |
The agent in Chatbox/OpenCode/Claude Code will call web_search when it needs
fresh info, and read_webpage to read a specific page — no extra wiring.
Config knobs (.env)
| Var | Default | Meaning |
|---|---|---|
HOST |
0.0.0.0 |
bind address. 127.0.0.1 = localhost-only (+auto DNS-rebinding protection) |
ALLOWED_HOSTS |
— | comma list of hostnames clients use (enables Host-header validation). Set when binding 0.0.0.0 |
PORT |
8787 |
listen port |
GATEWAY_TOKEN |
— | if set, require Authorization: Bearer <token>. Empty = no auth (network-ACL only) |
BROWSER_MODE |
cdp |
persistent / storagestate / cdp — see topology section |
CDP_ENDPOINT |
http://127.0.0.1:9222 |
cdp mode: the attached browser’s CDP URL (usually a tunneled port) |
STORAGE_STATE_FILE |
./auth.json |
storagestate mode: login snapshot exported on Windows, copied here |
BROWSER_PROFILE_DIR |
./.profile |
persistent mode: Chrome profile holding the shared login |
HEADLESS |
true |
false only for debugging |
MAX_CONCURRENT_PAGES |
4 |
concurrency cap (one Chrome, isolated tabs) |
PAGE_TIMEOUT_MS |
20000 |
per-page hard timeout |
SEARCH_ENGINE |
bing |
bing (tuned extractor) / google / duck / custom |
SEARCH_URL_TEMPLATE |
— | custom URL with {q} placeholder, e.g. https://wiki.internal/search?q={q} (overrides engine URL) |
RESULT_COUNT |
10 |
results per query |
SEARXNG_URL |
— | optional public-search fallback (needs outbound internet), e.g. http://127.0.0.1:8080 |
Adding a custom internal-portal extractor
extractBing in src/tools.ts is tuned for Bing’s DOM. For an internal portal,
add extractPortal(page, count) and select it on the engine name in
searchWithBrowser. The generic extractGeneric already returns anchor links +
nearby text as a passable fallback for unknown DOMs.
Security & ops notes
- Bind & expose: prefer keeping the gateway on the internal network. If you
bind
0.0.0.0, setALLOWED_HOSTSand use a firewall / network ACL, or setGATEWAY_TOKEN, or put it behind an SSO reverse proxy. - Shared profile = shared identity: every search is attributed to the shared account. Fine for a department service account; review if the target audits per-user or has quota.
- Session renewal: re-run
npm run loginwhen SSO expires. Consider a weekly cron that emails a reminder, or a health probe that detects a login wall (read_webpageon a known-login-required URL returns the login page text). - Concurrency / scale: one Chrome with isolated tabs handles a small
department. Grow to a browser pool (N persistent contexts) if it saturates —
the
withPageseam is the only place to change. - Headless Chrome on Linux:
--no-sandbox --disable-dev-shm-usageare already set (container-friendly).
Development & testing
- Probes live in
scripts/and import from../dist/, so build first:npm run build.scripts/probe-search.mjs "<query>"— drives the shared browser directly (bypasses MCP); validates CDP attach + the Bing extractor.scripts/probe-mcp.mjs <url> "<query>"— connects to a running gateway over Streamable HTTP (the real client path), lists tools, callsweb_search. Start the gateway first:node --env-file=.env dist/server.js.
- Dev mode (
npm start→ tsx): under npm 11,tsx's transitiveesbuildpostinstall is blocked byallow-scriptsby default. Approve it once (npm approve-scripts) or just use the compiled path everywhere:npm run build && node --env-file=.env dist/server.js.
Status
This is a reviewable PoC / skeleton — verified against the v2 MCP SDK API
(@modelcontextprotocol/server 2.x, createMcpHandler / createMcpExpressApp
/ requireBearerAuth / toNodeHandler) and Playwright’s persistent-context
API. Before production: pin exact dependency versions, add tests, and harden the
auth layer (JWT / introspection instead of a static token) if you expose it
beyond a trusted internal network.
Design context (Mode A/B/C topologies, the cross-OS cookie-encryption gotcha, SearXNG boundaries) is in the “Headless Linux servers + a Windows PC for login” section above.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。