Skip to content

enhancement(installer): config-driven MCP tool grants for GSD agents, applied at install for every runtime #4032

Description

@davdittrich

Pre-submission checklist

  • I have confirmed this improves existing behavior — it does not add a new command, workflow, or concept
  • I have searched existing issues and this enhancement has not already been proposed
  • I have read CONTRIBUTING.md and understand I must wait for approved-enhancement before writing any code
  • I can clearly describe the concrete benefit — not just "it would be nicer"

What existing feature or behavior does this improve?

Install-time agent tool policy. applyAgentFrontmatterExtensions (src/runtime-artifact-conversion.cts:3572-3599) already injects per-agent tool policy into agent artifacts as they are written — an effort: key, and a disallowedTools: key sourced from the hardcoded READONLY_AGENT_DISALLOWED_TOOLS map (:3540-3548).

The mechanism exists. It is hardcoded, subtractive-only, and reaches one runtime. This proposes generalizing it, not introducing it.

Current behavior

There is no supported way to grant a GSD agent an MCP server.

The tools: frontmatter in agents/*.md is the only additive mechanism available. Host config cannot substitute: Claude Code's settings.json can only subtract subagent tools (permissions.deny), and mcpServers: frontmatter is ignored outright for plugin subagents. So a user running a code-intelligence MCP server cannot let gsd-executor or gsd-code-reviewer use it — those agents fall back to Grep/Glob and re-derive structure an index already holds.

Every workaround decays:

  • Editing installed agents/*.md by handsrc/install-engine.cts:569 writes agent files as-is, so the next gsd install reverts it, and a plugin update lands a new version directory that never had the edit.
  • Patching from a post-install or host hook — per-machine and per-host, reverted by the same two paths, and a Claude hook does nothing for codex, zcode, or antigravity.
  • Forking — disproportionate for a two-entry allowlist.

The result is per-machine drift in files that are supposed to be derived artifacts.

Proposed behavior

A config key, resolved at install time, that appends tools to each agent's existing allowlist.

Reuse the model_overrides resolver pattern verbatim (src/install-model-override-resolver.cts:71, :100, :253) — global ~/.gsd/defaults.json merged with project .planning/config.json, project winning on conflict:

{
  "agent_tools": {
    "*":            ["mcp__codegraph__*", "mcp__serena__*"],
    "gsd-executor": ["mcp__headroom__*"]
  }
}

Additive only — entries append, never replace, so each agent's deliberate least-privilege posture is preserved. "*" applies to all GSD agents; a per-agent key adds on top.

Apply it between steps 2 and 3 of the per-file pipeline, not at step 4. The pipeline is documented at src/install-profiles.cts:936-942 (ADR-1235 §1):

1. applyAgentPathRewrites
2. processAttribution
                                    <- HERE
3. converter                        <- derives per-runtime permissions FROM the `tools:` line
4. applyAgentFrontmatterExtensions  <- "no-op for a runtime that declares nothing,
                                        e.g. every non-Claude runtime today"
5. normalizeAgentBodyForRuntime

Step 4 is the wrong seam for two reasons, both load-bearing:

  1. It is Claude-only by construction. It early-returns unless the runtime descriptor declares hostBehaviors.agentFrontmatterExtensions, and Claude is the sole declared consumer (capabilities/claude/capability.json:108["effort"]). Extending it there reproduces the per-host problem this proposal exists to remove.
  2. It runs after the converter. Each runtime has already translated tools: into its own permission model by then, so a late edit is invisible in every non-Claude artifact.

Applying before conversion means all 18 runtimes in runtimeTierDefaults inherit the grant with no per-runtime work and no new frontmatter key — which matters, because the same docstring notes OpenCode/Qwen/Hermes reject unknown frontmatter keys. Extending an existing value is safe where adding a key is not.

Reason and benefit

Concrete and measurable: a GSD agent that can query a code-intelligence MCP server answers structural questions in one call instead of a grep/read loop across many files. On an indexed repo that is the difference between one tool call and dozens, per agent, per phase — GSD dispatches agents constantly, so it compounds.

The second benefit is correctness of a promise already made. gsd-core ships agents whose tools: lines already carry MCP wildcards — agents/gsd-project-researcher.md:4 lists mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*. So MCP grants in agent frontmatter are established, shipped idiom. There is simply no way for a user to add one.

Scope of changes

  • src/ — one resolver mirroring install-model-override-resolver.cts, plus one call site in the staging pipeline before the converter.
  • tests/ — both tools: YAML forms round-trip; global/project merge precedence; additive (non-replacing) contract; idempotency across repeated installs.
  • Docs — the config key.

Explicitly out of scope: any change to the shipped agents/*.md allowlists. The grant is a user's, not a shipped default.

Breaking changes

None. Absent the config key, behavior is byte-identical — the resolver returns nothing and the tools: line is untouched. It is opt-in and additive; no agent can lose a tool it has today, and read-only agents keep their disallowedTools.

One behavior worth stating: an entry resolving to no tool is harmless, because a subagent fails to launch only when no entry in its list resolves, and built-ins always do. The six MCP wildcards already shipping on gsd-project-researcher demonstrate this in practice.

Alternatives considered

Ranked on performance, then simplicity/LOC, then ecosystem support, then maintenance.

Mechanism Perf Simplicity / LOC Ecosystem Maintenance Verdict
1. Config-driven augmentation of tools: before the converter 0 — install-time only ~1 resolver reusing the model_overrides shape + 1 call site All 18 runtimes, no per-runtime work One seam; new runtimes inherit it automatically CHOSEN
2. Add a 'tools' flag to agentFrontmatterExtensions, inject at step 4 0 Smaller — the injection helper exists Claude only unless all 18 descriptors opt in; runs after conversion so non-Claude artifacts never see it 18 descriptors to touch and the ordering bug remains Rejected — reproduces the problem it is meant to solve
3. Hardcode the MCP servers into agents/*.md upstream 0 Smallest diff All runtimes Forces every user to carry specific servers; unmaintainable as the list grows Rejected — opinionated defaults, not a mechanism
4. Post-install or host hook that rewrites agent files 0 ~100 LOC of frontmatter parsing across two YAML forms Per-host; a Claude hook does nothing for codex or zcode Silently reverted by the next install or plugin update; per-machine Rejected — this is the status quo workaround, restated
5. Omit tools: so agents inherit every subagent tool 0 -1 line per agent All runtimes Destroys per-agent least privilege; read-only auditors gain Write Rejected — the allowlists are deliberate

Area affected

Installer (primary), agents, config.

Additional context

Agents whose tools: is a YAML block list rather than an inline CSV — gsd-security-auditor, gsd-nyquist-auditor — must keep that form; a correct implementation round-trips both.

Acceptance criteria I would expect:

  1. agent_tools in ~/.gsd/defaults.json grants the listed tools to every GSD agent after gsd install, on at least Claude, codex, and one kilo-family runtime.
  2. .planning/config.json overrides the global value per project.
  3. Grants are additive — no agent loses a tool, and read-only agents keep their disallowedTools.
  4. Both tools: YAML forms round-trip.
  5. Re-running gsd install is idempotent — no duplicate entries.

Companion issue: adding crush as a supported runtime. The two are independent by design — because this proposal acts before per-runtime conversion, a new runtime inherits the feature the moment its descriptor exists, with no second change and no ordering dependency.

Metadata

Metadata

Assignees

No one assigned

    Labels

    approved-enhancementEnhancement approved — contributor may begin coding

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions