Skip to content

feat(mcp): add ToolHive operator and mcp-gateway workloads - #62

Merged
xnoto merged 1 commit into
mainfrom
feat/mcp-gateway
Aug 26, 2026
Merged

feat(mcp): add ToolHive operator and mcp-gateway workloads#62
xnoto merged 1 commit into
mainfrom
feat/mcp-gateway

Conversation

@xnoto

@xnoto xnoto commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a Kubernetes MCP gateway (Stacklok ToolHive) to the cluster and wires the opencode web service to it, giving every agentic session one universal, aggregated MCP backend.

operators/toolhive/ — ToolHive operator 0.44.0 via two ArgoCD OCI-chart Applications (toolhive-operator-crds wave 0, toolhive-operator wave 1). Chart resource defaults are emptied per the single-node policy; update check / usage metrics disabled.

workloads/mcp-gateway/ (new mcp namespace):

  • MCPGroup/gateway + VirtualMCPServer/gateway → aggregated endpoint http://vmcp-gateway.mcp.svc:4483/mcp (service name/port verified against the 0.44.0 operator source)
  • MCPServer/kubernetescontainers/kubernetes-mcp-server:v0.0.66 (digest-pinned) with --read-only and a dedicated ClusterRole that excludes Secrets (KSOPS-decrypted secrets must not become readable through the gateway)
  • MCPRemoteProxy/context7 — public, unauthenticated
  • MCPRemoteProxy/githubapi.githubcopilot.com with MCPExternalAuthConfig headerInjection; the PAT is injected server-side from the KSOPS secret mcp-github (clients never hold it)
  • VirtualMCPServer incomingAuth: anonymous is deliberate: ClusterIP-only, flannel doesn't enforce NetworkPolicy, and edge auth is Cloudflare Access (see paired tfroot-cloudflare PR)

opencode — ConfigMap gains an mcp block pointing at vmcp-gateway. Remote-only by design: the opencode image (Alpine + static Bun binary, no python/uv/node) cannot run stdio MCPs, and its pod hardening forbids runtime installs.

wait-for-crds — added the five ToolHive CRDs to the PreSync gate.

Authentication model

  1. Edge (enforcement): Cloudflare Access on mcp.makeitwork.cloud — service-token (non_identity) for agent clients + admins GitHub SSO for debug. Unauthenticated traffic dies at the edge.
  2. In-cluster: anonymous on ClusterIP (see above).
  3. Credentials: GitHub PAT lives only as a KSOPS secret; k8s backend is RBAC-read-only + --read-only.

Required sequence — read before merging

  1. First: merge + apply the paired tfroot-cloudflare PR (creates the mcp CNAME and the Access applications). Do not merge this PR's TunnelBinding before that apply — the TunnelBinding is deliberately excluded from this PR to avoid the documented DNS race (81053); it lands as a small follow-up PR after the CNAME exists.
  2. Merge this PR. ArgoCD syncs: CRDs → operator → group/backends → vMCP.
  3. Set the real GitHub PAT: sops workloads/mcp-gateway/github-secret.yaml (value must be Bearer <pat>), commit, push. Until then only the github proxy is degraded; context7 + kubernetes work.
  4. Follow-up PR: workloads/mcp-gateway/tunnel-binding.yamlmcp.makeitwork.cloudhttp://vmcp-gateway.mcp.svc:4483.
  5. Verify: kubectl --context k3s get mcpservers,virtualmcpservers,mcpremoteproxies -n mcp, then opencode mcp debug mcp-gateway from a session.

Local client config (after steps 1–4)

"mcp-gateway": {
  "type": "remote",
  "url": "https://mcp.makeitwork.cloud/mcp",
  "enabled": true,
  "oauth": false,
  "headers": {
    "CF-Access-Client-Id": "{env:CF_ACCESS_CLIENT_ID}",
    "CF-Access-Client-Secret": "{env:CF_ACCESS_CLIENT_SECRET}"
  }
}

Validation

  • kustomize build operators/toolhive ✅ (2 Applications), kustomize build workloads/apps ✅ (10 objects)
  • workloads/mcp-gateway local build blocked by KSOPS exec plugin — pre-existing repo limitation, identical on main (verified via stash); structure linted instead
  • pre-commit run on all changed files ✅ (check-yaml, KubeLinter, detect-private-key, secrets)
  • Secret scan of the diff ✅ (placeholder PAT is SOPS-encrypted)

Not done (explicit)

  • No TunnelBinding (step 4 above)
  • No tofu plan/apply — paired tfroot PR, applies by repo owner
  • vMCP OIDC against Dex — documented upgrade path, deferred

Install the ToolHive operator (0.44.0) via ArgoCD OCI-chart Applications
and add workloads/mcp-gateway: a VirtualMCPServer aggregating a read-only
in-cluster kubernetes-mcp-server plus MCPRemoteProxy backends for context7
and github (token injected server-side from a KSOPS secret). Point the
opencode web deployment at the aggregated vmcp-gateway endpoint. External
access is gated by Cloudflare Access (tfroot-cloudflare); the TunnelBinding
lands in a follow-up once the CNAME exists.
@xnoto

xnoto commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Paired tfroot-cloudflare PR: makeitworkcloud/tfroot-cloudflare#26 (must merge + apply first — see "Required sequence" above).

xnoto added a commit to makeitworkcloud/tfroot-cloudflare that referenced this pull request Aug 26, 2026
## Summary

Edge authentication for the new cluster MCP gateway (paired with
makeitworkcloud/kustomize-cluster#62).

- **`cf-tunnels.tf`** — adds `mcp` to the cluster-apps CNAME list.
**This must be applied before the TunnelBinding lands in
kustomize-cluster** (documented DNS race, error `81053`); the
TunnelBinding PR follows this apply.
- **`cf-access-mcp.tf`** — dedicated `mcp-gateway` Access service token
(`duration = "forever"`, rotate via `client_secret_version`) +
`self_hosted` application on `mcp.makeitwork.cloud`: `non_identity`
service-token policy for headless agent clients (opencode, Claude Code,
etc. send `CF-Access-Client-*` headers) and an admins GitHub-SSO policy
for interactive debug.
- **`outputs.tf`** — exposes the service-token `client_id` and
`client_secret` (sensitive) so agent clients can be configured after
apply.

**Scope change during review:** the `cf-access-opencode.tf` gate was
dropped — #25 deliberately removed the opencode Access gate because it
breaks `opencode attach`; opencode web stays on the SOPS-managed server
password. Re-gating needs an attach-compatible design first.

## Validation

- `tofu fmt` ✅, `tofu init -backend=false` + `tofu validate` ✅
- `make test` ✅ (canonical pre-commit: validate, tflint, checkov, fmt,
terraform-docs README regenerated)

## Not run

- `tofu plan` / `tofu apply` — CI posts the plan here and applies on
merge per the shared workflow.

## Post-apply

1. `tofu output mcp_gateway_service_token_client_id` / `tofu output -raw
mcp_gateway_service_token_client_secret` → local agent sessions as
`CF_ACCESS_CLIENT_ID` / `CF_ACCESS_CLIENT_SECRET` (keyring/env only).
2. Then the follow-up TunnelBinding PR in kustomize-cluster is safe to
merge.
@xnoto
xnoto merged commit 47870a1 into main Aug 26, 2026
2 checks passed
@xnoto
xnoto deleted the feat/mcp-gateway branch August 26, 2026 03:25
xnoto added a commit that referenced this pull request Aug 26, 2026
Follow-up to #62 as planned: TunnelBinding `mcp.makeitwork.cloud` →
`http://vmcp-gateway.mcp.svc:4483`.

Sequencing verified before opening:
- `mcp` CNAME applied via tfroot-cloudflare#26 (no `81053` race)
- Access app live: unauthenticated `GET
https://mcp.makeitwork.cloud/mcp` → 302 to Access login
- vMCP healthy: `tools/list` via port-forward returns 16 aggregated
tools (context7×2, kubernetes×14)

Post-merge validation: external `tools/list` with the service token;
302/403 without it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant