# web-search-mcp - Doramagic AI Context Pack

> 定位：安装前体验与判断资产。它帮助宿主 AI 有一个好的开始，但不代表已经安装、执行或验证目标项目。

## 充分原则

- **充分原则，不是压缩原则**：AI Context Pack 应该充分到让宿主 AI 在开工前理解项目价值、能力边界、使用入口、风险和证据来源；它可以分层组织，但不以最短摘要为目标。
- **压缩策略**：只压缩噪声和重复内容，不压缩会影响判断和开工质量的上下文。

## 给宿主 AI 的使用方式

你正在读取 Doramagic 为 web-search-mcp 编译的 AI Context Pack。请把它当作开工前上下文：帮助用户理解适合谁、能做什么、如何开始、哪些必须安装后验证、风险在哪里。不要声称你已经安装、运行或执行了目标项目。

## Claim 消费规则

- **事实来源**：Repo Evidence + Claim/Evidence Graph；Human Wiki 只提供显著性、术语和叙事结构。
- **事实最低状态**：`supported`
- `supported`：可以作为项目事实使用，但回答中必须引用 claim_id 和证据路径。
- `weak`：只能作为低置信度线索，必须要求用户继续核实。
- `inferred`：只能用于风险提示或待确认问题，不能包装成项目事实。
- `unverified`：不得作为事实使用，应明确说证据不足。
- `contradicted`：必须展示冲突来源，不得替用户强行选择一个版本。

## 它最适合谁

- **AI 研究者或研究型 Agent 构建者**：README 明确围绕研究、实验或论文工作流展开。 证据：`README.md` Claim：`clm_0002` supported 0.86
- **正在使用 Claude/Codex/Cursor/Gemini 等宿主 AI 的开发者**：README 或插件配置提到多个宿主 AI。 证据：`README.md` Claim：`clm_0003` supported 0.86

## 它能做什么

- **项目知识预览**（可做安装前预览）：项目可被阅读和解释，但当前证据不足以确认可安装能力或运行入口。 证据：`README.md`, `.env.example`, `docker-compose.yml`, `fusion/__init__.py` 等 Claim：`clm_0001` supported 0.86

## 怎么开始

- 项目证据中没有稳定 Quick Start 命令；此项应留空，而不是由 Doramagic 编造。

## 继续前判断卡

- **当前建议**：需要管理员/安全审批
- **为什么**：继续前可能涉及密钥、账号、外部服务或敏感上下文，建议先经过管理员或安全审批。

### 30 秒判断

- **现在怎么做**：需要管理员/安全审批
- **最小安全下一步**：先跑 Prompt Preview；若涉及凭证或企业环境，先审批再试装
- **先别相信**：工具权限边界不能在安装前相信。
- **继续会触碰**：环境变量 / API Key、宿主 AI 上下文

### 现在可以相信

- **适合人群线索：AI 研究者或研究型 Agent 构建者**（supported）：有 supported claim 或项目证据支撑，但仍不等于真实安装效果。 证据：`README.md` Claim：`clm_0002` supported 0.86
- **适合人群线索：正在使用 Claude/Codex/Cursor/Gemini 等宿主 AI 的开发者**（supported）：有 supported claim 或项目证据支撑，但仍不等于真实安装效果。 证据：`README.md` Claim：`clm_0003` supported 0.86
- **能力存在：项目知识预览**（supported）：可以相信项目包含这类能力线索；是否适合你的具体任务仍要试用或安装后验证。 证据：`README.md`, `.env.example`, `docker-compose.yml`, `fusion/__init__.py` 等 Claim：`clm_0001` supported 0.86

### 现在还不能相信

- **工具权限边界不能在安装前相信。**（unverified）：MCP/tool 类项目通常会触碰文件、网络、浏览器或外部 API，必须真实检查权限和日志。
- **真实输出质量不能在安装前相信。**（unverified）：Prompt Preview 只能展示引导方式，不能证明真实项目中的结果质量。
- **宿主 AI 版本兼容性不能在安装前相信。**（unverified）：Claude、Cursor、Codex、Gemini 等宿主加载规则和版本差异必须在真实环境验证。
- **不会污染现有宿主 AI 行为，不能直接相信。**（inferred）：Skill、plugin、AGENTS/CLAUDE/GEMINI 指令可能改变宿主 AI 的默认行为。
- **可安全回滚不能默认相信。**（unverified）：除非项目明确提供卸载和恢复说明，否则必须先在隔离环境验证。
- **真实安装后是否与用户当前宿主 AI 版本兼容？**（unverified）：兼容性只能通过实际宿主环境验证。
- **项目输出质量是否满足用户具体任务？**（unverified）：安装前预览只能展示流程和边界，不能替代真实评测。

### 继续会触碰什么

- **环境变量 / API Key**：项目入口文档明确出现 API key、token、secret 或账号凭证配置。 原因：如果真实安装需要凭证，应先使用测试凭证并经过权限/合规判断。 证据：`NOTES.md`, `README.md`, `probes/phase-1-probe.py`, `probes/phase-2-probe.py` 等
- **宿主 AI 上下文**：AI Context Pack、Prompt Preview、Skill 路由、风险规则和项目事实。 原因：导入上下文会影响宿主 AI 后续判断，必须避免把未验证项包装成事实。

### 最小安全下一步

- **先跑 Prompt Preview**：用安装前交互式试用判断工作方式是否匹配，不需要授权或改环境。（适用：任何项目都适用，尤其是输出质量未知时。）
- **只在隔离目录或测试账号试装**：避免安装命令污染主力宿主 AI、真实项目或用户主目录。（适用：存在命令执行、插件配置或本地写入线索时。）
- **不要使用真实生产凭证**：环境变量/API key 一旦进入宿主或工具链，可能产生账号和合规风险。（适用：出现 API、TOKEN、KEY、SECRET 等环境线索时。）
- **安装后只验证一个最小任务**：先验证加载、兼容、输出质量和回滚，再决定是否深用。（适用：准备从试用进入真实工作流时。）

### 退出方式

- **保留安装前状态**：记录原始宿主配置和项目状态，后续才能判断是否可恢复。
- **准备撤销测试 API key 或 token**：测试凭证泄露或误用时，可以快速止损。
- **如果没有回滚路径，不进入主力环境**：不可回滚是继续前阻断项，不应靠信任或运气继续。

## 哪些只能预览

- 解释项目适合谁和能做什么
- 基于项目文档演示典型对话流程
- 帮助用户判断是否值得安装或继续研究

## 哪些必须安装后验证

- 真实安装 Skill、插件或 CLI
- 执行脚本、修改本地文件或访问外部服务
- 验证真实输出质量、性能和兼容性

## 边界与风险判断卡

- **把安装前预览误认为真实运行**：用户可能高估项目已经完成的配置、权限和兼容性验证。 处理方式：明确区分 prompt_preview_can_do 与 runtime_required。 Claim：`clm_0004` inferred 0.45
- **待确认**：真实安装后是否与用户当前宿主 AI 版本兼容？。原因：兼容性只能通过实际宿主环境验证。
- **待确认**：项目输出质量是否满足用户具体任务？。原因：安装前预览只能展示流程和边界，不能替代真实评测。

## 开工前工作上下文

### 加载顺序

- 先读取 how_to_use.host_ai_instruction，建立安装前判断资产的边界。
- 读取 claim_graph_summary，确认事实来自 Claim/Evidence Graph，而不是 Human Wiki 叙事。
- 再读取 intended_users、capabilities 和 quick_start_candidates，判断用户是否匹配。
- 需要执行具体任务时，优先查 role_skill_index，再查 evidence_index。
- 遇到真实安装、文件修改、网络访问、性能或兼容性问题时，转入 risk_card 和 boundaries.runtime_required。

### 任务路由

- **项目知识预览**：先基于 role_skill_index / evidence_index 帮用户挑选可用角色、Skill 或工作流。 边界：可做安装前 Prompt 体验。 证据：`README.md`, `.env.example`, `docker-compose.yml`, `fusion/__init__.py` 等 Claim：`clm_0001` supported 0.86

### 上下文规模

- 文件总数：36
- 重要文件覆盖：36/36
- 证据索引条目：32
- 角色 / Skill 条目：3

### 证据不足时的处理

- **missing_evidence**：说明证据不足，要求用户提供目标文件、README 段落或安装后验证记录；不要补全事实。
- **out_of_scope_request**：说明该任务超出当前 AI Context Pack 证据范围，并建议用户先查看 Human Manual 或真实安装后验证。
- **runtime_request**：给出安装前检查清单和命令来源，但不要替用户执行命令或声称已执行。
- **source_conflict**：同时展示冲突来源，标记为待核实，不要强行选择一个版本。

## Prompt Recipes

### 适配判断

- 目标：判断这个项目是否适合用户当前任务。
- 预期输出：适配结论、关键理由、证据引用、安装前可预览内容、必须安装后验证内容、下一步建议。

```text
请基于 web-search-mcp 的 AI Context Pack，先问我 3 个必要问题，然后判断它是否适合我的任务。回答必须包含：适合谁、能做什么、不能做什么、是否值得安装、证据来自哪里。所有项目事实必须引用 evidence_refs、source_paths 或 claim_id。
```

### 安装前体验

- 目标：让用户在安装前感受核心工作流，同时避免把预览包装成真实能力或营销承诺。
- 预期输出：一段带边界标签的体验剧本、安装后验证清单和谨慎建议；不含真实运行承诺或强营销表述。

```text
请把 web-search-mcp 当作安装前体验资产，而不是已安装工具或真实运行环境。

请严格输出四段：
1. 先问我 3 个必要问题。
2. 给出一段“体验剧本”：用 [安装前可预览]、[必须安装后验证]、[证据不足] 三种标签展示它可能如何引导工作流。
3. 给出安装后验证清单：列出哪些能力只有真实安装、真实宿主加载、真实项目运行后才能确认。
4. 给出谨慎建议：只能说“值得继续研究/试装”“先补充信息后再判断”或“不建议继续”，不得替项目背书。

硬性边界：
- 不要声称已经安装、运行、执行测试、修改文件或产生真实结果。
- 不要写“自动适配”“确保通过”“完美适配”“强烈建议安装”等承诺性表达。
- 如果描述安装后的工作方式，必须使用“如果安装成功且宿主正确加载 Skill，它可能会……”这种条件句。
- 体验剧本只能写成“示例台词/假设流程”：使用“可能会询问/可能会建议/可能会展示”，不要写“已写入、已生成、已通过、正在运行、正在生成”。
- Prompt Preview 不负责给安装命令；如用户准备试装，只能提示先阅读 Quick Start 和 Risk Card，并在隔离环境验证。
- 所有项目事实必须来自 supported claim、evidence_refs 或 source_paths；inferred/unverified 只能作风险或待确认项。

```

### 角色 / Skill 选择

- 目标：从项目里的角色或 Skill 中挑选最匹配的资产。
- 预期输出：候选角色或 Skill 列表，每项包含适用场景、证据路径、风险边界和是否需要安装后验证。

```text
请读取 role_skill_index，根据我的目标任务推荐 3-5 个最相关的角色或 Skill。每个推荐都要说明适用场景、可能输出、风险边界和 evidence_refs。
```

### 风险预检

- 目标：安装或引入前识别环境、权限、规则冲突和质量风险。
- 预期输出：环境、权限、依赖、许可、宿主冲突、质量风险和未知项的检查清单。

```text
请基于 risk_card、boundaries 和 quick_start_candidates，给我一份安装前风险预检清单。不要替我执行命令，只说明我应该检查什么、为什么检查、失败会有什么影响。
```

### 宿主 AI 开工指令

- 目标：把项目上下文转成一次对话开始前的宿主 AI 指令。
- 预期输出：一段边界明确、证据引用明确、适合复制给宿主 AI 的开工前指令。

```text
请基于 web-search-mcp 的 AI Context Pack，生成一段我可以粘贴给宿主 AI 的开工前指令。这段指令必须遵守 not_runtime=true，不能声称项目已经安装、运行或产生真实结果。
```

## 角色 / Skill 索引

- 共索引 3 个角色 / Skill / 项目文档条目。

- **web search mcp**（project_doc）：A local MCP server exposing a unified search web tool that queries configured search backends, normalizes results into a shared schema, and returns an evidence-friendly payload for a downstream LLM agent — plus fetch url static page fetch + main-content extraction and search health provider connectivity / auth / mode-availability report . 激活提示：当用户需要理解项目结构、安装方式或边界时参考。 证据：`README.md`
- **Follow-ups - out-of-scope items for operator triage**（project_doc）：Follow-ups - out-of-scope items for operator triage 激活提示：当用户需要理解项目结构、安装方式或边界时参考。 证据：`FOLLOW_UPS.md`
- **NOTES**（project_doc）：Observations and "while I'm here" candidates surfaced during Phase 1 implementation, to be decided by the product owner before acting on them. 激活提示：当用户需要理解项目结构、安装方式或边界时参考。 证据：`NOTES.md`

## 证据索引

- 共索引 32 条证据。

- **web search mcp**（documentation）：A local MCP server exposing a unified search web tool that queries configured search backends, normalizes results into a shared schema, and returns an evidence-friendly payload for a downstream LLM agent — plus fetch url static page fetch + main-content extraction and search health provider connectivity / auth / mode-availability report . 证据：`README.md`
- **Per-request timeout seconds for provider HTTP calls.**（source_file）：Per-request timeout seconds for provider HTTP calls. Valid: any positive number. Example: 5, 10, 15. SEARCH TIMEOUT SECONDS=10 证据：`.env.example`
- **Docker Compose**（source_file）：services: web search mcp: build: . ports: - "127.0.0.1:8000:8000" env file: .env environment: - MCP TRANSPORT=http restart: unless-stopped networks: - default - mcp internal web search mcp chatgpt: build: . ports: - "127.0.0.1:8010:8000" env file: .env environment: - MCP TRANSPORT=http restart: unless-stopped networks: - default networks: mcp internal: external: true 证据：`docker-compose.yml`
- **Search Result**（source_file）：@dataclass class RawSearchResult ⋮---- provider: str raw rank: int title: str url: str snippet: str published date: Optional str = None extra: dict = field default factory=dict ⋮---- @dataclass class NormalizedResult ⋮---- domain: str providers: list str provider overlap: int published date: Optional str content type: str confidence: float ⋮---- rank score: Optional float = None ⋮---- raw ranks: dict = field default factory=dict ⋮---- def to dict self - dict 证据：`models/search_result.py`
- **Init**（source_file）：log = get logger name ⋮---- def build providers config: Config - list SearchProvider ⋮---- providers: list SearchProvider = ⋮---- all = "build providers" 证据：`providers/__init__.py`
- **Base**（source_file）：@runtime checkable class SearchProvider Protocol ⋮---- name: str 证据：`providers/base.py`
- **Brave exposes age short form like "2 days ago" and, on**（source_file）：log = get logger name ⋮---- class BraveProvider ⋮---- name = "brave" ⋮---- url = f"{self. api base}/res/v1/web/search" params: dict str, str int = { ⋮---- headers = { ⋮---- warnings: list str = ⋮---- resp = await client.get url, params=params, headers=headers ⋮---- body = resp.text or "" .strip .replace "\n", " " :200 body clause = f" body={body!r}" if body else "" ⋮---- payload = resp.json ⋮---- web = payload.get "web" or {} raw = web.get "results" if isinstance web, dict else None ⋮---- results: list RawSearchResult = ⋮---- link = entry.get "url" or "" ⋮---- title = entry.get "title" or "" description = entry.get "description" or "" Brave exposes age short form like "2 days ago" and, on ⋮… 证据：`providers/brave.py`
- **Exa**（source_file）：log = get logger name ⋮---- class ExaProvider ⋮---- name = "exa" ⋮---- url = f"{self. api base}/search" body = { headers = { ⋮---- warnings: list str = ⋮---- resp = await client.post url, json=body, headers=headers ⋮---- body = resp.text or "" .strip .replace "\n", " " :200 body clause = f" body={body!r}" if body else "" ⋮---- payload = resp.json ⋮---- raw = payload.get "results" ⋮---- results: list RawSearchResult = ⋮---- link = entry.get "url" or "" ⋮---- title = entry.get "title" or "" highlights = entry.get "highlights" ⋮---- snippet = " ... ".join str h for h in highlights if h ⋮---- snippet = "" published = entry.get "publishedDate" or None ⋮---- class ExaError RuntimeError 证据：`providers/exa.py`
- **Serper provides a 1-based position . Fall back to enumeration**（source_file）：log = get logger name ⋮---- class SerperProvider ⋮---- name = "serper" ⋮---- url = f"{self. api base}/search" body = { headers = { ⋮---- warnings: list str = ⋮---- resp = await client.post url, json=body, headers=headers ⋮---- body text = resp.text or "" .strip .replace "\n", " " :200 body clause = f" body={body text!r}" if body text else "" ⋮---- payload = resp.json ⋮---- raw = payload.get "organic" ⋮---- results: list RawSearchResult = ⋮---- link = entry.get "link" or "" ⋮---- title = entry.get "title" or "" snippet = entry.get "snippet" or "" Serper provides a 1-based position . Fall back to enumeration index when absent so raw rank stays well-defined. position = entry.get "position" raw… 证据：`providers/serper.py`
- **Pyproject**（source_file）：project name = "web-search-mcp" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = " =3.11" dependencies = "fastmcp =3.4.5, =0.28.1", "python-dotenv =1.2.2", "trafilatura =2.1.0", 证据：`pyproject.toml`
- **Server**（source_file）：log = get logger "web search mcp.server" ⋮---- config = load config providers = build providers config ⋮---- mcp = FastMCP name="web search mcp" ⋮---- @mcp.tool async def fetch url url: str - dict ⋮---- @mcp.tool async def search health - dict ⋮---- def main - None ⋮---- transport = os.environ.get "MCP TRANSPORT", "stdio" port = int os.environ.get "MCP PORT", "8000" 证据：`server.py`
- **Cache**（source_file）：CacheKey = tuple str, str, int ⋮---- store: dict CacheKey, dict = {} ⋮---- def make key query: str, mode: str, max results: int - CacheKey ⋮---- def get key: CacheKey - Optional dict ⋮---- def set key: CacheKey, value: dict - None ⋮---- def clear - None ⋮---- def size - int 证据：`utils/cache.py`
- **Config**（source_file）：DEFAULT SEARCH TIMEOUT SECONDS = 10.0 DEFAULT MAX RESULTS = 5 MAX RESULTS UPPER BOUND = 10 DEFAULT BRAVE API BASE = "https://api.search.brave.com" DEFAULT BRAVE SAFESEARCH = "moderate" DEFAULT RECENCY WINDOW DAYS = 30 BRAVE MAX RESULTS CEILING = 20 DEFAULT EXA API BASE = "https://api.exa.ai" DEFAULT EXA NUM RESULTS CEILING = 10 DEFAULT SERPER API BASE = "https://google.serper.dev" DEFAULT SERPER NUM RESULTS CEILING = 10 DEFAULT FETCH URL TIMEOUT SECONDS = 15.0 DEFAULT FETCH URL MAX BODY BYTES = 2 000 000 ⋮---- DEFAULT FETCH URL USER AGENT = "web search mcp/0.4 +fetch url " ⋮---- @dataclass frozen=True class Config ⋮---- search timeout seconds: float default max results: int brave api base:… 证据：`utils/config.py`
- **Fetch Cache**（source_file）：CacheKey = str ⋮---- store: dict CacheKey, dict = {} ⋮---- def make key url: str - CacheKey ⋮---- def get key: CacheKey - Optional dict ⋮---- value = store.get key ⋮---- def set key: CacheKey, value: dict - None ⋮---- def clear - None ⋮---- def size - int 证据：`utils/fetch_cache.py`
- **Logging**（source_file）：CONFIGURED = False ⋮---- def configure logging level: int = logging.INFO - None ⋮---- handler = logging.StreamHandler stream=sys.stderr ⋮---- root = logging.getLogger ⋮---- CONFIGURED = True ⋮---- def get logger name: str - logging.Logger 证据：`utils/logging.py`
- **Follow-ups - out-of-scope items for operator triage**（documentation）：Follow-ups - out-of-scope items for operator triage 证据：`FOLLOW_UPS.md`
- **NOTES**（documentation）：Observations and "while I'm here" candidates surfaced during Phase 1 implementation, to be decided by the product owner before acting on them. 证据：`NOTES.md`
- **.python-version**（source_file）：3.11 证据：`.python-version`
- **Dockerfile**（source_file）：FROM python:3.11-slim WORKDIR /app COPY pyproject.toml uv.lock ./ RUN pip install uv && uv sync --frozen --no-dev COPY . . EXPOSE 8000 CMD "uv", "run", "python", "server.py" 证据：`Dockerfile`
- **Canonicalize**（source_file）：EXACT TRACKING PARAMS = frozenset ⋮---- TRACKING PREFIXES = "utm ", ⋮---- def is tracking name: str - bool ⋮---- def canonicalize url url: str - str ⋮---- parts = urlsplit url ⋮---- scheme = parts.scheme.lower ⋮---- netloc = parts.netloc.lower ⋮---- path = parts.path ⋮---- path = path.rstrip "/" or "/" ⋮---- query pairs = query = urlencode query pairs, doseq=True 证据：`fusion/canonicalize.py`
- **fromisoformat accepts "Z" only from 3.11+; normalize defensively.**（source_file）：def first non empty values: list str - str ⋮---- def parse iso value: str - Optional datetime ⋮---- s = value or "" .strip ⋮---- fromisoformat accepts "Z" only from 3.11+; normalize defensively. dt = datetime.fromisoformat s.replace "Z", "+00:00" ⋮---- dt = dt.replace tzinfo=timezone.utc ⋮---- def merge published date values: list - Optional str ⋮---- non null = v for v in values if v is not None ⋮---- parsed: list tuple datetime, str = ⋮---- dt = parse iso v ⋮---- groups: dict str, list NormalizedResult = {} order: list str = ⋮---- key = canonicalize url r.url ⋮---- merged: list NormalizedResult = ⋮---- group = groups key head = group 0 ⋮---- providers union: list str = seen: set str = set… 证据：`fusion/dedupe.py`
- **Normalize**（source_file）：SUFFIX CATEGORY: dict str, str = { ⋮---- DOMAIN CATEGORY: dict str, str = { ⋮---- def extract domain url: str - str ⋮---- netloc = urlparse url .netloc.lower ⋮---- netloc = netloc 4: ⋮---- def classify content type domain: str - str ⋮---- def compute confidence raw rank: int - float ⋮---- def normalize raw: RawSearchResult - NormalizedResult ⋮---- domain = extract domain raw.url ⋮---- def normalize all raws: list RawSearchResult - list NormalizedResult 证据：`fusion/normalize.py`
- **Rank**（source_file）：log = get logger name ⋮---- TRUSTED EXACT DOMAINS: set str = {"wikipedia.org", "arxiv.org"} TRUSTED SUFFIX PATTERNS: set str = { ⋮---- OVERLAP BONUS = 2.0 TRUSTED BONUS = 1.0 RECENT BONUS = 1.0 ⋮---- def is trusted domain domain: str - bool ⋮---- d = domain.lower ⋮---- def parse published value: str - Optional datetime ⋮---- s = value.strip ⋮---- normalized = s.replace "Z", "+00:00" dt = datetime.fromisoformat normalized ⋮---- dt = dt.replace tzinfo=timezone.utc ⋮---- dt = parse published published date ⋮---- reference = now or datetime.now tz=timezone.utc delta = reference - dt ⋮---- def base score result: NormalizedResult - float ⋮---- best rank = min result.raw ranks.values ⋮---- now = d… 证据：`fusion/rank.py`
- **Two distinct URLs so they survive dedupe as separate results.**（source_file）：SERPER API BASE = os.environ.get "SERPER API BASE", "https://google.serper.dev" SERPER PROBE QUERY = "latest AI news" SEARCH TIMEOUT SECONDS = float os.environ.get "SEARCH TIMEOUT SECONDS", "10" ⋮---- def pass label: str, detail: str = "" - None ⋮---- suffix = f" — {detail}" if detail else "" ⋮---- def fail label: str, detail: str - None ⋮---- def warn label: str, detail: str - None ⋮---- def assertion i fastmcp tool registration api - bool ⋮---- label = " i FastMCP installs and exposes a tool-registration API" ⋮---- required = {"tool", "add tool", "list tools", "run stdio async"} missing = m for m in required if not hasattr FastMCP, m ⋮---- version = getattr fastmcp, " version ", "unknown"… 证据：`probes/phase-1-probe.py`
- **The fast coroutine must return its marker; the slow must surface as**（source_file）：BRAVE API BASE = os.environ.get "BRAVE API BASE", "https://api.search.brave.com" BRAVE PROBE QUERY = "python" SEARCH TIMEOUT SECONDS = float os.environ.get "SEARCH TIMEOUT SECONDS", "10" ⋮---- SLOW SLEEP = min SEARCH TIMEOUT SECONDS + 2.0, 12.0 ⋮---- def pass label: str, detail: str = "" - None ⋮---- suffix = f" — {detail}" if detail else "" ⋮---- def fail label: str, detail: str - None ⋮---- def warn label: str, detail: str - None ⋮---- def assertion i brave key present - tuple bool, bool ⋮---- """Returns passed, key present .""" label = " i BRAVE API KEY env var presence" key = os.environ.get "BRAVE API KEY" or None ⋮---- redacted = f"{key :4 }…{key -4: }" if len key = 8 else "set" ⋮----… 证据：`probes/phase-2-probe.py`
- **Phase 3 Probe**（source_file）：EXA API BASE = os.environ.get "EXA API BASE", "https://api.exa.ai" EXA PROBE QUERY = "python" SEARCH TIMEOUT SECONDS = float os.environ.get "SEARCH TIMEOUT SECONDS", "10" ⋮---- def pass label: str, detail: str = "" - None ⋮---- suffix = f" — {detail}" if detail else "" ⋮---- def fail label: str, detail: str - None ⋮---- def warn label: str, detail: str - None ⋮---- def assertion i exa key present - tuple bool, bool ⋮---- """Returns passed, key present .""" label = " i EXA API KEY env var presence" key = os.environ.get "EXA API KEY" or None ⋮---- redacted = f"{key :4 }…{key -4: }" if len key = 8 else "set" ⋮---- def assertion ii exa json key present: bool - bool ⋮---- label = " ii Exa /searc… 证据：`probes/phase-3-probe.py`
- **Phase 4 Probe**（source_file）：LIVE FETCH URL = os.environ.get ⋮---- REPO ROOT = os.path.abspath os.path.join os.path.dirname file , ".." ⋮---- FIXTURE HTML = ⋮---- ROBOTS DISALLOW ALL = "User-agent: \nDisallow: /\n" ⋮---- def pass label: str, detail: str = "" - None ⋮---- suffix = f" — {detail}" if detail else "" ⋮---- def fail label: str, detail: str - None ⋮---- def warn label: str, detail: str - None ⋮---- def red label: str, detail: str - None ⋮---- def phase4 modules missing - bool ⋮---- import tools.fetch url noqa: F401 import tools.search health noqa: F401 ⋮---- def synthetic config overrides ⋮---- """Build a Config with all keys unset unless overridden.""" ⋮---- base = dict ⋮---- def assertion i list tools missi… 证据：`probes/phase-4-probe.py`
- **Diag**（source_file）：REPO ROOT = os.path.abspath os.path.join os.path.dirname file , ".." ⋮---- def main - int ⋮---- command = sys.argv 1 ⋮---- config = load config ⋮---- response = asyncio.run run fetch url sys.argv 2 , config ⋮---- providers = build providers config response = asyncio.run run search health config, providers 证据：`scripts/diag.py`
- **Query**（source_file）：REPO ROOT = os.path.abspath os.path.join os.path.dirname file , ".." ⋮---- def main - int ⋮---- query = sys.argv 1 max results = int sys.argv 2 if len sys.argv 2 else 5 mode = sys.argv 3 if len sys.argv 3 else "balanced" ⋮---- config = load config providers = build providers config response = asyncio.run 证据：`scripts/query.py`
- **SSRF guard is explicitly disabled; skip DNS and pinning entirely.**（source_file）：log = get logger name ⋮---- THIN TEXT THRESHOLD CHARS = 200 ⋮---- MAX REDIRECT HOPS = 5 ⋮---- EXTRACTABLE CONTENT TYPES = "text/html", "application/xhtml+xml" ⋮---- CGNAT NET = ipaddress.ip network "100.64.0.0/10" ⋮---- robots cache: dict tuple str, str, int , Optional RobotFileParser = {} ⋮---- class FetchDeadlineExceeded Exception ⋮---- def remaining deadline: float - float ⋮---- remaining = deadline - time.monotonic ⋮---- def effective port parts - int ⋮---- def ip is blocked ip: ipaddress.IPv4Address ipaddress.IPv6Address - bool ⋮---- @contextlib.contextmanager def pin dns hostname: str, pinned ip: str ⋮---- real = socket.getaddrinfo ip obj = ipaddress.ip address pinned ip is v6 = isins… 证据：`tools/fetch_url.py`
- **Canonical provider order for report entries matches the**（source_file）：log = get logger name ⋮---- PROBE QUERY = "ping" PROBE MAX RESULTS = 1 ⋮---- PROVIDER ERRORS = BraveError, ExaError, SerperError HTTP CODE RE = re.compile r"returned HTTP \d{3} " AUTH FAILURE CODES = {401, 403, 422} ⋮---- Canonical provider order for report entries matches the build providers registration order in providers/ init .py . PROVIDER ORDER = "brave", "exa", "serper" ⋮---- def enabled map config: Config - dict str, bool ⋮---- def mode availability config: Config - dict str, dict ⋮---- enabled = {name for name, on in enabled map config .items if on} modes: dict str, dict = {} ⋮---- present = wanted & enabled missing = sorted wanted - enabled entry: dict = {"available": bool present… 证据：`tools/search_health.py`
- **Search Web**（source_file）：log = get logger name ⋮---- Mode = Literal "balanced", "recall", "precision" ALLOWED MODES = {"balanced", "recall", "precision"} ⋮---- MODE ROUTING: dict str, frozenset str = { ⋮---- PROVIDER REQUIRED ENV: dict str, str = { ⋮---- DOMAIN DIVERSITY THRESHOLD = 0.70 PROVIDER DOMINANCE THRESHOLD = 0.90 ⋮---- CONF OVERLAP BOOST = 0.2 CONF TRUSTED BOOST = 0.1 CONF RECENT BOOST = 0.1 ⋮---- def clamp max results requested: int, default: int, upper bound: int - int ⋮---- def normalize mode mode: str - Mode ⋮---- wanted = MODE ROUTING mode available names = {p.name for p in available} selected = p for p in available if p.name in wanted missing = sorted wanted - available names ⋮---- warnings: list st… 证据：`tools/search_web.py`

## 宿主 AI 必须遵守的规则

- **把本资产当作开工前上下文，而不是运行环境。**：AI Context Pack 只包含证据化项目理解，不包含目标项目的可执行状态。 证据：`README.md`, `.env.example`, `docker-compose.yml`
- **回答用户时区分可预览内容与必须安装后才能验证的内容。**：安装前体验的消费者价值来自降低误装和误判，而不是伪装成真实运行。 证据：`README.md`, `.env.example`, `docker-compose.yml`

## 用户开工前应该回答的问题

- 你准备在哪个宿主 AI 或本地环境中使用它？
- 你只是想先体验工作流，还是准备真实安装？
- 你最在意的是安装成本、输出质量、还是和现有规则的冲突？

## 验收标准

- 所有能力声明都能回指到 evidence_refs 中的文件路径。
- AI_CONTEXT_PACK.md 没有把预览包装成真实运行。
- 用户能在 3 分钟内看懂适合谁、能做什么、如何开始和风险边界。

---

## Doramagic Context Augmentation

下面内容用于强化 Repomix/AI Context Pack 主体。Human Manual 只提供阅读骨架；踩坑日志会被转成宿主 AI 必须遵守的工作约束。

## Human Manual 骨架

使用规则：这里只是项目阅读路线和显著性信号，不是事实权威。具体事实仍必须回到 repo evidence / Claim Graph。

宿主 AI 硬性规则：
- 不得把页标题、章节顺序、摘要或 importance 当作项目事实证据。
- 解释 Human Manual 骨架时，必须明确说它只是阅读路线/显著性信号。
- 能力、安装、兼容性、运行状态和风险判断必须引用 repo evidence、source path 或 Claim Graph。

- **项目概览**：importance `high`
  - source_paths: Dockerfile, README.md, pyproject.toml
- **Providers 模块**：importance `high`
  - source_paths: providers/__init__.py, providers/base.py, providers/brave.py, providers/exa.py, providers/serper.py
- **Utils 模块**：importance `high`
  - source_paths: utils/__init__.py, utils/cache.py, utils/config.py, utils/fetch_cache.py, utils/logging.py
- **Models 模块**：importance `high`
  - source_paths: models/__init__.py, models/search_result.py

## Repo Inspection Evidence / 源码检查证据

- repo_clone_verified: true
- repo_inspection_verified: true
- repo_commit: `d1b28df7493a27006761212a107e73bc1b2f46d2`
- inspected_files: `Dockerfile`, `README.md`, `docker-compose.yml`, `pyproject.toml`, `uv.lock`

宿主 AI 硬性规则：
- 没有 repo_clone_verified=true 时，不得声称已经读过源码。
- 没有 repo_inspection_verified=true 时，不得把 README/docs/package 文件判断写成事实。
- 没有 quick_start_verified=true 时，不得声称 Quick Start 已跑通。

## Doramagic Pitfall Constraints / 踩坑约束

这些规则来自 Doramagic 发现、验证或编译过程中的项目专属坑点。宿主 AI 必须把它们当作工作约束，而不是普通说明文字。

### Constraint 1: 能力判断依赖假设

- Trigger: README/documentation is current enough for a first validation pass.
- Host AI rule: 将假设转成下游验证清单。
- Why it matters: 假设不成立时，用户拿不到承诺的能力。
- Evidence: capability.assumptions | https://github.com/jimmytbc/web_search_mcp | README/documentation is current enough for a first validation pass.
- Hard boundary: 不要把这个坑点包装成已解决、已验证或可忽略，除非后续验证证据明确证明它已经关闭。

### Constraint 2: 维护活跃度未知

- Trigger: 未记录 last_activity_observed。
- Host AI rule: 补 GitHub 最近 commit、release、issue/PR 响应信号。
- Why it matters: 新项目、停更项目和活跃项目会被混在一起，推荐信任度下降。
- Evidence: evidence.maintainer_signals | https://github.com/jimmytbc/web_search_mcp | last_activity_observed missing
- Hard boundary: 不要把这个坑点包装成已解决、已验证或可忽略，除非后续验证证据明确证明它已经关闭。

- Trigger: no_demo
- Evidence: downstream_validation.risk_items | https://github.com/jimmytbc/web_search_mcp | no_demo; severity=medium
- Hard boundary: 不要把这个坑点包装成已解决、已验证或可忽略，除非后续验证证据明确证明它已经关闭。

### Constraint 4: 存在评分风险

- Trigger: no_demo
- Why it matters: 风险会影响是否适合普通用户安装。
- Evidence: risks.scoring_risks | https://github.com/jimmytbc/web_search_mcp | no_demo; severity=medium
- Hard boundary: 不要把这个坑点包装成已解决、已验证或可忽略，除非后续验证证据明确证明它已经关闭。

### Constraint 5: issue/PR 响应质量未知

- Trigger: issue_or_pr_quality=unknown。
- Host AI rule: 抽样最近 issue/PR，判断是否长期无人处理。
- Why it matters: 用户无法判断遇到问题后是否有人维护。
- Evidence: evidence.maintainer_signals | https://github.com/jimmytbc/web_search_mcp | issue_or_pr_quality=unknown
- Hard boundary: 不要把这个坑点包装成已解决、已验证或可忽略，除非后续验证证据明确证明它已经关闭。

### Constraint 6: 发布节奏不明确

- Trigger: release_recency=unknown。
- Host AI rule: 确认最近 release/tag 和 README 安装命令是否一致。
- Why it matters: 安装命令和文档可能落后于代码，用户踩坑概率升高。
- Evidence: evidence.maintainer_signals | https://github.com/jimmytbc/web_search_mcp | release_recency=unknown
- Hard boundary: 不要把这个坑点包装成已解决、已验证或可忽略，除非后续验证证据明确证明它已经关闭。
