fix(#3384): strip mcp__* tool grants from zcode-installed subagents (#3483)

* fix(#3384): strip mcp__* tool grants from zcode-installed subagents

ZCode's dispatcher treats every mcp__<server>__* entry in an agent's
tools: frontmatter as a required MCP server and hard-fails the subagent
spawn (CONFIGURATION_ERROR) when it is not connected, whereas Claude Code
treats the same grants as an optional allowlist. ZCode shared Claude's
verbatim agents copy (converter: null), so all 8 MCP-granted agents
failed to spawn out of the box with zero MCP servers configured.

Add convertClaudeAgentToZcodeAgent — a line-surgical converter that
filters mcp__* entries out of the frontmatter tools: grant list (both
inline comma and YAML block-list shapes) and preserves every other byte.
Declare it on both of zcode's capability.json agents entries and cut
zcode over to the descriptor-driven agents path
(_DESCRIPTOR_AGENTS_RUNTIMES) so the legacy inline loop stops
deleting+re-copying the converted agents raw. Claude Code, Kimi, and
Gemini install behavior is unchanged.

* chore(#3384): link changeset fragment to pr 3483

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-14 11:49:01 -04:00
committed by GitHub
parent 362d0434b2
commit 967bddba37
8 changed files with 293 additions and 9 deletions

View File

@@ -2370,6 +2370,102 @@ function convertClaudeAgentToQwenAgent(content) {
return `${fm}\n${body}`;
}
/**
* Convert a Claude Code agent .md for ZCode (#3384).
*
* ZCode is Claude-shaped (same frontmatter, same named-dispatch subagents), so
* the file is preserved verbatim EXCEPT the `tools:` grant list: ZCode's
* dispatcher treats every `mcp__<server>__*` entry as a REQUIRED MCP server and
* hard-fails the subagent spawn (CONFIGURATION_ERROR: "Required MCP server is
* not connected") whenever it is not connected, whereas Claude Code treats the
* same entries as an optional allowlist. The `mcp__*` entries are stripped at
* install time — the same exclusion Kimi's converter applies via
* convertKimiToolName — so subagent spawns succeed with zero MCP servers
* configured; connected servers' tools remain reachable (auto-discovered by the
* host, not granted by frontmatter).
*
* Line-surgical by design: ONLY `tools:` lines inside the frontmatter are
* touched, so every other byte (description, color, commented-out blocks, the
* body) survives identically. Handles both shapes GSD emits — the inline comma
* list (`tools: A, B, C`) and the YAML block list (`tools:` + `- A` items).
* An agent whose filtered grant list becomes empty (every grant was `mcp__*`)
* drops the `tools:` key entirely: an absent key inherits the full toolkit,
* which is the degrade-gracefully outcome, never a toolless subagent.
*
* Byte-identical for an agent with no `mcp__*` grants (the common case) and
* for an agent with no frontmatter at all.
*/
function convertClaudeAgentToZcodeAgent(content) {
// Fast path: no MCP grant token anywhere means nothing to strip. (A body
// mention alone is not a grant — the line scan below finds no tools-line
// change and returns `content` unchanged anyway; this just skips the scan.)
if (!content.includes('mcp__')) return content;
const lines = content.split('\n');
if (lines[0] !== '---') return content;
let fmEnd = -1;
for (let i = 1; i < lines.length; i++) {
if (lines[i] === '---') {
fmEnd = i;
break;
}
}
if (fmEnd === -1) return content; // unterminated frontmatter — leave verbatim
const out = [];
let changed = false;
let i = 1;
while (i < fmEnd) {
const line = lines[i];
const inlineTools = /^tools:[ \t]*(.+)$/.exec(line);
if (inlineTools) {
const grants = inlineTools[1].split(',').map((tool) => tool.trim()).filter((tool) => tool !== '');
const kept = grants.filter((tool) => !tool.startsWith('mcp__'));
if (kept.length === grants.length) {
out.push(line); // no mcp__* grants — keep the line byte-identical
} else if (kept.length > 0) {
out.push(`tools: ${kept.join(', ')}`);
changed = true;
} else {
changed = true; // every grant was mcp__*: drop the tools key entirely
}
i++;
continue;
}
if (/^tools:[ \t]*$/.test(line)) {
// Block-list form: collect the following `- item` lines.
const items = [];
let j = i + 1;
while (j < fmEnd && /^([ \t]*)-[ \t]*(\S.*)$/.test(lines[j])) {
items.push(lines[j]);
j++;
}
const kept = items.filter((item) => {
const name = /^([ \t]*)-[ \t]*(\S.*)$/.exec(item)[2].trim();
return !name.startsWith('mcp__');
});
if (kept.length !== items.length) {
changed = true;
if (kept.length > 0) {
out.push(line);
out.push(...kept);
} // else: drop the tools key and all its items
} else {
out.push(line, ...items);
}
i = j;
continue;
}
out.push(line);
i++;
}
if (!changed) return content;
// Opening delimiter + transformed frontmatter + closing delimiter + body.
out.unshift(lines[0]);
out.push(...lines.slice(fmEnd));
return out.join('\n');
}
function convertClaudeAgentToCodebuddyAgent(content) {
const converted = convertClaudeToCodebuddyMarkdown(content);
@@ -3139,6 +3235,11 @@ export = {
// conversionExports[converterName] dispatch (runtime-artifact-layout.cts)
// can resolve it from capabilities/qwen/capability.json's agents kind.
convertClaudeAgentToQwenAgent,
// #3384: ZCode agents are Claude-shaped but its dispatcher treats mcp__*
// tools grants as required MCP servers — registered by name for the same
// conversionExports[converterName] dispatch, resolved from
// capabilities/zcode/capability.json's agents kind.
convertClaudeAgentToZcodeAgent,
// #1511 ADR-1508 Phase 2: rewrite engine deep seam
// Low-level walkers (pathPrefix + attribution pre-resolved by caller):
applyRuntimeContentRewritesInPlace,