design: inline operation authoring - the .mcp.kdl defines the surface, drop the vendored OpenAPI as a build input #6

Closed
opened 2026-07-07 07:30:40 +00:00 by coilysiren · 4 comments
Owner

Decision (Kai, 2026-07-07)

Inline everywhere. A .mcp.kdl defines its operations inline - method, path-template, typed params - and is the whole contract. The vendored OpenAPI JSON, its .lock.json, the prune step, and op pins are dropped as build inputs. Where a real upstream OpenAPI exists (forgejo's swagger), it's a one-time authoring reference to copy from, never something the build consumes.

This amends docs/DESIGN.md, whose pipeline today bakes guardfile + <spec>.lock.json + specverb.lock + runtime and derives schemas "from the OpenAPI operation." Under inline authoring the schema is derived from the inline op definition, and the two locks leave the build.

Why (the pressure test that led here)

The strict structure was CLI-era discipline for a shipped binary wrapping an upstream in reproducible offline builds. None of its load-bearing reasons survive the move to a live MCP:

  • prune-to-granted + transitive $ref lock = offline-build determinism. An MCP image carries the surface directly and the guard denies everything ungranted anyway, so pruning is an optional size optimization, not correctness.
  • .lock.json = freeze a fetched upstream against drift. skillsmp / glama / trello have no upstream OpenAPI - the file is hand-authored, already the git-tracked source of truth, so the lock is redundant with git.
  • op pins = bind a grant to an operationId. Inline ops carry {method, path, params} directly, so there is no operationId to resolve or override - the concept evaporates. (The resolver nit that makes op needless for the spec-backed CLI path is filed in cli-guard separately; inline sidesteps it entirely.)

The live API is the truth and does not care about our versioning, so freezing a copy of its schema buys little for a surface we call live. What the strictness did buy - a fixed, reviewable surface - inline authoring keeps and improves: skillsmp collapses from three files (guardfile + openapi.json + lock) to one, which is exactly ward-mcp's safety pitch ("audit one small file, know the blast radius").

The grammar (sketch, to be settled here)

A tool defined inline, no spec, no op:

wrap skillsmp {
    base-url "skillsmp.com/api/v1/skills"
    auth bearer { value env SKILLSMP_API_KEY }

    tool search_skills {
        GET "/search"
        query q string required  "keyword search term"
        query sortBy string
        query limit int
        query page int
    }
    tool ai_search_skills {
        GET "/ai-search"
        query q string required  "natural-language query"
    }
}

Params carry their location (path / query / body) and type. That metadata is irreducible - request assembly and the metachar / restrict gates need it - it just moves from OpenAPI JSON into KDL. restrict / never / can-vs-tool policy semantics are unchanged; a never still means the tool is never minted.

The enabling engine capability (the real cost)

cli-guard's specverb builds operation descriptors from an OpenAPI spec (L0). Inline authoring needs a descriptor built from inline KDL instead. Picking how is the key design fork:

  • (a) cli-guard grows an inline-operation source in specverb, so the same guard / request / metachar engine runs on inline ops. cli-guard already does exactly this shape for local commands via execverb (inline KDL, no spec), so this extends the "author the surface inline" pattern to HTTP ops. Keeps ward-mcp a thin driver and the safety engine shared. Preferred, per the layering law (cli-guard is the engine).
  • (b) ward-mcp constructs the descriptor itself and feeds cli-guard's guard / request layers. Keeps the change out of cli-guard, but risks ward-mcp reimplementing engine surface.

Recommend (a); split a cli-guard issue for the inline-op source once this grammar settles.

Guardrails (so "more flex" does not overshoot)

  • No live schema fetch at serve time. Tempting from "the API is truth," but it floats the audited surface - the tool set could change with no diff to review, which kills "the guardfile IS the surface." Inline means more committed, not less. This is the line the flex stops at.
  • Keep the structured param metadata (location + type). Irreducible for request assembly and the gates.

Rollout

  1. Settle the inline grammar here and amend docs/DESIGN.md.
  2. cli-guard: inline-operation source in specverb (option a).
  3. ward-mcp runtime: parse inline .mcp.kdl, project one MCP tool per tool / can, derive the MCP inputSchema from the inline params.
  4. Reauthor the deploy examples inline: skillsmp first (supersedes the strict deploy#60 version), then forgejo (copy from its swagger once, inline the issue surface).

Related: deploy#60 (the strict skillsmp, the "before"), the cli-guard resolver nit (spec-backed path), coilysiren/inbox#164 (the runtime), docs/DESIGN.md (to amend). The safety model, HTTP/SSE transport, and interior-only scope from DESIGN.md are unchanged - only the operation-source layer changes.

## Decision (Kai, 2026-07-07) **Inline everywhere.** A `.mcp.kdl` defines its operations **inline** - method, path-template, typed params - and is the whole contract. The vendored OpenAPI JSON, its `.lock.json`, the prune step, and `op` pins are **dropped as build inputs**. Where a real upstream OpenAPI exists (forgejo's swagger), it's a **one-time authoring reference** to copy from, never something the build consumes. This amends [docs/DESIGN.md](docs/DESIGN.md), whose pipeline today bakes `guardfile + <spec>.lock.json + specverb.lock + runtime` and derives schemas "from the OpenAPI operation." Under inline authoring the schema is derived from the **inline op definition**, and the two locks leave the build. ## Why (the pressure test that led here) The strict structure was **CLI-era discipline** for a shipped binary wrapping an upstream in reproducible offline builds. None of its load-bearing reasons survive the move to a live MCP: - **prune-to-granted + transitive `$ref` lock** = offline-build determinism. An MCP image carries the surface directly and the guard denies everything ungranted anyway, so pruning is an optional size optimization, not correctness. - **`.lock.json`** = freeze a *fetched* upstream against drift. skillsmp / glama / trello have no upstream OpenAPI - the file is hand-authored, already the git-tracked source of truth, so the lock is redundant with git. - **`op` pins** = bind a grant to an operationId. Inline ops carry `{method, path, params}` directly, so there is no operationId to resolve or override - the concept evaporates. (The resolver nit that makes `op` needless for the spec-backed CLI path is filed in cli-guard separately; inline sidesteps it entirely.) The live API is the truth and does not care about our versioning, so freezing a copy of its schema buys little for a surface we call live. What the strictness *did* buy - a fixed, reviewable surface - inline authoring keeps and **improves**: skillsmp collapses from three files (guardfile + openapi.json + lock) to **one**, which is exactly ward-mcp's safety pitch ("audit one small file, know the blast radius"). ## The grammar (sketch, to be settled here) A tool defined inline, no `spec`, no `op`: ```kdl wrap skillsmp { base-url "skillsmp.com/api/v1/skills" auth bearer { value env SKILLSMP_API_KEY } tool search_skills { GET "/search" query q string required "keyword search term" query sortBy string query limit int query page int } tool ai_search_skills { GET "/ai-search" query q string required "natural-language query" } } ``` Params carry their **location** (path / query / body) and **type**. That metadata is irreducible - request assembly and the metachar / restrict gates need it - it just moves from OpenAPI JSON into KDL. `restrict` / `never` / `can`-vs-`tool` policy semantics are unchanged; a `never` still means the tool is never minted. ## The enabling engine capability (the real cost) cli-guard's `specverb` builds operation descriptors from an OpenAPI spec (L0). Inline authoring needs a descriptor built from **inline KDL** instead. Picking how is the key design fork: - **(a) cli-guard grows an inline-operation source in specverb**, so the same guard / request / metachar engine runs on inline ops. cli-guard already does exactly this shape for local commands via `execverb` (inline KDL, no spec), so this extends the "author the surface inline" pattern to HTTP ops. Keeps ward-mcp a thin driver and the safety engine shared. **Preferred**, per the layering law (cli-guard is the engine). - **(b) ward-mcp constructs the descriptor itself** and feeds cli-guard's guard / request layers. Keeps the change out of cli-guard, but risks ward-mcp reimplementing engine surface. Recommend (a); split a cli-guard issue for the inline-op source once this grammar settles. ## Guardrails (so "more flex" does not overshoot) - **No live schema fetch at serve time.** Tempting from "the API is truth," but it floats the audited surface - the tool set could change with no diff to review, which kills "the guardfile IS the surface." Inline means **more** committed, not less. This is the line the flex stops at. - **Keep the structured param metadata** (location + type). Irreducible for request assembly and the gates. ## Rollout 1. Settle the inline grammar here and amend `docs/DESIGN.md`. 2. cli-guard: inline-operation source in `specverb` (option a). 3. ward-mcp runtime: parse inline `.mcp.kdl`, project one MCP tool per `tool` / `can`, derive the MCP inputSchema from the inline params. 4. Reauthor the deploy examples inline: skillsmp first (supersedes the strict deploy#60 version), then forgejo (copy from its swagger once, inline the issue surface). Related: deploy#60 (the strict skillsmp, the "before"), the cli-guard resolver nit (spec-backed path), coilysiren/inbox#164 (the runtime), `docs/DESIGN.md` (to amend). The safety model, HTTP/SSE transport, and interior-only scope from DESIGN.md are unchanged - only the operation-source layer changes.
Author
Owner

Engine fork resolved: option (a), pushed as far down as it goes. Kai's directive - push as much impl into cli-guard as possible; ward-mcp stays a thin driver. Same discipline ward#265 is unwinding for ward, applied to ward-mcp from birth so it never grows the bloat.

Engine home filed as cli-guard#196 (transport-neutral core + inline-operation source). The boundary:

  • cli-guard (engine, transport-neutral): parse the inline tool block -> operation descriptor; expose the op's input JSON-schema; guarded-execute (validate -> restrict/metachar gates -> sign + fire the HTTP request). The inline-op source sits beside the existing OpenAPI source; execverb is the precedent (inline KDL, no spec).
  • ward-mcp (thin driver, transport only): envelope cli-guard's input-schema as an MCP tool, bind it to the SSE / streamable-HTTP server, and the image build. That is the whole of ward-mcp - no spec parsing, no schema derivation, no request assembly of its own.

So DESIGN.md's "grant -> MCP tool projection (op descriptor -> JSON-schema + handler)" splits: the descriptor + JSON-schema half comes down into cli-guard (shared with the CLI projection), ward-mcp keeps only the MCP-tool envelope + transport. Respects cli-guard#190 - no orchestration comes down, only the engine core.

This issue now owns the inline .mcp.kdl grammar design (the tool block shape); cli-guard#196 owns the engine that consumes it. They co-design, then cli-guard#196 implements the source and ward-mcp's runtime is a thin shell over it.

**Engine fork resolved: option (a), pushed as far down as it goes.** Kai's directive - push as much impl into cli-guard as possible; ward-mcp stays a thin driver. Same discipline ward#265 is unwinding for ward, applied to ward-mcp from birth so it never grows the bloat. Engine home filed as **cli-guard#196** (transport-neutral core + inline-operation source). The boundary: - **cli-guard (engine, transport-neutral):** parse the inline `tool` block -> operation descriptor; expose the op's input JSON-schema; guarded-execute (validate -> restrict/metachar gates -> sign + fire the HTTP request). The inline-op source sits beside the existing OpenAPI source; `execverb` is the precedent (inline KDL, no spec). - **ward-mcp (thin driver, transport only):** envelope cli-guard's input-schema as an MCP tool, bind it to the SSE / streamable-HTTP server, and the image build. That is the whole of ward-mcp - no spec parsing, no schema derivation, no request assembly of its own. So DESIGN.md's "grant -> MCP tool projection (op descriptor -> JSON-schema + handler)" splits: the descriptor + JSON-schema half comes **down** into cli-guard (shared with the CLI projection), ward-mcp keeps only the MCP-tool envelope + transport. Respects cli-guard#190 - no orchestration comes down, only the engine core. This issue now owns the **inline `.mcp.kdl` grammar** design (the `tool` block shape); cli-guard#196 owns the engine that consumes it. They co-design, then cli-guard#196 implements the source and ward-mcp's runtime is a thin shell over it.
Author
Owner

Inline .mcp.kdl grammar + distribution - co-designed with Kai, 2026-07-07

Locks the two halves ward-mcp#6 opened. Amends DESIGN.md. The engine that consumes this is cli-guard#196.

Grammar

Extend the existing can <verb> <resource> grant - not a new tool node - so one grammar carries both op-sources (spec-backed and inline) and restrict / never / inherit / action / tool-naming keep working. The inline body states the descriptor the spec used to resolve.

wrap <name> {                              // server name only; bucket/area dropped (CLI-mount concepts)
    base-url "<host/path>"
    auth <scheme> { value env <VAR> }      // unchanged: bearer / header-token / query-param
    restrict <param> matches <glob>        // unchanged

    can <verb> <resource> {
        path "/a/{p}/b"                    // method INFERRED from the verb; the path is the new bit
        describe "..."                    // -> the MCP tool description the agent reads
        query <name> <type> [required] "<desc>"   // <location> <name> <type> [required] "desc"
        body  <name> <type> [required] "<desc>"   // location = request-assembly only (no positional/flag)
        set   <field> "<value>"           // fixed-body toggle (close -> set state "closed")
    }

    never <verb> <resource> { message "..." }   // only to override an INHERITED grant; else absence = denial

    action <verb> <resource> {            // compose ops into one tool (unchanged shape)
        input <name> { required }
        call <verb> <resource> { <arg> $<name> } as <ref>
    }

    inherit "<file>"                      // read/write layering, unchanged
    override can <verb> <resource>
}

Decided rules

  • Method inferred from the verb - list/get/view/search → GET, create → POST, edit → PATCH/PUT, delete → DELETE, close/reopen → PATCH + fixed body, comment → POST sub-collection (cli-guard's existing verb→method convention, specverb-resolution.md). An unconventional verb states an explicit method.
  • Path params inferred from {name} tokens as required strings; an optional path <name> <type> line refines type/description.
  • No positional/flag rail - MCP inputs are all named JSON fields, so location (path/query/body/header) is request-assembly metadata only, not input shape.
  • Deny by absence - an undeclared op cannot be a tool, so the file lists exactly the surface. never survives only to override an inherited grant.
  • Unchanged: auth, restrict, describe, action / call, inherit / override can.

Worked: skillsmp (the simplest)

wrap skillsmp {
    base-url "skillsmp.com/api/v1/skills"
    auth bearer {
        value env SKILLSMP_API_KEY
    }
    can search skills {
        path "/search"
        describe "Keyword search across the skill directory."
        query q string required "search term"
        query sortBy string
        query limit int
        query page int
    }
    can ai-search skills {
        path "/ai-search"
        describe "Semantic search across the skill directory."
        query q string required "natural-language query"
    }
}

Worked: forgejo (path params, body, restrict, fixed-body, deny, compose)

wrap forgejo {
    base-url "forgejo.coilysiren.me/api/v1"
    auth header-token {
        header Authorization
        prefix "token "
        value env FORGEJO_TOKEN
    }
    restrict owner matches coily*

    can get issue {
        path "/repos/{owner}/{repo}/issues/{index}"
        describe "Get one issue."
    }
    can list issue {
        path "/repos/{owner}/{repo}/issues"
        describe "List issues (open by default)."
        query state string "open|closed|all"
        query page int
        query limit int
    }
    can create issue {
        path "/repos/{owner}/{repo}/issues"
        describe "Create an issue."
        body title string required
        body body string "markdown body"
    }
    can close issue {
        path "/repos/{owner}/{repo}/issues/{index}"
        describe "Close an issue."
        set state "closed"
    }
    can list issue-comment {
        path "/repos/{owner}/{repo}/issues/{index}/comments"
        describe "List an issue's comments."
    }

    never delete issue {
        message "issue deletion is irreversible; close it instead"
    }

    action view issue {
        describe "View an issue with its full comment thread."
        input owner { required }
        input repo { required }
        input index { required }
        call get issue { owner $owner repo $repo index $index } as issue
        call list issue-comment { owner $owner repo $repo index $index } as comments
    }
}

Distribution - generic runtime + Helm chart (ward-mcp ships both)

Retire per-guardfile image bake. One generic ward-mcp runtime image (cli-guard engine + SSE/HTTP server) serves any mounted .mcp.kdl. ward-mcp ships a Helm chart that templates Deployment (runtime + the guardfile as a ConfigMap mount) + Service + Authelia route + token Secret. The .mcp.kdl is chart values; deploy supplies per-MCP values.

helm install skillsmp ward-mcp \
  --set-file spec=skillsmp.mcp.kdl \
  --set route.host=skillsmp.coilysiren.me \
  --set secret.SKILLSMP_API_KEY=<external-secret-ref>
# add forgejo-mcp = another values file, same chart

DESIGN.md amendments

  • Pipeline flips: guardfile → ward mcp build → OCI image → deploy becomes guardfile = chart values → helm → pod mounting the one runtime image. "Landing the guardfile IS publishing the image" becomes "landing the guardfile IS a values change / helm upgrade."
  • ward mcp build repurposes from image-bake to an optional local test-serve.
  • ward-mcp's interior/exterior boundary redraws: ward-mcp ships the runtime image and the chart template; deploy supplies values (spec, route host, secret ref, public-vs-tailnet). The old "image interior only, k8s is deploy's" split becomes template-vs-values.
  • Auditability holds: the reviewed surface is the git-tracked .mcp.kdl, now a ConfigMap rather than an image digest.

Ripples

  • deploy#61 (reauthor skillsmp inline) - target becomes a chart values file + the inline .mcp.kdl, not the forgejo-mcp per-service manifest layout.
  • forgejo-mcp - collapses to one inline .mcp.kdl (deploy#46), deployed via the chart.
  • deploy#30 (generalize the MCP-fleet Authelia overlay) - the chart's route template is that generalization for ward-mcp servers.
  • cli-guard#196 - unchanged: still the transport-neutral engine that parses the inline op + guarded-execute.
## Inline `.mcp.kdl` grammar + distribution - co-designed with Kai, 2026-07-07 Locks the two halves ward-mcp#6 opened. Amends DESIGN.md. The engine that consumes this is cli-guard#196. ## Grammar Extend the existing `can <verb> <resource>` grant - **not** a new `tool` node - so one grammar carries both op-sources (spec-backed and inline) and `restrict` / `never` / `inherit` / `action` / tool-naming keep working. The inline body *states* the descriptor the spec used to *resolve*. ```kdl wrap <name> { // server name only; bucket/area dropped (CLI-mount concepts) base-url "<host/path>" auth <scheme> { value env <VAR> } // unchanged: bearer / header-token / query-param restrict <param> matches <glob> // unchanged can <verb> <resource> { path "/a/{p}/b" // method INFERRED from the verb; the path is the new bit describe "..." // -> the MCP tool description the agent reads query <name> <type> [required] "<desc>" // <location> <name> <type> [required] "desc" body <name> <type> [required] "<desc>" // location = request-assembly only (no positional/flag) set <field> "<value>" // fixed-body toggle (close -> set state "closed") } never <verb> <resource> { message "..." } // only to override an INHERITED grant; else absence = denial action <verb> <resource> { // compose ops into one tool (unchanged shape) input <name> { required } call <verb> <resource> { <arg> $<name> } as <ref> } inherit "<file>" // read/write layering, unchanged override can <verb> <resource> } ``` ### Decided rules - **Method inferred from the verb** - list/get/view/search → GET, create → POST, edit → PATCH/PUT, delete → DELETE, close/reopen → PATCH + fixed body, comment → POST sub-collection (cli-guard's existing verb→method convention, `specverb-resolution.md`). An unconventional verb states an explicit method. - **Path params inferred** from `{name}` tokens as required strings; an optional `path <name> <type>` line refines type/description. - **No positional/flag rail** - MCP inputs are all named JSON fields, so `location` (path/query/body/header) is request-assembly metadata only, not input shape. - **Deny by absence** - an undeclared op cannot be a tool, so the file lists exactly the surface. `never` survives only to override an inherited grant. - **Unchanged:** `auth`, `restrict`, `describe`, `action` / `call`, `inherit` / `override can`. ### Worked: skillsmp (the simplest) ```kdl wrap skillsmp { base-url "skillsmp.com/api/v1/skills" auth bearer { value env SKILLSMP_API_KEY } can search skills { path "/search" describe "Keyword search across the skill directory." query q string required "search term" query sortBy string query limit int query page int } can ai-search skills { path "/ai-search" describe "Semantic search across the skill directory." query q string required "natural-language query" } } ``` ### Worked: forgejo (path params, body, restrict, fixed-body, deny, compose) ```kdl wrap forgejo { base-url "forgejo.coilysiren.me/api/v1" auth header-token { header Authorization prefix "token " value env FORGEJO_TOKEN } restrict owner matches coily* can get issue { path "/repos/{owner}/{repo}/issues/{index}" describe "Get one issue." } can list issue { path "/repos/{owner}/{repo}/issues" describe "List issues (open by default)." query state string "open|closed|all" query page int query limit int } can create issue { path "/repos/{owner}/{repo}/issues" describe "Create an issue." body title string required body body string "markdown body" } can close issue { path "/repos/{owner}/{repo}/issues/{index}" describe "Close an issue." set state "closed" } can list issue-comment { path "/repos/{owner}/{repo}/issues/{index}/comments" describe "List an issue's comments." } never delete issue { message "issue deletion is irreversible; close it instead" } action view issue { describe "View an issue with its full comment thread." input owner { required } input repo { required } input index { required } call get issue { owner $owner repo $repo index $index } as issue call list issue-comment { owner $owner repo $repo index $index } as comments } } ``` ## Distribution - generic runtime + Helm chart (ward-mcp ships both) Retire per-guardfile image bake. **One** generic `ward-mcp` runtime image (cli-guard engine + SSE/HTTP server) serves any mounted `.mcp.kdl`. ward-mcp ships a **Helm chart** that templates Deployment (runtime + the guardfile as a ConfigMap mount) + Service + Authelia route + token Secret. The `.mcp.kdl` is chart values; deploy supplies per-MCP values. ```sh helm install skillsmp ward-mcp \ --set-file spec=skillsmp.mcp.kdl \ --set route.host=skillsmp.coilysiren.me \ --set secret.SKILLSMP_API_KEY=<external-secret-ref> # add forgejo-mcp = another values file, same chart ``` ### DESIGN.md amendments - Pipeline flips: `guardfile → ward mcp build → OCI image → deploy` becomes `guardfile = chart values → helm → pod mounting the one runtime image`. "Landing the guardfile IS publishing the image" becomes "landing the guardfile IS a values change / `helm upgrade`." - `ward mcp build` repurposes from image-bake to an optional local test-serve. - ward-mcp's interior/exterior boundary redraws: ward-mcp ships the runtime image **and** the chart template; deploy supplies **values** (spec, route host, secret ref, public-vs-tailnet). The old "image interior only, k8s is deploy's" split becomes **template-vs-values**. - Auditability holds: the reviewed surface is the git-tracked `.mcp.kdl`, now a ConfigMap rather than an image digest. ## Ripples - **deploy#61** (reauthor skillsmp inline) - target becomes a **chart values file** + the inline `.mcp.kdl`, not the forgejo-mcp per-service manifest layout. - **forgejo-mcp** - collapses to one inline `.mcp.kdl` (deploy#46), deployed via the chart. - **deploy#30** (generalize the MCP-fleet Authelia overlay) - the chart's route template is that generalization for ward-mcp servers. - **cli-guard#196** - unchanged: still the transport-neutral engine that parses the inline op + guarded-execute.
Member

🔒 Reserved by ward agent --harness codex — container engineer-codex-ward-mcp-6 on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-07-08T17:09:06Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

Do not comment on or edit this issue to steer the run while it is reserved. The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a new issue, dispatched fresh — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494).

run seed context — what this run is carrying (ward#609)
  • Resolved: coilyco-flight-deck/ward-mcp#6 · branch issue-6 · harness codex · workflow direct-main
  • Run: engineer-codex-ward-mcp-6 · ward v0.451.0 · dispatched 2026-07-08T17:09:06Z
  • Comment thread: 2 included in the pre-flight read, 0 stripped (ward's own automated comments).

Issue body as seeded:

## Decision (Kai, 2026-07-07)

**Inline everywhere.** A `.mcp.kdl` defines its operations **inline** - method, path-template, typed params - and is the whole contract. The vendored OpenAPI JSON, its `.lock.json`, the prune step, and `op` pins are **dropped as build inputs**. Where a real upstream OpenAPI exists (forgejo's swagger), it's a **one-time authoring reference** to copy from, never something the build consumes.

This amends [docs/DESIGN.md](docs/DESIGN.md), whose pipeline today bakes `guardfile + <spec>.lock.json + specverb.lock + runtime` and derives schemas "from the OpenAPI operation." Under inline authoring the schema is derived from the **inline op definition**, and the two locks leave the build.

## Why (the pressure test that led here)

The strict structure was **CLI-era discipline** for a shipped binary wrapping an upstream in reproducible offline builds. None of its load-bearing reasons survive the move to a live MCP:

- **prune-to-granted + transitive `$ref` lock** = offline-build determinism. An MCP image carries the surface directly and the guard denies everything ungranted anyway, so pruning is an optional size optimization, not correctness.
- **`.lock.json`** = freeze a *fetched* upstream against drift. skillsmp / glama / trello have no upstream OpenAPI - the file is hand-authored, already the git-tracked source of truth, so the lock is redundant with git.
- **`op` pins** = bind a grant to an operationId. Inline ops carry `{method, path, params}` directly, so there is no operationId to resolve or override - the concept evaporates. (The resolver nit that makes `op` needless for the spec-backed CLI path is filed in cli-guard separately; inline sidesteps it entirely.)

The live API is the truth and does not care about our versioning, so freezing a copy of its schema buys little for a surface we call live. What the strictness *did* buy - a fixed, reviewable surface - inline authoring keeps and **improves**: skillsmp collapses from three files (gua

… (truncated to 2000 chars; full body is on this issue)

Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.451.0).

— Codex, via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --harness codex` — container `engineer-codex-ward-mcp-6` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-07-08T17:09:06Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. **Do not comment on or edit this issue to steer the run while it is reserved.** The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a **new issue, dispatched fresh** — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494). <details><summary>run seed context — what this run is carrying (ward#609)</summary> - **Resolved:** `coilyco-flight-deck/ward-mcp#6` · branch `issue-6` · harness `codex` · workflow `direct-main` - **Run:** `engineer-codex-ward-mcp-6` · ward `v0.451.0` · dispatched `2026-07-08T17:09:06Z` - **Comment thread:** 2 included in the pre-flight read, 0 stripped (ward's own automated comments). - included: @coilysiren (2026-07-07T07:37:12Z), @coilysiren (2026-07-07T07:59:37Z) **Issue body as seeded:** ``` ## Decision (Kai, 2026-07-07) **Inline everywhere.** A `.mcp.kdl` defines its operations **inline** - method, path-template, typed params - and is the whole contract. The vendored OpenAPI JSON, its `.lock.json`, the prune step, and `op` pins are **dropped as build inputs**. Where a real upstream OpenAPI exists (forgejo's swagger), it's a **one-time authoring reference** to copy from, never something the build consumes. This amends [docs/DESIGN.md](docs/DESIGN.md), whose pipeline today bakes `guardfile + <spec>.lock.json + specverb.lock + runtime` and derives schemas "from the OpenAPI operation." Under inline authoring the schema is derived from the **inline op definition**, and the two locks leave the build. ## Why (the pressure test that led here) The strict structure was **CLI-era discipline** for a shipped binary wrapping an upstream in reproducible offline builds. None of its load-bearing reasons survive the move to a live MCP: - **prune-to-granted + transitive `$ref` lock** = offline-build determinism. An MCP image carries the surface directly and the guard denies everything ungranted anyway, so pruning is an optional size optimization, not correctness. - **`.lock.json`** = freeze a *fetched* upstream against drift. skillsmp / glama / trello have no upstream OpenAPI - the file is hand-authored, already the git-tracked source of truth, so the lock is redundant with git. - **`op` pins** = bind a grant to an operationId. Inline ops carry `{method, path, params}` directly, so there is no operationId to resolve or override - the concept evaporates. (The resolver nit that makes `op` needless for the spec-backed CLI path is filed in cli-guard separately; inline sidesteps it entirely.) The live API is the truth and does not care about our versioning, so freezing a copy of its schema buys little for a surface we call live. What the strictness *did* buy - a fixed, reviewable surface - inline authoring keeps and **improves**: skillsmp collapses from three files (gua ``` … (truncated to 2000 chars; full body is on this issue) Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.451.0). </details> <!-- ward-agent-signature --> — Codex, via `ward agent`
coilyco-ops referenced this issue from a commit 2026-07-08 17:14:50 +00:00
Member

WARD-OUTCOME: done - documented inline .mcp.kdl authoring as the whole contract and removed the stale OpenAPI build-input story
PR-BODY-NOTE: ADVISORY-ONLY REVIEW: no heterogeneous reviewer family was available besides the worker (codex), so the adversarial panel could not run and did NOT gate this diff. Dropped: opencode (unavailable: opencode not on PATH); codex (worker's own family - never reviews its own diff). A human should review this change with the extra scrutiny an unrun panel would have applied.

This felt mostly like documentation cleanup because the runtime already lived on the inline path. The only friction was reconciling the remote main update before landing, which widened the merge a bit, but the final shape is still straightforward and I am confident in it.

WARD-OUTCOME: done - documented inline .mcp.kdl authoring as the whole contract and removed the stale OpenAPI build-input story PR-BODY-NOTE: ADVISORY-ONLY REVIEW: no heterogeneous reviewer family was available besides the worker (codex), so the adversarial panel could not run and did NOT gate this diff. Dropped: opencode (unavailable: opencode not on PATH); codex (worker's own family - never reviews its own diff). A human should review this change with the extra scrutiny an unrun panel would have applied. This felt mostly like documentation cleanup because the runtime already lived on the inline path. The only friction was reconciling the remote `main` update before landing, which widened the merge a bit, but the final shape is still straightforward and I am confident in it.
Sign in to join this conversation.
No description provided.