Kdl over mcp #217

Closed
opened 2026-06-19 04:31:03 +00:00 by coilysiren · 4 comments
Owner
No description provided.
Member

Re-triage (goose-triage method, claude-macos-kais-macbook-pro-2 as the judgment engine, 2026-06-24)

  • P4 - Kdl-over-mcp, speculative, empty body, icebox.
  • consult - Vague direction needing a human call.
<!-- goose-triage --> **Re-triage** (goose-triage method, claude-macos-kais-macbook-pro-2 as the judgment engine, 2026-06-24) - **P4** - Kdl-over-mcp, speculative, empty body, icebox. - **consult** - Vague direction needing a human call.
Member

Re-triage (goose-triage method, claude-macos-kais-macbook-pro-2 as the judgment engine, 2026-06-24)

  • P4 - Kdl-over-mcp, speculative, empty body, icebox.
  • consult - Vague direction needing a human call.
<!-- goose-triage --> **Re-triage** (goose-triage method, claude-macos-kais-macbook-pro-2 as the judgment engine, 2026-06-24) - **P4** - Kdl-over-mcp, speculative, empty body, icebox. - **consult** - Vague direction needing a human call.
Member

Flipped headless with a spec-first gate: propose an MCP server exposing ward-kdl verb surfaces (design doc/PR first, Kai reviews there). Recorded by Claude Code (Fable) during the 2026-07-01 ward launch triage session with Kai.

Flipped headless with a spec-first gate: propose an MCP server exposing ward-kdl verb surfaces (design doc/PR first, Kai reviews there). Recorded by Claude Code (Fable) during the 2026-07-01 ward launch triage session with Kai.
Member

🔎 ward agent advisor

ward agent advisor --driver claude ran a one-shot standard research pass on this question:

Design 'KDL over MCP' for ward#217 (empty body) and post a dispatchable spec as a comment. Read-only, no code. Concept: expose ward-kdl's audited guardfile verb surface (ops/exec dialects) over the Model Context Protocol so an MCP-speaking agent can invoke the least-privilege ward-kdl verbs the way a human runs ward ops . Cover: what an MCP server over ward-kdl looks like (tools = the generated verbs, sourced from the SAME specverb/guardfile source of truth so it stays in sync, not a hand-maintained parallel surface), how the read/write/admin tiers map to MCP tool exposure, the auth/token flow (reuse ward-kdl's cred model, never hand a raw token to the MCP client), and whether it is a new ward-kdl mcp server or a wrapper. Relate to docs/ward-kdl-surface.md. Phased breakdown + recommendation. End noting it is a plan to verify. Repo voice rules (no em-dashes, no semicolons in prose, name the actor).


KDL over MCP - design spec for ward#217

Scope note. This is the spec-first deliverable the 2026-07-01 triage asked for: a design for Kai to review before any code. It is a plan to verify, not a landed contract. Nothing below is implemented. Where it names a cli-guard internal (the surface model, surface.Markdown()), that is a claim to confirm against the engine before building, called out in the open questions.

The concept in one line

Expose ward-kdl's already-audited, least-privilege verb surface (the ops/exec guardfile dialects) over the Model Context Protocol, so an MCP-speaking agent invokes the exact same tiered verbs a human runs as ward ops <area> <verb> - with the token sealed behind the guard, never handed to the agent.

What an MCP server over ward-kdl looks like

MCP clients discover capability through tools/list and invoke through tools/call, each tool carrying a JSON-Schema input. The mapping is direct:

  • One MCP tool = one granted ward-kdl verb. can list issue in the forgejo guardfile becomes a tool; an ungranted verb has no tool. The MCP tool list is the machine-readable twin of the flat index in docs/ward-kdl-surface.md.
  • Tool name = the verb path (a stable slug, e.g. forgejo_issue_list, or a namespaced ops.forgejo.issue.list - see open questions).
  • Tool description = the guardfile's describe "..." text.
  • Tool inputSchema = derived from the bound OpenAPI/Swagger operation (specverb already binds each verb to one operation and its parameters) for spec surfaces, or from the can run <verb> grant shape for exec surfaces.

Same source of truth, not a parallel surface. This is the load-bearing design decision. specverb and execverb already parse the guardfile plus spec into an in-memory surface model, and surface.Markdown() walks that model to emit the per-area reference docs. An MCP emitter must be a second emitter over the exact same model - call it surface.MCPTools() - not a hand-maintained tool registry that shells out to the CLI and re-parses stdout. A shell-out wrapper would be a parallel surface that drifts from the guardfiles and loses the audit and restrict paths. In-process, the invariant carries across the MCP boundary for free: add a can grant and rebuild, the tool appears; withhold it, the tool is absent from tools/list, not merely denied on call. That is ward-kdl's whole "absent at compile time, not denied at runtime" property, preserved over MCP.

How read/write/admin tiers map to MCP tool exposure

ward-kdl already compiles the tiers as three separate binaries (ward-kdl-{read,write,admin}) layered by inherit, read ⊂ write ⊂ admin, each its own binary so a withheld verb is absent at compile time (docs/ward-kdl-tiers.md).

The MCP server inherits that model exactly: one tier per server process, fixed at launch, never a runtime argument the client can pass to escalate. A read-tier MCP server literally has no create/edit/delete tools in its tools/list. So the three entry points are ward-kdl-read mcp, ward-kdl-write mcp, ward-kdl-admin mcp, each serving exactly its tier's verb set. Anti-pattern to avoid: a single server exposing a tier selector as a tool argument - that turns a compile-time absence into a runtime check, which is the thing the tier design exists to prevent. The agent's harness config picks the binary (tier) it is allowed to spawn.

Auth / token flow

Reuse ward-kdl's credential model unchanged, and never hand a raw token to the MCP client:

  • Guardfile auth is a value ssm "<path>" reference resolved lazily at call time (docs/guardfile-grammar.md). The source carries the token's location, not the token.
  • Over MCP this is the key security property: the MCP client sees only tool names, schemas, and result bodies. The SSM fetch, the Authorization header injection, and the restrict owner matches <glob> scope gate all happen server-side inside the ward-kdl process, on the exact same specverb/execverb call path the CLI uses. The client holds no credential and cannot widen scope.
  • Transport = stdio first. The agent spawns ward-kdl-read mcp as a subprocess and it inherits the host's AWS/SSM credentials the same way the CLI does. No network-exposed token, no separate client-auth surface. An HTTP/SSE transport would need its own client authentication and is a larger attack surface - defer it until a remote agent genuinely needs it.
  • Audit is not bypassed. Because calls route through the same specverb/execverb path, every MCP tools/call should write the same audit JSONL row as the equivalent CLI invocation (docs/audit.md). The MCP boundary must be an audited surface, not a hole around the log. (Confirm ops verbs already audit - see open questions.)

New ward-kdl mcp server, or a wrapper?

A new mcp verb on the existing binaries, not a wrapper. The adapter should live where the surface model lives (cli-guard, alongside surface.Markdown() and the specverb/execverb executors) and be exposed as an mcp subcommand on the existing ward-kdl / ward-kdl-{read,write,admin} binaries, serving MCP stdio by walking the already-compiled embedded surface and executing through the existing call path. A separate wrapper process that shells ward ops forgejo ... is explicitly rejected - it re-introduces the drift, stdout-parsing, and lost-audit problems above.

Following the ward#284 auto-mount pattern, the user-facing entry should mirror into the product binary as ward mcp (or ward ops <area> mcp), the same way exec-dialect surfaces already auto-mount into ward. That keeps the standard split: authored in cli-guard/ward-kdl, embedded and run in ward.

Relation to docs/ward-kdl-surface.md

ward-kdl-surface.md is the human-readable flat index of the verb surface, emitted from the surface model by surface.Markdown(). The MCP tool list is the machine-readable twin of that same index, from the same model, via a sibling emitter. When this lands, ward-kdl-surface.md gains a short "MCP" section pointing at the new mcp verb, plus a dedicated docs/ward-kdl-mcp.md walkthrough and a docs/FEATURES.md entry.

Phased breakdown

  • Phase 0 - spec gate (this comment). Kai reviews this design in a doc or PR before any code. No implementation until the shape is approved.
  • Phase 1 - surface→MCP emitter (cli-guard). Add surface.MCPTools() (sibling to surface.Markdown()) that walks the compiled surface model and returns MCP tool definitions: name, description from describe, inputSchema from the bound OpenAPI operation params (spec surfaces) or can run grant (exec surfaces). Pure function, unit-tested against the forgejo read tier. No server yet.
  • Phase 2 - stdio MCP server verb. Add the mcp subcommand serving tools/list + tools/call over stdio, executing through the existing specverb/execverb call path so SSM resolution, the restrict gate, and the audit row all come along. One tier per process. Ship the forgejo read tier as the reference vertical slice.
  • Phase 3 - tier + multi-area coverage. Wire the mcp verb across all three tier binaries, then extend to signoz, aws/kubectl exec, and the rest as their tiers land. This dovetails with the ward#339 tiering migration - the placeholder areas (trello, tailscale, glitchtip, glama, skillsmp) light up over MCP as they get real tier verbs.
  • Phase 4 - ward mount + docs + harness registration. Add the ward mcp user entry via the ward#284 auto-mount mechanism. Update ward-kdl-surface.md, add docs/ward-kdl-mcp.md and a FEATURES.md entry, and register the stdio server in the agent harness MCP configs (claude/codex/opencode) so a dispatched agent can spawn its allowed tier.

Recommendation

Build an in-process emitter plus a stdio MCP server on the existing tier binaries (not a shell-out wrapper), one tier per process with tier fixed at launch, tokens sealed behind the existing SSM-lazy cred model so the client never sees a credential, tools generated from the same surface model that emits ward-kdl-surface.md so the two never drift, and an audit row on every call. Start with the forgejo read tier as the thinnest end-to-end slice, then fan out by tier and area.

Open questions - plan to verify before building

  1. Surface model richness. Does cli-guard's surface model already expose per-verb parameter schemas well enough to emit JSON Schema at build time, or does specverb only bind params at call time? Phase 1 depends on this - verify against the surface package API first.
  2. Ops-verb auditing. docs/audit.md names ward exec repo verbs and pkg brew as audited. Confirm whether ops/spec-dialect verbs already write audit rows, since Phase 2 assumes the MCP path inherits that.
  3. Transport. stdio is the recommended default. Confirm no near-term need for a remote HTTP/SSE server before deferring it.
  4. Tool naming. Flat slug (forgejo_issue_list) vs namespaced (ops.forgejo.issue.list) - pick one convention and mirror it in the reference doc.
  5. restrict-owner surfacing. Decide whether the restrict owner matches <glob> gate presents to the client as a fixed server-side filter (out-of-scope calls simply rejected) or is reflected in the tool schema. Rejection is safer and simpler.

This whole design is a plan to verify. The engine claims in questions 1 and 2 gate Phases 1 and 2 and should be checked against cli-guard before an engineer picks up the build.


Researched and posted automatically by ward agent advisor --driver claude (ward#179). This is one-shot research, not a carried change - verify before acting on it.

— Claude (she/her), via ward agent

### 🔎 ward agent advisor `ward agent advisor --driver claude` ran a one-shot **standard** research pass on this question: > Design 'KDL over MCP' for ward#217 (empty body) and post a dispatchable spec as a comment. Read-only, no code. Concept: expose ward-kdl's audited guardfile verb surface (ops/exec dialects) over the Model Context Protocol so an MCP-speaking agent can invoke the least-privilege ward-kdl verbs the way a human runs ward ops <area>. Cover: what an MCP server over ward-kdl looks like (tools = the generated verbs, sourced from the SAME specverb/guardfile source of truth so it stays in sync, not a hand-maintained parallel surface), how the read/write/admin tiers map to MCP tool exposure, the auth/token flow (reuse ward-kdl's cred model, never hand a raw token to the MCP client), and whether it is a new ward-kdl mcp server or a wrapper. Relate to docs/ward-kdl-surface.md. Phased breakdown + recommendation. End noting it is a plan to verify. Repo voice rules (no em-dashes, no semicolons in prose, name the actor). --- ## KDL over MCP - design spec for ward#217 **Scope note.** This is the spec-first deliverable the 2026-07-01 triage asked for: a design for Kai to review before any code. It is a **plan to verify**, not a landed contract. Nothing below is implemented. Where it names a cli-guard internal (the surface model, `surface.Markdown()`), that is a claim to confirm against the engine before building, called out in the open questions. ### The concept in one line Expose ward-kdl's already-audited, least-privilege verb surface (the `ops`/`exec` guardfile dialects) over the Model Context Protocol, so an MCP-speaking agent invokes the exact same tiered verbs a human runs as `ward ops <area> <verb>` - with the token sealed behind the guard, never handed to the agent. ### What an MCP server over ward-kdl looks like MCP clients discover capability through `tools/list` and invoke through `tools/call`, each tool carrying a JSON-Schema input. The mapping is direct: - **One MCP tool = one granted ward-kdl verb.** `can list issue` in the forgejo guardfile becomes a tool; an ungranted verb has no tool. The MCP tool list is the machine-readable twin of the flat index in [`docs/ward-kdl-surface.md`](https://forgejo.coilysiren.me/coilyco-flight-deck/ward/src/branch/main/docs/ward-kdl-surface.md). - **Tool name** = the verb path (a stable slug, e.g. `forgejo_issue_list`, or a namespaced `ops.forgejo.issue.list` - see open questions). - **Tool description** = the guardfile's `describe "..."` text. - **Tool inputSchema** = derived from the bound OpenAPI/Swagger operation (specverb already binds each verb to one operation and its parameters) for spec surfaces, or from the `can run <verb>` grant shape for exec surfaces. **Same source of truth, not a parallel surface.** This is the load-bearing design decision. specverb and execverb already parse the guardfile plus spec into an in-memory surface model, and `surface.Markdown()` walks that model to emit the per-area reference docs. An MCP emitter must be a **second emitter over the exact same model** - call it `surface.MCPTools()` - not a hand-maintained tool registry that shells out to the CLI and re-parses stdout. A shell-out wrapper would be a parallel surface that drifts from the guardfiles and loses the audit and restrict paths. In-process, the invariant carries across the MCP boundary for free: add a `can` grant and rebuild, the tool appears; withhold it, the tool is **absent from `tools/list`**, not merely denied on call. That is ward-kdl's whole "absent at compile time, not denied at runtime" property, preserved over MCP. ### How read/write/admin tiers map to MCP tool exposure ward-kdl already compiles the tiers as **three separate binaries** (`ward-kdl-{read,write,admin}`) layered by `inherit`, read ⊂ write ⊂ admin, each its own binary so a withheld verb is absent at compile time ([`docs/ward-kdl-tiers.md`](https://forgejo.coilysiren.me/coilyco-flight-deck/ward/src/branch/main/docs/ward-kdl-tiers.md)). The MCP server inherits that model exactly: **one tier per server process, fixed at launch, never a runtime argument the client can pass to escalate.** A read-tier MCP server literally has no `create`/`edit`/`delete` tools in its `tools/list`. So the three entry points are `ward-kdl-read mcp`, `ward-kdl-write mcp`, `ward-kdl-admin mcp`, each serving exactly its tier's verb set. **Anti-pattern to avoid:** a single server exposing a `tier` selector as a tool argument - that turns a compile-time absence into a runtime check, which is the thing the tier design exists to prevent. The agent's harness config picks the binary (tier) it is allowed to spawn. ### Auth / token flow Reuse ward-kdl's credential model unchanged, and **never hand a raw token to the MCP client**: - Guardfile auth is a `value ssm "<path>"` reference resolved **lazily at call time** ([`docs/guardfile-grammar.md`](https://forgejo.coilysiren.me/coilyco-flight-deck/ward/src/branch/main/docs/guardfile-grammar.md)). The source carries the token's **location**, not the token. - Over MCP this is the key security property: the MCP client sees only tool names, schemas, and result bodies. The SSM fetch, the `Authorization` header injection, and the `restrict owner matches <glob>` scope gate all happen **server-side inside the ward-kdl process**, on the exact same specverb/execverb call path the CLI uses. The client holds no credential and cannot widen scope. - **Transport = stdio first.** The agent spawns `ward-kdl-read mcp` as a subprocess and it inherits the host's AWS/SSM credentials the same way the CLI does. No network-exposed token, no separate client-auth surface. An HTTP/SSE transport would need its own client authentication and is a larger attack surface - defer it until a remote agent genuinely needs it. - **Audit is not bypassed.** Because calls route through the same specverb/execverb path, every MCP `tools/call` should write the same audit JSONL row as the equivalent CLI invocation ([`docs/audit.md`](https://forgejo.coilysiren.me/coilyco-flight-deck/ward/src/branch/main/docs/audit.md)). The MCP boundary must be an audited surface, not a hole around the log. (Confirm ops verbs already audit - see open questions.) ### New ward-kdl mcp server, or a wrapper? **A new `mcp` verb on the existing binaries, not a wrapper.** The adapter should live where the surface model lives (cli-guard, alongside `surface.Markdown()` and the specverb/execverb executors) and be exposed as an `mcp` subcommand on the existing `ward-kdl` / `ward-kdl-{read,write,admin}` binaries, serving MCP stdio by walking the already-compiled embedded surface and executing through the existing call path. A separate wrapper process that shells `ward ops forgejo ...` is explicitly rejected - it re-introduces the drift, stdout-parsing, and lost-audit problems above. Following the ward#284 auto-mount pattern, the **user-facing entry** should mirror into the product binary as `ward mcp` (or `ward ops <area> mcp`), the same way exec-dialect surfaces already auto-mount into `ward`. That keeps the standard split: authored in cli-guard/ward-kdl, embedded and run in `ward`. ### Relation to docs/ward-kdl-surface.md `ward-kdl-surface.md` is the human-readable flat index of the verb surface, emitted from the surface model by `surface.Markdown()`. The MCP tool list is the **machine-readable twin of that same index, from the same model, via a sibling emitter**. When this lands, `ward-kdl-surface.md` gains a short "MCP" section pointing at the new `mcp` verb, plus a dedicated `docs/ward-kdl-mcp.md` walkthrough and a `docs/FEATURES.md` entry. ### Phased breakdown - **Phase 0 - spec gate (this comment).** Kai reviews this design in a doc or PR before any code. No implementation until the shape is approved. - **Phase 1 - surface→MCP emitter (cli-guard).** Add `surface.MCPTools()` (sibling to `surface.Markdown()`) that walks the compiled surface model and returns MCP tool definitions: name, description from `describe`, inputSchema from the bound OpenAPI operation params (spec surfaces) or `can run` grant (exec surfaces). Pure function, unit-tested against the forgejo read tier. No server yet. - **Phase 2 - stdio MCP server verb.** Add the `mcp` subcommand serving `tools/list` + `tools/call` over stdio, executing through the existing specverb/execverb call path so SSM resolution, the `restrict` gate, and the audit row all come along. One tier per process. Ship the forgejo read tier as the reference vertical slice. - **Phase 3 - tier + multi-area coverage.** Wire the `mcp` verb across all three tier binaries, then extend to signoz, aws/kubectl exec, and the rest as their tiers land. This dovetails with the ward#339 tiering migration - the placeholder areas (trello, tailscale, glitchtip, glama, skillsmp) light up over MCP as they get real tier verbs. - **Phase 4 - ward mount + docs + harness registration.** Add the `ward mcp` user entry via the ward#284 auto-mount mechanism. Update `ward-kdl-surface.md`, add `docs/ward-kdl-mcp.md` and a `FEATURES.md` entry, and register the stdio server in the agent harness MCP configs (claude/codex/opencode) so a dispatched agent can spawn its allowed tier. ### Recommendation Build an in-process emitter plus a stdio MCP server on the **existing tier binaries** (not a shell-out wrapper), **one tier per process** with tier fixed at launch, tokens sealed behind the existing SSM-lazy cred model so the client never sees a credential, tools generated from the **same surface model** that emits `ward-kdl-surface.md` so the two never drift, and an audit row on every call. Start with the **forgejo read tier** as the thinnest end-to-end slice, then fan out by tier and area. ### Open questions - plan to verify before building 1. **Surface model richness.** Does cli-guard's surface model already expose per-verb parameter schemas well enough to emit JSON Schema at build time, or does specverb only bind params at call time? Phase 1 depends on this - verify against the `surface` package API first. 2. **Ops-verb auditing.** `docs/audit.md` names `ward exec` repo verbs and `pkg brew` as audited. Confirm whether `ops`/spec-dialect verbs already write audit rows, since Phase 2 assumes the MCP path inherits that. 3. **Transport.** stdio is the recommended default. Confirm no near-term need for a remote HTTP/SSE server before deferring it. 4. **Tool naming.** Flat slug (`forgejo_issue_list`) vs namespaced (`ops.forgejo.issue.list`) - pick one convention and mirror it in the reference doc. 5. **restrict-owner surfacing.** Decide whether the `restrict owner matches <glob>` gate presents to the client as a fixed server-side filter (out-of-scope calls simply rejected) or is reflected in the tool schema. Rejection is safer and simpler. This whole design is a plan to verify. The engine claims in questions 1 and 2 gate Phases 1 and 2 and should be checked against cli-guard before an engineer picks up the build. --- Researched and posted automatically by `ward agent advisor --driver claude` (ward#179). This is one-shot research, not a carried change - verify before acting on it. <!-- ward-agent-reply --> <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
coilyco-flight-deck/ward#217
No description provided.