No description
  • Go 92.2%
  • Go Template 5.3%
  • Dockerfile 2.5%
Find a file
2026-07-15 03:55:13 +00:00
.forgejo/workflows chore(ci): add Telegram main-failure alerts 2026-07-10 09:02:57 +00:00
chart fix(chart): generate oauth cookie secret in-cluster 2026-07-08 17:13:34 +00:00
cmd/ward-mcp add upstream mcp proxy backend 2026-07-15 03:50:28 +00:00
docs add upstream mcp proxy backend 2026-07-15 03:50:28 +00:00
examples docs: align ward-mcp with inline op authoring 2026-07-08 17:13:41 +00:00
internal/mcpserver add upstream mcp proxy backend 2026-07-15 03:50:28 +00:00
Dockerfile feat(runtime): generic ward-mcp serve runtime over cli-guard http/opcore 2026-07-07 09:46:24 +00:00
go.mod refactor: adopt the MCP Go SDK 2026-07-14 10:17:02 +00:00
go.sum refactor: adopt the MCP Go SDK 2026-07-14 10:17:02 +00:00
README.md add upstream mcp proxy backend 2026-07-15 03:50:28 +00:00

ward-mcp

A cli-guard Guardfile, no handwritten code, becomes a Docker image that serves a working MCP over HTTP.

ward-mcp renders a cli-guard Guardfile into a guarded MCP server, baked into an OCI image. One generic runtime, many guardfiles. No per-server Go, no per-server Dockerfile, no per-server MCP handler - and no per-tool input schema, because cli-guard's engine derives it from the inline operation definition in the .mcp.kdl.

The spec configures only the image interior: which upstream, which auth, which grants become which tools. The image serves MCP over the official Go SDK's streamable HTTP transport at /mcp (never stdio - these run as remote k3s pods reached by URL). ward-mcp has no relation to the ward codebase, and uses cli-mcp as a code reference only, not a dependency.

The exposed MCP surface is exactly the guardfile's grants: an unwritten delete issue grant means no delete_issue tool can ever be served (deny-by-absence), and restrict owner matches coilyco-* bounds every path. Audit one small file, hand a write-capable MCP to an agent, know the blast radius.

Quickstart

The generic ward-mcp serve runtime renders any .mcp.kdl into a guarded MCP server. Run it directly:

# initialize, then reuse the session id for tools/list and tools/call
FORGEJO_TOKEN=... go run ./cmd/ward-mcp serve examples/forgejo-issues.mcp.kdl --http :8080

# list the derived tools (SDK-backed streamable HTTP transport at /mcp)
curl -s -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' localhost:8080/mcp

Or as the image (one runtime, many specs - the spec is mounted, not baked):

docker build -t ward-mcp .
docker run -p 8080:8080 -e SKILLSMP_API_KEY \
  -v $PWD/examples/skillsmp.mcp.kdl:/spec/skillsmp.mcp.kdl \
  ward-mcp serve /spec/skillsmp.mcp.kdl --http :8080

For a passthrough MCP wrapper over a private upstream, use serve-upstream with an allowlist:

ward-mcp serve-upstream --name grubhub-mcp \
  --upstream http://playwright-mcp.namespace.svc.cluster.local/mcp \
  --tool browser_navigate --tool browser_click --http :8080

The forgejo example serves create_issue, get_issue, list_issue, comment_issue, close_issue - each guarded, each scoped to coilyco-* / kai owners.

The runtime is a thin shell over cli-guard's http/opcore engine: opcore.ParseInline parses the inline spec, each grant projects to one MCP tool, and every call fires through the self-guarding opcore.Operation.Execute (metachar gate, restrict, auth). ward-mcp adds only the grant→tool projection and the SDK-backed transport/session layer.

Distributes as image + chart

The product ships two artifacts (ward-mcp#6): the generic runtime image above, and a generic Helm chart (chart/) that templates the k3s exposure. Deploying an MCP is then a values file plus helm upgrade - no per-guardfile image build, no per-service manifest fork:

helm upgrade --install skillsmp ward-mcp \
  -f skillsmp.values.yaml \
  --set-file spec=skillsmp.mcp.kdl \
  --set image.tag=<built-runtime-sha>

The .mcp.kdl rides in as chart values (a ConfigMap mounted into the runtime); the chart templates the Deployment, Service, token Secret wiring, and - for a public MCP - the Authelia-JWT route. The chart stays generic and spec-opaque (it never parses the guardfile), so the interior-only scope of the spec holds: the spec never reaches down into the chart, the chart never reaches up into the spec. deploy consumes the chart per-MCP with one values file (deploy#61 skillsmp, deploy#46 forgejo) and owns the rollout, the same way it already hosts steam-mcp, reddit-mcp, and node-stats-mcp.

Layout

  • cmd/ward-mcp - the serve entrypoint: parse a spec, project tools, bind the SDK-backed HTTP listener.
  • internal/mcpserver - the thin shell: grant→MCP-tool projection, the SDK-backed streamable HTTP/session layer, and the non-MCP /healthz plus /admin/* operator endpoints.
  • examples/forgejo-issues.mcp.kdl - the worked "hello world": Forgejo issues as an MCP. Its body is the frozen ward-mcp inline grammar (opcore.ParseInline), and it is the whole contract.
  • examples/skillsmp.mcp.kdl - the first end-to-end target: two read tools over the SDK-backed transport against skillsmp.com.
  • examples/*.values.yaml - reference deploy-side chart values: skillsmp (public, Authelia-gated read) and forgejo-issues (tailnet-only write).
  • chart/ - the generic ward-mcp Helm chart. See docs/chart.md.
  • docs/DESIGN.md - the spec→image pipeline, the interior-only scope, and the SDK-backed transport + safety model.
  • docs/chart.md - the chart's templates, values reference, and the runtime contract it targets.
  • docs/FEATURES.md - the living inventory of what ships today.

Status

The ward-mcp serve runtime is implemented (ward-mcp#7): it parses a .mcp.kdl, serves the derived tools over MCP at /mcp, guarded-executes each call through opcore, and exposes operator-only /healthz plus /admin/describe and /admin/reload HTTP endpoints outside the MCP tool surface. The generic Helm chart that runs this image is also in (ward-mcp#8). Tracking coilysiren/inbox#164 (concept) and coilyco-bridge/deploy#40 (first consumer).