search-mcp
by 7-e1even
README.md
# 🔍 Search MCP Server
> 安全研究搜索聚合 MCP 服务器 —— 一站式搜索微信公众号、奇安信攻防社区、先知社区的安全技术文章,可选叠加 KimiCode 通用全网搜索。
## ✨ 特性
- 🔎 **多源聚合** — 微信公众号 / 奇安信攻防社区 / 先知社区,一个指令搜遍安全垂直站点
- 🌐 **通用全网(可选)** — 接入 KimiCode 搜索/抓取接口,补齐三个垂直源覆盖不到的全网内容
- 🛡️ **TLS 指纹规避** — `curl_cffi` Chrome TLS 指纹,降低搜狗反爬/验证码触发;先知文章走 Playwright 过 WAF JS 挑战
- 🧠 **智能召回** — 本地源 0 命中时自动剥离版本号/噪声词、拆中英连写并重搜一次(如 `fastjson1.2.80利用` → `fastjson`)
- 🧹 **结果整理** — 跨源按 URL 去重、摘要空白归一化与预览截断、可选按日期排序
- 📄 **文章全文提取** — 自动识别来源提取干净正文,保留代码块;未知来源回退 KimiCode 抓取
- ⚡ **并发搜索** — `search_all` 多源同时查询
## 📦 安装
```bash
pip install -e .
python -m playwright install chromium # 先知文章获取需要
```
## ⚙️ MCP 配置
```json
{
"mcpServers": {
"search-mcp": {
"command": "python",
"args": ["-m", "search_mcp"],
"env": {
"KIMI_CODE_API_KEY": "sk-kimi-xxxxxxxx"
}
}
}
}
```
- `command` 需指向**装了本包的那个 Python**(`pip install -e .` 所在环境),也可用控制台脚本 `search-mcp`。
- `KIMI_CODE_API_KEY`:**KimiCode** 的密钥(`sk-kimi-...`),**仅** `search_web` 与 `search_all(include_web=True)` 需要;不配也不影响三个本地源。
- ⚠️ 它**不是** Kimi/Moonshot 开放平台的 `KIMI_API_KEY`,两者不通用。
- 缺失时相关工具返回友好提示而非报错。
## 🛠️ 工具一览
| Tool | 参数 | 说明 |
| ---------------- | ------------------------------------------------- | ---------------------------------------------------------- |
| `search_weixin` | `query`, `page=1` | 搜索微信公众号文章(搜狗微信搜索) |
| `search_butian` | `query`, `page=1` | 搜索奇安信攻防社区文章 |
| `search_xianzhi` | `query`, `page=1` | 搜索先知社区文章 |
| `search_all` | `query`, `page=1`, `include_web=False`, `sort_by_date=False` | 并发搜索本地三源(`include_web=True` 叠加全网),去重合并,附 `sources` 状态 |
| `search_web` | `query`, `limit=10`, `include_content=False` | KimiCode 通用全网搜索(需 key;query 出网至 api.kimi.com) |
| `fetch_article` | `url` | 获取文章全文,自动识别来源路由;未知来源回退 KimiCode 抓取 |
> 💡 本地源(butian/xianzhi/微信)的站内搜索是字面匹配,**带版本号或中文连写复合词易 0 命中**——已由智能召回兜底;查具体版本/CVE 也可直接用 `search_web`。
### 返回格式
```json
{
"results": [
{
"title": "文章标题",
"url": "https://...",
"abstract": "摘要预览(空白已归一化,约 200 字内)",
"source": "weixin",
"author": "公众号名 / 站点名",
"date": "2025-01-01",
"content": "完整正文(仅 search_web include_content=True 时)"
}
],
"count": 10,
"sources": { "weixin": 10, "butian": 8, "xianzhi": 0, "web": 4 }
}
```
- 空字段自动省略;`sources` 仅 `search_all` 返回(值为条数或 `"error"`,便于区分"没结果"与"该源被限流/验证码挡住")。
- 跨源结果按归一化 URL 去重,本地原生抓取优先于全网的同链接重复项;`count` 为去重后的数量。
## 🔐 反爬绕过原理
| 来源 | 搜索 | 文章获取 | 技术方案 |
| ------------ | ------------------ | ---------- | ------------------------------------------------ |
| 微信(搜狗) | curl_cffi | curl_cffi | Chrome TLS 指纹 + JS 跳转 URL 拼接解析 |
| 奇安信 | curl_cffi | curl_cffi | 无反爬限制 |
| 先知 | curl_cffi + CSRF | Playwright | CSRF Token + Alibaba WAF JS Challenge (headless) |
| 全网(Kimi) | KimiCode HTTP API | KimiCode | 官方 search/fetch 接口(需密钥) |
> TLS 指纹用于**规避/降低**搜狗反爬触发,并非验证码自动求解;被拦截时对应源返回空结果。
## 🧩 扩展新来源
在 `search_mcp/scrapers/` 下新建 scraper,三步搞定:
```python
# search_mcp/scrapers/your_source.py
from .base import BaseScraper
from ..types import SearchResult
class YourScraper(BaseScraper):
name = "your_source"
async def search(self, query: str, page: int = 1) -> list[SearchResult]:
# 实现搜索逻辑;用 await self._fetch(url) 发请求
...
async def fetch_article(self, url: str) -> str:
# 实现文章提取逻辑
...
```
然后在 `scrapers/__init__.py` 与 `server.py` 中注册即可。
## 📁 项目结构
```
search_mcp/
├── __init__.py
├── __main__.py # 入口
├── server.py # MCP 工具注册 + 去重/清洗/召回兜底
├── types.py # SearchResult 数据模型
└── scrapers/
├── base.py # BaseScraper (curl_cffi AsyncSession)
├── weixin.py # 微信搜索
├── butian.py # 奇安信攻防社区
├── xianzhi.py # 先知社区
└── kimi.py # KimiCode 通用全网搜索 / 抓取
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing