欢迎回来
登录你的知识库账户
忘记密码?
还没有账户?立即注册
创建账户
注册你的专属知识库
已有账户?去登录
找回密码
输入注册邮箱获取验证码
返回登录
请输入图片中的验证码以继续注册
加载中...
取消
新建收藏
手动添加你喜欢的内容
取消
编辑头像与昵称
上传新头像或修改你的显示昵称
支持 JPG/PNG,最大 2MB
取消

问题反馈

notebasewww.notebase.cn
控制台
内容库
动态
管理
账户
U
用户
--
在线
v0.8.7 · 知识库
笔记
KnowledgeBase
网络无边,知识有迹。
0笔记
0工具
30推荐

分类导航

按主题直达

编辑精选

站内用户贡献 · 真实笔记

最新收录

每日更新
继续浏览全部内容 →
>
笔记
0
加载中...
工具
0
此页用于记录用户反馈问题后的每一次改进
笔记用法

“写笔记”支持四种格式——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目录扫描分析工具(万字解说)

2026/7/4网络安全

前言:为什么想搞这个工具

先说说背景。做渗透测试的朋友应该都清楚,目录扫描是个又脏又累的活。跑完 dirsearch 之后,面对几百上千条结果,你得一条条去看哪些是真正有用的信息。状态码 200 的可能是正常页面,也可能是敏感文件泄露;403 的可能是权限配置不当;500 的可能暴露了服务器版本或者绝对路径。

我之前一直是手动撸,后来想想,既然现在大模型这么火,能不能让 AI 帮我干这个脏活?于是就有了这个项目——把 MCP 协议和 dirsearch 结合起来,让 LLM 自动分析扫描结果,输出漏洞报告。

这篇文章我会把整个开发过程、踩的坑、以及技术细节都掰开揉碎了讲清楚,希望能给想搞类似工具的朋友一些参考。

一、整体思路

先画个简单的架构图(脑补一下):

用户输入URL
    ↓
MCP Server 调用 dirsearch 扫描
    ↓
扫描结果写入 JSON 文件
    ↓
数据去重筛选(状态码+返回包大小)
    ↓
LLM 分析非200响应页面
    ↓
输出漏洞报告

核心思想是:不让 LLM 处理原始扫描结果。原因有两个:

  1. 原始结果里大量重复数据(同一个路径返回不同状态码),浪费 token
  2. 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 扫描结果存储

扫描结果需要持久化存储,原因有二:

  1. 审计需求:原始扫描结果可以用于后续对比分析
  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文件中

关键点:

  1. 明确指定要扫描的 URL:避免 LLM 自己猜测
  2. 要求对非200页面调用 get_content:这是深度分析的关键
  3. 指定输出格式:表格 + md 文件,方便后续处理
  4. 明确漏洞分析维度:目录、状态码、危害、利用方法、修复方法

六、效果展示

跑一次扫描的效果大概是这样的(脑补一下):

  1. LLM 调用 scan_dir 对目标进行扫描
  2. 扫描完成后,LLM 拿到结构化的 JSON 结果
  3. LLM 遍历 non_200_results,对每个 URL 调用 get_content
  4. LLM 分析页面内容,识别出:
    • Apache/2.4.41 版本泄露(500 页面)
    • /var/www/html/ 绝对路径泄露(403 页面)
    • 管理员登录页面(200 页面)
  5. 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 里加个延时,或者用信号量控制并发。

八、后续计划

这个工具目前还比较基础,后续准备做几件事:

  1. 漏洞验证:对 LLM 识别出的漏洞进行自动验证,比如尝试利用
  2. 报告生成:自动生成 HTML/PDF 格式的渗透测试报告
  3. 模型微调:用标注好的数据微调一个小模型,专门做目录扫描结果分析
  4. 多工具集成:除了 dirsearch,还可以集成 nmap、sqlmap 等工具

九、免责声明

本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用,且应遵守《中华人民共和国网络安全法》等相关法律法规。未经授权对他人网站进行扫描测试属于违法行为,请务必在获得授权的情况下使用本工具。

十、参考资料

  • MCP 协议文档:https://modelcontextprotocol.io/
  • dirsearch 项目:https://github.com/maurosoria/dirsearch
  • Cline 插件:https://github.com/cline/cline
编写使用方法
Markdown 格式 · Ctrl+Enter 确定
新建笔记
预览
数据表格
点击单元格编辑 · Tab 移动
A1fx
Sheet1
BIH1H2≡🔗</>
隐私提醒

取消
编辑工具
取消