主题
MCP 与 Skills 扩展配置
本章目标:
- 讲清
extensions_config.json的结构(mcpServers映射与skills映射)及其字段语义,理解为什么这套「扩展配置」要独立于config.yaml。- 掌握配置文件的四级解析优先级(显式参数 → 环境变量 → 项目根 → 兼容回退),以及与
config.yaml的职责分工。- 理解运行时通过 Gateway API(
PUT /api/mcp/config、PUT /api/skills/{name})更新配置后,如何落盘并触发跨进程缓存失效。
TL;DR
extensions_config.json 是 DeerFlow 的「可热更新扩展配置」,用一个 JSON 文件统一描述 MCP 服务器(mcpServers)和技能开关(skills)。它与定义系统骨架的 config.yaml 职责分离:config.yaml 描述模型/工具/沙箱等启动期结构,extensions_config.json 描述用户随时可增删的外部能力。Gateway API 写入该文件后,LangGraph 运行进程通过文件 mtime 比对自动失效 MCP 工具缓存与技能系统提示词缓存,无需重启。本章只讲配置层面;MCP/Skills 的运行机制见第 21 章与第 22 章。
Overview
为什么不把 MCP 和 skills 也塞进 config.yaml?核心原因是变更频率与责任主体不同。
config.yaml 描述的是系统骨架:模型类路径、工具组、沙箱 provider、记忆参数等。这些字段在部署时确定,改动需要谨慎,通常由运维或开发者维护。而 MCP 服务器和技能开关是终端用户在运行时频繁增删的扩展能力——用户可能上午接入一个 GitHub MCP server,下午又关闭某个技能。把这类高频、用户驱动的状态独立成一个专用 JSON 文件,带来三个好处:
- 独立写入面:Gateway 的
PUT /api/mcp/config与PUT /api/skills/{name}只需序列化一个结构简单的 JSON 文件,不必触碰庞大且含敏感模型密钥的config.yamlbackend/app/gateway/routers/mcp.py:155。 - 跨进程缓存可观测:扩展配置是「可选的」——找不到文件就返回空配置 backend/packages/harness/deerflow/config/extensions_config.py:137-139。Gateway 进程写文件,LangGraph 运行进程靠 mtime 比对发现变更 backend/packages/harness/deerflow/mcp/cache.py:49-51。
- 格式贴合生态:
mcpServers这个 key 名(通过 pydantic alias 映射到mcp_servers)与社区 MCP 客户端的约定一致 backend/packages/harness/deerflow/config/extensions_config.py:60-64。
Architecture
扩展配置子系统由「数据模型」「路径解析」「单例缓存」「Gateway 写入端点」「跨进程失效」五块组成。
Source 列表:
- backend/packages/harness/deerflow/config/extensions_config.py —
ExtensionsConfig数据模型、路径解析、单例缓存。 - backend/packages/harness/deerflow/config/skills_config.py —
SkillsConfig,即config.yaml中的skills段(目录位置),与扩展配置的技能开关互补。 - backend/packages/harness/deerflow/config/runtime_paths.py —
project_root()/existing_project_file(),路径优先级的底层支撑。 - backend/app/gateway/routers/mcp.py —
GET/PUT /api/mcp/config端点。 - backend/app/gateway/routers/skills.py —
PUT /api/skills/{skill_name}端点。 - backend/packages/harness/deerflow/mcp/cache.py — MCP 工具缓存与 mtime 失效逻辑。
Components / Subsystems
ExtensionsConfig 数据模型
顶层模型只有两个字段:mcp_servers(JSON 中写作 mcpServers,通过 pydantic alias 映射)和 skills。模型设置了 extra="allow" 与 populate_by_name=True,因此既能接受 mcpServers 别名也能接受 mcp_servers,且未知字段(如示例文件中的 mcpInterceptors)不会报错而被保留 backend/packages/harness/deerflow/config/extensions_config.py:57-69。
McpServerConfig 描述单个 MCP 服务器:enabled 默认 True;type 默认 stdio(另支持 sse、http);stdio 用 command + args + env;sse/http 用 url + headers + 可选 oauth backend/packages/harness/deerflow/config/extensions_config.py:36-48。
McpOAuthConfig 为 HTTP/SSE 传输提供 OAuth token 注入,支持 client_credentials 与 refresh_token 两种 grant,并可配置 token 字段名与刷新提前量(refresh_skew_seconds 默认 60 秒)backend/packages/harness/deerflow/config/extensions_config.py:13-33。
SkillStateConfig 极简,只有一个 enabled 布尔 backend/packages/harness/deerflow/config/extensions_config.py:51-54。技能的元数据(名称、描述、license)来自磁盘上的 SKILL.md,而非这里——这个文件只记开关状态。
环境变量解析
from_file() 读入 JSON 后会递归调用 resolve_env_variables():任何以 $ 开头的字符串值都被替换为同名环境变量;若环境变量不存在,则替换为空字符串(而非保留字面 $VAR,避免下游 MCP server 收到无效的 token)backend/packages/harness/deerflow/config/extensions_config.py:151-180。这与 config.yaml 的 $ 解析行为一致,因此可以把 "GITHUB_TOKEN": "$GITHUB_TOKEN" 写进配置而不泄露密钥 extensions_config.example.json:26-28。
单例缓存与失效函数
模块级 _extensions_config 单例由 get_extensions_config() 惰性构建 backend/packages/harness/deerflow/config/extensions_config.py:210-222。配套三个管理函数:reload_extensions_config() 重新从文件加载并替换缓存 backend/packages/harness/deerflow/config/extensions_config.py:225-240;reset_extensions_config() 清空缓存;set_extensions_config() 注入 mock(测试用)backend/packages/harness/deerflow/config/extensions_config.py:243-263。
注意技能加载路径不依赖这个单例:load_skills() 每次调用都直接 ExtensionsConfig.from_file() 重新读盘并合并 enabled 状态,以便另一个进程的写入能立即被感知 backend/packages/harness/deerflow/skills/storage/skill_storage.py:231-238。技能默认启用规则:配置里没有该技能条目时,public 和 custom 分类默认启用 backend/packages/harness/deerflow/config/extensions_config.py:200-204。
Gateway 写入端点
PUT /api/mcp/config 接收 mcp_servers 映射,但写文件前会先读取当前配置以保留 skills 段,反之 PUT /api/skills/{name} 写文件时也保留 mcpServers 段——两个端点共享同一个文件,各自只改自己负责的部分 backend/app/gateway/routers/mcp.py:145-156 backend/app/gateway/routers/skills.py:324-333。PUT /api/skills/{name} 还会额外调用 refresh_skills_system_prompt_cache_async() 失效系统提示词中的技能清单缓存 backend/app/gateway/routers/skills.py:336-337 backend/packages/harness/deerflow/agents/lead_agent/prompt.py:163-164。
Data Flow
下图展示用户在 Gateway UI 关闭一个技能后,配置如何落盘并跨进程失效缓存。
跨进程失效的关键在 mcp/cache.py:Gateway 进程写文件后,LangGraph 运行进程并不会被主动通知。它在 get_cached_mcp_tools() 时调用 _is_cache_stale(),把缓存初始化时记录的 _config_mtime 与当前文件 mtime 比较,只要文件变新就 reset_mcp_tools_cache() 重新加载 backend/packages/harness/deerflow/mcp/cache.py:82-101。这就是为什么 PUT /api/mcp/config 的注释明确写「无需在此 reload/reset,LangGraph Server 会通过 mtime 自动检测」backend/app/gateway/routers/mcp.py:160-161。
速查表
extensions_config.json 字段矩阵:
| 字段路径 | 类型 | 默认值 | 说明 | Source |
|---|---|---|---|---|
mcpServers | object | {} | MCP 服务器名 → 配置映射(别名 mcp_servers) | extensions_config.py:60-64 |
mcpServers.*.enabled | bool | true | 是否启用该服务器 | extensions_config.py:39 |
mcpServers.*.type | str | stdio | 传输类型:stdio / sse / http | extensions_config.py:40 |
mcpServers.*.command | str|null | null | stdio 启动命令 | extensions_config.py:41 |
mcpServers.*.args | list | [] | stdio 命令参数 | extensions_config.py:42 |
mcpServers.*.env | dict | {} | 注入服务器的环境变量(支持 $VAR) | extensions_config.py:43 |
mcpServers.*.url | str|null | null | sse/http 服务器 URL | extensions_config.py:44 |
mcpServers.*.headers | dict | {} | sse/http HTTP 头 | extensions_config.py:45 |
mcpServers.*.oauth | object|null | null | HTTP/SSE 的 OAuth token 注入配置 | extensions_config.py:46 |
mcpServers.*.oauth.grant_type | str | client_credentials | OAuth grant:client_credentials / refresh_token | extensions_config.py:18-21 |
mcpServers.*.oauth.refresh_skew_seconds | int | 60 | 过期前多少秒刷新 token | extensions_config.py:31 |
mcpServers.*.description | str | "" | 人类可读的服务器描述 | extensions_config.py:47 |
skills | object | {} | 技能名 → 状态映射 | extensions_config.py:65-68 |
skills.*.enabled | bool | true | 技能是否启用(无条目时 public/custom 默认启用) | extensions_config.py:54 |
mcpInterceptors(额外字段) | list | — | extra="allow" 保留,运行机制见第 21 章 | extensions_config.example.json:1-4 |
Configuration
extensions_config.json 路径解析优先级
resolve_config_path() 按以下顺序查找,扩展配置本身是可选的,全部落空时返回 None backend/packages/harness/deerflow/config/extensions_config.py:71-122:
| 优先级 | 来源 | 行为 | Source |
|---|---|---|---|
| 1 | 显式 config_path 参数 | 文件不存在则抛 FileNotFoundError | extensions_config.py:95-99 |
| 2 | DEER_FLOW_EXTENSIONS_CONFIG_PATH 环境变量 | 文件不存在则抛 FileNotFoundError | extensions_config.py:100-104 |
| 3 | 项目根的 extensions_config.json,回退 mcp_config.json | 由 existing_project_file() 在 project_root() 下查找 | extensions_config.py:106-108 |
| 4 | monorepo 兼容回退:backend/ 与仓库根下的 extensions_config.json / mcp_config.json | 依次探测,命中即返回 | extensions_config.py:110-119 |
| 5 | 全部落空 | 返回 None,from_file() 给出空配置 | extensions_config.py:121-139 |
其中第 3 步的「项目根」由 project_root() 决定:优先读环境变量 DEER_FLOW_PROJECT_ROOT,否则取当前工作目录 Path.cwd() backend/packages/harness/deerflow/config/runtime_paths.py:7-16。
与 config.yaml 的职责分工
技能的「目录在哪、容器里挂载到哪」由 config.yaml 的 skills 段(SkillsConfig)决定;技能的「哪些开/关」由 extensions_config.json 的 skills 段决定。二者互补:
| 关注点 | 文件 | 字段 | Source |
|---|---|---|---|
| 技能存储实现类 | config.yaml | skills.use | skills_config.py:19-22 |
| 技能目录主机路径 | config.yaml | skills.path(默认项目根 skills/,可被 DEER_FLOW_SKILLS_PATH 覆盖) | skills_config.py:23-59 |
| 技能容器挂载路径 | config.yaml | skills.container_path(默认 /mnt/skills) | skills_config.py:27-30 |
| 单个技能启用开关 | extensions_config.json | skills.*.enabled | extensions_config.py:51-54 |
| MCP 服务器全部配置 | extensions_config.json | mcpServers.* | extensions_config.py:36-48 |
Common Pitfalls/Tips
mcpServers是 JSON 中的 key,mcp_servers是 Python 字段名。两者通过 pydantic alias 等价,但写 JSON 文件务必用mcpServers(Gateway 序列化时也用这个 key)backend/app/gateway/routers/mcp.py:149-152。$VAR未设置会变成空字符串,不会报错。如果 MCP server 启动失败而报「token 为空」,先检查对应环境变量是否真的导出 backend/packages/harness/deerflow/config/extensions_config.py:167-171。- 缓存失效靠 mtime,不是 inotify。LangGraph 进程只在下一次
get_cached_mcp_tools()调用时才比对 mtime;若文件系统时间精度不足或时钟回拨,可能不会被判定 stale backend/packages/harness/deerflow/mcp/cache.py:42-53。 - 配置文件不存在时不会报错。
from_file()在解析不到路径时返回空ExtensionsConfig,因此「没有任何 MCP/技能」是合法状态;若期望某 MCP 生效却没生效,先确认文件落在了优先级解析能找到的位置 backend/packages/harness/deerflow/config/extensions_config.py:136-139。 - 两个写端点共享一个文件,各自保留对方的段。直接手动编辑文件时,确保
mcpServers与skills两个顶层 key 都保留,否则下一次 API 写入会以内存中的配置为准 backend/app/gateway/routers/skills.py:327-330。 - 改技能开关会失效系统提示词缓存,改 MCP 不会。
PUT /api/skills/{name}显式调用refresh_skills_system_prompt_cache_async();MCP 端点不需要,因为 MCP 工具走的是 mtime 失效路径 backend/app/gateway/routers/skills.py:336-337。
References
- backend/packages/harness/deerflow/config/extensions_config.py —
ExtensionsConfig/McpServerConfig/SkillStateConfig数据模型、路径解析、环境变量解析、单例缓存。 - backend/packages/harness/deerflow/config/skills_config.py —
config.yaml中skills段(目录与挂载路径)。 - backend/packages/harness/deerflow/config/runtime_paths.py —
project_root()/existing_project_file()。 - backend/app/gateway/routers/mcp.py —
GET/PUT /api/mcp/config端点与保留 skills 段逻辑。 - backend/app/gateway/routers/skills.py —
PUT /api/skills/{name}端点与提示词缓存刷新。 - backend/packages/harness/deerflow/mcp/cache.py — MCP 工具缓存与 mtime 失效。
- backend/packages/harness/deerflow/skills/storage/skill_storage.py —
load_skills()每次读盘合并enabled状态。 - extensions_config.example.json — 配置示例文件。
Related Pages
| 页面 | 关系 |
|---|---|
| 04-配置系统与AppConfig | 上位:config.yaml 主配置体系与 AppConfig,本章的扩展配置与之职责互补 |
| 05-模型配置与Model工厂 | 同组:config.yaml 中模型配置的 $ 环境变量解析与本章一致 |
| 21-MCP集成 | 下游:本章只讲 MCP 配置,服务器连接、工具加载、OAuth 注入的运行机制见此 |
| 22-技能系统 | 下游:本章只讲技能开关配置,技能发现、解析、注入系统提示词的运行机制见此 |
| 09-Harness与App分层边界 | 相关:配置模型在 harness 层、写端点在 app 层,跨进程失效设计与分层边界相关 |