English summary — EnvYamlSchema is z.object({ variables: z.array(...).default([]) }).
A env/env.yaml written in the natural shorthand form (JIRA_PASSWORD: "x") has no
top-level variables: key, so zod strips it and the .default([]) silently yields an
empty array. EnvHandler.pullItem() then hits if (envConfig.variables.length === 0) return;
and returns with no output of any kind. Result: the variable table is empty for the
whole run, every MCP entry referencing ${VAR} is skipped across all tools with
unresolved variable(s), and teamai doctor still prints All checks passed!.
Nothing in the output points at env.yaml's shape being the cause.
Description
env/env.yaml 用最自然的写法时:
JIRA_PASSWORD: "<secret>"
teamai pull 会完全静默地把变量表当成空的,连带把所有工具的 MCP 注入全部跳过,但输出里没有任何一行指向 env.yaml 的结构问题。
现象链:
$ teamai pull
[mcp] claude/jira: skipped — unresolved variable(s): JIRA_PASSWORD
[mcp] cursor/jira: skipped — unresolved variable(s): JIRA_PASSWORD
[mcp] codebuddy/jira: skipped — unresolved variable(s): JIRA_PASSWORD
...(每个启用的工具各一行)
$ teamai mcp list
secrets: (none)
installed: (none)
$ teamai doctor
✔ All checks passed!
于是用户看到的是「MCP 装不上」,自然去查 MCP 定义、查工具配置、查网络——而真正的原因在 env/env.yaml 的顶层键名上,且这条信息在整个输出里一次都没出现过。
改成 variables: 数组后,同样的定义立刻全部注入成功:
variables:
- key: JIRA_PASSWORD
value: "<secret>"
$ teamai mcp list
secrets: JIRA_PASSWORD (all set)
installed: claude, cursor, codebuddy
也就是说:一个 YAML 键名的差异,决定全部 MCP 注入的成败,而 CLI 对这两种写法给出的反馈完全一样(都是零)。
Root cause
EnvYamlSchema 把 variables 设成了带默认值的数组,而 zod 默认会剥掉未声明的顶层键,所以简写形式不仅不报错,还会"成功地"解析出一个空数组:
EnvYamlSchema = z.object({
variables: z.array(EnvVariableSchema).default([])
});
env handler 拿到空数组后直接 return,没有任何 log:
async pullItem(item, teamConfig, localConfig) {
const content = await readFileSafe(item.sourcePath);
if (!content) return;
let envConfig;
try {
const raw = YAML7.parse(content);
envConfig = EnvYamlSchema.parse(raw);
} catch (e) {
log.warn(`Invalid env.yaml format: ${e.message}`); // 只有 YAML 语法错才走到这里
return;
}
if (envConfig.variables.length === 0) return; // ← 简写形式落在这,静默返回
...
}
注意 catch 里的 Invalid env.yaml format 只在 YAML 语法错误时触发。简写形式是合法 YAML,只是结构不符,所以连这句警告都不会出现。
同样地,countEnvVars() 也把异常吞掉并返回 0:
async countEnvVars(sourcePath) {
const content = await readFileSafe(sourcePath);
if (!content) return 0;
try {
const raw = YAML7.parse(content);
const envConfig = EnvYamlSchema.parse(raw);
return envConfig.variables.length;
} catch {
return 0; // 静默
}
}
下游 MCP 侧的失败信息也只有一句,指向的是变量名而不是源头:
const { def: resolved, missing } = resolvePlaceholders(raw, vars);
if (missing.length > 0) {
changes.push({
tool: target.tool,
server: raw.name,
action: "skipped",
reason: `unresolved variable(s): ${missing.join(", ")}`
});
continue;
}
而 doctor 里没有任何一项检查会覆盖 env.yaml 的可解析性/非空性,所以三项检查全绿,和实际状态完全脱节。
Reproduction
-
Windows / macOS 任一平台,teamai 0.24.0,团队仓库里放一个用简写写法的 env/env.yaml:
JIRA_PASSWORD: "whatever"
-
团队仓库里有一个引用 ${JIRA_PASSWORD} 的 MCP 定义(例如 mcp/mcp.yaml 中的 jira 服务,env.JIRA_PASSWORD: "${JIRA_PASSWORD}")。
-
teamai pull → 每个启用的工具各打一行 skipped — unresolved variable(s): JIRA_PASSWORD,没有任何一行提到 env.yaml。
-
teamai mcp list → secrets: (none) / installed: (none)。
-
teamai doctor → ✔ All checks passed!(假通过)。
-
只把 env.yaml 改成 variables: 数组,其它一律不动,重跑 teamai pull → 全部注入成功。
Environment
- OS: Windows 11 25H2 (10.0.26200)
- Node.js:
v22.22.2
- teamai:
0.24.0
- Provider: GitHub
- AI tool(s): WorkBuddy / CodeBuddy(但跳过行为对所有工具生效)
Suggested fix
按性价比排序,任一条都能把静默失败变成可诊断的失败:
- 空数组要出声。 把
if (envConfig.variables.length === 0) return; 改成带警告的返回,例如:
log.warn("env/env.yaml resolved to 0 variables. Expected a top-level variables: list of {key, value}.")。
这一句就能把绝大多数此类问题在 5 秒内定位。
- 识别并拒绝简写形式。 把
EnvYamlSchema 收紧为 .strict(),或在解析前检查顶层是否含有非 variables 的键;也可主动兼容 KEY: value 的平铺写法(很多用户的第一直觉就是那样写),二选一都比静默留空好。
countEnvVars() 不要吞异常。 空结果与解析失败是两件事,现在都返回 0,调用方无从区分。
- 给
doctor 加一项检查。 断言「env.yaml 存在 ⇒ 解析出的变量数 > 0 且已写入 env.sh」,否则报错并给出修复提示。目前三项检查全绿而 MCP 一个都没装上的组合,是最容易被误判成"没问题"的状态。
Logs
简写形式下的完整输出(无任何 env.yaml 相关提示)
$ cat env/env.yaml
JIRA_PASSWORD: "***"
$ teamai pull
...
[mcp] claude/jira: skipped — unresolved variable(s): JIRA_PASSWORD
[mcp] cursor/jira: skipped — unresolved variable(s): JIRA_PASSWORD
[mcp] codebuddy/jira: skipped — unresolved variable(s): JIRA_PASSWORD
$ teamai mcp list
secrets: (none)
installed: (none)
$ teamai doctor
✔ All checks passed!
改成 variables: 数组后(其它未动)
$ cat env/env.yaml
variables:
- key: JIRA_PASSWORD
value: "***"
$ teamai mcp list
secrets: JIRA_PASSWORD (all set)
installed: claude, cursor, codebuddy
Description
env/env.yaml用最自然的写法时:teamai pull会完全静默地把变量表当成空的,连带把所有工具的 MCP 注入全部跳过,但输出里没有任何一行指向 env.yaml 的结构问题。现象链:
于是用户看到的是「MCP 装不上」,自然去查 MCP 定义、查工具配置、查网络——而真正的原因在
env/env.yaml的顶层键名上,且这条信息在整个输出里一次都没出现过。改成
variables:数组后,同样的定义立刻全部注入成功:也就是说:一个 YAML 键名的差异,决定全部 MCP 注入的成败,而 CLI 对这两种写法给出的反馈完全一样(都是零)。
Root cause
EnvYamlSchema把variables设成了带默认值的数组,而 zod 默认会剥掉未声明的顶层键,所以简写形式不仅不报错,还会"成功地"解析出一个空数组:env handler 拿到空数组后直接 return,没有任何 log:
注意
catch里的Invalid env.yaml format只在 YAML 语法错误时触发。简写形式是合法 YAML,只是结构不符,所以连这句警告都不会出现。同样地,
countEnvVars()也把异常吞掉并返回 0:下游 MCP 侧的失败信息也只有一句,指向的是变量名而不是源头:
而
doctor里没有任何一项检查会覆盖 env.yaml 的可解析性/非空性,所以三项检查全绿,和实际状态完全脱节。Reproduction
Windows / macOS 任一平台,
teamai0.24.0,团队仓库里放一个用简写写法的env/env.yaml:团队仓库里有一个引用
${JIRA_PASSWORD}的 MCP 定义(例如mcp/mcp.yaml中的 jira 服务,env.JIRA_PASSWORD: "${JIRA_PASSWORD}")。teamai pull→ 每个启用的工具各打一行skipped — unresolved variable(s): JIRA_PASSWORD,没有任何一行提到 env.yaml。teamai mcp list→secrets: (none)/installed: (none)。teamai doctor→✔ All checks passed!(假通过)。只把 env.yaml 改成
variables:数组,其它一律不动,重跑teamai pull→ 全部注入成功。Environment
v22.22.20.24.0Suggested fix
按性价比排序,任一条都能把静默失败变成可诊断的失败:
if (envConfig.variables.length === 0) return;改成带警告的返回,例如:log.warn("env/env.yaml resolved to 0 variables. Expected a top-levelvariables:list of {key, value}.")。这一句就能把绝大多数此类问题在 5 秒内定位。
EnvYamlSchema收紧为.strict(),或在解析前检查顶层是否含有非variables的键;也可主动兼容KEY: value的平铺写法(很多用户的第一直觉就是那样写),二选一都比静默留空好。countEnvVars()不要吞异常。 空结果与解析失败是两件事,现在都返回0,调用方无从区分。doctor加一项检查。 断言「env.yaml 存在 ⇒ 解析出的变量数 > 0 且已写入env.sh」,否则报错并给出修复提示。目前三项检查全绿而 MCP 一个都没装上的组合,是最容易被误判成"没问题"的状态。Logs
简写形式下的完整输出(无任何 env.yaml 相关提示)
改成
variables:数组后(其它未动)