feat(chart): generic ward-mcp Helm chart - one runtime image, guardfile as ConfigMap, Authelia route (ward-mcp ships it) #8

Closed
opened 2026-07-07 08:17:27 +00:00 by coilysiren · 2 comments
Owner

What

The generic ward-mcp Helm chart - the distribution vehicle ward-mcp ships (ward-mcp#6). One chart, one runtime image, many releases: the .mcp.kdl rides in as chart values (a ConfigMap), and the chart templates all the k8s exposure. Adding an MCP becomes a values file + helm upgrade, no per-guardfile image build.

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>

Templates

  • ConfigMap - the .mcp.kdl from --set-file spec=..., mounted into the runtime at /spec/<name>.mcp.kdl.
  • Deployment - the one generic runtime image, ward-mcp serve /spec/<name>.mcp.kdl --http :8080, the token injected as env.
  • Service - ClusterIP (+ optional NodePort for the tailnet local-harness path, mirroring reddit-mcp).
  • Route - Traefik + Authelia ForwardAuth (public, gated), with a public vs tailnet-only values toggle. This is deploy#30's fleet auth-overlay, generalized for ward-mcp servers.
  • Secret wiring - an ExternalSecret (or a values-supplied Secret ref) mapping the SSM token to the env var the guardfile names in value env <VAR>.

Values (the deploy-supplied surface)

spec (the .mcp.kdl, via --set-file), route.host, route.public (bool), secret.<ENV> refs, image.tag, resource limits. deploy consumes the chart per-MCP with one values file (deploy#61 skillsmp, deploy#46 forgejo).

Runtime contract this targets (pin it; the runtime issue implements it)

  • image ENTRYPOINT ward-mcp serve /spec/<name>.mcp.kdl --http :8080
  • MCP served over SSE / streamable-HTTP at /mcp
  • token supplied as env, name per the guardfile's value env <VAR>
  • .mcp.kdl read from the mounted path, not baked

Sequencing

Buildable against the pinned contract above in parallel with the runtime; finalize the mount path / port / health probe once the runtime lands. Ships in the ward-mcp repo (the product distributes as image + chart), consumed by deploy as values.

Related: ward-mcp#6 (distribution decision), the ward-mcp runtime issue (the image the chart runs), deploy#30 (fleet auth overlay), deploy#61 / deploy#46 (first consumers).

## What The generic **ward-mcp Helm chart** - the distribution vehicle ward-mcp ships (ward-mcp#6). One chart, one runtime image, many releases: the `.mcp.kdl` rides in as chart values (a ConfigMap), and the chart templates all the k8s exposure. Adding an MCP becomes a values file + `helm upgrade`, no per-guardfile image build. ```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> ``` ## Templates - **ConfigMap** - the `.mcp.kdl` from `--set-file spec=...`, mounted into the runtime at `/spec/<name>.mcp.kdl`. - **Deployment** - the one generic runtime image, `ward-mcp serve /spec/<name>.mcp.kdl --http :8080`, the token injected as env. - **Service** - ClusterIP (+ optional NodePort for the tailnet local-harness path, mirroring reddit-mcp). - **Route** - Traefik + Authelia ForwardAuth (public, gated), with a `public` vs `tailnet-only` values toggle. This is deploy#30's fleet auth-overlay, generalized for ward-mcp servers. - **Secret wiring** - an ExternalSecret (or a values-supplied Secret ref) mapping the SSM token to the env var the guardfile names in `value env <VAR>`. ## Values (the deploy-supplied surface) `spec` (the .mcp.kdl, via --set-file), `route.host`, `route.public` (bool), `secret.<ENV>` refs, `image.tag`, resource limits. deploy consumes the chart per-MCP with one values file (deploy#61 skillsmp, deploy#46 forgejo). ## Runtime contract this targets (pin it; the runtime issue implements it) - image ENTRYPOINT `ward-mcp serve /spec/<name>.mcp.kdl --http :8080` - MCP served over SSE / streamable-HTTP at `/mcp` - token supplied as env, name per the guardfile's `value env <VAR>` - `.mcp.kdl` read from the mounted path, not baked ## Sequencing Buildable against the pinned contract above in parallel with the runtime; finalize the mount path / port / health probe once the runtime lands. Ships **in the ward-mcp repo** (the product distributes as image + chart), consumed by deploy as values. Related: ward-mcp#6 (distribution decision), the ward-mcp runtime issue (the image the chart runs), deploy#30 (fleet auth overlay), deploy#61 / deploy#46 (first consumers).
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-ward-mcp-8 on host KAI-DESKTOP-TOWER is carrying this issue (reserved 2026-07-07T09:33:54Z). 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#8 · branch issue-8 · driver claude · workflow direct-main
  • Run: engineer-claude-ward-mcp-8 · ward v0.419.0 · dispatched 2026-07-07T09:33:54Z
  • Comment thread: 0 included in the pre-flight read, 0 stripped (ward's own automated comments).

Issue body as seeded:

## What

The generic **ward-mcp Helm chart** - the distribution vehicle ward-mcp ships (ward-mcp#6). One chart, one runtime image, many releases: the `.mcp.kdl` rides in as chart values (a ConfigMap), and the chart templates all the k8s exposure. Adding an MCP becomes a values file + `helm upgrade`, no per-guardfile image build.

` ` `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>
` ` `

## Templates

- **ConfigMap** - the `.mcp.kdl` from `--set-file spec=...`, mounted into the runtime at `/spec/<name>.mcp.kdl`.
- **Deployment** - the one generic runtime image, `ward-mcp serve /spec/<name>.mcp.kdl --http :8080`, the token injected as env.
- **Service** - ClusterIP (+ optional NodePort for the tailnet local-harness path, mirroring reddit-mcp).
- **Route** - Traefik + Authelia ForwardAuth (public, gated), with a `public` vs `tailnet-only` values toggle. This is deploy#30's fleet auth-overlay, generalized for ward-mcp servers.
- **Secret wiring** - an ExternalSecret (or a values-supplied Secret ref) mapping the SSM token to the env var the guardfile names in `value env <VAR>`.

## Values (the deploy-supplied surface)

`spec` (the .mcp.kdl, via --set-file), `route.host`, `route.public` (bool), `secret.<ENV>` refs, `image.tag`, resource limits. deploy consumes the chart per-MCP with one values file (deploy#61 skillsmp, deploy#46 forgejo).

## Runtime contract this targets (pin it; the runtime issue implements it)

- image ENTRYPOINT `ward-mcp serve /spec/<name>.mcp.kdl --http :8080`
- MCP served over SSE / streamable-HTTP at `/mcp`
- token supplied as env, name per the guardfile's `value env <VAR>`
- `.mcp.kdl` read from the mounted path, not baked

## Sequencing

Buildable against the pinned contract above in parallel with the runtime; finalize the mount path / port / health probe once the runtime lands. Ships **in the ward-mcp repo** (the product dist

… (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.419.0).

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `engineer-claude-ward-mcp-8` on host `KAI-DESKTOP-TOWER` is carrying this issue (reserved 2026-07-07T09:33:54Z). 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#8` · branch `issue-8` · driver `claude` · workflow `direct-main` - **Run:** `engineer-claude-ward-mcp-8` · ward `v0.419.0` · dispatched `2026-07-07T09:33:54Z` - **Comment thread:** 0 included in the pre-flight read, 0 stripped (ward's own automated comments). **Issue body as seeded:** ``` ## What The generic **ward-mcp Helm chart** - the distribution vehicle ward-mcp ships (ward-mcp#6). One chart, one runtime image, many releases: the `.mcp.kdl` rides in as chart values (a ConfigMap), and the chart templates all the k8s exposure. Adding an MCP becomes a values file + `helm upgrade`, no per-guardfile image build. ` ` `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> ` ` ` ## Templates - **ConfigMap** - the `.mcp.kdl` from `--set-file spec=...`, mounted into the runtime at `/spec/<name>.mcp.kdl`. - **Deployment** - the one generic runtime image, `ward-mcp serve /spec/<name>.mcp.kdl --http :8080`, the token injected as env. - **Service** - ClusterIP (+ optional NodePort for the tailnet local-harness path, mirroring reddit-mcp). - **Route** - Traefik + Authelia ForwardAuth (public, gated), with a `public` vs `tailnet-only` values toggle. This is deploy#30's fleet auth-overlay, generalized for ward-mcp servers. - **Secret wiring** - an ExternalSecret (or a values-supplied Secret ref) mapping the SSM token to the env var the guardfile names in `value env <VAR>`. ## Values (the deploy-supplied surface) `spec` (the .mcp.kdl, via --set-file), `route.host`, `route.public` (bool), `secret.<ENV>` refs, `image.tag`, resource limits. deploy consumes the chart per-MCP with one values file (deploy#61 skillsmp, deploy#46 forgejo). ## Runtime contract this targets (pin it; the runtime issue implements it) - image ENTRYPOINT `ward-mcp serve /spec/<name>.mcp.kdl --http :8080` - MCP served over SSE / streamable-HTTP at `/mcp` - token supplied as env, name per the guardfile's `value env <VAR>` - `.mcp.kdl` read from the mounted path, not baked ## Sequencing Buildable against the pinned contract above in parallel with the runtime; finalize the mount path / port / health probe once the runtime lands. Ships **in the ward-mcp repo** (the product dist ``` … (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.419.0). </details> <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

WARD-OUTCOME: done - generic ward-mcp Helm chart (spec-as-ConfigMap, token Secret wiring, public/tailnet Authelia route toggle) landed on main, closes #8.

This one went smoothly, mostly because the raw material was already sitting in /substrate/deploy. reddit-mcp's main.yml is the entire deploy#30 auth overlay written out longhand, and agent-proxy's chart is a clean Helm skeleton. The work was really generalizing the former into the shape of the latter - lift the ForwardAuth + oauth2-proxy + RFC 9728 metadata sidecar out of one hardcoded service and parameterize host/client-id/audiences/SSM-paths. Having both references verbatim meant almost no guessing.

What took the most thought was the secret values surface: the issue's --set secret.SKILLSMP_API_KEY=<ref> example wants a map of env-var -> source, but a source can be either an SSM path (mint an ExternalSecret) or an existing Secret ref. I settled on string-vs-map detection (kindIs) so both forms live in one key, and verified the ExternalSecret only renders when at least one entry is a bare string.

Confidence is high on the templating itself - helm lint + helm template piped through a YAML parser pass for both a public read (13 docs) and a tailnet-only write (4 docs), and I eyeballed the fiddly bits (the @kubernetescrd middleware ref form, the well-known JSON, the args). The honest caveat is I can't render against a live cluster from here, so the runtime contract (mount path, :8080, /mcp, probe) is pinned rather than proven - it finalizes once the runtime image (inbox#164) actually exists. I also had to reconcile README/DESIGN, which previously declared all exposure out-of-scope-lives-in-deploy; the reconciliation hinges on the chart being spec-opaque, so the interior-only scope genuinely still holds.

Rough edges worth a follow-up: the fleet's NodePort convention (30000 + last-three) can't be auto-derived from :8080 without collisions, so it stays a per-MCP values knob the deploy operator must pick by hand - fine, but easy to fumble. And the default image.repository points at the public Forgejo path to keep this repo public-safe; deploy must override it to the in-cluster registry mirror or pulls will miss.

WARD-OUTCOME: done - generic ward-mcp Helm chart (spec-as-ConfigMap, token Secret wiring, public/tailnet Authelia route toggle) landed on main, closes #8. This one went smoothly, mostly because the raw material was already sitting in `/substrate/deploy`. reddit-mcp's `main.yml` is the entire deploy#30 auth overlay written out longhand, and agent-proxy's chart is a clean Helm skeleton. The work was really *generalizing the former into the shape of the latter* - lift the ForwardAuth + oauth2-proxy + RFC 9728 metadata sidecar out of one hardcoded service and parameterize host/client-id/audiences/SSM-paths. Having both references verbatim meant almost no guessing. What took the most thought was the `secret` values surface: the issue's `--set secret.SKILLSMP_API_KEY=<ref>` example wants a map of env-var -> source, but a source can be either an SSM path (mint an ExternalSecret) or an existing Secret ref. I settled on string-vs-map detection (`kindIs`) so both forms live in one key, and verified the ExternalSecret only renders when at least one entry is a bare string. Confidence is high on the templating itself - `helm lint` + `helm template` piped through a YAML parser pass for both a public read (13 docs) and a tailnet-only write (4 docs), and I eyeballed the fiddly bits (the `@kubernetescrd` middleware ref form, the well-known JSON, the args). The honest caveat is I can't render against a live cluster from here, so the runtime contract (mount path, `:8080`, `/mcp`, probe) is *pinned* rather than *proven* - it finalizes once the runtime image (inbox#164) actually exists. I also had to reconcile README/DESIGN, which previously declared all exposure out-of-scope-lives-in-deploy; the reconciliation hinges on the chart being spec-opaque, so the interior-only scope genuinely still holds. Rough edges worth a follow-up: the fleet's NodePort convention (30000 + last-three) can't be auto-derived from `:8080` without collisions, so it stays a per-MCP values knob the deploy operator must pick by hand - fine, but easy to fumble. And the default `image.repository` points at the public Forgejo path to keep this repo public-safe; deploy must override it to the in-cluster registry mirror or pulls will miss.
Sign in to join this conversation.
No description provided.