- Go 92.2%
- Go Template 5.3%
- Dockerfile 2.5%
| .forgejo/workflows | ||
| chart | ||
| cmd/ward-mcp | ||
| docs | ||
| examples | ||
| internal/mcpserver | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
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- theserveentrypoint: 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/healthzplus/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) andforgejo-issues(tailnet-only write).chart/- the generic ward-mcp Helm chart. Seedocs/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).