为本地ollama设置网页搜索mcp服务器

一、原理

核心思路 :利用开源元搜索引擎 SearXNG 作为免费的搜索后端(聚合 Google, Bing, DuckDuckGo 等结果,无需 API Key),并在其前部署一个自定义的 MCP ServerFastAPI 中间件。该中间件负责接收 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新闻"}'
相关推荐
艾莉丝努力练剑2 小时前
确保多进程命名管道权限一致的方法
java·linux·运维·服务器·开发语言·网络·c++
NGC_66112 小时前
TCP三次握手
运维·服务器·网络
陈皮糖..2 小时前
Docker Compose 学习之多容器应用编排与运维实践 —— 基于 Nginx+MySQL+Redis 服务栈的部署与管理
运维·redis·学习·mysql·nginx·docker
大傻^2 小时前
OpenClaw 生产级部署实录:Ubuntu 服务器 × MiniMax × 飞书(Lark) 完整集成指南
服务器·ubuntu·飞书·minimax·openclaw
桌面运维家2 小时前
Windows自动运维:VHD虚拟磁盘大屏监控实践
运维
深圳市恒讯科技2 小时前
数据存储服务器配置方案:大规模数据业务如何选择服务器
运维·服务器
夜月yeyue2 小时前
Linux 邻接(Neighbor)子系统架构与 NUD 状态机
linux·运维·服务器·嵌入式硬件·算法·系统架构
令狐少侠20112 小时前
openclaw运维
运维·ai
wbs_scy2 小时前
Linux 动态链接与动态库加载深度解析
linux·运维·服务器