一、原理
基于 SearXNG + mcp-server-search 的本地代理架构说明
核心思路 :利用开源元搜索引擎 SearXNG 作为免费的搜索后端(聚合 Google, Bing, DuckDuckGo 等结果,无需 API Key),并在其前部署一个自定义的 MCP Server 或 FastAPI 中间件。该中间件负责接收 Open WebUI/Ollama 的请求,调用 SearXNG,执行安全清洗,然后将结果返回给模型
(一)架构组件
- 搜索后端: SearXNG (Docker 部署,完全免费,无 API 限制)。
- 中间件/MCP Server: 一个用 Python 编写的轻量级服务,实现 MCP 标准或兼容 Open WebUI 的 Tool 定义。
- 安全层: 在中间件中集成内容过滤和 URL 沙箱预检。
- 客户端: Open WebUI (配置自定义工具指向该中间件)。
二、实施步骤
(一)Docker中配置SearXng
在内网机器上通过 Docker 部署 SearXNG,它不依赖任何付费 API。
1. 拖拽searxng镜像
SearXNG 官方文档里,镜像名是这样的:
docker pull docker.io/searxng/searxng:latest
2. 创建配置目录:
在你的 Ubuntu 服务器上创建一个文件夹:
#创建一个searxng-config文件夹
mkdir -p ~/searxng-config
#进入该文件夹
cd ~/searxng-config
#分别创建一个data和settings文件夹
mkdir data
mkdir settings
因为我已经安装好了Docker,此处省略
3. 创建 settings.yml 配置文件:
为了安全,我们需要禁用一些不稳定的引擎,并设置基础 URL。(编辑器自选)
#进入searxng/settins目录
cd settings
#创建并编辑settings.yml文件,作为searxng运行时的配置文件
nano settings.yml
4. 编辑settings.yml文件
粘贴以下内容(注意修改 <你的内网IP> 为你的实际 IP,例如 192.168.1.100):
# SearXNG 稳定配置文件
# 必须开启此项,否则需要手动配置所有分类和全局参数
use_default_settings: true
general:
instance_name: "SEARXNG"
debug: false
private: true
search:
safe_search: 0
formats:
- html
- json
server:
# 密钥将由环境变量 SEARXNG_SECRET 覆盖,这里填个占位符
secret_key: "placeholder_key"
limiter: false
image_proxy: true
# 确保绑定到所有接口
bind_address: "0.0.0.0"
# 引擎配置策略:
# 因为开启了 use_default_settings,默认会加载所有引擎。
# 我们需要在这里显式禁用不稳定的,并确保需要的启用。
engines:
# --- 1. 确保你想要的引擎启用 (disabled: false) ---
- name: google
disabled: false
- name: bing
disabled: false
- name: duckduckgo
disabled: false
- name: qwant
disabled: false
- name: wikipedia
disabled: false
# --- 2. 显式禁用容易超时或不稳定的引擎 (关键!) ---
# 这样它们会被加载但立即禁用,不会发起网络请求导致超时崩溃
- name: wikidata
disabled: true
- name: yahoo
disabled: true
- name: yandex
disabled: true
- name: brave
disabled: true
- name: mojeek
disabled: true
- name: metager
disabled: true
# 如果有其他特定引擎想禁用,继续加在这里
# - name: 引擎名
# disabled: true
# 注意:不要在这里写 valkey 或 redis 配置,除非你有外部服务
# 如果没有 external_redis,SearXNG 会使用内存存储,适合单机
5.启动 Docker 容器:
docker run -d --name searxng\
--restart unless-stopped\
-p 8080:8080\
-v $(pwd)/settings:/etc/searxng\
-v $(pwd)/data:/var/cache/searxng\
-e SEARXNG_BASE_URL=http://127.0.0.1:8080/\
docker.io/searxng/searxng:latest
替换 '127.0.0.1'为你的真实 IP,因为我的就在本地,使用了本地回环地址。
6. 验证:
在浏览器访问 http://<你设置的内网IP>:8080,如果能搜出结果,说明 SearXNG 部署成功。

(二)编写安全中间件 (Python)
这是最关键的一步。我们将编写一个 Python 服务,它实现了 Open WebUI 兼容的 Tool 接口,并内置了安全清洗逻辑。
1.准备环境
sudo apt update
#如果无法配置python虚拟环境的话再执行
sudo apt install python3-pip python3-venv -y
#新建一个secure-search-tool目录
mkdir -p ~/secure-search-tool
#进入该目录
cd ~/secure-search-tool
#创建python的虚拟环境,命名为venv
python3 -m venv venv
#激活该环境
source venv/bin/activate
#在该环境下安装python依赖包
pip install fastapi uvicorn httpx beautifulsoup4 lxml
2. 创建主程序 main.py
nano main.py
粘贴以下代码(请仔细阅读注释中的配置项):
import os
import re
import httpx
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import logging
from typing import List, Optional
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = FastAPI(title="Secure Search MCP")
# ================= 配置区域 =================
# 如果 SearXNG 和此脚本在同一台机器,使用 127.0.0.1
# 如果 SearXNG 在另一台机器,请改为 http://<SearXNG_IP>:8080/search
SEARXNG_URL = "http://127.0.0.1:8080/search"
MAX_RESULTS = 5 # 每次搜索返回的最大结果数
TIMEOUT_SECONDS = 15 # 稍微增加超时时间,防止网络波动
# ===========================================
class SearchRequest(BaseModel):
query: str
def sanitize_text(text: str) -> str:
"""清洗文本:移除潜在的控制字符、过长的空白"""
if not text:
return ""
# 移除不可见控制字符 (除了换行 \n 和制表符 \t)
text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', text)
# 压缩多余空白 (将多个空格/换行合并为一个空格)
text = re.sub(r'\s+', ' ', text).strip()
# 简单的长度截断,防止单个结果过长 (每个结果限制 500 字)
if len(text) > 500:
text = text[:497] + "..."
return text
def is_safe_url(url: str) -> bool:
"""简单的 URL 安全检查"""
if not url:
return False
dangerous_keywords = ['.onion', 'javascript:', 'data:', 'file:', 'vbscript:']
url_lower = url.lower()
return not any(keyword in url_lower for keyword in dangerous_keywords)
@app.post("/search")
async def secure_search(request: SearchRequest):
query = request.query
logger.info(f"Received search request: {query}")
# 1. 输入安全校验 (防提示词注入)
injection_patterns = [
r"ignore previous instructions",
r"system prompt",
r"output your initial prompt",
r"print the system message",
r"bypass security",
r"root access"
]
for pattern in injection_patterns:
if re.search(pattern, query, re.IGNORECASE):
logger.warning(f"🚫 Security Filter Blocked: {query}")
# 不抛出异常,而是返回一个安全的提示,防止模型困惑
return {"result": "Error: Security filter blocked potentially malicious query."}
try:
# 定义伪装成浏览器的 Headers (解决 403 的关键)
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Connection": "keep-alive"
}
# 2. 调用 SearXNG
async with httpx.AsyncClient(timeout=TIMEOUT_SECONDS, headers=headers) as client:
params = {
"q": query,
"format": "json",
"language": "zh-CN"
}
logger.debug(f"Requesting SearXNG: {SEARXNG_URL} with params {params}")
response = await client.get(SEARXNG_URL, params=params)
# 专门处理 403 和其他错误状态
if response.status_code == 403:
logger.error(f"❌ SearXNG returned 403 Forbidden. Response Body: {response.text[:200]}")
raise HTTPException(status_code=503, detail="SearXNG blocked the request (403). Check SearXNG limiter settings.")
response.raise_for_status() # 如果是其他 4xx/5xx 也会抛出
try:
data = response.json()
except ValueError:
logger.error(f"Invalid JSON response from SearXNG: {response.text[:200]}")
raise HTTPException(status_code=502, detail="SearXNG returned invalid JSON.")
results = data.get("results", [])
logger.info(f"SearXNG returned {len(results)} results.")
safe_output: List[str] = []
# 3. 处理并清洗结果
for item in results[:MAX_RESULTS]:
title = item.get("title", "No Title")
content = item.get("content", "No Content")
url = item.get("url", "")
# 安全检查 URL
if not is_safe_url(url):
logger.info(f"⚠️ Skipped unsafe URL: {url}")
continue
# 清洗文本内容
clean_title = sanitize_text(str(title))
clean_content = sanitize_text(str(content))
# 如果内容为空,跳过
if not clean_content and not clean_title:
continue
# 格式化输出
entry = f"[Source]: {clean_title}\n[Content]: {clean_content}\n[URL]: {url}"
safe_output.append(entry)
if not safe_output:
logger.warning("No safe results found after filtering.")
return {"result": "No relevant or safe search results found for this query."}
# 4. 返回给模型
final_result = "\n\n---\n\n".join(safe_output)
logger.info(f"Successfully returning {len(safe_output)} cleaned results.")
return {"result": final_result}
except httpx.ConnectError as e:
logger.error(f"Connection failed to SearXNG: {str(e)}")
raise HTTPException(status_code=503, detail=f"Cannot connect to Search Engine: {str(e)}")
except Exception as e:
logger.error(f"Unexpected error: {str(e)}", exc_info=True)
# 避免将内部堆栈信息直接暴露给模型,只返回简要错误
raise HTTPException(status_code=500, detail=f"Search service internal error.")
if __name__ == "__main__":
import uvicorn
# 监听 0.0.0.0 以便内网其他机器访问
logger.info("🚀 Starting Secure Search MCP on port 9000...")
uvicorn.run(app, host="0.0.0.0", port=9000)
注意 :代码中的 SEARXNG_URL 如果你的 SearXNG 和这个脚本在同一台机器,保持 127.0.0.1 即可;如果在不同机器,请改为 SearXNG 的 IP。
3. 运行中间件:
# 确保在 venv 环境中
source venv/bin/activate
nohup python main.py > search.log 2>&1 &
(三)测试
curl -X POST http://127.0.0.1:9000/search \
-H "Content-Type: application/json" \
-d '{"query": "2026年最新的AI新闻"}'
