“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
我用MCP开发了一个AI目录扫描分析工具(万字解说)
前言:为什么想搞这个工具
先说说背景。做渗透测试的朋友应该都清楚,目录扫描是个又脏又累的活。跑完 dirsearch 之后,面对几百上千条结果,你得一条条去看哪些是真正有用的信息。状态码 200 的可能是正常页面,也可能是敏感文件泄露;403 的可能是权限配置不当;500 的可能暴露了服务器版本或者绝对路径。
我之前一直是手动撸,后来想想,既然现在大模型这么火,能不能让 AI 帮我干这个脏活?于是就有了这个项目——把 MCP 协议和 dirsearch 结合起来,让 LLM 自动分析扫描结果,输出漏洞报告。
这篇文章我会把整个开发过程、踩的坑、以及技术细节都掰开揉碎了讲清楚,希望能给想搞类似工具的朋友一些参考。
一、整体思路
先画个简单的架构图(脑补一下):
用户输入URL
↓
MCP Server 调用 dirsearch 扫描
↓
扫描结果写入 JSON 文件
↓
数据去重筛选(状态码+返回包大小)
↓
LLM 分析非200响应页面
↓
输出漏洞报告
核心思想是:不让 LLM 处理原始扫描结果。原因有两个:
- 原始结果里大量重复数据(同一个路径返回不同状态码),浪费 token
- LLM 上下文有限,一次塞太多数据容易遗漏关键信息
所以我在中间加了一层数据清洗,以"状态码+返回包大小"作为唯一标识,同样的组合只保留一条。这样既省 token,又保证分析质量。
二、为什么选 SSE 而不是 stdio
这是个关键的技术决策,我踩了坑才明白的。
MCP 协议支持两种传输方式:
- stdio:通过标准输入输出通信,适合本地工具调用
- sse:通过 HTTP 长连接通信,适合远程服务
我一开始用的 stdio,结果发现一个问题:dirsearch 扫描经常超过 30 秒,而 MCP 的 stdio 协议里,工具超时时间是硬编码的 30 秒,改不了。
查阅 MCP 源码后发现,这个超时限制在 mcp/client/session.py 里写死了:
# 伪代码示意
async def call_tool(self, name, arguments):
# 这里有个硬编码的 30 秒超时
await asyncio.wait_for(self._send_request(...), timeout=30)
而 SSE 方案就不一样了,它走的是 HTTP 协议,超时时间可以在客户端配置。目前只有新版的 Cline 插件支持修改这个超时时间。如果你用 Cursor 或者其他客户端,可能就得自己搓客户端了。
所以我最终选了 SSE 方案,把 MCP 服务跑在 8000 端口上。
三、环境搭建
3.1 初始化项目
为了不污染主环境,我用 uv 创建虚拟环境:
uv init ai_dirscan
cd ai_dirscan
uv venv
这时候目录结构应该是这样的:
ai_dirscan/
├── .venv/
├── pyproject.toml
├── main.py
激活虚拟环境:
# Linux/Mac
source .venv/bin/activate
# Windows
.venv\Scripts\activate.bat
安装依赖:
uv add "mcp[cli]"
uv add requests
然后配置 dirsearch。我假设你已经 clone 了 dirsearch 项目:
cd dirsearch
pip install -r requirements.txt
pip install setuptools
测试一下 dirsearch 是否可用:
python3 dirsearch.py -u https://src.sjtu.edu.cn/
如果看到扫描界面,说明环境配置成功。
3.2 项目结构
最终的项目结构:
ai_dirscan/
├── .venv/
├── dirsearch/
├── scan_results/ # 扫描结果存放目录
├── main.py # MCP 服务主程序
└── pyproject.toml
四、MCP 服务端开发
4.1 模块导入和服务初始化
首先导入需要的模块:
import json
import subprocess
from collections import defaultdict
import requests
from datetime import datetime
from pathlib import Path
from mcp.server.fastmcp import FastMCP
初始化 MCP 服务,指定端口为 8000:
mcp = FastMCP("ai_dirscan", port=8000)
这里 FastMCP 是 MCP 提供的一个便捷类,帮我们处理了底层的协议细节。你只需要注册 tool 函数,它就会自动暴露给客户端调用。
4.2 扫描结果存储
扫描结果需要持久化存储,原因有二:
- 审计需求:原始扫描结果可以用于后续对比分析
- 微调数据:LLM 的输出可以和原始结果对比,用来微调模型
配置存储路径:
SCAN_RESULT_DIR = Path("scan_results")
SCAN_RESULT_DIR.mkdir(exist_ok=True)
生成带时间戳的文件名,避免覆盖:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_file = SCAN_RESULT_DIR / f"scan_{timestamp}.json"
4.3 核心扫描函数
这是整个工具的核心,我注册了一个 scan_dir 工具:
@mcp.tool()
def scan_dir(url: str) -> str:
"""
执行网站目录扫描,返回结构化扫描结果
Args:
url (str): 目标网站URL,需包含协议头(如http/https)
Returns:
str: JSON格式响应,包含:
- status_200: 200状态的有效路径列表
- non_200_results: 非200状态的有效结果列表(包含状态码)
- report_path: 结果文件路径
- stats: 各类状态码统计
"""
函数注解的重要性:MCP 客户端在调用 tool 时,会读取函数下的 """ """ 注解,然后根据这些描述来决定是否调用这个函数。所以注解要写得清晰明确,最好格式化一下,让 LLM 能理解返回格式。
接着是调用 dirsearch 的逻辑:
# 构建扫描命令
base_cmd = [
"python3",
"./dirsearch/dirsearch.py",
"-u", url,
"-o", str(output_file),
"--format=json",
"-q",
"--no-color",
]
try:
# 执行扫描命令
subprocess.run(
base_cmd,
check=True,
capture_output=True,
timeout=300, # 5分钟超时,足够大多数扫描
text=True
)
这里有几个关键点:
--format=json:让 dirsearch 输出 JSON 格式,方便解析-q:安静模式,减少输出--no-color:去掉颜色,避免干扰解析timeout=300:设置子进程超时,防止卡死
4.4 数据去重和分类
这是节省 token 的关键步骤。原始扫描结果里,同一个路径可能因为不同原因返回多个状态码,比如 /admin 可能同时返回 200 和 403。如果全部喂给 LLM,既浪费 token 又增加噪声。
我的去重策略是:以"状态码+返回包大小"作为唯一标识。
# 加载原始扫描结果
with open(output_file, "r", encoding="utf-8") as f:
scan_data = json.load(f)
# 初始化数据结构
status_200 = []
non_200_results = []
status_counter = defaultdict(int)
unique_tracker = set()
# 结果分类处理
for entry in scan_data.get("results", []):
status = entry["status"]
url_path = entry["url"]
content_length = entry["content-length"]
# 状态码统计
status_counter[status] += 1
# 生成唯一标识符防止重复
entry_key = f"{status}|{content_length}"
if entry_key in unique_tracker:
continue
unique_tracker.add(entry_key)
# 分类存储结果
if status == 200:
status_200.append(url_path)
elif content_length != 0:
non_200_results.append({
"url": url_path,
"status": status,
"content_length": content_length
})
为什么要这样去重?因为同一个状态码+相同返回包大小,大概率是同一个页面或者同一个错误模板。比如多个路径都返回 404,且内容长度都是 1234 字节,那它们很可能是同一个 404 页面,没必要重复分析。
4.5 结果返回格式
为了让 LLM 更容易解析,我统一返回 JSON 格式:
# 构建统计信息
stats = {
"total_200": len(status_200),
"total_non_200": len(non_200_results),
"status_distribution": dict(status_counter)
}
# 生成最终响应
response = {
"status": 200,
"data": {
"status_200": status_200,
"non_200_results": non_200_results,
"report_path": str(output_file),
"stats": stats
},
"message": "扫描完成,结果已分类"
}
这里我把结果分成了两类:
- status_200:正常响应的路径,通常不需要深度分析(也可能是敏感文件)
- non_200_results:非正常响应的路径,包含状态码和返回包大小,是分析的重点
4.6 错误处理
为了让 LLM 能正确识别错误并给出修复建议,我加了详细的错误处理:
except json.JSONDecodeError as e:
response = {"status": 500, "message": f"结果解析失败: {str(e)}"}
except subprocess.TimeoutExpired:
response = {"status": 408, "message": "扫描超时"}
except Exception as e:
response = {"status": 500, "message": f"扫描失败: {str(e)}"}
这样 LLM 就能根据返回的 status 和 message 来判断发生了什么问题,并给出相应的建议。
4.7 深度分析工具
为了分析非 200 响应页面的内容,我写了第二个工具 get_content:
@mcp.tool()
def get_content(url: str) -> str:
"""
获取非200响应界面的网页内容
:param url: 需要检测的网页地址
:return: 返回页面的完整内容(若目标页面返回非200状态码)
"""
try:
response = requests.get(
url,
headers={"User-Agent": "Mozilla/5.0"},
timeout=5
)
if response.status_code != 200:
return response.text
return f"200响应页面,无需进行深度分析"
except requests.exceptions.RequestException as e:
return f"请求异常:{str(e)}"
这个工具的作用是:对于非 200 的页面,获取其原始内容,让 LLM 分析是否存在信息泄露。比如:
- 500 页面可能泄露服务器版本、绝对路径
- 403 页面可能泄露目录结构
- 401 页面可能泄露认证方式
4.8 启动服务
最后是启动 MCP 服务的代码:
if __name__ == "__main__":
mcp.run(transport='sse')
这里选择 SSE 模式,因为前面说的超时问题。
4.9 完整代码
整合起来,完整的 main.py 如下:
import json
import subprocess
from collections import defaultdict
import requests
from datetime import datetime
from pathlib import Path
from mcp.server.fastmcp import FastMCP
# 初始化 MCP 服务
mcp = FastMCP("ai_dirscan", port=8000)
# 配置存储路径
SCAN_RESULT_DIR = Path("scan_results")
SCAN_RESULT_DIR.mkdir(exist_ok=True)
@mcp.tool()
def scan_dir(url: str) -> str:
"""
执行网站目录扫描,返回结构化扫描结果
Args:
url (str): 目标网站URL,需包含协议头(如http/https)
Returns:
str: JSON格式响应,包含:
- status_200: 200状态的有效路径列表
- non_200_results: 非200状态的有效结果列表(包含状态码)
- report_path: 结果文件路径
- stats: 各类状态码统计
"""
# 生成结果文件路径
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_file = SCAN_RESULT_DIR / f"scan_{timestamp}.json"
# 构建扫描命令
base_cmd = [
"python3",
"./dirsearch/dirsearch.py",
"-u", url,
"-o", str(output_file),
"--format=json",
"-q",
"--no-color",
]
response = {"status": 500, "message": "初始化失败"}
try:
# 执行目录扫描
subprocess.run(
base_cmd,
check=True,
capture_output=True,
timeout=300,
text=True
)
# 加载原始扫描结果
with open(output_file, "r", encoding="utf-8") as f:
scan_data = json.load(f)
# 初始化数据结构
status_200 = []
non_200_results = []
status_counter = defaultdict(int)
unique_tracker = set()
# 结果分类处理
for entry in scan_data.get("results", []):
status = entry["status"]
url_path = entry["url"]
content_length = entry["content-length"]
# 状态码统计
status_counter[status] += 1
# 生成唯一标识符防止重复
entry_key = f"{status}|{content_length}"
if entry_key in unique_tracker:
continue
unique_tracker.add(entry_key)
# 分类存储结果
if status == 200:
status_200.append(url_path)
elif content_length != 0:
non_200_results.append({
"url": url_path,
"status": status,
"content_length": content_length
})
# 构建统计信息
stats = {
"total_200": len(status_200),
"total_non_200": len(non_200_results),
"status_distribution": dict(status_counter)
}
# 生成最终响应
response = {
"status": 200,
"data": {
"status_200": status_200,
"non_200_results": non_200_results,
"report_path": str(output_file),
"stats": stats
},
"message": "扫描完成,结果已分类"
}
except json.JSONDecodeError as e:
response = {"status": 500, "message": f"结果解析失败: {str(e)}"}
except subprocess.TimeoutExpired:
response = {"status": 408, "message": "扫描超时"}
except Exception as e:
response = {"status": 500, "message": f"扫描失败: {str(e)}"}
return json.dumps(response, ensure_ascii=False, indent=2)
@mcp.tool()
def get_content(url: str) -> str:
"""
获取非200响应界面的网页内容
:param url: 需要检测的网页地址
:return: 返回页面的完整内容(若目标页面返回非200状态码)
"""
try:
response = requests.get(
url,
headers={"User-Agent": "Mozilla/5.0"},
timeout=5
)
if response.status_code != 200:
return response.text
return f"非404页面,当前状态码:{response.status_code}"
except requests.exceptions.RequestException as e:
return f"请求异常:{str(e)}"
def error_response(exception, code, message, filepath):
"""构建错误响应模板"""
return {
"status": code,
"error": {
"type": exception.__class__.__name__ if exception else "UnknownError",
"details": str(exception) if exception else ""
},
"message": message,
"failed_path": str(filepath) if filepath else None
}
if __name__ == "__main__":
mcp.run(transport='sse')
五、客户端对接
5.1 启动服务
首先启动 MCP 服务:
uv run main.py
如果看到类似这样的输出,说明服务启动成功:
INFO: Started server process [12345]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000
服务会监听 http://0.0.0.0:8000/sse 端点。
5.2 配置 Cline
我以 Cline 为例(因为只有它支持修改超时时间)。在 Cline 的 MCP 配置中添加:
{
"mcpServers": {
"ai_dirscan": {
"url": "http://localhost:8000/sse",
"timeout": 600 // 10分钟超时,视扫描时长而定
}
}
}
如果配置后无法连接,建议点击 "Configure MCP Servers" 选项,然后对弹出的配置 JSON 进行 Ctrl+S 保存。有时候 Cline 不会自动刷新配置,手动保存可以触发重新加载。
配置成功后,在 Cline 的 MCP 工具列表中就能看到我们注册的两个工具:scan_dir 和 get_content,以及它们的描述信息。
5.3 提示词优化
经过多次测试,我发现下面这个提示词效果最好:
请帮我使用已有的mcp工具扫描网站https://xxx.xxx.top/,非200响应页面都要调用get_content函数获取内容,判断是否存在版本目录泄露等漏洞,并输出得到的目录,状态码,危害,利用方法,修复方法,以表格的形式统一给我写在md文件中
关键点:
- 明确指定要扫描的 URL:避免 LLM 自己猜测
- 要求对非200页面调用 get_content:这是深度分析的关键
- 指定输出格式:表格 + md 文件,方便后续处理
- 明确漏洞分析维度:目录、状态码、危害、利用方法、修复方法
六、效果展示
跑一次扫描的效果大概是这样的(脑补一下):
- LLM 调用
scan_dir对目标进行扫描 - 扫描完成后,LLM 拿到结构化的 JSON 结果
- LLM 遍历
non_200_results,对每个 URL 调用get_content - LLM 分析页面内容,识别出:
Apache/2.4.41版本泄露(500 页面)/var/www/html/绝对路径泄露(403 页面)- 管理员登录页面(200 页面)
- LLM 生成 md 报告,包含漏洞详情和修复建议
七、踩坑记录
7.1 stdio 超时问题
前面已经说过了,这是最大的坑。如果你的扫描任务不会超过 30 秒,用 stdio 完全没问题。但目录扫描这种耗时操作,建议直接上 SSE。
7.2 Token 消耗控制
原始扫描结果可能很大,比如扫描 1000 个路径,原始 JSON 可能有几 MB。直接喂给 LLM 的话,一次对话可能就消耗几万 token。通过去重,通常能减少 50%-70% 的数据量。
7.3 LLM 幻觉问题
LLM 有时候会"编造"不存在的漏洞。解决方法是:保留原始扫描结果文件,这样我们可以对比 LLM 的输出和原始数据,发现不一致的地方就记录下来,用于后续微调。
7.4 并发请求限制
如果非 200 页面很多(比如几百个),LLM 可能会同时发起大量 get_content 请求,导致目标服务器被封或者自身被限流。可以在 get_content 里加个延时,或者用信号量控制并发。
八、后续计划
这个工具目前还比较基础,后续准备做几件事:
- 漏洞验证:对 LLM 识别出的漏洞进行自动验证,比如尝试利用
- 报告生成:自动生成 HTML/PDF 格式的渗透测试报告
- 模型微调:用标注好的数据微调一个小模型,专门做目录扫描结果分析
- 多工具集成:除了 dirsearch,还可以集成 nmap、sqlmap 等工具
九、免责声明
本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用,且应遵守《中华人民共和国网络安全法》等相关法律法规。未经授权对他人网站进行扫描测试属于违法行为,请务必在获得授权的情况下使用本工具。
十、参考资料
- MCP 协议文档:https://modelcontextprotocol.io/
- dirsearch 项目:https://github.com/maurosoria/dirsearch
- Cline 插件:https://github.com/cline/cline